最近接触到了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。
在自举阶段, 通用的内存管理器还没有建立起来的时候, 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 所有的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 是怎样实现和工作的,我们先看一下它的使用。 在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 包含两步:
我们遍历所有已加进的 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 还提供API获取memory 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 中有许多对 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 还支持 debugfs。 如果内核运行在非 x86 的架构上, 你可以存取
来获取 memblock 的内容。
这是Linux 内核内存管理的第一部分。 如果你有任何问题或建议,请用以下方法联系我: