提要:本系列文章主要参考MIT 6.828课程
以及两本书籍《深入理解Linux内核》
《深入Linux内核架构》
对Linux内核内容进行总结。
内存管理的实现覆盖了多个领域:
- 内存中的物理内存页的管理
- 分配大块内存的伙伴系统
- 分配较小内存的slab、slub、slob分配器
- 分配非连续内存块的vmalloc分配器
- 进程的地址空间
上一节介绍了内存管理相关的主要数据结构以及它们之间的关系,本节主要介绍这些数据结构的初始化,方便在介绍内存分配时读者思路更加清晰(这部分内容主要参考《深入Linux内核架构》
)。
函数start_kernel()负责完成Linux内核的初始化工作,内存管理相关数据结构的初始化也是从这里进行的。下图给出 start_kernel的代码流程图,其中只包括内存管理相关的系统初始化函数。
各个函数的作用简述如下:
函数名 | 描述 |
---|---|
setup_arch | 特定于体系结构的设置函数,由于内存管理实际上管理的是真正物理内存的应用,务必会与不同体系结构有联系,在本方法中还会初始化自举分配器 |
setup_per_cpu_areas | 在SMP系统上,初始化源代码中定义的per-cpu变量(这种变量每个CPU有一个副本,因此叫per-cpu),在非SMP系统上该函数是一个空操作 |
build_all_zonelists | 建立结点和内存域的数据结构(重要) |
mem_init | 用于停用bootmem分配器(自举分配器)并迁移到实际的内存管理函数 |
kmem_cache_init | 初始化内核内部用于小块内存区的分配器 |
setup_per_cpu_pageset | 为struct zone中的pageset数组的第一个元素分配内存,负责冷热分配器 相关的设置 |
为了简化记忆,笔者做了如下的总结:
- 由于内存管理是对物理内存的管理,因此必须了解整体体系结构相关信息,因此先需要通过BIOS等提供的API获取这些信息并进行配置。
- 在内存管理初期,需要对内存管理本身使用的空间进行分配,因此需要初始化一个
自举分配器
完成这项工作 - 通过已经存在的
自举分配器
就可以创建3层数据结构,即下图结构:
- 内存管理的基本数据结构已经初始化,剩下的内存管理功能可以交给伙伴系统了,就需要将
自举分配器
占用的内存释放掉或者交由伙伴系统管理 - 为slab分配器进行一些简单的配置
那我们依序开始。
setup_arch
由于内存管理管理的是真实的物理内存,因此,需要和体系结构强相关,setup_arch函数主要负责这一部分内容。下图给出了setup_arch中与内存管理相关的代码流程图:
各个函数职责如下:
方法名 | 职责 |
---|---|
machine_specific_memory_setup | 正如上一节中介绍的,为了获取物理内存中的保留内存地址(没有被使用的,即未来管理的真实内存),BIOS提供了一个一组物理地址范围和其对应的内存类型的表 ,生成该表就是在这个方法 |
parse_cmdline_early | 内核通过该方法分析命令行,进而获取mem=XXX[KkmM]、highmem=XXX[kKmM]或memmap=XXX[KkmM]" "@XXX[KkmM]这类参数。 |
setup_memory | 该函数主要负责如下3件事:1. 确定(每个结点)可用的物理内存页的数目。2. 初始化bootmem分配器(这部分会单独介绍)。3. 接下来分配各种内存区,例如,运行第一个用户空间过程所需的最初的RAM磁盘 |
paging_init | 初始化内核页表并启用内存分页 |
zone_sizes_init | 初始化系统中所有结点的pgdat_t实例,首先使用add_active_range,对可用的物理内存建立一个相对简单的列表。体系结构无关的函数free_area_init_nodes接下来使用该信息建立完备的内核数据结构。 |
本部分主要介绍两个内容:
- 因为Linux极大程度使用页式管理,为了保证完成性,这里简单介绍paging_init函数(可以跳过)
- 为了加速访问页,Linux实现了一个冷热分配器(hot-n-cold allocator),这里介绍冷热缓存的初始化(引出
free_area_init_nodes
,这个函数很重要)
paging_init
paging_init负责按照指定方式(通常是3:1)划分虚拟地址空间,代码流程图如下:
- pagetable_init首先初始化系统的页表,以swapper_pg_dir为基础(该变量此前用于保存临时数据)。接下来启用在所有现代IA-32系统上可用的两个扩展:
- 对超大内存页的支持。这些特别标记的页,其长度为4 MiB,而不是普通的4 KiB。该选项用于不会换出的内核页
- 如有可能,内核页会设置另一个属性(_PAGE_GLOBAL)。在上下文切换期间,设置了_PAGE_GLOBAL位的页,对应的TLB缓存项不从TLB刷出。由于内核总是出现于虚拟地址空间中同样的位置,这提高了系统性能
- 借助于kernel_physical_mapping_init,将物理内存页(或前896 MiB,正如上一节的讨论)映射到虚拟地址空间中从PAGE_OFFSET开始的位置。
- 接下来建立固定映射项和持久内核映射对应的内存区。同样是用适当的值填充页表。
- 在用pagetable_init完成页表初始化之后,则将cr3寄存器设置为指向全局页目录(swapper_pg_dir)的指针。此时必须激活新的页表
- 由于TLB缓存项仍然包含了启动时分配的一些内存地址数据,此时也必须刷出。__flush_all_tlb可完成所需的工作。与上下文切换期间相反,设置了_PAGE_GLOBAL位的页也要刷出。
- kmap_init初始化全局变量kmap_pte。在从高端内存域将页映射到内核地址空间时,会使用该变量存入相应内存区的页表项。此外,用于高端内存内核映射的第一个固定映射内存区的地址保存在全局变量kmem_vstart中。
冷热缓存的初始化
struct zone的pageset成员用于实现冷热分配器(hot-n-cold allocator)。
- 页是热的,意味着页已经加载到CPU高速缓存,与在内存中的页相比,其数据能够更快地访问。
- 页是冷的,则不在高速缓存中。
struct zone {
...
struct per_cpu_pageset pageset[NR_CPUS];
...
};
在多处理器系统上每个CPU都有一个或多个高速缓存,各个CPU的管理必须是独立的(从struct 名字也可以看出来per_cpu
_pageset)。这里的NR_CPUS是一个可以在编译时间配置的宏常,表示内核支持的CPU的最大数目。struct per_cpu_pageset
代码如下:
struct per_cpu_pageset {
struct per_cpu_pages pcp[2]; /* 索引0对应热页,索引1对应冷页 */
} ____cacheline_aligned_in_smp;
由注释可以看到,对于每个cpu都包含了一个struct per_cpu_pages数组
,长度为2,pcp[0]存储热页,pcp[1]