1.4 物理页管理之伙伴系统与本章总结
不过这次不同的是,每一段内存地址范围后面都跟上了node的信息,例如 on node 0、on node 1 等。
1.5 物理页管理之伙伴系统
前面小节我们讲到Linux内核在启动的时候创建了 memblock 内存分配器。但它只是 Linux 启动时运行的一个临时内存分配器,管理内存的颗粒度太大,并不适用于内核运行时小块内存的分配。操作系统在运行时经常需要管理更小颗粒度如4KB的内存,这就需要使用另外一套更复杂但更高效的物理页管理算法 —— 伙伴系统。
1.5.1 伙伴系统相关数据结构
在 NUMA 初始化的时候,Linux内核会从固件 ACPI 中读取 NUMA 信息,其中包括当前系统 node 的划分。在你的机器上,你可以使用 numactl 你可以看到每个 node 的情况。
内核会为每个 node 都申请一个管理对象。之后会为在每个 node 下创建各个 zone。
[ 0.010806] reserved[0x1] [0x0000000001000000-0x000000000340cfff],
0x000000000240d000 bytes on node 0 flags: 0x0
[ 0.010807] reserved[0x2] [0x0000000034f31000-0x000000003678ffff],
0x000000000185f000 bytes on node 0 flags: 0x0
[ 0.010808] reserved[0x3] [0x00000000bffe0000-0x00000000bffe3d7d],
0x0000000000003d7e bytes on node 0 flags: 0x0
[ 0.010809] reserved[0x4] [0x000000023fffb000-0x000000023fffffff],
0x0000000000005000 bytes flags: 0x0
[ 0.010810] reserved[0x5] [0x000000043fff9000-0x000000043fffdfff],
0x0000000000005000 bytes flags: 0x0
[ 0.010811] reserved[0x6] [0x000000043fffe000-0x000000043fffffff],
0x0000000000002000 bytes on node 1 flags: 0x0# numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 65419 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 65536 MBzone 表示内存中的一块范围,有不同的类型。
ZONE_DMA:地址段最低的一块内存区域,ISA (Industry Standard Architecture) 设备 DMA 访问。ZONE_DMA32:该 Zone 用于支持 32-bits 地址总线的 DMA 设备,只在 64-bits 系统里才有效。ZONE_NORMAL:在 X86-64 架构下,DMA 和 DMA32 之外的内存全部在 NORMAL 的 Zone 里管理。
历史上在 32 位机时代的时候还有个 ZONE_HIGHMEM,不过到了 64 位机后由于寻址空间大幅度增加,这个 zone 就被淘汰了。在每个 zone 下,都包含了许许多多个 Page(页面),在 linux 下一个 Page 的大小一般是 4 KB。
在你的机器上,你可以使用通过 zoneinfo 查看到你机器上 zone 的划分,也可以看到每个 zone 下所管理的页面有多少个。
理解了 node、zone 和页面的关系后,我们再来看它们在内核中定义的数据结构。在 Linux 操作系统中,所有的 node 信息是保存在 node_data 全局数组中的。node 在内核中的结构体名字叫 pglist_data。
# cat /proc/zoneinfo
Node 0, zone DMA
pages free 3973
managed 3973
Node 0, zone DMA32
pages free 390390
managed 427659
Node 0, zone Normal
pages free 15021616
managed 15990165
Node 1, zone Normal
pages free 16012823
managed 16514393 //file:arch/x86/mm/numa.c
struct pglist_data *node_data[MAX_NUMNODES]每一个 node 下会有多个 zone,所以在 pglist_data 结构体内部包含了一个 struct zone 类型的数组。数组大小是 __MAX_NR_ZONES,是 zone 枚举定义中的最大值。
在每个 zone 下面的一个数组 free_area 管理了绝大部分可用的空闲页面。这个数组就是伙伴系统实现的重要数据结构。
在这里其实大家可以看到,在内核中其实不是只有一个伙伴系统,而是在每个 zone 下都会有一个 struct free_area 定义的伙伴系统。
1.5.2 伙伴系统管理空闲页面
上节通过 cat /proc/zoneinfo 我们可以看到每个 zone 下面都有如此之多的页面,Linux 使用伙伴系统对这些页面进行高效的管理。伙伴系统中的 free_area 是一个 11 个元素的数组,在每一个数组分别代表的是空闲可分配连续 4 K、8 K、16 K、…、4M 内存链表。
//file:include/linux/mmzone.h
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
int node_id;
...
}//file:include/linux/mmzone.h
enum zone_type {
ZONE_DMA,
ZONE_DMA32,
ZONE_NORMAL,
ZONE_HIGHMEM,
ZONE_MOVABLE,
__MAX_NR_ZONES
};//file:include/linux/mmzone.h
struct zone {
......
// zone的名称
const char *name;
// 管理zone下面所有页面的伙伴系统
struct free_area free_area[MAX_ORDER];
......
}通过 cat /proc/pagetypeinfo , 你可以看到当前系统里伙伴系统里各个尺寸的可用连续内存块数量。
内核提供分配器函数 alloc_pages 到上面的多个链表中寻找可用连续页面。
//file: include/linux/mmzone.h
#define MAX_ORDER 11
struct zone {
free_area free_area[MAX_ORDER];
......
}当内核或者用户进程中需要物理页的时候,就可以调用 alloc_pages 来申请真正的物理内存了。alloc_pages 从 zone 的 free_area 空闲页面链表中寻找合适的内存块返回。
关于
alloc_pages的工作过程我们举个简单的小例子来帮助大家理解。假如要申请 8K-连续两个页框的内存。为了方便描述,我们先暂时忽略 UNMOVEABLE、RECLAIMABLE 等不同类型。
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)第一步,先到
free_area[1]中申请,因为这个链表正好管理的就是 8KB 的连续内存块。但不巧的是发现这个链表中是空的,表示无内存可用。那接着就会到更大的空闲内存链表free_area[2]中去查找。这时候发现终于有内存了。就会从链表中拿一个 16KB 的空闲内存块下来。由于我们要申请的只是 8KB 的内存,把这个 16KB 内存块全部返回太浪费了。所以这里会涉及到一次切割,把 16KB 切成两个 8KB 的内存块。其中一个返回给用户,另外一个放到专门管理 8KB 空闲页的free_area[1]中管理起来。所以在基于伙伴系统的内存分配中,有可能需要将大块内存拆分成两个小伙伴。在释放中,也可能会将两个小伙伴合并再次组成更大块的连续内存。通过这种方式灵活地应对各种不同大小内存块的申请。就这样,内核通过伙伴系统将物理页高效地管理了起来。并通过
alloc_page函数对内核其它模块提供物理页分配功能。
1.5.3 memblock 向伙伴系统交接物理内存
内核在启动时经过内存检测、memblock 内存分配器构建、页管理机制初始化等步骤后,创建了伙伴系统所需要使用的 pglist_data、zone 等对象。之后再在 mm_init -> mem_init -> memblock_free_all 中, memblock 开启了它给伙伴系统交接内存的交接仪式。
我们来看下内存的交接过程。
//file:mm/memblock.c
void __init memblock_free_all(void)
{
unsigned long pages;
......
pages = free_low_memory_core_early();
totalram_pages_add(pages);
}
//file:mm/memblock.c
static unsigned long __init free_low_memory_core_early(void)
{
// reserve 内存交接
memmap_init_reserved_pages();
// 可用内存交接
for_each_free_mem_range(i, NUMA_NO_NODE, MEMBLOCK_NONE, &start, &end,
NULL)
count += __free_memory_core(start, end);
...
}具体的释放是在 free_low_memory_core_early 中进行的。值得注意的是,memblock 是把 reserved 和可用内存是分开来交接的。这样保证 reserved 内存即使交接给了伙伴系统,伙伴系统也不会把它分配出去给用户程序使用。
其中可用内存部分是通过 for_each_free_mem_range 来遍历,然后调用 __free_memory_core 进行释放。接着依次调用 __free_pages_memory、memblock_free_pages、__free_pages_core、__free_pages_ok、__free_one_page 后将页面放到 zone 的 free_area 数组中对应的位置上去。
1.6 本章总结
start_kernel
-> setup_arch
---> e820__memory_setup // 内核把物理内存检测保存从boot_params.e820_table保存到e820_table中,并打印出来
---> e820__memblock_setup // 根据e820信息构建memblock内存分配器,开启调试能打印
---> x86_init.paging.pagetable_init(native_pagetable_init)
-----> paging_init // 页管理机制的初始化
->mm_init
--->mem_init
-----> memblock_free_all // 向伙伴系统移交控制权在本章中,我们介绍了内核是如何获得可用内存地址范围、内核初期 memblock 内存分配器的创建、内存 NUMA 信息的读取、以及物理页管理之伙伴系统。本章相关的核心函数如下。
在 Linux 操作系统刚启动的时候,操作系统通过 e820 读取到了内存的布局,并将它打印到了日志中。
[ 0.000000] BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000bffd9fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000bffda000-0x00000000bfffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000feff4000-0x00000000feffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000043fffffff] usable接着内核创建了 memblock 内存分配器来进行系统启动时的内存管理。如果开启了 memblock=debug 启动参数,同样能把它打印出来。
[ 0.010238] MEMBLOCK configuration:
[ 0.010239] memory size = 0x00000003fff78c00 reserved size = 0x0000000003c6d144
[ 0.010240] memory.cnt = 0x3
[ 0.010241] memory[0x0] [0x0000000000001000-0x000000000009efff], 0x000000000009e000
bytes flags: 0x0
[ 0.010243] memory[0x1] [0x0000000000100000-0x00000000bffd9fff], 0x00000000bfeda000
bytes flags: 0x0
[ 0.010244] memory[0x2] [0x0000000100000000-0x000000043fffffff], 0x000000034[ 0.010245] reserved.cnt = 0x4
[ 0.010246] reserved[0x0] [0x0000000000000000-0x0000000000000fff],
0x0000000000001000 bytes flags: 0x0
[ 0.010247] reserved[0x1] [0x00000000000f5a40-0x00000000000f5b83],
0x0000000000000144 bytes flags: 0x0
[ 0.010248] reserved[0x2] [0x0000000001000000-0x000000000340cfff],
0x000000000240d000 bytes flags: 0x0
[ 0.010249] reserved[0x3] [0x0000000034f31000-0x000000003678ffff],
0x000000000185f000 bytes flags: 0x0
不过到这里,Linux操作系统还不知道内存的NUMA信息.它接着通过ACPI接口读取固件中的SRAT表,将NUMA信息保存到 `numa_meminfo` 数组中.从此,Linux就知道了硬件上的NUMA信息,并对memblock内存分配器也设置了node信息.并再次将其打印了出来.这次memblock的每一个region中就都携带了node信息.
接着内核会根据NUMA信息创建node、zone等相关的对象.在每一个zone中都使用一个伙伴系统来管理所有的空闲物理页.之后再在 `mm_init -> mem_init -> memblock_free_all` 中,memblock开启了它给伙伴系统交接内存的交接仪式.
我们再回头来看本章开篇提到的几个问题.
1)内核是通过什么手段来识别可用内存硬件范围的?
在计算机的体系结构中,除了操作系统和硬件外,中间还存在着一层固件(firmware).起着在硬件和操作系统中间承上启下的作用.它对外提供接口规范是高级配置和电源接口(ACPI,Advanced Configuration and Power Interface).在ACPI规范中定义了探测可用内存范围的E820机制.感兴趣的同学可以参考下面这个最新的规范文档中的第15章.
规范地址:https://uefi.org/sites/default/files/resources/ACPI_Spec_6_5_Aug29.pdf.
操作系统在刚开始的时候,对内存的可用地址范围、NUMA分组信息都是一无所知.会在启动时的 `detect_memory_e820` 函数中调用ACPI规范中定义的接口,以获取到可用的物理内存地址范围.这个探测结果可以使用 `dmsg` 命令输出的日志来查看.
2)内核管理物理内存都使用了哪些技术手段?
```text
[ 0.010796] MEMBLOCK configuration:
[ 0.010797] memory size = 0x00000003fff78c00 reserved size = 0x0000000003d7bd7e
[ 0.010797] memory.cnt = 0x4
[ 0.010799] memory[0x0] [0x0000000000001000-0x000000000009efff], 0x000000000009e000
bytes on node 0 flags: 0x0
[ 0.010800] memory[0x1] [0x0000000000100000-0x00000000bffd9fff], 0x00000000bfeda000
bytes on node 0 flags: 0x0
[ 0.010801] memory[0x2] [0x0000000100000000-0x000000023fffffff], 0x0000000140000000
bytes on node 0 flags: 0x0
[ 0.010802] memory[0x3] [0x0000000240000000-0x[ 0.010803] reserved.cnt = 0x7
[ 0.010804] reserved[0x0] [0x0000000000000000-0x00000000000fffff],
0x0000000000100000 bytes on node 0 flags: 0x0
[ 0.010806] reserved[0x1] [0x0000000001000000-0x000000000340cfff],
0x000000000240d000 bytes on node 0 flags: 0x0
[ 0.010807] reserved[0x2] [0x0000000034f31000-0x000000003678ffff],
0x000000000185f000 bytes on node 0 flags: 0x0
[ 0.010808] reserved[0x3] [0x00000000bffe0000-0x00000000bffe3d7d],
0x0000000000003d7e bytes on node 0 flags: 0x0
[ 0.010809] reserved[0x4] [0x000000023fffb000-0x000000023fffffff],
0x0000000000005000 bytes flags: 0x0
[ 0.010810] reserved[0x5] [0x000000043fff9000-0x000000043fffdfff],
0x0000000000005000 bytes flags: 0x0
[ 0.010811] reserved[0x6] [0x000000043fffe000-0x000000043fffffff],
0x0000000000002000 bytes on node 1 flags: 0x0
> [!INFO] 本页(第25-30页)包含以下图片占位符:
>
> - Image 126 (Page 25)
> - Image 131 (Page 26)
> - Image 139 (Page 28)
> - Image 140 (Page 28)
> - Image 147 (Page 29)
> - Image 152 (Page 30)
>
> 原始教材中这些图片展示了伙伴系统的数据结构、分配与合并过程等示意图。由于此处无法直接显示图片,请参考原书对应页面。