一、内核启动过程中,内存管理的变化
可以把linux内核的内存管理分三个阶段:
阶段 | 起点 | 终点 | 描述 |
---|---|---|---|
第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 |
第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 |
第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 |
start_kernel是如何初始化系统的内存,只截取出其中与内存管理初始化相关的部分, 如下所示:
asmlinkage __visible void __init start_kernel(void)
{
/* 设置特定架构的信息
* 同时初始化memblock */
setup_arch(&command_line);
mm_init_cpumask(&init_mm);
setup_per_cpu_areas();
/* 初始化内存结点和内段区域 */
build_all_zonelists(NULL, NULL);
page_alloc_init();
/*
* These use large bootmem allocations and must precede
* mem_init();
* kmem_cache_init();
*/
mm_init();
kmem_cache_init_late();
kmemleak_init();
setup_per_cpu_pageset();
rest_init();
}
函数 | 功能 |
---|---|
setup_arch | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 |
mm_init_cpumask | 初始化CPU屏蔽字 |
setup_per_cpu_areas | 函数(查看定义)给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.在SMP系统中, setup_per_cpu_areas初始化源代码中(使用per_cpu宏)定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本. 此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本 在非SMP系统中这是一个空操作 |
build_all_zonelists | 建立并初始化结点和内存域的数据结构 |
mm_init | 建立了内核的内存分配器, 其中通过mem_init停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统) 然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 |
kmem_cache_init_late | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器slab, slob, slub都会定义此函数 |
kmemleak_init | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 /sys/kernel/debug/kmemleak中, Kmemcheck能够帮助定位大多数内存错误的上下文 |
setup_per_cpu_pageset | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. |
先 介绍 memblock;
二、获取内存大小
在arm linux下,设备相关属性的描述都是通过dts文件来完成的。linux在启动过程中,会解析这些dts文件,从而获取相关设备属性信息。 下面这段就是dts里很典型的一种描述内存信息的内容:
memory@200000000 {
device_type = "memory";
reg = <0x2 0x0 0x2 0x0>;
};
“memory"节点描述物理内存布局,“@”后面的设备地址用来区分名字相同的节点,如果节点有属性“reg”,那么设备地址必须是属性“reg”的第一个地址。如果有多块内存,可以使用多个“memory”节点来描述,也可以使用一个“memory”节点的属性“reg”的地址/长度列表来描述。
属性“reg”定义物理内存范围,值是一个地址/长度列表。这里前面两个数表示了起始地址,0x2是高32位,紧接着的0表示低32位,所以这里的起始地址就是0x200000000,同理后面的两个数字表示内存大小,大小就是0x200000000,说明内存大小总共是8G。
那么这个解析过程是怎样的呢?这个主要内核代码的early_init_dt_scan_memory函数中, 该函数的调用链为:
start_kernel()->setup_arch()->setup_machine_fdt()->early_init_dt_scan()->early_init_dt_scan_nodes()->early_init_dt_scan_memory()
看一下这个函数:
int __init early_init_dt_scan_memory(unsigned long node, const char *uname,
int depth, void *data)
{
const char *type = of_get_flat_dt_prop(node, "device_type", NULL);
const __be32 *reg, *endp;
int l;
bool hotpluggable;
if (type == NULL || strcmp(type, "memory") != 0)
return 0;
reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);
if (reg == NULL)
reg = of_get_flat_dt_prop(node, "reg", &l);
if (reg == NULL)
return 0;
endp = reg + (l / sizeof(__be32));
hotpluggable = of_get_flat_dt_prop(node, "hotpluggable", NULL);
pr_debug("memory scan node %s, reg size %d,\n", uname, l);
while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
u64 base, size;
base = dt_mem_next_cell(dt_root_addr_cells, ®);
size = dt_mem_next_cell(dt_root_size_cells, ®);
if (size == 0)
continue;
pr_debug(" - %llx , %llx\n", (unsigned long long)base,
(unsigned long long)size);
early_init_dt_add_memory_arch(base, size);
if (!hotpluggable)
continue;
if (early_init_dt_mark_hotplug_memory_arch(base, size))
pr_warn("failed to mark hotplug range 0x%llx - 0x%llx\n",
base, base + size);
}
return 0;
}
函数一开始就过滤掉type不是"memory"的属性,然后解析"memory"属性,获取到起始地址和大小,然后加入到memblock系统中。
memblock系统
可能大家都知道linux内核的内存管理采用的是伙伴系统管理方法,但是是初始化的过程中,也会涉及到内存的分配,此时内核提供了临时的引导内存分配器,在完成初始化之后,把空闲的物理页交给也分配器管理,再丢弃引导内存分配器。目前的内核使用的内核引导分配器就是memblock。所以上面会将memory加入到memblock中。
数据结构
memblock用到的主要数据结构如下:
struct memblock {
bool bottom_up;
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
};
总体结构看,memblock管理了两段区域:memblock.memory和memblock.reserved。所有物理上可用的内存区域都会被添加到memblock.memory。而被分配或者被系统占用的区域则会添加到memblock.reserved。 bottom_up表示分配内存的方式,值为真表示从低地址向上分配,否则从高地址向下分配。 current_limit表示可分配内存的最大物理地址。
memblock_type的结构如下:
enum memblock_flags {
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 */
};
struct memblock_region {
phys_addr_t base;
phys_addr_t size;
enum memblock_flags flags;
#ifdef CONFIG_NEED_MULTIPLE_NODES
int nid;
#endif
};
struct memblock_type {
unsigned long cnt;
unsigned long max;
phys_addr_t total_size;
struct memblock_region *regions;
char *name;
};
- regions指向内存块区域数组
- cnt是内存块区域的数量
- max是数组的元素个数
- total_size是所有内存区域的总长度
- name是内存块类型的名称。
初始化
setup_arch()->arm64_memblock_init();
arm64的memblock初始化位于arch/arm64/mm/init.c。初始化过程,首先如上节先将物理内存加入到memblock.memory中,然后在函数arm64_memblock_init中初始化memblock。
//start_kernel() ->setup_arch() ->arm64_memblock_init()
void __init arm64_memblock_init(void)
{
const s64 linear_region_size = BIT(vabits_actual - 1);
fdt_enforce_memory_region();
memblock_remove(1ULL << PHYS_MASK_SHIFT, ULLONG_MAX);
memstart_addr = round_down(memblock_start_of_DRAM(),
ARM64_MEMSTART_ALIGN);
memblock_remove(max_t(u64, memstart_addr + linear_region_size,
__pa_symbol(_end)), ULLONG_MAX);
if (memstart_addr + linear_region_size < memblock_end_of_DRAM()) {
/* ensure that memstart_addr remains sufficiently aligned */
memstart_addr = round_up(memblock_end_of_DRAM() - linear_region_size,
ARM64_MEMSTART_ALIGN);
memblock_remove(0, memstart_addr);
}
if (IS_ENABLED(CONFIG_ARM64_VA_BITS_52) && (vabits_actual != 52))
memstart_addr -= _PAGE_OFFSET(48) - _PAGE_OFFSET(52);
if (memory_limit != PHYS_ADDR_MAX) {
memblock_mem_limit_remove_map(memory_limit);
memblock_add(__pa_symbol(_text), (u64)(_end - _text));
}
if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) {
u64 base = phys_initrd_start & PAGE_MASK;
u64 size = PAGE_ALIGN(phys_initrd_start + phys_initrd_size) - base;
if (WARN(base < memblock_start_of_DRAM() ||
base + size > memblock_start_of_DRAM() +
linear_region_size,
"initrd not fully accessible via the linear mapping -- please check your bootloader ...\n")) {
phys_initrd_size = 0;
} else {
memblock_remove(base, size);
memblock_add(base, size);
memblock_reserve(base, size);
}
}
if (IS_ENABLED(CONFIG_RANDOMIZE_BASE)) {
extern u16 memstart_offset_seed;
u64 range = linear_region_size -
(memblock_end_of_DRAM() - memblock_start_of_DRAM());
if (memstart_offset_seed > 0 && range >= ARM64_MEMSTART_ALIGN) {
range /= ARM64_MEMSTART_ALIGN;
memstart_addr -= ARM64_MEMSTART_ALIGN *
((range * memstart_offset_seed) >> 16);
}
}
memblock_reserve(__pa_symbol(_text), _end - _text);
if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) {
/* the generic initrd code expects virtual addresses */
initrd_start = __phys_to_virt(phys_initrd_start);
initrd_end = initrd_start + phys_initrd_size;
}
early_init_fdt_scan_reserved_mem();
if (IS_ENABLED(CONFIG_ZONE_DMA)) {
zone_dma_bits = ARM64_ZONE_DMA_BITS;
arm64_dma_phys_limit = max_zone_phys(ARM64_ZONE_DMA_BITS);
}
if (IS_ENABLED(CONFIG_ZONE_DMA32))
arm64_dma32_phys_limit = max_zone_phys(32);
else
arm64_dma32_phys_limit = PHYS_MASK + 1;
reserve_crashkernel();
reserve_elfcorehdr();
high_memory = __va(memblock_end_of_DRAM() - 1) + 1;
dma_contiguous_reserve(arm64_dma32_phys_limit);
}
函数主要做了以下事情:
(1)全局变量memstart_addr记录内存的起始物理地址;
(2)把线性映射区域不能覆盖的物理内存范围从memblock.memory中删除;
(3)dts文件中节点“/chosen”的属性“bootargs”指定的命令行中,可以使用参数“mem”指定可用内存的大小。如果指定了内存的大小,那么把超过可用长度的物理内存范围从memblock.memory中删除;
(4)把内核镜像占用的物理内存范围添加到memblock.reserved中;
(5)从dts文件中的内存保留区域(memory reserve map,对应设备树源文件的字段“/memreserve/”)和节点“/reserved-memory”读取保留的物理内存范围,添加到memblock.reserved中。
API接口
memblock_add
int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)
{
phys_addr_t end = base + size - 1;
return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);
}
static int __init_memblock memblock_add_range(struct memblock_type *type,
phys_addr_t base, phys_addr_t size,
int nid, enum memblock_flags flags)
{
bool insert = false;
phys_addr_t obase = base;
phys_addr_t end = base + memblock_cap_size(base, &size);
int idx, nr_new;
struct memblock_region *rgn;
repeat:
base = obase;
nr_new = 0;
for_each_memblock_type(idx, type, rgn) {
phys_addr_t rbase = rgn->base;
phys_addr_t rend = rbase + rgn->size;
if (rbase > base) {
nr_new++;
if (insert)
memblock_insert_region(type, idx++, base,
rbase - base, nid,
flags);
}
base = min(rend, end);
}
if (base < end) {
nr_new++;
if (insert)
memblock_insert_region(type, idx, base, end - base,
nid, flags);
}
if (!insert) {
while (type->cnt + nr_new > type->max)
if (memblock_double_array(type, obase, size) < 0)
return -ENOMEM;
insert = true;
goto repeat;
} else {
memblock_merge_regions(type);
return 0;
}
}
注意add的过程是按照区间从小到大的顺序加入的,如果有重叠的情况,会进行合并。
memblock_remove
删除内存块区域
int __init_memblock memblock_remove(phys_addr_t base, phys_addr_t size)
{
return memblock_remove_range(&memblock.memory, base, size);
}
static int __init_memblock memblock_remove_range(struct memblock_type *type,
phys_addr_t base, phys_addr_t size)
{
int start_rgn, end_rgn;
int i, ret;
ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn);
if (ret)
return ret;
for (i = end_rgn - 1; i >= start_rgn; i--)
memblock_remove_region(type, i);
return 0;
}
static void __init_memblock memblock_remove_region(struct memblock_type *type, unsigned long r)
{
type->total_size -= type->regions[r].size;
memmove(&type->regions[r], &type->regions[r + 1],
(type->cnt - (r + 1)) * sizeof(type->regions[r]));
type->cnt--;
if (type->cnt == 0) {
WARN_ON(type->total_size != 0);
type->cnt = 1;
type->regions[0].base = 0;
type->regions[0].size = 0;
type->regions[0].flags = 0;
memblock_set_region_node(&type->regions[0], MAX_NUMNODES);
}
}
memblock_alloc
分配主要通过函数memblock_alloc_range_nid完成
phys_addr_t __init memblock_alloc_range_nid(phys_addr_t size,
phys_addr_t align, phys_addr_t start,
phys_addr_t end, int nid,
bool exact_nid)
{
enum memblock_flags flags = choose_memblock_flags();
phys_addr_t found;
found = memblock_find_in_range_node(size, align, start, end, nid,
flags);
if (nid != NUMA_NO_NODE && !exact_nid) {
found = memblock_find_in_range_node(size, align, start,
end, NUMA_NO_NODE,
flags);
if (found && !memblock_reserve(found, size))
goto done;
}
}
static phys_addr_t __init_memblock memblock_find_in_range_node(phys_addr_t size,
phys_addr_t align, phys_addr_t start,
phys_addr_t end, int nid,
enum memblock_flags flags)
{
phys_addr_t kernel_end, ret;
start = max_t(phys_addr_t, start, PAGE_SIZE);
end = max(start, end);
kernel_end = __pa_symbol(_end);
if (memblock_bottom_up() && end > kernel_end) {
phys_addr_t bottom_up_start;
bottom_up_start = max(start, kernel_end);
ret = __memblock_find_range_bottom_up(bottom_up_start, end,
size, align, nid, flags);
}
return __memblock_find_range_top_down(start, end, size, align, nid,
flags);
}
static phys_addr_t __init_memblock
__memblock_find_range_bottom_up(phys_addr_t start, phys_addr_t end,
phys_addr_t size, phys_addr_t align, int nid,
enum memblock_flags flags)
{
phys_addr_t this_start, this_end, cand;
u64 i;
for_each_free_mem_range(i, nid, flags, &this_start, &this_end, NULL) {
this_start = clamp(this_start, start, end);
this_end = clamp(this_end, start, end);
cand = round_up(this_start, align);
if (cand < this_end && this_end - cand >= size)
return cand;
}
return 0;
}
函数从高到底遍历memblock.memory的内存块区域数组,针对这个内存区域M1,从高到低遍历memblock.reserved的内存块区域数组,针 对每个内存块区域M2,目标区域是内存块区域M2和前一个内存块区 域之间的区域,如果目标区域属于内存块区域M1,并且长度大于或等 于请求分配的长度,那么可以从目标区域分配内存。 然后,调用函数memblock_reserve,把分配出去的内存块区域添加到memblock.reserved中。
在将内存加入memblock系统后,会首先建立页表,然后再初始化zone。映射这部分在之前页表建立的文章中已经讲过,这里不再涉及。 在分析zone初始化之前,需要先了解下linux物理内存的组织结构。这部分先留待下一篇。
三、memblock的初始化(arm64架构)
从start_kernel
开始, 进入setup_arch()
, 并完成了早期内存分配器的初始化和设置工作.
void __init setup_arch(char **cmdline_p)
{
/* 初始化memblock */
arm64_memblock_init( );
/* 分页机制初始化 */
paging_init();
bootmem_init();
}
流程 | 描述 |
---|---|
arm64_memblock_init | 初始化memblock内存分配器 |
paging_init | 初始化分页机制 |
bootmem_init | 初始化内存管理 |
其中arm64_memblock_init
就完成了arm64架构下的memblock的初始化
arm64_memblock_init(),上面已经分析过了。
paging_init():
第一步是为 swapper_pg_dir 建立映射表,
map_kernel(pgdp) ,内核细粒度映射,对各个内核段进行映射;map_mem(pgdp) 线性映射;
map_mem(pgdp)-> _map_memblock(),会对memblock 建立页表。
paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响。
具体可以参考内存管理(1)。