首页 > 解决方案 > 关于 linux v5.4 内核 arm64_memblock_init() / fdt_enforce_memory_region()

问题描述

linux,usable-memory-range财产有什么用?

背景:

最近在分析Linux内核是如何初始化内存的。当我进入arm64_memblock_init函数时arch/arm64/mm/init.c,我遇到了一个名为 的函数fdt_enforce_memory_region()。该函数处理linux,usable-memory-range设备树中所选节点的属性。

函数调用链是:arm64_memblock_init()-> fdt_enforce_memory_region()-> memblock_cap_memory_range()

void __init arm64_memblock_init(void)
{
    const s64 linear_region_size = BIT(vabits_actual - 1);

    /* Handle linux,usable-memory-range property */
    fdt_enforce_memory_region();
    ...
}
static void __init fdt_enforce_memory_region(void)
{
    struct memblock_region reg = {
        .size = 0,
    };

    of_scan_flat_dt(early_init_dt_scan_usablemem, &reg);

    if (reg.size)
        memblock_cap_memory_range(reg.base, reg.size);
}
void __init memblock_cap_memory_range(phys_addr_t base, phys_addr_t size)
{
    int start_rgn, end_rgn;
    int i, ret;

    if (!size)
        return;

    ret = memblock_isolate_range(&memblock.memory, base, size,
                        &start_rgn, &end_rgn);
    if (ret)
        return;
    /* codes below this line make me confused */
    /* remove all the MAP regions */
    for (i = memblock.memory.cnt - 1; i >= end_rgn; i--)  /*-- 1 --*/
        if (!memblock_is_nomap(&memblock.memory.regions[i]))
            memblock_remove_region(&memblock.memory, i);

    for (i = start_rgn - 1; i >= 0; i--)                 /*-- 2 --*/
        if (!memblock_is_nomap(&memblock.memory.regions[i]))
            memblock_remove_region(&memblock.memory, i);

    /* truncate the reserved regions */
    memblock_remove_range(&memblock.reserved, 0, base); /*-- 3 --*/
    memblock_remove_range(&memblock.reserved,          /*-- 4 --*/
            base + size, PHYS_ADDR_MAX);
}

在阅读了一些补丁/Documentation/devicetree/bindings/chosen.txt,我了解到linux,usable-memory-range属性与故障转储内核有关。它应该与kdump有一些关系。但是为什么要把函数上方标明的'memory'和'reserved'类型的内存从1到4去掉,而只保留[base, base + size]的内存呢memblock_cap_memory_range() 我无法理解这一点。

但是从一些书籍中,他们说“linux,usable-memory-range”属性是内存的可用范围,并memblock_cap_memory_range()删除超出此范围的内存。如果这句话是真的,我可以理解这一点。然而,正如我上面展示的,一些补丁这篇文章以及/Documentation/devicetree/bindings/chosen.txt所说的道具与 kdump 相关。

那么,linux,usable-memory-range财产有什么用呢?

标签: memorymemory-managementlinux-kernelkernelarm64

解决方案


首先是一些背景:

Kexec 允许 Linux 充当引导加载程序并运行另一个内核(不一定是 Linux 内核)。用户空间工具(来自 kexec-tools 的 kexec)创建一个 kexec 映像,包括新内核和启动所需的任何其他二进制文件,例如 initrd/initramfs 映像、设备树映像、一些用于跳转到新内核的帮助代码(炼狱)等。 kimage 还包括有关这些二进制文件在需要时将在新系统内存中重新定位的位置的信息。然后该工具将 kimage 加载到正在运行的内核(kexec -l),当请求时(kexec -e),当前内核将退出,将 kimage 解压到内存中并跳转到新内核。

Kdump 允许正在运行的内核在系统崩溃的情况下使用 kexec 并跳转到另一个内核,而不是挂起或执行硬件重启。新内核(也称为崩溃内核或转储捕获内核)的目的是从崩溃内核的内存中转储调试数据,然后重新使用 kexec 重新启动到正常系统(也称为主内核)。主内核向崩溃内核提供一个 ELF Core 头文件,而崩溃内核使用它来提供 /proc/vmcore 作为崩溃内核内存的入口。这是一个非常酷的功能,它允许从操作系统崩溃中快速恢复(硬件重启可能非常慢),同时还为调试每次崩溃提供日志数据。

有关 kexec/kdump 的更多信息:https ://www.kernel.org/doc/Documentation/kdump/kdump.txt

现在回答你的问题:

当我们跳转到崩溃内核时,保存崩溃内核的内容而不覆盖任何内容是非常重要的。出于这个原因,为了使用 kdump,我们需要使用 crashkernel= 命令行属性在主内核上预先分配一个连续的内存区域。崩溃内核将只能使用该保留的内存区域,这意味着整个 kimage 必须适合其中,而不仅仅是内核。这也使得跳转到新内核更简单,因为不需要重新定位 kimage 的各个部分,它已经“解包”了。

然而,崩溃内核需要以某种方式知道这一点,如果它重新使用主内核中的设备树,没有什么可以阻止它弄乱崩溃内核的内存。所以我们有几个选择:

a) 覆盖崩溃内核设备树上的 /memory 节点,以便仅包括我们通过 crashkernel= 在主内核上保留的内存区域。但是,当我们尝试重新启动到主内核时,我们将不知道如何将 /memory 节点恢复到其原始值。

b) 使用 linux,usable-memory 属性(参见 early_init_dt_scan_memory),它将覆盖 /memory 节点的 reg 属性,并允许崩溃内核只看到保留的内存区域。然后,我们可以从崩溃内核的设备树中删除该属性,并使用原始 /memory 区域重新启动到主内核。

c) 采用 ARM 的方式,由于某种原因引入了一个新的绑定,linux,usable-memory-range,它转到 /chosen 节点并用于完全相同的事情。只是现在系统已经解析了 /memory 节点并准备使用整个内存,我们需要尽快排除主内核的内存,这就是 memblock_cap_memory_range() 的用途。

请注意,在创建 kimage 时,它​​是在崩溃内核的设备树上添加 linux、usable-memory、linux、usable-memory-range 和 linux、elfcorehdr 属性的用户空间工具。这些属性在正常操作下会丢失,您不会在 /sys/firmware/fdt 上看到它们。

现在您已经了解了 memblock_cap_memory_range() 的全部内容,为了更好地理解代码,我想您应该从 memblock_isolate_range() 开始。请记住,内存可能会被拆分为多个内存块,因为 /memory 节点可能包含一个区域数组,或者设备树可能有多个 /memory 节点(也称为稀疏内存布局)。因此 memblock_cap_memory_range() 将使用崩溃内核保留区域的开始/结束地址调用,然后调用 memblock_isolate_range() 将遍历内存 memblocks 并确保开始/结束地址是 memblock 屏障。

例如,考虑内存范围 1-10 和 3 个内存块 A[0-3]、B[4-8]、C[9-10],崩溃内核位于 2-6。在这种情况下 memblock_isolate_range() 会将 A 拆分为两个块 A1[0-1] 和 A2[2-3],并将 B 拆分为 B1[4-6] 和 B2[6-8],并将 A1 的索引在 start_rgn 上和 end_rgn 上的 B2 索引。因此,在 start_rgn 和 end_rgn 之间,我们将获得包含保留区域的 A2[2-3] 和 B1[4-6]。

代码的下一部分将从内存 memblock 集合中删除保留区域之前和之后的 memblock,因此在我们的示例中,它将删除 A1、B2 和 C。由于标记有 no-map 标志的内存块不会无论如何都不能使用,使用 memblock_is_nomap() 进行检查以跳过它们。

最后一部分处理崩溃内核区域之外的保留内存块。请记住,用 nomap 标记的保留 memblock 不会出现在保留的 memblocks / memory memblocks 的集合中,因此这里不需要特殊处理。

我希望这比令人困惑更有帮助。


推荐阅读