Linux 的memblock 内存管理器

Linux 的memblock 内存管理器

最近接触到了linux 在启动阶段的内存管理器memblock, 它是bootmem 的后续者。 本来想自己写一篇关于memblock的文章的, 但看到了这篇文章, 就把它翻译过来了:https://0xax.gitbooks.io/linux-insides/content/MM/linux-mm-1.html# 。

简介

内存管理是操作系统最复杂的子系统之一(而我认为不需要加之一)。 在 kernel 入口前的最后准备 一节中, 我们在start_kernel 之前停了下来。 你可能还记得我们在启动阶段创建早期的page tables,identity page tables 和 fixmap page tables。复杂的内存管理还没有工作。 当start_kernel 开始运行时, 我们将看到向更复杂的数据结构和技术的转变。为了更好地了解内核的初始化过程, 我们需要对这些技术有一个清晰的理解。 本章首先从memblock开始, 详细介绍Linux 内存管理的框架及其API。

Memblcok

在自举阶段, 通用的内存管理器还没有建立起来的时候, memblock 是管理内存区域的方法之一。 原先它叫做 Logical Memory Block, 经过了Yinghai Lu 的补丁之后, 它被命名为memblock。 由于linux x86_64 内核使用该技术, 我们已经在 kernel 入口前的最后准备 中遇到过它了。 现在我们更仔细地考察它是如何实现的。

我们先从memblock 相关的数据结构开始 。 它们定义在头文件 include/linux/memblock.h 里:

第一个结构是 memblock:

struct memblock {
         bool bottom_up;
         phys_addr_t current_limit;
         struct memblock_type memory;   --> array of memblock_region
         struct memblock_type reserved; --> array of memblock_region
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
         struct memblock_type physmem;
#endif
};

该结构包含5个成员: bootom_up 为 true 的时候表明自底向上分配内存。 current_limit 是 memory block 的限制尺寸。 接下来的三个成员表示memory block的类型, 它们可以是: memory, reserved 和 physical memory(当CONFIG_HAVE_MEMBLOCK_PHYS_MAP 使能时)。 然后我们看另一个数据结构 - memblock_type:

struct memblock_type {
    unsigned long cnt;
    unsigned long max;
    phys_addr_t total_size;
    struct memblock_region *regions;
};

该结构提供关于内存类型的信息。 它包含成员描述memory region 在当前memory block里的个数和尺寸, 以及指向memblock_region 的指针。 而memblock_region 结构描述一块内存区域:

struct memblock_region {
        phys_addr_t base;
        phys_addr_t size;
        unsigned long flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
        int nid;
#endif
};

它提供了内存区域的基地址和大小, 还有一个标志域, 可能的值有:

enum {
    MEMBLOCK_NONE    = 0x0,    /* No special request */
    MEMBLOCK_HOTPLUG    = 0x1,    /* hotpluggable region */
    MEMBLOCK_MIRROR    = 0x2,    /* mirrored region */
    MEMBLOCK_NOMAP    = 0x4,    /* don't add to kernel direct mapping */
};

如果CONFIG_HAVE_MEMBLOCK_NODE_MAP定义了, 还有一个 numa 节点选择项 nid。

我们可以用下图表示以上三个数据结构的关系:

+---------------------------+   +---------------------------+
|         memblock          |   |                           |
|  _______________________  |   |                           |
| |        memory         | |   |       Array of the        |
| |      memblock_type    |-|-->|      memblock_region      |
| |_______________________| |   |                           |
|                           |   +---------------------------+
|  _______________________  |   +---------------------------+
| |       reserved        | |   |                           |
| |      memblock_type    |-|-->|       Array of the        |
| |_______________________| |   |      memblock_region      |
|                           |   |                           |
+---------------------------+   +---------------------------+

Memblock 的初始化

memblock 所有的API都定义在头文件 include/linux/memblock.h里, 而它们的实现都在文件 mm/memblock.c 里。 在该文件的开始处, 我们看到 memblock 结构的初始化:

struct memblock memblock __initdata_memblock = {
    .memory.regions        = memblock_memory_init_regions,
    .memory.cnt            = 1,
    .memory.max            = INIT_MEMBLOCK_REGIONS,

    .reserved.regions    = memblock_reserved_init_regions,
    .reserved.cnt        = 1,
    .reserved.max        = INIT_MEMBLOCK_REGIONS,

#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
    .physmem.regions    = memblock_physmem_init_regions,
    .physmem.cnt        = 1,
    .physmem.max        = INIT_PHYSMEM_REGIONS,
#endif
    .bottom_up            = false,
    .current_limit        = MEMBLOCK_ALLOC_ANYWHERE,
};

此处的变量 memblock 与数据结构同名。首先注意到 __initdata_memblock 的定义是:

#ifdef CONFIG_ARCH_DISCARD_MEMBLOCK
    #define __init_memblock __meminit
    #define __initdata_memblock __meminitdata
#else
    #define __init_memblock
    #define __initdata_memblock
#endif

它取决于 CONFIG_ARCH_DISCARD_MEMBLOCK 。 如果该设置使能了, 则memblock 的代码和数据会放到 .init section 里去,它们占用的内存在内核完成启动后被释放。 然后我们考察memblock 的成员 memblock_type memory, memblock_type reserved 和 memblock_type physmem 的初始化, 我们只对memblock_type.regions 的初始化过程感兴趣。 注意到每个 memblock_type.regions 被赋值为一个 memblock_regions 数组:

static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock;
#endif

每个数组包含128个memory regions。 因为 INIT_MEMBLOCK_REGIONS 默认定义为 128:

#define INIT_MEMBLOCK_REGIONS   128

而且所有的数组也有 __initdata_memblock 宏,说明它们能在内核启动结束后被释放。

memblock 的最后两个成员 bottom_up 被设置为false, 而当前的memblock 的上限为:

#define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0)

即 0xffffffffffffffff 。

这样memblock的初始化就完成了,接下来我们看memblock API。

Memblock API

为了更好地理解 memblock 是怎样实现和工作的,我们先看一下它的使用。 在Linux 内核里有好些地方使用memblock。 比如, 以 arch/x86/kernel/e820.c 里的 memblock_x86_fill 为例, 该函数遍历e820提供的内存块,调用memblock_add 函数,把内核要保留的memory region 加到memblock里去。 该函数接收一个物理基地址和memory region 的大小作为参数, 它其实并不做什么, 仅调用

memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);

我们传进 memblock type - memory, 物理基地址和尺寸, 和最大的node 数目: 如果 CONFIG_NODES_SHIFT 设置了, 则为1;如果了,则为 1 << CONFIG_NODES_SHIFT 。 memblock_add_range 函数把一个新的memory region 加到memory block里。 它先检查给定memory region 的大小,如果为0则返回。 然后它检查相应的memblock_type 里是否有memory region, 如果没有,我们就用给定的参数填充一个新的 memory_region 并返回(我们在 First touch of the linux kernel memory manager framework 里已经看到过). 如果 memblock_type 不为空, 我们开始往给定的memblock_type 里加一块memory region。 首先, 我们获取结束地址:

phys_addr_t end = base + memblock_cap_size(base, &size);

memblock_cap_size 调整 size 使得 base + size 不会溢出。 它的实现很简单:

static inline phys_addr_t memblock_cap_size(phys_addr_t base, phys_addr_t *size)
{
    return *size = min(*size, (phys_addr_t)ULLONG_MAX - base);
}

它返回 size 和 UULONG_MAX - base 两者中的最小值。

然后我们获得了新memory region 的结束地址。 memblock_add_range 检查与已经加进的memory region 的重叠和合并的条件。 插入新的memory region 包含两步:

  • 把不重叠部分作为单独的region加进去
  • 合并相邻的region

我们遍历所有已加进的 memory region 并检查与新来的region 的重叠情况:

    for (i = 0; i < type->cnt; i++) {
        struct memblock_region *rgn = &type->regions[i];
        phys_addr_t rbase = rgn->base;
        phys_addr_t rend = rbase + rgn->size;

        if (rbase >= end)
            break;
        if (rend <= base)
            continue;
        ...
        ...
        ...
    }

如果新的memory region 与现有的region 不重叠,则插入memblock, 这是第一步。 我们检查它能否合适memblock, 否则就调用 memblock_double_array:

while (type->cnt + nr_new > type->max)
    if (memblock_double_array(type, obase, size) < 0)
        return -ENOMEM;
    insert = true;
    goto repeat;

它把给定的region array 扩大一倍, 接着我们置 insert 为 true 并跳转到标号 repeat 。 第二步, 从repeat 处, 我们走相同的循环, 调用 memblock_insert_region 把region 插入到memory block里:

    if (base < end) {
        nr_new++;
        if (insert)
            memblock_insert_region(type, i, base, end - base,
                           nid, flags);
    }

由于insert 已经置为true, memblock_insert_region 会被调用。 它的实现跟我们以前见过的插入空白的memblock_type几乎一样。 它先获取最后一个region:

struct memblock_region *rgn = &type->regions[idx];

并拷贝这些区域:

memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn));

然后填充memblock_region 的base, szie 等信息, 并增加memblock_type 的大小。 在结束运行前, memblock_add_range 调用 memblock_merge_regions 来合并相邻的可兼容的region。

在第二阶段, 新的region 可能与现有的region 重叠。 比如, 我们已经有了 region1:

0                    0x1000
+-----------------------+
|                       |
|                       |
|        region1        |
|                       |
|                       |
+-----------------------+

而我们想加入region2,它的base address 和size 是这样的:

0x100                 0x2000
+-----------------------+
|                       |
|                       |
|        region2        |
|                       |
|                       |
+-----------------------+

这样新 region 的基地址是:

base = min(rend, end);

在我们的例子里,就是0x1000。 然后就像以前做过的那样插入该 region :

if (base < end) {
    nr_new++;
    if (insert)
        memblock_insert_region(type, i, base, end - base, nid, flags);
}

此时我们仅插入重叠的部分(我们只插入高端部分, 因为低端部分已经在重叠的memory region 里了), 然后用memblock_merge_regions 把相邻部分合并起来。 它遍历给定的memblock_type, 取两个相邻的region: type->regions[i] 和 type->regions[i+1], 检查它们是否有相同的标志、属于相同的node、而且第一个region 的结束地址不等于第二个的开始地址:

while (i < type->cnt - 1) {
    struct memblock_region *this = &type->regions[i];
    struct memblock_region *next = &type->regions[i + 1];
    if (this->base + this->size != next->base ||
        memblock_get_region_node(this) !=
        memblock_get_region_node(next) ||
        this->flags != next->flags) {
        BUG_ON(this->base + this->size > next->base);
        i++;
        continue;
    }

如果以上条件都满足, 我们用第二个region的尺寸来更新第一个region的尺寸:

this->size += next->size;

然后我们把第二个region 后面的区域都往前挪一位:

memmove(next, next + 1, (type->cnt - (i + 2)) * sizeof(*next));

memmove 函数把next 后面的所有region 移到next 原来所占的位置。 最后我们减小memblock_type 的region 数量:

type->cnt--;

最后,我们得到合并成一块的memory region:

0                                             0x2000
+------------------------------------------------+
|                                                |
|                                                |
|                   region1                      |
|                                                |
|                                                |
+------------------------------------------------+

总结一下, 我们减小了memblock的region 的数量, 增加了第一块region的尺寸, 并移动第二块之后的所有region到第二块的位置, 这就是 memblock_add_range 的主要工作。

memblock_reserve 函数做跟 memblock_add 相同的工作,不过它操作的是 memblock 的 memblock_type.reserved 成员。 当然这不是全部的API。 Memblock 还提供:

  • memblock_remove - 从 memblock 中删除 memory region
  • memblock_find_in_range - 在给定范围内寻找空闲区域
  • memblock_free - 释放memblock中的memory region
  • for_each_mem_range - 遍历memblock的所有区域

获取memory region 的信息

Memblock 还提供API获取memory region 的信息, 它分为两部分:

  • get_allocated_memblock_memory_regions_info - 获取memory region 的信息
  • get_allocated_memblock_reserved_regions_info - 获取 reserved region 的信息

这些函数的实现很简单。 以get_allocated_memblock_reserved_regions_info 为例:

phys_addr_t __init_memblock get_allocated_memblock_reserved_regions_info(
                    phys_addr_t *addr)
{
    if (memblock.reserved.regions == memblock_reserved_init_regions)
        return 0;

    *addr = __pa(memblock.reserved.regions);

    return PAGE_ALIGN(sizeof(struct memblock_region) *
              memblock.reserved.max);
}

首先该函数检查 memblock 是否含有 reserved memory region,如果没有,它返回0。 否则我们把reserved memory region 的物理地址写到 addr 里, 返回分配的数组的大小并且以PAGE对齐。 注意宏PAGE_ALIGN 用来对齐,它跟PAGE SIZE 相关:

#define PAGE_ALIGN(addr) ALIGN(addr, PAGE_SIZE)

get_allocated_memblock_memory_regions_info 的实现是一样的,只不过它使用memblock_type.memory 而不是memblock_type.reserved 。

Memblock debugging

Memblock 中有许多对 memblock_dbg 的调用。 如果你传送内核参数 memblock=debug,该函数就会被调用。 其实memblock_dbg 是扩展为printk 的宏:

#define memblock_dbg(fmt, ...) \
         if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)

例如你在memblock_reserve 函数里看到了如下调用:

memblock_dbg("memblock_reserve: [%#016llx-%#016llx] flags %#02lx %pF\n",
             (unsigned long long)base,
             (unsigned long long)base + size - 1,
             flags, (void *)_RET_IP_);

你会看到这样的输出:

memblock debug

Memblock 还支持 debugfs。 如果内核运行在非 x86 的架构上, 你可以存取

  • /sys/kernel/debug/memblock/memory
  • /sys/kernel/debug/memblock/reserved
  • /sys/kernel/debug/memblock/physmem

来获取 memblock 的内容。

小结

这是Linux 内核内存管理的第一部分。 如果你有任何问题或建议,请用以下方法联系我:

链接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章