内核在启动时通过E820机制获得到可⽤的内存地址范围后,还需要将这些内存都管理起来,以应对后⾯系统运⾏时 的各种功能的内存申请。内存分配器包括两种。刚启动时采⽤是初期分配器。这种内存分配器仅仅只为了满⾜系统 启动时间内对内存⻚的简单管理,管理粒度较粗。另外⼀种是在系统起来后正常运⾏时采⽤的复杂⼀些但能⾼效管 理4KB粒度⻚⾯的伙伴系统,是运⾏时的主要物理⻚内存分配器。
在初期分配器中,在Linux 的早期版本采⽤的是 bootmem。但在 2010 年之后,就慢慢替换成了 memblock 内存 分配器。关于这个替换参⻅这个历史 commit https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/ commit/mm/memblock.c?id=95f72d1ed41a66f1c1c29c24d479de81a0bea36f。本书中我们只介绍较新的 memblock分配器。
1.2 memblock内存分配器的创建
内核在通过E820机制检测到可⽤的内存地址范围后,调⽤e820__memory_setup来把检测结果保存到了把检测结 果保存到了e820_table全局数据结构中。紧接着下⼀步就是调⽤e820__memblock_setup创建memblock内存分配器。
file:// arch/x86/kernel/setup.c
void __init setup_arch(char **cmdline_p) {
...
// 保存物理内存检测结果
e820__memory_setup();
...
// membloc内存分配器初始化
e820__memblock_setup();
...
}
在看创建memblock之前我们先来看看这种内存分配器是⻓什么样⼦的。memblock的实现⾮常简单,就是按照检 测到的内存地址范围是 usable还是reserved分成两个对象,然后分别⽤memblock_region数组给存起来。
memblock分配器定义相关的源码位于mm/memblock.c⽂件下。
// file:include/linux/memblock.h
/**
* struct memblock_region - represents a memory region
* @base: base address of the region
* @size: size of the region
* @flags: memory region attributes
* @nid: NUMA node id
*/
struct memblock_region {
phys_addr_t base;
phys_addr_t size;
enum memblock_flags flags;
#ifdef CONFIG_NUMA
int nid;
#endif
};
// file:include/linux/memblock.h
/**
* struct memblock_type - collection of memory regions of certain type
* @cnt: number of regions
* @max: size of the allocated array
* @total_size: size of all regions
* @regions: array of regions
* @name: the memory type symbolic name
*/
struct memblock_type {
unsigned long cnt;
unsigned long max;
phys_addr_t total_size;
struct memblock_region *regions;
char *name;
};
// file:include/linux/memblock.h
/**
* struct memblock - memblock allocator metadata
* @bottom_up: is bottom up direction?
* @current_limit: physical address of the current allocation limit
* @memory: usable memory regions
* @reserved: reserved memory regions
*/
struct memblock {
bool bottom_up; /* is bottom up direction? */
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
};
// file:mm/memblock.c
struct memblock memblock __initdata_memblock = {
.memory.regions = memblock_memory_init_regions,
.memory.cnt = 1, /* empty dummy entry */
.memory.max = INIT_MEMBLOCK_MEMORY_REGIONS,
.memory.name = "memory",
.reserved.regions = memblock_reserved_init_regions,
.reserved.cnt = 1, /* empty dummy entry */
.reserved.max = INIT_MEMBLOCK_RESERVED_REGIONS,
.reserved.name = "reserved",
.bottom_up = false,
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
};
static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_MEMORY_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_RESERVED_REGIONS] __initdata_memblock;
#define INIT_MEMBLOCK_REGIONS 128
#define INIT_PHYSMEM_REGIONS 4
#ifndef INIT_MEMBLOCK_RESERVED_REGIONS
# define INIT_MEMBLOCK_RESERVED_REGIONS INIT_MEMBLOCK_REGIONS
#endif
#ifndef INIT_MEMBLOCK_MEMORY_REGIONS
#define INIT_MEMBLOCK_MEMORY_REGIONS INIT_MEMBLOCK_REGIONS
#endif
e820__memblock_setup会根据e820_table来对 memblock 内存分配器进⾏创建。
void __init e820__memblock_setup(void)
{
int i;
u64 end;
/*
* The bootstrap memblock region count maximum is 128 entries
* (INIT_MEMBLOCK_REGIONS), but EFI might pass us more E820 entries
* than that - so allow memblock resizing.
*
* This is safe, because this call happens pretty late during x86 setup,
* so we know about reserved memory regions already. (This is important
* so that memblock resizing does no stomp over reserved areas.)
*/
memblock_allow_resize();
for (i = 0; i < e820_table->nr_entries; i++) {
struct e820_entry *entry = &e820_table->entries[i];
end = entry->addr + entry->size;
if (end != (resource_size_t)end)
continue;
if (entry->type == E820_TYPE_SOFT_RESERVED)
memblock_reserve(entry->addr, entry->size);
if (entry->type != E820_TYPE_RAM && entry->type != E820_TYPE_RESERVED_KERN)
continue;
memblock_add(entry->addr, entry->size);
}
/* Throw away partial pages: */
memblock_trim_memory(PAGE_SIZE);
memblock_dump_all();
}
创建过程是遍历 e820 table 中的每⼀段内存区域。判断如果是预留内存就调⽤ memblock_reserve 添加到 reserved 成员中,也就是预留内存列表。添加过程是会修改 reserved 中的区域数量 cnt,然后在设置 regions 中 的⼀个元素。如果是可⽤内存就调⽤ memblock_add 添加到 memory 成员中,也就是可⽤内存列表,添加过程同 上。
在 memblock 创建完成后,紧接着还调⽤ memblock_dump_all() 进⾏了⼀次打印输出。这个输出信息对于我们观 察memblock的创建过程⾮常的有帮助。Linux内核会把启动时的各种⽇志信息记录下来,后⾯可以使⽤dmsg命令 来查看。不过memblock_dump_all输出的信息要需要修改Linux启动参数,添加memblock=debug并重启才可以。
我的修改⽅式是编辑/boot/grub/grub.cfg⽂件找到启动参数⾏,在最后⾯添加“memblock=debug”(不同的发⾏ 版可能修改⽅式会有⼀些出⼊)。
重启后通过查看 /proc/cmdline输出中是否包含了“memblock=debug”,来确认开启⽣效。
然后就可以通过dmseg可以看到Linux启动时的memblock内存分配器输出的⽇志信息了。