简介
内存管理是操作系统内核最复杂的部分之一( 我认为它是最复杂的 )。在 内核入口前最后的准备 文中, 我们刚好讲到了start_kernel
函数调用之前内核发生的行为。start_kernel
函数在内核运行第一个init
进程之前会初始化所有的内核( 包括架构相关的特征 )。你可能还记得,在系统启动期间我们构造了 early 页表,identity 页表和 fixmap 页表,此时还没涉及复杂的内存管理。当start_kernel
函数被调用时,我们将过渡到更复杂的与内存管理有关的数据结构和技巧。为了能够很好的理解 linux 内核的初始化过程,我们需要清晰地了解这些技术。本章将综述 linux 内核内存管理框架的各个不同部分和相应的 API, 首先介绍memblock
。
Memblock
Memblock 是在早期引导过程中管理内存的方法之一,此时内核内存分配器还没运行。Memblock 以前被定义为Logical Memory Block( 逻辑内存块 )
, 但根据 Yinghai Lu 的 补丁, 它被重命名为memblock
。x86_64
架构的 linux 内核就采用了这种方法。我们在 内核入口前最后的准备 文中已经提到了memblock
。现在我们将进一步了解memblock
是如何实现的。
我们首先从数据结构着手来了解memblock
。所有有关数据结构的定义都可以在 include/linux/memblock.h 头文件中找到。
第一个数据结构的名字如本节标题,如下所示,
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 };
该结构体包含五个域。如果bottom_up
为true
, 则允许由下而上地分配内存。current_limit
指出了内存块的大小限制。接下来的三个域描述了内存块的类型,即预留型,内存型和物理内存型( 如果宏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; };
该结构体存储的是内存类型信息。它包含的域分别描述了当前内存块含有的内存区域数量,所有内存区域的总共大小,已经分配的内存区域大小和一个指向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 };
memblock_region
结构体提供了内存区域的基地址和大小,标志可以是:
#define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0) #define MEMBLOCK_ALLOC_ACCESSIBLE 0 #define MEMBLOCK_HOTPLUG 0x1
如果宏CONFIG_HAVE_MEMBLOCK_NODE_MAP
被定义了,memblock_region
还提供一个整数域 —— numa 节点选择器。
图示法可以用来展示以上结构体之间的关系:
+---------------------------+ +---------------------------+ | memblock | | | | _______________________ | | | | | memory | | | Array of the | | | memblock_type |-|-->| membock_region | | |_______________________| | | | | | +---------------------------+ | _______________________ | +---------------------------+ | | reserved | | | | | | memblock_type |-|-->| Array of the | | |_______________________| | | memblock_region | | | | | +---------------------------+ +---------------------------+
Memblock
主要包含三个结构体:memblock
, memblock_type
和memblock_region
。现在我们已了解了Memblock
, 接下来我们将看到Memblock
的初始化过程。
Memblock 初始化
memblock
的所有 API 描述在 include/linux/memblock.h 头文件中,所有这些 API 的实现在 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
的初始化变量名和结构体名相同 —— 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
代码段,在内核启动完成后 memblock 代码会从.init
代码段释放。
接下来的是memblock
结构体中memblock_type memory
,memblock_type reserved
和memblock_type physmem
的初始化。本文中我们只研究memblock_type.regions
的初始化过程。需要注意的是每一个memblock_type
域都是通过memblock_region
数组初始化的:
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个内存区域,可以查看INIT_MEMBLOCK_REGIONS
的宏定义:
#define INIT_MEMBLOCK_REGIONS 128
需要注意的是所有的数组定义都带有__initdata_memblock
宏,该宏定义已在memblock
结构体初始化时提到过( 如果忘了请回顾上文 )
memblock
结构体中最后两个域:bottom_up
内存分配模式被禁用;当前 Memblock 的大小限制是:
#define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0)
即 0xffffffffffffffff
。
一旦memblock
结构体完成初始化,我们接下来就研究 Memblock API。
Memblock API
我们已经完成了memblock
结构体的初始化,接下来我们将研究 Memblock API 和它的实现。在上文中我提到过所有关于memblock
的实现都在 mm/memblock.c 源文件中。要理解memblock
是如何工作和实现的,我们首先看一下它的用法。在 linux 内核中有几处用到了 memblock,例如 arch/x86/kernel/e820.c 中的函数memblock_x86_fill
。该函数遍历由 e820 提供的内存映射表并且通过memblock_add
函数把内核预留的内存区域添加到memblock
。既然我们首先遇到了memblock_add
函数,那就从它开始吧。
memblock_add
函数有两个参数:物理基址和内存区域大小,并且把该内存区域添加到memblock
。memblock_add
函数本身并没有什么,它只是调用了
memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);
函数。我们传递的参数依次是:内存块类型(memory
),物理基址,内存区域大小,最大节点数( 0 如果CONFIG_NODES_SHIFT
没有在配置文件中设置,不然就是CONFIG_NODES_SHIFT
)和标志。memblock_add_range
函数添加新的内存区域到内存块中。首先,该函数检查给定的内存区域大小,如果是 0 就返回。在这之后,memblock_add_range
用给定的memblock_type
检查memblock
结构体中是否存在内存区域。如果没有,我们就用给定的值填充新的memory_region
然后返回 (我们已经在 linux 内核内存管理框架的第一次接触 一文中看到过实现)。如果memblock_type
不为空,我们就把新的内存区域添加到memblock_type
类型的memblock
中。
首先,我们用如下代码获得内存区域的结束位置:
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); }
memblock_cap_size
返回size
和ULLONG_MAX - base
中的最小值。
在那之后我们得到了新的内存区域的结束地址,memblock_add_range
函数检查内存区域是否重叠并和已经添加到memblock
中的内存区域合并。把新的内存区域插入到memblock
中包含两步:
- 把新的内存区域中非重叠的部分作为独立的区域加入到 memblock
- 合并所有相邻的内存区域
接下来遍历所有已经存储的内存区域并检查有没有和新的内存区域重叠:
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; ... ... ... }
如果新内存区域没有和已经存储在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;
memblock_double_array
函数加倍给定的内存区域大小,然后把insert
设为true
再转到repeat
标签。第二次循环,从repeat
标签开始经过同样的循环然后用memblock_insert_region
函数把当前内存区域插入到内存块:
if (base < end) { nr_new++; if (insert) memblock_insert_region(type, i, base, end - base, nid, flags); }
由于我们在第一次循环中把insert
设为true
, 现在memblock_insert_region
函数将会被调用。memblock_insert_region
函数几乎和把新内存区域插入到空的memblock_type
代码块有同样的实现( 见上文 ),该函数获得最后一个内存区域:
struct memblock_region *rgn = &type->regions[idx];
然后调用memmove
函数移动该内存区域:
memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn));
紧接着填充新内存区域memblock_region
的base
域,size
域等等, 然后增大memblock_type
的大小。最后memblock_add_range
函数调用memblock_merge_regions
合并所有相邻且兼容的内存区域。
在第二种情况下,新内存区域可能和已存储的内存区域重叠。例如,在memblock
中已经有了region
:
0 0x1000 +-----------------------+ | | | | | region1 | | | | | +-----------------------+
现在我们想把region2
加到memblock
中,region2
含有以下基址和大小:
0x100 0x2000 +-----------------------+ | | | | | region2 | | | | | +-----------------------+
本例中,把新内存区域的基址设为重叠内存区域的结束地址:
base = min(rend, end);
即base
为0x1000
。和第二次循环做法一样,我们用以下代码把它添加到 memblock:
if (base < end) { nr_new++; if (insert) memblock_insert_region(type, i, base, end - base, nid, flags); }
本例中,我们先插入overlapping portion( 重叠部分 )
( 只插入地址更高的部分,因为低地址部分已经在重叠内存区域 ),然后插入剩余部分,最后调用memblock_merge_regions
合并这些部分内存区域。memblock_merge_regions
函数合并相邻且兼容的内存区域。该函数遍历所有memblock_type
类型的内存区域,每次取出两个邻近的内存区域 —— type->regions[i] 和type->regions[i + 1]
, 然后检查它们是否有相同的标志,属于相同的节点,第一个内存区域的结束地址不等于第二个内存区域的基地址:
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; }
如果这些条件一个都不满足,更新第一个内存区域的大小:
this->size += next->size;
因为我们把第二个内存区域大小加到第一个内存区域大小,所以我们要调用memmove
函数把当前(this
)内存区域后的每一个( 靠每次循环 )内存区域移动到其前一个内存区域:
memmove(next, next + 1, (type->cnt - (i + 2)) * sizeof(*next));
然后把memblock_type
类型的内存区域数量减一:
type->cnt--;
此后我们就会把两个内存区域合并为一个:
0 0x2000 +------------------------------------------------+ | | | | | region1 | | | | | +------------------------------------------------+
以上就是memblock_add_range
函数的全部工作原理。
还有memblock_reserve
函数,除了一处不同外其余均与memblock_add函数一致。memblock_reserve
函数把memblock_type.reserved
类型的内存区域存到 memblock 中,而不是memblck_type.memory
。
Memblock 不仅提供了添加memory
和reserved
类型的内存区域的 API,还包括:
- memblock_remove —— 从 memblock 中移除内存区域
- memblock_find_in_range —— 在给定的范围内找到未使用的内存
- memblock_free —— 释放 memblcok 中的内存区域
- for_each_mem_range —— 反复迭代 memblock
还有更多。。。
获取内存区域信息
Memblock 也提供了 API 来获取memblock
中已分配内存区域的信息,分为两个部分:
- get_allocated_memblock_memory_regions_info —— 获取内存区域信息
- get_allocated_memblock_reserved_regions_info —— 获取预留内存区域信息
这两个函数的实现很简单。以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
是否包含预留内存区域。如果memblock
不包含则返回 0,否则我们把预留内存区域的物理地址赋给addr
然后返回已分配的数组经对齐过后的大小。对齐用的是PAGE_ALIGN
宏,它依赖于页的大小:
#define PAGE_ALIGN(addr) ALIGN(addr, PAGE_SIZE)
函数get_allocated_memblock_memory_regions_info
的实现和上面一样,唯一不同的是用到了memblock_type.memory
而不是memblock_type.reserved
。
Memblock 调试
在 memblock 的实现中多次调用了memblock_dbg
函数。如果在内核命令行传入memblock=debug
选项,就会调用memblock_dbg
函数。其实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 %pFn", (unsigned long long)base, (unsigned long long)base + size - 1, flags, (void *)_RET_IP_);
结果如下图:
Memblock 还支持 debugfs。如果你的内核不是运行在X86架构上,你可以访问:
- /sys/kernel/debug/memblock/memory
- /sys/kernel/debug/memblock/reserved
- /sys/kernel/debug/memblock/physmem
来获得memblock
内容的转储。
总结
关于 linux 内核内存管理第一部分到此结束。如果有任何疑问或建议,在 twitter 0xAX 上联系我,或给我发 邮件,或 提交一个 issue。
超链接
网友评论已有0条评论, 我也要评论