[TOC]

Linux 内存模型之基石:FLATMEM 深度解析

FLATMEM 是 Linux 内核中最基础、最常用,也是最高效的内存模型。正如其名,“平坦内存”模型假定系统的物理内存是一个单一、连续、不存在大间隙的地址空间。


一、 核心原理:mem_map 数组

FLATMEM 模型的全部精髓都围绕着一个核心数据结构:一个巨大的、全局的 struct page 数组,通常被称为 mem_map

  1. 什么是 struct page

    • 在 Linux 内核中,物理内存不是按字节管理的,而是按固定大小的块来管理的,这个块被称为页帧 (Page Frame)(通常是 4KB)。
    • 内核为每一个物理页帧都分配了一个 struct page 描述符。这个结构体包含了该物理页帧的所有元数据,例如:它的引用计数、它是否是脏页、是否被锁定、属于哪个地址空间映射等。
  2. mem_map 的作用

    • FLATMEM 模型在内核启动的极早期,会分配一个足够大的连续内存区域,用来存放一个 struct page 数组。这个数组的大小足以覆盖从物理地址 0(或某个起始偏移)到系统最大物理内存地址之间的所有页帧。
    • 这个 mem_map 数组就成为了一个全局的物理内存数据库
  3. 核心转换机制
    FLATMEM 的高效正源于其极其简单的地址转换逻辑。它在物理地址 (Physical Address)页帧号 (Page Frame Number, PFN)struct page 描述符之间建立了直接、快速的算术映射关系:

    • 物理地址 -> PFN:
      pfn = physical_address >> PAGE_SHIFT (即 physical_address / PAGE_SIZE)
      这是一个简单的位移操作。

    • PFN -> struct page*:
      struct page* page = mem_map + (pfn - ARCH_PFN_OFFSET)
      这是一个简单的数组索引操作!ARCH_PFN_OFFSET 是一个架构相关的偏移量,代表 mem_map 所管理的第一个页帧的 PFN。

    • struct page* -> PFN:
      pfn = (page - mem_map) + ARCH_PFN_OFFSET
      这同样是一个简单的指针减法操作。

总结一下:在 FLATMEM 模型中,给你任何一个物理地址,内核都可以通过几次算术运算,以 O(1) 的时间复杂度 快速定位到管理这个物理地址的 struct page 描述符。反之亦然。这种简单、直接的映射是其性能的保证。


二、 优点与适用场景

  1. 极高的效率: 所有的地址/PFN/page 转换都是简单的算术运算,几乎没有性能开销。这对于性能敏感的内存管理子系统至关重要。
  2. 逻辑简单: 实现非常直观,代码易于理解和维护。
  3. 广泛的适用性: 绝大多数消费级和服务器级计算机(非 NUMA 架构)、以及大量的嵌入式系统,其物理内存布局都是或近似是连续的。因此,FLATMEM 是这些系统上最理想、最常见的选择。

三、 缺点与局限性

FLATMEM 的简单性也带来了其最大的缺点:缺乏灵活性和潜在的内存浪费

  1. 巨大的内存开销: mem_map 数组必须覆盖从内存起点到最大物理地址之间的整个地址空间范围,即使中间的某些地址范围没有插内存条。

    • 举例: 假设一个 64 位系统,最大支持 64GB 物理内存。每个 struct page 描述符大约需要 64 字节。那么 mem_map 数组本身就需要占用:
      (64 * 1024^3 / 4096) * 64 字节 ≈ 512 MB
      这 512MB 的内存必须在启动时被预留出来,用于存放 mem_map 数组本身。如果系统实际只插了 16GB 内存,mem_map 仍然会按 64GB 的最大可能来分配(取决于具体的架构实现),造成浪费。
  2. 无法处理大的物理内存间隙 (Holes):

    • 如果一个系统的物理内存布局是 [0-2GB][8GB-10GB],中间有 6GB 的巨大空洞。FLATMEM 模型为了覆盖到 10GB,其 mem_map 数组也必须覆盖整个 [0-10GB] 的范围。那么对应于 [2GB-8GB] 这个空洞的 struct page 条目就完全被浪费了。
  3. 不兼容高级内存架构:

    • NUMA (Non-Uniform Memory Access): 在 NUMA 架构中,内存分布在不同的“节点 (Node)”上,访问不同节点的内存延迟不同。FLATMEM 的单一连续模型无法描述这种节点化的、非均匀的内存布局。
    • 内存热插拔 (Memory Hotplug): FLATMEM 在启动时就固定了 mem_map 的大小,无法动态地扩展以支持新插入的内存。

四、 FLATMEM 在内存模型演进中的位置

为了克服 FLATMEM 的局限性,Linux 内核引入了更复杂的内存模型:

  1. DISCONTIGMEM (非连续内存模型):

    • 这是早期的解决方案,主要为了支持 NUMA。它为每个不连续的内存区域(或 NUMA 节点)都维护一个独立的 mem_map 数组。
    • 它的缺点是代码逻辑复杂,需要在不同节点之间进行转换,且已基本被淘汰
  2. SPARSEMEM (稀疏内存模型):

    • 这是当前最先进、最灵活的模型,也是 DISCONTIGMEM 的现代替代品。
    • 它的核心思想是:不再一次性分配一个巨大的 mem_map 数组。而是将整个物理地址空间划分为固定大小的块(section,例如 128MB)。只有当一个 section 中确实存在物理内存时,内核才会为这个 section 分配一个 mem_map 数组。
    • SPARSEMEM 完美地解决了内存空洞和内存热插拔的问题,同时也自然地支持 NUMA 架构。
    • 它唯一的缺点是,PFN 到 struct page 的转换不再是简单的数组索引,而需要先计算 PFN 属于哪个 section,再在该 sectionmem_map 中索引,多了一步间接查找,理论上比 FLATMEM 稍慢。
    • SPARSEMEM_VMEMMAP: 这是对 SPARSEMEM 的一个重要优化。它通过虚拟内存技术,将所有零散的 mem_map 小数组映射到一个虚拟上连续的巨大地址空间中。这样一来,PFN 到 struct page 的转换又变回了简单的算术运算,使得 SPARSEMEM 几乎拥有了和 FLATMEM 一样的高效率,同时保留了其灵活性。

五、 总结

FLATMEM 是 Linux 内存管理的基础。它基于“物理内存是连续的”这一简单假设,通过一个全局的 mem_map 数组和高效的算术转换,提供了最快速、最直接的物理内存管理机制。

尽管它的刚性设计无法适应 NUMA 和内存热插拔等高级场景,但这并不妨碍它在绝大多数计算机和嵌入式设备上作为默认和最佳选择。理解 FLATMEM 的工作原理,是理解 Linux 为何以及如何演进出 SPARSEMEM 等更复杂内存模型的关键。

include/linux/mm_types.h

MM_MT_FLAGS 配置内存管理结构的多线程行为

1
2
3
/* 组合多个标志,支持范围分配、外部锁保护和 RCU 同步机制 */
#define MM_MT_FLAGS (MT_FLAGS_ALLOC_RANGE | MT_FLAGS_LOCK_EXTERN | \
MT_FLAGS_USE_RCU)

include/linux/mm.h

mm_zero_struct_page 设置结构体页面为零

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
* 在某些架构上,为小尺寸调用 memset() 是昂贵的。
* 如果架构决定实现自己的 mm_zero_struct_page 版本,他们应该将下面的定义包装在 #ifndef 中,并在 <asm/pgtable.h 中定义他们自己的宏版本>
*/
#if BITS_PER_LONG == 64
/* 当 struct page 的大小增长到 96 以上或减小到 56 以下时,必须更新此函数。
* 编译器优化 switch() 语句,只留下 move/store 指令的想法。
* 此外,如果 write 语句都是赋值并且可以重新排序,则编译器可以组合 write 语句,这可能会导致此处的多个写入被丢弃。
*/
#define mm_zero_struct_page(pp) __mm_zero_struct_page(pp)
static inline void __mm_zero_struct_page(struct page *page)
{
unsigned long *_pp = (void *)page;

/* Check that struct page is either 56, 64, 72, 80, 88 or 96 bytes */
BUILD_BUG_ON(sizeof(struct page) & 7);
BUILD_BUG_ON(sizeof(struct page) < 56);
BUILD_BUG_ON(sizeof(struct page) > 96);

switch (sizeof(struct page)) {
case 96:
_pp[11] = 0;
fallthrough;
case 88:
_pp[10] = 0;
fallthrough;
case 80:
_pp[9] = 0;
fallthrough;
case 72:
_pp[8] = 0;
fallthrough;
case 64:
_pp[7] = 0;
fallthrough;
case 56:
_pp[6] = 0;
_pp[5] = 0;
_pp[4] = 0;
_pp[3] = 0;
_pp[2] = 0;
_pp[1] = 0;
_pp[0] = 0;
}
}
#else
#define mm_zero_struct_page(pp) ((void)memset((pp), 0, sizeof(struct page)))
#endif

1. 减少函数调用开销

  • memset 是一个通用的库函数,通常需要处理多种情况(如不同的内存大小、对齐方式等),因此它的实现包含了额外的逻辑和分支判断。
  • __mm_zero_struct_page 是针对特定结构体大小(如 struct page)优化的内联函数,直接展开为一系列赋值操作,避免了函数调用的开销。

2. 编译器优化

  • __mm_zero_struct_page 中,使用了 switch 语句和 fallthrough 关键字,编译器可以根据 struct page 的大小生成最优的指令序列。
  • 编译器可能会将连续的赋值操作合并为更高效的指令(如 SIMD 指令或块存储指令),从而进一步提升性能。

3. 避免通用性开销

  • memset 是为任意大小的内存块设计的,因此需要处理对齐、边界条件等问题。
  • __mm_zero_struct_page 针对特定大小的 struct page 进行了优化,避免了这些额外的处理。

4. 减少分支预测失败

  • memset 的实现可能包含多个分支(如处理小块内存和大块内存的不同路径),这些分支可能导致分支预测失败,影响性能。
  • __mm_zero_struct_page 的实现是固定的赋值操作,没有额外的分支,减少了分支预测失败的可能性。

5. 内存对齐的优势

  • __mm_zero_struct_page 中,struct page 被转换为 unsigned long 类型的指针,并按 64 位(或更大)对齐的块进行操作。这种对齐方式通常能更好地利用 CPU 的缓存和内存带宽。
  • memset 需要处理未对齐的情况,可能会引入额外的开销。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static inline void set_page_zone(struct page *page, enum zone_type zone)
{
page->flags &= ~(ZONES_MASK << ZONES_PGSHIFT);
page->flags |= (zone & ZONES_MASK) << ZONES_PGSHIFT;
}

static inline void set_page_node(struct page *page, unsigned long node)
{
page->flags &= ~(NODES_MASK << NODES_PGSHIFT);
page->flags |= (node & NODES_MASK) << NODES_PGSHIFT;
}

static inline void set_page_links(struct page *page, enum zone_type zone,
unsigned long node, unsigned long pfn)
{
set_page_zone(page, zone);
set_page_node(page, node); //没有page node
#ifdef SECTION_IN_PAGE_FLAGS
set_page_section(page, pfn_to_section_nr(pfn));
#endif
}

mm/init.c: Linux内存管理子系统的引导者与初始化核心

介绍

mm/init.c 是 Linux 内核中负责初始化整个内存管理(MM)子系统的关键文件。它在内核启动过程中扮演着“点火器”和“交接官”的角色。它在系统启动的极早期被调用,其核心任务是建立起所有高级内存管理机制(如伙伴系统、Slab 分配器、vmalloc 等),并从临时的引导内存分配器(memblock)手中接管对所有物理内存的控制权。


一、 核心职责

mm/init.c 的工作是一次性的,但在内核的生命周期中至关重要。它的职责可以分解为以下几个关键步骤:

  1. 从引导内存分配器 (memblock) 过渡: 在 mm_init 运行之前,内核使用一个非常简单、临时的 memblock 分配器来管理内存。mm_init.c 的首要任务是初始化真正的、高效的内存管理数据结构,然后将 memblock 管理的所有可用内存“释放”到新的管理体系中。

  2. 初始化页区分配器 (Zone Allocator / Buddy System): 这是最核心的一步。它负责为系统的物理内存划分不同的页区 (Zones),并为每个页区建立伙伴系统(Buddy System)所需的数据结构(如 free_area 列表)。在此之后,内核才拥有了以页(PAGE_SIZE)为单位进行高效分配和释放物理内存的能力。

  3. 初始化 Slab/SLUB/SLOB 分配器: 伙伴系统只能分配整个页。为了高效地管理小于页的小块内存(供 kmalloc 使用),内核需要 Slab 分配器。mm/init.c 负责调用 Slab 分配器的初始化函数,而 Slab 分配器自身依赖于已经准备就绪的伙伴系统来获取大块内存。

  4. 初始化 vmalloc 区域: vmalloc 用于分配虚拟地址连续但物理地址不一定连续的大块内存。mm/init.c 负责为其建立所需的数据结构和地址空间范围。

  5. 释放初始化内存: 内核中大量的初始化代码和数据在系统启动完成后就不再需要了。mm/init.c 包含了回收这部分内存的逻辑,将其交还给伙伴系统,从而为系统增加数兆字节的可用 RAM。


二、 核心函数深度解析

1. mm_init() - 总指挥

这是 init/main.c 中的 start_kernel() 函数调用的顶层入口。它像一个总指挥,按严格的顺序协调调用其他初始化函数。

典型的 mm_init() 执行流程:

  1. mem_init(): 调用此函数来初始化伙伴系统。这是所有后续内存分配的基础。
  2. kmem_cache_init(): 初始化 Slab/SLOB/SLUB 分配器。从此 kmalloc 才真正可用。
  3. vmalloc_init(): 初始化 vmalloc 机制。
  4. 其他初始化:可能还包括页表相关的初始化等。

这个函数标志着 Linux 从一个只能使用临时分配器的简单程序,转变为一个拥有复杂、动态内存管理能力的成熟操作系统内核。

2. mem_init() - 伙伴系统的奠基者

这是 mm_init() 调用的第一个关键函数,也是 mm/init.c 中技术含量最高的部分之一。

它的核心任务: 将 memblock 报告的所有可用物理内存,逐一“注册”到伙伴系统的管理数据结构中。

执行步骤:

  1. 清空 mem_map: 将 FLATMEMSPARSEMEM 模型提供的 struct page 数组(mem_map)中与保留页(内核代码、已分配内存等)无关的条目清零。
  2. 计算页区边界: 内核将物理内存划分为不同的区域,称为“页区 (Zone)”,以应对不同的硬件限制:
    • ZONE_DMA/DMA32: 用于只能对低地址内存进行 DMA 的老旧设备。
    • ZONE_NORMAL: 内核可以直接映射和访问的“常规”内存。
    • ZONE_HIGHMEM: (仅在 32 位系统上)超出内核直接映射范围的高端物理内存。
    • ZONE_MOVABLE: 用于可移动页,以减少内存碎片。
      mem_init() 会根据架构和内存大小计算出这些 Zone 的 PFN(页帧号)边界。
  3. 初始化 free_area: 调用 free_area_init_nodes() 或类似函数,为每个 NUMA 节点(在 UMA 系统上只有一个节点)的每个 Zone 初始化其 free_area 数组。这个数组是伙伴系统的核心,包含了指向不同大小(order)的空闲内存块链表的指针。
  4. 释放内存到伙伴系统: 遍历 memblock.memory 列表(即所有可用物理内存段),对每一段内存,调用 memblock_free() 或直接调用底层函数,将其以页为单位添加到对应 Zone 的 free_area 中。这个过程就是伙伴系统“获得”其初始可用内存的过程。

最终结果: mem_init() 执行完毕后,alloc_pages()__get_free_pages() 等基于伙伴系统的物理页分配函数就完全可用了。

3. free_initmem() - “过河拆桥”的艺术

这是在内核启动后期(在 smp_init() 之后)被调用的一个函数,它体现了内核对内存资源利用的极致追求。

背景:

  • 内核代码和数据中,有很大一部分被 __init__initdata 宏标记。
  • __init: 标记一个函数是初始化函数。
  • __initdata: 标记一个变量是初始化数据。
  • 链接器会将所有被这些宏标记的代码和数据,都集中放置在一个特殊的、连续的内存区域(.init 段)。

free_initmem() 的工作:

  1. 内核启动过程已经完成,所有标记为 __init 的函数都已被执行,所有标记为 __initdata 的数据都已被使用。它们在内核的后续运行中永远不会再被访问
  2. free_initmem() 计算出这个 .init 段的起始和结束物理地址。
  3. 它像对待普通可用内存一样,将这整块内存(通常有好几 MB)释放回伙伴系统

最终结果: 内核通过“丢弃”掉自己的初始化代码,回收了宝贵的内存资源,供系统运行时使用。


三、 在内核启动流程中的位置

mm/init.c 的代码位于一个承上启下的关键节点:

  • 之前: 架构相关的汇编代码 (head.S) 已经完成了最基础的设置(如启用分页、建立临时页表)。memblock 分配器已经通过解析 Bootloader 传递的参数(如设备树、E820 表),建立了一个物理内存的初步布局图。
  • 调用: init/main.c 中的 start_kernel() 函数,在完成了 Trap 初始化、调度器早期初始化等步骤后,调用 mm_init()
  • 之后: 完整的内存管理子系统上线。内核可以动态地分配和释放各种大小的内存,为设备驱动的加载、进程的创建等所有后续操作铺平了道路。

四、 总结

mm/init.c 是 Linux 内核从一个静态加载的程序转变为一个全功能动态操作系统的转换开关。它不是一个驱动,也不是一个持续运行的子系统,而是一个一次性的、至关重要的初始化模块

它通过精心设计的顺序,用高级的内存管理机制替换掉临时的引导机制,并最终通过回收自身的初始化代码来最大化可用内存。理解 mm/init.c 的工作流程,是深入理解 Linux 内存管理如何从无到有建立起来的关键。

find_usable_zone_for_movable 查找可用于ZONE_MOVABLE页面的区域

  • 按照从高到低的顺序查找可用于 ZONE_MOVABLE 的区域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* 这将查找可用于ZONE_MOVABLE页面的区域。
* 假设节点中的区域按单调递增的内存地址排序,以便使用“最高”填充的区域
*/
static void __init find_usable_zone_for_movable(void)
{
int zone_index;
for (zone_index = MAX_NR_ZONES - 1; zone_index >= 0; zone_index--) {
if (zone_index == ZONE_MOVABLE)
continue;

if (arch_zone_highest_possible_pfn[zone_index] >
arch_zone_lowest_possible_pfn[zone_index])
break;
}

VM_BUG_ON(zone_index == -1);
movable_zone = zone_index;
}

mirrored_kernelcore 设置内核内存的镜像

  • 该选项用于设置内核内存的镜像。
  • 该选项在内核启动时解析,并在内存块初始化期间使用。
  • 该选项的值可以是“mirror”或“mirror=0”。
  • 该选项的默认值为“mirror=0”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool mirrored_kernelcore __initdata_memblock;
static unsigned long required_kernelcore __initdata;
static unsigned long required_kernelcore_percent __initdata;

/*
* kernelcore=size sets the amount of memory for use for allocations that
* cannot be reclaimed or migrated.
*/
static int __init cmdline_parse_kernelcore(char *p)
{
/* parse kernelcore=mirror */
if (parse_option_str(p, "mirror")) {
mirrored_kernelcore = true;
return 0;
}

return cmdline_parse_core(p, &required_kernelcore,
&required_kernelcore_percent);
}
early_param("kernelcore", cmdline_parse_kernelcore);

find_zone_movable_pfns_for_nodes 寻找可移动区域的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static void __init find_zone_movable_pfns_for_nodes(void)
{
int i, nid;
unsigned long usable_startpfn;
unsigned long kernelcore_node, kernelcore_remaining;
/* 在借用 NodeMask 之前保存状态*/
nodemask_t saved_node_state = node_states[N_MEMORY];
unsigned long totalpages = early_calculate_totalpages();
int usable_nodes = nodes_weight(node_states[N_MEMORY]);
struct memblock_region *r;

/*指定 movable_node 时需要更早地找到movable_zone。 */
find_usable_zone_for_movable();

/*
* 如果指定了 movable_node,则忽略 kernelcore 和 movablecore 选项。
*/
if (movable_node_is_enabled()) { //!CONFIG_MEMORY_HOTPLUG = false
}
//如果指定了 kernelcore=mirror,则忽略 movablecore 选项
if (mirrored_kernelcore) {
}

/*
* 如果指定了 kernelcore=nn% 或 movablecore=nn%,则计算必要的内存量。
*/
if (required_kernelcore_percent)
required_kernelcore = (totalpages * 100 * required_kernelcore_percent) /
10000UL;
if (required_movablecore_percent)
required_movablecore = (totalpages * 100 * required_movablecore_percent) /
10000UL;

/*
* 如果指定了 movablecore=,则计算对应的 kernelcore 大小,以便均匀分布可用于任何分配类型的内存。如果同时指定了 kernelcore 和 movablecore,则 kernelcore 的值将用于required_kernelcore如果该值大于 movablecore 允许的值。
*/
if (required_movablecore) {
}

/*
* 如果未指定 kernelcore 或 kernelcore 大小大于 totalpages,则没有ZONE_MOVABLE(可移动的内存页面)。
*/
if (!required_kernelcore || required_kernelcore >= totalpages)
goto out;
out:
/*恢复 node_state */
node_states[N_MEMORY] = saved_node_state;
}

calculate_node_totalpages 计算节点总页数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static void __init calculate_node_totalpages(struct pglist_data *pgdat,
unsigned long node_start_pfn,
unsigned long node_end_pfn)
{
unsigned long realtotalpages = 0, totalpages = 0;
enum zone_type i;

for (i = 0; i < MAX_NR_ZONES; i++) {
struct zone *zone = pgdat->node_zones + i;
unsigned long zone_start_pfn, zone_end_pfn;
unsigned long spanned, absent;
unsigned long real_size;

spanned = zone_spanned_pages_in_node(pgdat->node_id, i,
node_start_pfn,
node_end_pfn,
&zone_start_pfn,
&zone_end_pfn);
absent = zone_absent_pages_in_node(pgdat->node_id, i,
zone_start_pfn,
zone_end_pfn);

real_size = spanned - absent;

if (spanned)
zone->zone_start_pfn = zone_start_pfn;
else
zone->zone_start_pfn = 0;
zone->spanned_pages = spanned;
zone->present_pages = real_size;
#if defined(CONFIG_MEMORY_HOTPLUG)
zone->present_early_pages = real_size;
#endif

totalpages += spanned;
realtotalpages += real_size;
}

pgdat->node_spanned_pages = totalpages;
pgdat->node_present_pages = realtotalpages;
pr_debug("On node %d totalpages: %lu\n", pgdat->node_id, realtotalpages);
}

pgdat_init_internals 初始化pgdat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void __meminit pgdat_init_internals(struct pglist_data *pgdat)
{
int i;
// 无实际执行
// pgdat_resize_init(pgdat);
// pgdat_kswapd_lock_init(pgdat);
// pgdat_init_split_queue(pgdat);
// pgdat_init_kcompactd(pgdat);

init_waitqueue_head(&pgdat->kswapd_wait);
init_waitqueue_head(&pgdat->pfmemalloc_wait);

for (i = 0; i < NR_VMSCAN_THROTTLE; i++)
init_waitqueue_head(&pgdat->reclaim_wait[i]);

// 无实际执行
pgdat_page_ext_init(pgdat);
lruvec_init(&pgdat->__lruvec); //LRU集合初始化
}

free_area_init_core 初始化区域核心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void __meminit zone_init_internals(struct zone *zone, enum zone_type idx, int nid,
unsigned long remaining_pages)
{
atomic_long_set(&zone->managed_pages, remaining_pages);
zone_set_nid(zone, nid);
zone->name = zone_names[idx];
zone->zone_pgdat = NODE_DATA(nid);
spin_lock_init(&zone->lock);
zone_seqlock_init(zone);
zone_pcp_init(zone);
}

static void __init free_area_init_core(struct pglist_data *pgdat)
{
enum zone_type j;
int nid = pgdat->node_id;

pgdat_init_internals(pgdat);
pgdat->per_cpu_nodestats = &boot_nodestats;

for (j = 0; j < MAX_NR_ZONES; j++) {
struct zone *zone = pgdat->node_zones + j;
unsigned long size = zone->spanned_pages;

/*
* 将 zone->managed_pages 初始化为 0 ,当 memblock 分配器将页面释放到 buddy 系统时,它会被重置。
*/
zone_init_internals(zone, j, nid, zone->present_pages);
//没有跨页继续
if (!size)
continue;

setup_usemap(zone);
init_currently_empty_zone(zone, zone->zone_start_pfn, size);
}
}

free_area_init_node 初始化节点空闲区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void __init free_area_init_node(int nid)
{
pg_data_t *pgdat = NODE_DATA(nid);
unsigned long start_pfn = 0;
unsigned long end_pfn = 0;

/* pg_data_t should be reset to zero when it's allocated */
WARN_ON(pgdat->nr_zones || pgdat->kswapd_highest_zoneidx);

get_pfn_range_for_nid(nid, &start_pfn, &end_pfn);

pgdat->node_id = nid;
pgdat->node_start_pfn = start_pfn;
pgdat->per_cpu_nodestats = NULL;

if (start_pfn != end_pfn) {
pr_info("Initmem setup node %d [mem %#018Lx-%#018Lx]\n", nid,
(u64)start_pfn << PAGE_SHIFT,
end_pfn ? ((u64)end_pfn << PAGE_SHIFT) - 1 : 0);

calculate_node_totalpages(pgdat, start_pfn, end_pfn);
} else {
pr_info("Initmem setup node %d as memoryless\n", nid);

reset_memoryless_node_totalpages(pgdat);
}
/* Skip empty nodes
if (!pgdat->node_spanned_pages) return
*/
alloc_node_mem_map(pgdat);
//CONFIG_DEFERRED_STRUCT_PAGE_INIT
pgdat_set_deferred_range(pgdat);

free_area_init_core(pgdat);
lru_gen_init_pgdat(pgdat); ////无执行代码
}

calc_nr_kernel_pages 计算内核页面数量

  • nr代表number数量的意思
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static unsigned long nr_kernel_pages __initdata;
static unsigned long nr_all_pages __initdata;

static void __init calc_nr_kernel_pages(void)
{
unsigned long start_pfn, end_pfn;
phys_addr_t start_addr, end_addr;
u64 u;
#ifdef CONFIG_HIGHMEM
unsigned long high_zone_low = arch_zone_lowest_possible_pfn[ZONE_HIGHMEM];
#endif

for_each_free_mem_range(u, NUMA_NO_NODE, MEMBLOCK_NONE, &start_addr, &end_addr, NULL) {
start_pfn = PFN_UP(start_addr);
end_pfn = PFN_DOWN(end_addr);

if (start_pfn < end_pfn) {
nr_all_pages += end_pfn - start_pfn;
#ifdef CONFIG_HIGHMEM
start_pfn = clamp(start_pfn, 0, high_zone_low);
end_pfn = clamp(end_pfn, 0, high_zone_low);
#endif
nr_kernel_pages += end_pfn - start_pfn;
}
}
}

__init_single_page 初始化单个页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __meminit __init_single_page(struct page *page, unsigned long pfn,
unsigned long zone, int nid)
{
mm_zero_struct_page(page);
set_page_links(page, zone, nid, pfn);
init_page_count(page); //atomic_set(&page->_refcount, v);
atomic_set(&page->_mapcount, -1);
page_cpupid_reset_last(page);
page_kasan_tag_reset(page);

INIT_LIST_HEAD(&page->lru);
#ifdef WANT_PAGE_VIRTUAL
/* The shift won't overflow because ZONE_NORMAL is below 4G. */
if (!is_highmem_idx(zone))
set_page_address(page, __va(pfn << PAGE_SHIFT));
#endif
}

memmap_init_range 初始化内存映射范围

  • 此函数是Linux内核内存管理子系统在初始化阶段的核心部分。它的主要职责是为一段物理内存范围内的每一页(Page)初始化其对应的元数据结构(struct page)。这个过程是内核从早期引导内存管理器(memblock)过渡到最终的伙伴系统(Buddy System)分配器的关键步骤。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/*
* 最初所有的页都处于保留状态 - 空闲的页会在早期引导过程结束后
* 由 memblock_free_all() 函数来释放。这是一个非原子的、单次通过的初始化过程。
*
* 所有对齐的页块(pageblock)都会被初始化为指定的迁移类型
* (通常是 MIGRATE_MOVABLE)。除了设置迁移类型,不会触及相关的
* zone 统计数据(例如,nr_isolate_pageblock)。
*/
void __meminit memmap_init_range(unsigned long size, int nid, unsigned long zone,
unsigned long start_pfn, unsigned long zone_end_pfn,
enum meminit_context context,
struct vmem_altmap *altmap, int migratetype)
{
// pfn: 当前正在处理的页帧号(Page Frame Number)。
// end_pfn: 本次调用需要初始化的页帧范围的结束地址(不包含)。
unsigned long pfn, end_pfn = start_pfn + size;
// 指向与一个页帧号对应的 page 结构体的指针。
// page 结构体包含了内核管理一个物理页所需的所有元数据。
struct page *page;

// 更新内核已知的最大页帧号。这对于内存管理系统确定其管辖范围至关重要。
if (highest_memmap_pfn < end_pfn - 1)
highest_memmap_pfn = end_pfn - 1;

/*
* 对于 ZONE_DEVICE 类型的内存,此段代码处理由驱动程序请求的预留空间。
* 我们将要初始化的页面总数限制在可能包含内存映射的那些页面。
* 我们将推迟 ZONE_DEVICE 页面的初始化,直到释放热插拔锁之后。
* 在STM32这样的嵌入式系统中,这可以用于管理特殊的内存区域,
* 但不是常规SRAM初始化的主要路径。
*/
if (zone == ZONE_DEVICE) {
// 如果是 ZONE_DEVICE,必须提供 altmap 描述符。
if (!altmap)
return;

// 如果是设备内存区域的起始,则跳过驱动程序预留的头部空间。
if (start_pfn == altmap->base_pfn)
start_pfn += altmap->reserve;
// 最终的结束pfn由altmap的偏移量决定。
end_pfn = altmap->base_pfn + vmem_altmap_offset(altmap);
}

// 循环遍历指定范围内的每一个页帧号。
// 注意循环的pfn++是在循环体内部手动执行的。
for (pfn = start_pfn; pfn < end_pfn; ) {
/*
* 传递给此函数的引导时 mem_map[] 中可能存在空洞。
* 这种情况在热插拔的内存上不会发生。
* 在STM32上,这对应于物理地址空间中不连续的SRAM区域。
*/
// 如果是在内核启动早期进行初始化...
if (context == MEMINIT_EARLY) {
// 检查当前pfn是否与已初始化的区域重叠,如果是则跳过。
if (overlap_memmap_init(zone, &pfn))
continue;
// 检查是否需要推迟此页的初始化(主要用于NUMA架构优化)。
// 在单核STM32上,此函数通常返回false。
if (defer_init(nid, pfn, zone_end_pfn)) {
deferred_struct_pages = true;
break;
}
}

// 将页帧号(pfn)转换为 page 结构体指针。
// 在无MMU的系统中,`mem_map`是一个巨大的`struct page`数组,
// 此操作等价于 `&mem_map[pfn]`,是一个直接的数组索引操作。
page = pfn_to_page(pfn);
// 调用核心辅助函数,初始化这一个 page 结构体的内部字段,
// 如设置标志位、所属的zone和节点ID(nid)等。
__init_single_page(page, pfn, zone, nid);

// 如果是为内存热插拔(hotplug)而初始化...
if (context == MEMINIT_HOTPLUG) {
// ZONE_DEVICE 类型的内存页特殊处理,设置为“保留”。
if (zone == ZONE_DEVICE)
__SetPageReserved(page);
else
// 普通热插拔内存页则设置为“离线”(Offline),
// 在完全准备好之前,内核的其他部分不能使用它。
__SetPageOffline(page);
}

/*
* 通常,我们希望将页块标记为 MIGRATE_MOVABLE(可移动),
* 这样不可移动的内存分配请求就不会在系统启动期间分散得到处都是,
* 这是一种对抗内存碎片的策略。
*/
// 检查当前pfn是否为一个页块(pageblock)的起始地址。
// 页块是比页大得多的内存管理单位。
if (pageblock_aligned(pfn)) {
// 为整个页块设置迁移类型。
set_pageblock_migratetype(page, migratetype);
// 这是一个条件调度点。初始化大量内存会很耗时,
// 此调用允许内核响应更高优先级的任务,防止系统卡死。
// 在单核系统上,这仍然是保持响应性的重要机制。
cond_resched();
}
// 手动递增页帧号,继续处理下一页。
pfn++;
}
}

memmap_init_zone_range 初始化内存映射区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void __init memmap_init_zone_range(struct zone *zone,
unsigned long start_pfn,
unsigned long end_pfn,
unsigned long *hole_pfn)
{
unsigned long zone_start_pfn = zone->zone_start_pfn;
unsigned long zone_end_pfn = zone_start_pfn + zone->spanned_pages;
int nid = zone_to_nid(zone), zone_id = zone_idx(zone);

start_pfn = clamp(start_pfn, zone_start_pfn, zone_end_pfn);
end_pfn = clamp(end_pfn, zone_start_pfn, zone_end_pfn);

if (start_pfn >= end_pfn)
return;

memmap_init_range(end_pfn - start_pfn, nid, zone_id, start_pfn,
zone_end_pfn, MEMINIT_EARLY, NULL, MIGRATE_MOVABLE);
}

memmap_init 初始化内存映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void __init memmap_init(void)
{
unsigned long start_pfn, end_pfn;
unsigned long hole_pfn = 0;
int i, j, zone_id = 0, nid;

for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, &nid) {
struct pglist_data *node = NODE_DATA(nid);

for (j = 0; j < MAX_NR_ZONES; j++) {
struct zone *zone = node->node_zones + j;

if (!populated_zone(zone))
continue;

memmap_init_zone_range(zone, start_pfn, end_pfn,
&hole_pfn);
zone_id = j;
}
}

/*
* Initialize the memory map for hole in the range [memory_end,
* section_end] for SPARSEMEM and in the range [memory_end, memmap_end]
* for FLATMEM.
* Append the pages in this hole to the highest zone in the last
* node.
*/
#ifdef CONFIG_SPARSEMEM
end_pfn = round_up(end_pfn, PAGES_PER_SECTION);
#else
end_pfn = round_up(end_pfn, MAX_ORDER_NR_PAGES);
#endif
if (hole_pfn < end_pfn)
init_unavailable_range(hole_pfn, end_pfn, zone_id, nid);
}

free_area_init 初始化所有pg_data_t和区域数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
* free_area_init - 初始化所有pg_data_t和区域数据
* @max_zone_pfn:每个区域的最大 PFN 数组
*
* 这将为系统中的每个活动节点调用 free_area_init_node()。
* 使用 memblock_set_node() 提供的页面范围,计算每个节点中每个区域的大小及其孔洞。
* 如果两个相邻区域之间的最大 PFN 匹配,则假定该区域为空。
* 例如,如果 arch_max_dma_pfn == arch_max_dma32_pfn,则假定 arch_max_dma32_pfn 没有页面。
* 还假定一个区域从前一个区域结束的位置开始。例如,ZONE_DMA32 从 arch_max_dma_pfn 开始。
*/
void __init free_area_init(unsigned long *max_zone_pfn)
{
unsigned long start_pfn, end_pfn;
int i, nid, zone;
bool descending;

/* 记录区域边界所在的位置 */
memset(arch_zone_lowest_possible_pfn, 0,
sizeof(arch_zone_lowest_possible_pfn));
memset(arch_zone_highest_possible_pfn, 0,
sizeof(arch_zone_highest_possible_pfn));

start_pfn = PHYS_PFN(memblock_start_of_DRAM());
descending = arch_has_descending_max_zone_pfns();

for (i = 0; i < MAX_NR_ZONES; i++) {
if (descending)
zone = MAX_NR_ZONES - i - 1;
else
zone = i;

if (zone == ZONE_MOVABLE)
continue;

end_pfn = max(max_zone_pfn[zone], start_pfn);
arch_zone_lowest_possible_pfn[zone] = start_pfn;
arch_zone_highest_possible_pfn[zone] = end_pfn;

start_pfn = end_pfn;
}

/* 查找每个节点中 ZONE_MOVABLE 开始的 PFN */
memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
find_zone_movable_pfns_for_nodes(); //寻找可移动区域的页面

/* 打印出区域范围*/
pr_info("Zone ranges:\n");
for (i = 0; i < MAX_NR_ZONES; i++) {
if (i == ZONE_MOVABLE)
continue;
pr_info(" %-8s ", zone_names[i]);
if (arch_zone_lowest_possible_pfn[i] ==
arch_zone_highest_possible_pfn[i])
pr_cont("empty\n");
else
pr_cont("[mem %#018Lx-%#018Lx]\n",
(u64)arch_zone_lowest_possible_pfn[i]
<< PAGE_SHIFT,
((u64)arch_zone_highest_possible_pfn[i]
<< PAGE_SHIFT) - 1);
}

/* 打印出每个节点中ZONE_MOVABLE开始的 PFN */
pr_info("Movable zone start for each node\n");
for (i = 0; i < MAX_NUMNODES; i++) {
if (zone_movable_pfn[i])
pr_info(" Node %d: %#018Lx\n", i,
(u64)zone_movable_pfn[i] << PAGE_SHIFT);
}

/*
* 打印出早期节点 map,并相对于活动在线内存范围初始化 subsection-map,以启用内存 map 的未来 “sub-section” 扩展。
*/
pr_info("Early memory node ranges\n");
for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, &nid) {
pr_info(" node %3d: [mem %#018Lx-%#018Lx]\n", nid,
(u64)start_pfn << PAGE_SHIFT,
((u64)end_pfn << PAGE_SHIFT) - 1);
subsection_map_init(start_pfn, end_pfn - start_pfn);
}

/* 初始化每个节点 */
mminit_verify_pageflags_layout();
setup_nr_node_ids();
set_pageblock_order();
/* 这三个没有执行 */

for_each_node(nid) {
pg_data_t *pgdat;

if (!node_online(nid))
alloc_offline_node_data(nid);

pgdat = NODE_DATA(nid); // &contig_page_data;
free_area_init_node(nid); //初始化节点空闲区域

/*
* 不会通过 register_one_node() 为无内存节点创建 sysfs 层次结构,
* 因为这里它没有标记为 N_MEMORY,以后也不会在线设置。
* 好处是用户空间程序不会与无内存节点的 sysfs 文件/目录混淆。
* 当内存热插拔到此节点时,pgdat 将通过 hotadd_init_pgdat() 完全初始化。
*/
if (pgdat->node_present_pages) {
node_set_state(nid, N_MEMORY); //无效果
check_for_memory(pgdat); //无效果
}
}
//无效果
for_each_node_state(nid, N_MEMORY)
sparse_vmemmap_init_nid_late(nid);

calc_nr_kernel_pages(); //计算内核页面数量
memmap_init();

/* 为具有单个节点的系统禁用哈希分配 */
fixup_hashdist();

set_high_memory();
}

alloc_large_system_hash 分配大型系统哈希

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/*
* 从 bootmem 分配一个大型系统哈希表
* - 假设哈希表必须包含确切的 2 次幂数量的条目
* - limit 是哈希存储桶的数量,而不是总分配大小
*/
void *__init alloc_large_system_hash(const char *tablename,
unsigned long bucketsize,
unsigned long numentries,
int scale,
int flags,
unsigned int *_hash_shift,
unsigned int *_hash_mask,
unsigned long low_limit,
unsigned long high_limit)
{
unsigned long long max = high_limit;
unsigned long log2qty, size;
void *table;
gfp_t gfp_flags;
bool virt;
bool huge;

/* 允许内核 cmdline 有发言权*/
if (!numentries) {
/*将适用的内存大小向上舍入到最接近的兆字节 */
numentries = nr_kernel_pages; //8192

/*当 PAGE_SIZE >= 1MB 时没有必要*/
if (PAGE_SIZE < SZ_1M)
numentries = round_up(numentries, SZ_1M / PAGE_SIZE); //8192

/* limit to 1 bucket per 2^scale bytes of low memory */
if (scale > PAGE_SHIFT) //13 > 12
numentries >>= (scale - PAGE_SHIFT); //4096
else
numentries <<= (PAGE_SHIFT - scale);

if (unlikely((numentries * bucketsize) < PAGE_SIZE))
numentries = PAGE_SIZE / bucketsize;
}
numentries = roundup_pow_of_two(numentries);

/* limit allocation size to 1/16 total memory by default */
if (max == 0) {
max = ((unsigned long long)nr_all_pages << PAGE_SHIFT) >> 4; //2097152
do_div(max, bucketsize); //2097152 / 4 = 524288
}
max = min(max, 0x80000000ULL);

if (numentries < low_limit)
numentries = low_limit;
if (numentries > max)
numentries = max;

log2qty = ilog2(numentries); //12

gfp_flags = (flags & HASH_ZERO) ? GFP_ATOMIC | __GFP_ZERO : GFP_ATOMIC;
do {
virt = false;
size = bucketsize << log2qty; //4 << 12 = 16384
if (flags & HASH_EARLY) {
if (flags & HASH_ZERO)
table = memblock_alloc(size, SMP_CACHE_BYTES);
else
table = memblock_alloc_raw(size,
SMP_CACHE_BYTES);
} else if (get_order(size) > MAX_PAGE_ORDER || hashdist) {
table = vmalloc_huge(size, gfp_flags);
virt = true;
if (table)
huge = is_vm_area_hugepages(table);
} else {
/*
* If bucketsize is not a power-of-two, we may free
* some pages at the end of hash table which
* alloc_pages_exact() automatically does
*/
table = alloc_pages_exact(size, gfp_flags);
kmemleak_alloc(table, size, 1, gfp_flags);
}
} while (!table && size > PAGE_SIZE && --log2qty);

if (!table)
panic("Failed to allocate %s hash table\n", tablename);
//Dentry cache hash table entries: 4096 (order: 2, 16384 bytes, linear)
pr_info("%s hash table entries: %ld (order: %d, %lu bytes, %s)\n",
tablename, 1UL << log2qty, ilog2(size) - PAGE_SHIFT, size,
virt ? (huge ? "vmalloc hugepage" : "vmalloc") : "linear");

if (_hash_shift)
*_hash_shift = log2qty;
if (_hash_mask)
*_hash_mask = (1 << log2qty) - 1;

return table;
}

mem_debugging_and_hardening_init 内存调试和硬化初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
DECLARE_STATIC_KEY_MAYBE(CONFIG_INIT_ON_ALLOC_DEFAULT_ON, init_on_alloc);
static inline bool want_init_on_alloc(gfp_t flags)
{
if (static_branch_maybe(CONFIG_INIT_ON_ALLOC_DEFAULT_ON,
&init_on_alloc))
return true;
return flags & __GFP_ZERO;
}

DECLARE_STATIC_KEY_MAYBE(CONFIG_INIT_ON_FREE_DEFAULT_ON, init_on_free);
static inline bool want_init_on_free(void)
{
return static_branch_maybe(CONFIG_INIT_ON_FREE_DEFAULT_ON,
&init_on_free);
}

/*
* 启用与各种内存调试和强化选项相关的 static key。
* 有些会覆盖其他 Params,并依赖于按出现顺序计算的早期参数。
* 因此,我们需要首先收集已启用内容的完整情况,然后做出决策。
*/
static void __init mem_debugging_and_hardening_init(void)
{
bool page_poisoning_requested = false;
bool want_check_pages = false;

if ((_init_on_alloc_enabled_early || _init_on_free_enabled_early) &&
page_poisoning_requested) {
pr_info("mem auto-init: CONFIG_PAGE_POISONING is on, "
"will take precedence over init_on_alloc and init_on_free\n");
_init_on_alloc_enabled_early = false;
_init_on_free_enabled_early = false;
}
/* init_on_alloc
作用: 当启用 init_on_alloc 时,内核会在每次分配内存时将分配的内存块初始化为零。
这可以防止分配的内存中残留的旧数据被误用或泄露。
优点: 增强安全性,防止敏感数据泄露。
避免因使用未初始化内存导致的未定义行为。
缺点: 会增加内存分配的开销,因为每次分配都需要额外的时间来清零内存。
*/
if (_init_on_alloc_enabled_early) {
want_check_pages = true;
static_branch_enable(&init_on_alloc);
} else {
static_branch_disable(&init_on_alloc);
}
/* init_on_free
作用: 当启用 init_on_free 时,内核会在每次释放内存时将内存块清零。
这确保了即使内存被重新分配,也不会泄露之前存储的数据。
优点: 防止敏感数据在内存释放后被其他进程或内核组件访问。
提高系统的安全性,特别是在多用户环境中。
缺点: 增加内存释放的开销,因为每次释放都需要额外的时间来清零内存。
*/
if (_init_on_free_enabled_early) {
want_check_pages = true;
static_branch_enable(&init_on_free);
} else {
static_branch_disable(&init_on_free);
}

if (IS_ENABLED(CONFIG_KMSAN) &&
(_init_on_alloc_enabled_early || _init_on_free_enabled_early))
pr_info("mem auto-init: please make sure init_on_alloc and init_on_free are disabled when running KMSAN\n");

#ifdef CONFIG_DEBUG_PAGEALLOC
if (debug_pagealloc_enabled()) {
want_check_pages = true;
static_branch_enable(&_debug_pagealloc_enabled);

if (debug_guardpage_minorder())
static_branch_enable(&_debug_guardpage_enabled);
}
#endif

/*
* Any page debugging or hardening option also enables sanity checking
* of struct pages being allocated or freed. With CONFIG_DEBUG_VM it's
* enabled already.
*/
if (!IS_ENABLED(CONFIG_DEBUG_VM) && want_check_pages)
static_branch_enable(&check_pages_enabled);
}

report_meminit 报告内存初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 报告此启动的内存自动初始化状态。 */
static void __init report_meminit(void)
{
const char *stack;
//栈会被初始化为某种特定的模式(pattern),通常用于调试目的,以便更容易发现未初始化的堆栈使用。
if (IS_ENABLED(CONFIG_INIT_STACK_ALL_PATTERN))
stack = "all(pattern)";
//表示堆栈会被初始化为全零值,增强了安全性,防止未初始化的堆栈数据泄露。
else if (IS_ENABLED(CONFIG_INIT_STACK_ALL_ZERO))
stack = "all(zero)";
//启用了 GCC 插件,用于检测和清零所有通过引用传递的结构体,防止敏感数据泄露。
else if (IS_ENABLED(CONFIG_GCC_PLUGIN_STRUCTLEAK_BYREF_ALL))
stack = "byref_all(zero)";
//启用了 GCC 插件,用于检测和清零部分通过引用传递的结构体。
else if (IS_ENABLED(CONFIG_GCC_PLUGIN_STRUCTLEAK_BYREF))
stack = "byref(zero)";
//启用了 GCC 插件,用于清零用户空间传递的结构体数据,防止用户空间数据泄露到内核空间。
else if (IS_ENABLED(CONFIG_GCC_PLUGIN_STRUCTLEAK_USER))
stack = "__user(zero)";
else
stack = "off";
//mem auto-init: stack:off, heap alloc:off, heap free:off
pr_info("mem auto-init: stack:%s, heap alloc:%s, heap free:%s\n",
stack, str_on_off(want_init_on_alloc(GFP_KERNEL)),
str_on_off(want_init_on_free()));
if (want_init_on_free())
pr_info("mem auto-init: clearing system memory may take some time...\n");
}

mm_core_init 设置内核内存分配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*
* 设置内核内存分配器
*/
void __init mm_core_init(void)
{
arch_mm_preinit();
hugetlb_bootmem_alloc();

/*依赖于 SMP 设置的初始化*/
BUILD_BUG_ON(MAX_ZONELISTS > 2);
build_all_zonelists(NULL);
page_alloc_init_cpuhp();
alloc_tag_sec_init();
/*
* page_ext需要连续的页面,大于 MAX_PAGE_ORDER 的页面,除非 SPARSEMEM。
*/
page_ext_init_flatmem();
mem_debugging_and_hardening_init();
kfence_alloc_pool_and_metadata();
report_meminit();
kmsan_init_shadow();
stack_depot_early_init();
memblock_free_all();
mem_init();
kmem_cache_init();
/*
* page_owner必须在 Buddy 准备好后进行初始化,并且 afterslab 也准备好了,以便 stack_depot_init() 正常工作
*/
// page_ext_init_flatmem_late();
// kmemleak_init();
// ptlock_cache_init();
// pgtable_cache_init();
// debug_objects_mem_init();
// vmalloc_init();
/* 如果没有延迟的 init 现在page_ext,因为 vmap 已经完全初始化*/
// if (!deferred_struct_pages)
// page_ext_init();
/* Should be run before the first non-init thread is created */
// init_espfix_bsp();
/* Should be run after espfix64 is set up. */
// pti_init();
// kmsan_init_runtime();
mm_cache_init();
// execmem_init();
}

reserve_bootmem_region 标记由引导内存分配器(bootmem allocator)分配的内存区域中的页面为“保留”(PageReserved

  • 这些页面在初始化阶段不会被普通内存管理机制使用,通常用于内核自身的用途或特殊的硬件需求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct page *mem_map;
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))

/*
* 遍历由引导内存分配器分配的内存范围,并将这些页面标记为 PageReserved
* 被标记为 PageReserved 的页面不会被普通的内存分配器(如伙伴系统)使用
* 剩余未标记的有效页面会被移交给伙伴系统(buddy page allocator)进行管理
*/
void __meminit reserve_bootmem_region(phys_addr_t start,
phys_addr_t end, int nid)
{
unsigned long start_pfn = PFN_DOWN(start);
unsigned long end_pfn = PFN_UP(end);

for (; start_pfn < end_pfn; start_pfn++) {
if (pfn_valid(start_pfn)) { //检查当前页面帧号是否有效(即是否对应实际的物理内存)。
struct page *page = pfn_to_page(start_pfn); // 将页面帧号转换为对应的 struct page 结构,这是内核用于管理物理页面的核心数据结构

init_deferred_page(start_pfn, nid); // 初始化延迟页面(deferred page),并将其与指定的 NUMA 节点关联

//将页面标记为 PageReserved,表示该页面已被保留,不会被普通内存分配器使用
//由于此时页面尚未对外可见,因此不需要使用原子操作来设置标志位
__SetPageReserved(page);
}
}
}

memblock_free_pages 释放内存块

1
2
3
4
5
6
void __init memblock_free_pages(struct page *page, unsigned long pfn,
unsigned int order)
{
//调用核心函数释放页块,并将其标记为早期内存初始化(MEMINIT_EARLY)
__free_pages_core(page, order, MEMINIT_EARLY);
}

set_zone_contiguous 检查一个内存区域(zone)中的所有页块(pageblock)是否都存在对应的物理页帧

  • set_zone_contiguous 是内存管理子系统中的一个函数。它的作用是检查一个内存区域(zone)中的所有页块(pageblock)是否都存在对应的物理页帧。如果整个区域的内存是物理上连续且没有“空洞”的,它就会将该区域的 contiguous 标志位设置为 true。这个标志对于需要大块连续物理内存的设备(如一些DMA设备)或巨页(HugeTLB)分配非常重要。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 设置一个内存区域(zone)的 contiguous 标志位。
void set_zone_contiguous(struct zone *zone)
{
// 获取该内存区域的起始页帧号(PFN)。
unsigned long block_start_pfn = zone->zone_start_pfn;
// 用于存储每个页块结束位置的页帧号。
unsigned long block_end_pfn;

// 计算第一个页块的结束页帧号。pageblock_end_pfn 接受一个起始 pfn,返回该 pfn 所在页块的结束 pfn。
block_end_pfn = pageblock_end_pfn(block_start_pfn);
// 这是一个 for 循环,用于遍历内存区域(zone)中的每一个页块(pageblock)。
// 页块是内存管理的基本单位之一,通常大小为几MB。
for (; block_start_pfn < zone_end_pfn(zone); // 循环条件:只要页块的起始地址还在zone的范围内
block_start_pfn = block_end_pfn, // 在每次迭代后,将下一个页块的起始地址更新为当前页块的结束地址
block_end_pfn += pageblock_nr_pages) { // 计算理论上(不考虑zone边界)下一个页块的结束地址

// 确保页块的结束地址不会超出整个zone的边界。
// 取理论上的结束地址和zone的实际结束地址中较小的一个。
block_end_pfn = min(block_end_pfn, zone_end_pfn(zone));

// 这个函数是核心检查点。它会检查从 block_start_pfn 到 block_end_pfn 之间的所有页帧
// 是否都存在有效的 page 结构体。在一些体系结构中,物理内存可能存在“空洞”(holes),
// 即某些物理地址范围没有映射到实际的RAM。
// 如果发现任何一个页块内存在空洞(即没有有效的page结构),该函数返回false。
if (!__pageblock_pfn_to_page(block_start_pfn,
block_end_pfn, zone))
// 如果发现空洞,说明这个zone不是物理连续的,函数直接返回,
// zone->contiguous 标志将保持默认的 false。
return;
// 这是一个条件调度点。如果在遍历过程中花费了太长时间,
// cond_resched() 会检查是否需要进行一次调度,以允许其他更高优先级的任务运行,
// 防止系统长时间无响应。
cond_resched();
}

/* We confirm that there is no hole */
// 如果循环成功完成,意味着我们已经检查了zone中的所有页块,并且没有发现任何空洞。
/* 我们确认了这里没有空洞 */
// 将该内存区域的 contiguous 标志设置为 true,表示该区域是物理连续的。
zone->contiguous = true;
}

page_alloc_sysctl_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// 定义一个用于页分配器sysctl接口的控制表。
// `static const` 意味着此表为本文件私有,且内容在编译后不可更改。
static const struct ctl_table page_alloc_sysctl_table[] = {
// --- 第一个条目:min_free_kbytes ---
{
// .procname: 在/proc/sys/vm/下创建的文件名。
.procname = "min_free_kbytes",
// .data: 指向此文件所控制的内核变量的指针。
.data = &min_free_kbytes,
// .maxlen: 该变量的最大长度(字节),用于安全检查。
.maxlen = sizeof(min_free_kbytes),
// .mode: 在/proc中创建的文件的权限,0644表示所有者可读写,其他用户只读。
.mode = 0644,
// .proc_handler: 当读写此文件时调用的处理函数。
// 此函数负责验证输入值并更新内核水线(watermarks)。
.proc_handler = min_free_kbytes_sysctl_handler,
// .extra1: 传递给处理函数的额外参数,这里为0。
.extra1 = SYSCTL_ZERO,
},
// --- 第二个条目:watermark_boost_factor ---
{
// .procname: 文件名为 "watermark_boost_factor"。
.procname = "watermark_boost_factor",
// .data: 控制内核变量 `watermark_boost_factor`。此变量用于在内存压力下动态提升水线,以更积极地回收内存。
.data = &watermark_boost_factor,
.maxlen = sizeof(watermark_boost_factor),
.mode = 0644,
// .proc_handler: 一个通用的整数处理函数,它会检查输入值是否在extra1和extra2指定的范围内。
.proc_handler = proc_dointvec_minmax,
// .extra1: 允许的最小值为0。
.extra1 = SYSCTL_ZERO,
},
// --- 第三个条目:watermark_scale_factor ---
{
// .procname: 文件名为 "watermark_scale_factor"。
.procname = "watermark_scale_factor",
// .data: 控制内核变量 `watermark_scale_factor`。它根据系统内存大小按比例调整水线。
.data = &watermark_scale_factor,
.maxlen = sizeof(watermark_scale_factor),
.mode = 0644,
// .proc_handler: 一个自定义处理函数,用于安全地更新水线缩放因子。
.proc_handler = watermark_scale_factor_sysctl_handler,
// .extra1: 传递给处理函数的最小值参数,这里为1。
.extra1 = SYSCTL_ONE,
// .extra2: 传递给处理函数的最大值参数,这里为3000。
.extra2 = SYSCTL_THREE_THOUSAND,
},
// --- 第四个条目:defrag_mode ---
{
// .procname: 文件名为 "defrag_mode"。
.procname = "defrag_mode",
// .data: 控制内核变量 `defrag_mode`。此变量决定内核内存碎片整理的模式,
// 在STM32上,这对于获取连续的物理内存块用于DMA操作依然重要。
.data = &defrag_mode,
.maxlen = sizeof(defrag_mode),
.mode = 0644,
.proc_handler = proc_dointvec_minmax,
.extra1 = SYSCTL_ZERO,
.extra2 = SYSCTL_ONE,
},
// --- 第五个条目:percpu_pagelist_high_fraction ---
{
// .procname: 文件名为 "percpu_pagelist_high_fraction"。
.procname = "percpu_pagelist_high_fraction",
// .data: 控制内核变量 `percpu_pagelist_high_fraction`。它设置了每个CPU页缓存列表的上限。
// 在单核STM32上,这只影响唯一的那个CPU的页缓存。
.data = &percpu_pagelist_high_fraction,
.maxlen = sizeof(percpu_pagelist_high_fraction),
.mode = 0644,
.proc_handler = percpu_pagelist_high_fraction_sysctl_handler,
.extra1 = SYSCTL_ZERO,
},
// --- 第六个条目:lowmem_reserve_ratio ---
{
// .procname: 文件名为 "lowmem_reserve_ratio"。
.procname = "lowmem_reserve_ratio",
// .data: 控制一个数组 `sysctl_lowmem_reserve_ratio`。
// 该参数用于在不同的内存域(zone)之间预留内存,防止低地址内存(如DMA内存)被高地址内存分配请求耗尽。
.data = &sysctl_lowmem_reserve_ratio,
.maxlen = sizeof(sysctl_lowmem_reserve_ratio),
.mode = 0644,
.proc_handler = lowmem_reserve_ratio_sysctl_handler,
},
/*
* 以下部分被 #ifdef CONFIG_NUMA 宏包围。
* NUMA (非一致性内存访问) 是用于大型多处理器服务器的架构。
* STM32H750是单核、统一内存访问(UMA)架构,内核编译时不会定义CONFIG_NUMA。
* 因此,以下所有条目都不会被编译进最终的内核镜像中。
*/
#ifdef CONFIG_NUMA
{
.procname = "numa_zonelist_order",
.data = &numa_zonelist_order,
.maxlen = NUMA_ZONELIST_ORDER_LEN,
.mode = 0644,
.proc_handler = numa_zonelist_order_handler,
},
{
.procname = "min_unmapped_ratio",
.data = &sysctl_min_unmapped_ratio,
.maxlen = sizeof(sysctl_min_unmapped_ratio),
.mode = 0644,
.proc_handler = sysctl_min_unmapped_ratio_sysctl_handler,
.extra1 = SYSCTL_ZERO,
.extra2 = SYSCTL_ONE_HUNDRED,
},
{
.procname = "min_slab_ratio",
.data = &sysctl_min_slab_ratio,
.maxlen = sizeof(sysctl_min_slab_ratio),
.mode = 0644,
.proc_handler = sysctl_min_slab_ratio_sysctl_handler,
.extra1 = SYSCTL_ZERO,
.extra2 = SYSCTL_ONE_HUNDRED,
},
#endif
};
// `__init` 是一个宏,它告诉编译器将此函数的代码放入一个特殊的".init.text"段。
// 内核在启动过程的末尾会释放这个段的内存,因为初始化函数在启动后不再需要。
// 这对于像STM32这样内存资源有限的设备来说是一项重要的优化。
void __init page_alloc_sysctl_init(void)
{
// 调用 register_sysctl_init 函数。
// 第一个参数 "vm" 指定了顶级目录名,即 /proc/sys/vm/。
// 第二个参数是上面定义的表。
// 该函数会遍历表中的每一个条目,并在/proc/sys/vm/下创建对应的文件。
register_sysctl_init("vm", page_alloc_sysctl_table);
}

page_alloc_init_late 对页分配器(Page Allocator)子系统进行的后期初始化函数

  • page_alloc_init_late 是Linux内核在启动过程中,对页分配器(Page Allocator)子系统进行的后期初始化函数。它在核心的伙伴系统(Buddy System)已经可以工作之后被调用,用于完成一些依赖于完整内存信息或可以被推迟的初始化任务。
  • 其主要工作包括:
    • (可选)并行初始化页描述符:在拥有大量内存的系统上,通过创建内核线程来并行初始化struct page数组,以缩短启动时间。
    • 打印内存信息:在内存信息稳定后,向内核日志打印最终的内存布局和使用情况。
    • 释放早期内存管理器:丢弃memblock分配器自身使用的数据结构,回收这部分内存。
    • 增强安全性:通过随机化空闲页链表来增加内存布局的不可预测性,抵御某些内存攻击。
    • 完成最后的配置:初始化与页分配相关的sysctl接口,允许用户在运行时查看和调整内存管理参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 定义页分配器的后期初始化函数。
// __init 宏表示该函数及其数据在内核初始化完成后会被释放,以节约内存。
void __init page_alloc_init_late(void)
{
struct zone *zone; // 用于迭代内存区域(zone)的指针
int nid; // 用于迭代NUMA节点ID(node ID)的变量

// 这是一个条件编译指令。下面的代码块仅在内核配置了 CONFIG_DEFERRED_STRUCT_PAGE_INIT 时才被编译。
// 这个配置用于拥有非常大内存的系统,它将 struct page 数组的初始化工作推迟到内核线程中并行执行,以加速启动。
// 对于内存较小的嵌入式系统(如STM32),通常不开启此选项。
#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT

/* 将会有 num_node_state(N_MEMORY) 个线程被创建 */
// 设置一个原子计数器,其值为系统中含有内存的NUMA节点数量。
// 这个计数器用于追踪还有多少个节点的内存描述符尚未完成初始化。
atomic_set(&pgdat_init_n_undone, num_node_state(N_MEMORY));

// for_each_node_state 是一个宏,用于遍历所有指定状态的NUMA节点。
// 这里遍历所有包含物理内存的节点(N_MEMORY)。
// 在非NUMA系统上,这个循环只会执行一次,且 nid 为 0。
for_each_node_state(nid, N_MEMORY) {
// kthread_run 创建并启动一个内核线程。
// deferred_init_memmap 是线程将要执行的函数。
// NODE_DATA(nid) 是传递给该函数的参数,即节点的 pg_data_t 结构体。
// "pgdatinit%d" 是线程的名称格式,方便调试。
kthread_run(deferred_init_memmap, NODE_DATA(nid), "pgdatinit%d", nid);
}

/* 阻塞直到所有节点的初始化全部完成 */
// wait_for_completion 是一个同步原语。主启动线程会在这里等待,
// 直到所有 pgdatinit 线程都完成了它们的初始化工作并调用了 complete()。
wait_for_completion(&pgdat_init_all_done_comp);

/*
* 我们已经初始化了剩余的延迟页面。现在永久性地禁用
* 按需(on-demand)的 struct page 初始化路径。
*/
// static_branch_disable 是一个性能优化。它会动态地修改代码,
// 将一个条件分支(if/else)替换为无条件执行的路径,因为延迟初始化的逻辑在此之后不再需要。
static_branch_disable(&deferred_pages);

/* 在内核启动后,基于空闲页数重新初始化一些限制 */
// 此时总内存和可用内存已经完全确定,可以精确地设置文件系统的最大打开文件数等限制。
files_maxfiles_init();
#endif // CONFIG_DEFERRED_STRUCT_PAGE_INIT 结束

/* 到此为止,总内存和空闲内存的统计是稳定的。*/
// 调用此函数向内核日志(dmesg)打印详细的内存使用信息。
mem_init_print_info();
// 初始化缓冲区高速缓存(buffer cache)相关的数据结构。
buffer_init();

/* 丢弃 memblock 的私有内存 */
// memblock 是内核在非常早期的启动阶段使用的内存管理器。
// 此时,伙伴(buddy)页分配器已经完全接管,所以可以释放 memblock 自身所占用的管理数据结构了。
// memblock_discard();

// 再次遍历所有内存节点。
// for_each_node_state(nid, N_MEMORY)
// 对每个节点的空闲页链表进行随机化处理。
// 这是一个安全加固措施,通过打乱空闲页的分配顺序,
// 使得依赖于可预测内存布局的攻击(如堆喷射)更难成功。
// shuffle_free_memory(NODE_DATA(nid));

// for_each_populated_zone 是一个宏,用于遍历所有包含物理页面的内存区域(zone)。
// 例如 ZONE_DMA, ZONE_NORMAL。
for_each_populated_zone(zone)
// 检查该区域(zone)内的所有物理内存页是否是连续的。
// 如果是,则设置一个标志位(ZONE_CONTIG),这个信息可以用于优化巨页(Huge Page)的分配。
set_zone_contiguous(zone);

/* 在所有 struct page 初始化完毕后,初始化 page ext */
// deferred_struct_pages 是一个全局变量,指示延迟初始化是否被使用。
if (deferred_struct_pages)
// page_ext_init 初始化 struct page 的扩展数据区。
// page_ext 用于存储一些不常用的页信息,避免主 struct page 结构体过于臃肿。
// 它必须在所有 struct page 自身都初始化完毕后才能进行。
page_ext_init();

// 初始化页分配器相关的 sysctl 接口。
// 这将在 /proc/sys/vm/ 目录下创建一些文件(如 min_free_kbytes),
// 允许系统管理员在运行时查看和调整内存管理的行为。
page_alloc_sysctl_init();
}

mm_sysfs_init: 初始化内存管理(MM)相关的sysfs接口

此函数在内核启动过程中被调用, 其唯一且核心的作用是在sysfs文件系统中创建顶层的 /sys/kernel/mm/ 目录。这个目录是一个命名空间, 作为所有与内核内存管理(Memory Management)相关的可调参数和统计信息文件的父容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
* mm_sysfs_init: 内存管理 sysfs 接口的初始化函数.
* 这是一个静态函数, 标记为 __init, 表示它仅在内核初始化期间执行,
* 在初始化完成后, 其占用的内存可能会被回收.
* @return: 成功时返回0, 失败时返回负值的错误码.
*/
static int __init mm_sysfs_init(void)
{
/*
* 调用 kobject_create_and_add, 在 sysfs 中创建一个名为 "mm" 的目录.
* @ "mm": 目录的名称.
* @ kernel_kobj: 父 kobject 的指针. kernel_kobj 代表 /sys/kernel 目录.
* 因此, 这行代码会创建 /sys/kernel/mm/ 目录.
* 返回的 kobject 指针被存入全局变量 mm_kobj, 以便其他内存管理子系统可以在此目录下创建文件.
*/
mm_kobj = kobject_create_and_add("mm", kernel_kobj);
/*
* 检查目录是否创建成功. 如果失败(通常因为内存不足), 函数返回 NULL.
*/
if (!mm_kobj)
/*
* 如果创建失败, 返回 -ENOMEM (内存不足) 错误码.
*/
return -ENOMEM;

/*
* 所有操作成功, 返回0.
*/
return 0;
}
/*
* 使用 postcore_initcall 宏来注册 mm_sysfs_init 函数.
* 这会将该函数放入一个特定的初始化函数列表中, 确保它在内核的 "post-core" 阶段被调用.
* 这个阶段晚于核心子系统(如sysfs和kernel_kobj)的初始化, 确保父目录 /sys/kernel 已存在.
*/
postcore_initcall(mm_sysfs_init);