[TOC]

mm/page_alloc.c 伙伴系统内存分配器(Buddy System Memory Allocator) 内核物理内存管理的核心

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术是为了解决操作系统内核中最根本的问题之一:如何高效、可靠地管理整个系统的物理内存(RAM)。内核自身及其服务的子系统(如进程管理、文件系统缓存、网络协议栈)都需要动态地申请和释放内存。mm/page_alloc.c 中实现的伙伴系统(Buddy System)旨在满足以下核心需求:

  • 管理基本单位:将物理内存划分为固定大小的“页”(通常是4KB),并以此为基本单位进行管理。
  • 提供连续内存:许多硬件设备(特别是进行直接内存访问DMA的设备)和某些内核数据结构要求得到的内存块在物理上是连续的。伙伴系统的设计目标之一就是能够分配不同大小的、物理连续的内存块。
  • 对抗外部碎片:在长时间运行的系统中,频繁地分配和释放不同大小的内存块会导致物理内存中产生许多不连续的小空闲块。这种现象称为“外部碎片”,它会导致即使总的空闲内存很多,也无法满足一个较大连续内存的申请。伙伴系统通过其独特的合并机制来持续地、主动地对抗外部碎片。

它的发展经历了哪些重要的里程碑或版本迭代?

伙伴系统算法本身是一个经典的计算机科学算法,但在Linux内核中,它经历了持续的演进和优化。

  • 分区内存管理(Zoned Memory):早期的硬件存在限制,例如,一些老的ISA设备只能对RAM的第一个16MB进行DMA操作。为了应对这种情况,内核将内存划分为不同的区(Zone),如ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM(用于32位系统上无法直接映射的“高端内存”)。伙伴分配器被扩展为按区管理内存。
  • NUMA支持(Non-Uniform Memory Access):在多处理器的服务器架构中,CPU访问不同物理内存节点的延迟不同。伙伴分配器被改造成NUMA感知的,为每个NUMA节点都维护一套独立的内存区和伙伴系统数据结构,并遵循“本地分配优先”的原则,以获得最佳性能。
  • 每CPU页缓存(Per-CPU Pagesets):为了减少在多核系统上分配和释放单页内存时对全局锁的竞争,引入了每CPU页缓存。当CPU释放一个页面时,它会先被放入这个本地缓存中;申请时也优先从本地缓存获取。这极大地提高了单页分配的性能和可扩展性。
  • 反碎片机制:为了进一步解决在长期运行的系统上难以获得大块连续内存的问题,引入了页迁移(Page Migration)和CMA(Contiguous Memory Allocator)等机制。系统会将一些“可移动”的页面(如用户进程的匿名页)挪走,从而整理出连续的空闲空间。伙伴系统与这些机制紧密集成。

目前该技术的社区活跃度和主流应用情况如何?

mm/page_alloc.c 是Linux内核最核心、最基础的组件之一。它是整个内存管理子系统的基石,其性能和稳定性对整个系统至关重要。它是一个非常成熟、经过数十年优化和验证的代码库,被Linux支持的所有体系结构(从微小的嵌入式设备到巨型超级计算机)普遍使用。社区对它的任何修改都极为谨慎,并需要经过严格的审查和测试。

核心原理与设计

它的核心工作原理是什么?

mm/page_alloc.c 实现的是一种二进制伙伴系统(Binary Buddy System)

  1. 基本单位与阶(Order):内存被划分为页。分配的单位是 2 的幂次方个页,这个幂次被称为阶(order)。一个 order-0 的块是 2^0=1个页,一个 order-1 的块是 2^1=2个页,一个 order-3 的块是 2^3=8个页,以此类推,最大可达 order-10(1024个页,即4MB)。
  2. 空闲链表数组:伙伴系统为每个阶维护一个空闲内存块的链表。例如,free_area[3] 指向一个由所有空闲的、8页大小的内存块组成的链表。
  3. 分配过程
    • 当请求一个 order-n 的块时,系统首先检查 order-n 的空闲链表。
    • 如果链表不为空,就直接从链表中取下一个块,分配出去。
    • 如果链表为空,系统会去检查 order-n+1 的链表。
    • 如果 order-n+1 链表有块,就取出一个,将其**分裂(Split)**成两个大小相等的 order-n 的块。一个块用于满足请求,另一个块(它的“伙伴”)被放入 order-n 的空闲链表中。
    • 如果 order-n+1 的链表也为空,就继续向上查找 order-n+2,直到找到一个更大的空闲块,然后逐级分裂下来,直到得到一个 order-n 的块。
  4. 释放过程(核心)
    • 当一个 order-n 的块被释放时,系统会计算出它的伙伴块的地址(伙伴地址可以通过简单的异或运算得到)。
    • 然后系统检查这个伙伴块是否也处于空闲状态。
    • 如果伙伴也空闲,系统就会将这两个 order-n 的块从 order-n 链表中移除,然后**合并(Merge)**成一个 order-n+1 的大块,并将其放入 order-n+1 的空闲链表中。
    • 这个合并过程是递归的。新合并成的 order-n+1 的块会继续尝试与它的伙伴合并,可能形成 order-n+2 的块,以此类推。这种积极的合并策略是伙伴系统对抗外部碎片的关键。

它的主要优势体现在哪些方面?

  • 高效对抗外部碎片:释放内存时积极的、快速的合并操作,使得小的空闲块能不断地重新组合成大的连续空闲块。
  • 分配和释放速度快:查找伙伴和进行分裂/合并的操作都非常快,主要是指针操作和位运算。
  • 满足连续内存需求:能够直接分配多种大小的、物理上连续的内存块。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 内部碎片:这是伙伴系统最主要的缺点。由于分配大小必须是2的幂次方个页,所以当应用程序请求一个非2的幂次大小的内存时,会造成浪费。例如,请求9KB内存(3个页),伙伴系统必须分配一个 order-2 的块(4个页,16KB),其中有7KB被浪费掉了。这种在一个已分配块内部的浪费称为内部碎片
  • 大块内存分配困难:尽管伙伴系统努力对抗碎片,但在一个长期运行且内存使用复杂的系统上,要分配一个非常大(如 order-10)的连续内存块仍然可能失败。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

伙伴系统是内核物理内存管理的唯一解决方案,它是所有其他内存分配机制的基础。内核开发者通常不会直接调用伙伴系统的函数(如 alloc_pages()),而是通过更高层的接口。

  • Slab分配器(SLUB/SLAB):当内核需要为许多小的、相同大小的对象(如inode结构体、task_struct)分配内存时,Slab分配器会向伙伴系统申请一个或多个连续的页(一个Slab),然后在这些页内部进行更精细的切分。这是为了解决伙伴系统的内部碎片问题。
  • 进程地址空间:当一个用户进程通过 brk()mmap() 扩展其堆或映射新内存时,内核需要为其分配物理页面来承载数据。这些物理页面最终都来源于伙伴系统。
  • DMA缓冲区:需要大块物理连续内存进行数据传输的设备驱动程序,会通过DMA API(最终调用伙伴系统)来申请一个高阶的内存块。
  • 内核栈和模块加载:为新线程创建内核栈,或者加载内核模块(.ko文件)时,所需的连续内存页也是由伙伴系统提供的。

是否有不推荐使用该技术的场景?为什么?

不应该直接使用伙伴系统(即 alloc_pages())的场景是:

  • 分配小的、对象大小的内存:对于几十或几百字节的内存需求,直接从伙伴系统分配一个完整的页(4KB)是极大的浪费。这种场景必须使用 kmalloc()(它背后是Slab分配器)或专门的内存池。

对比分析

请将其 与 其他相似技术 进行详细对比。

对比 Slab 分配器 (SLUB/SLAB)

特性 伙伴系统 (page_alloc.c) Slab 分配器 (mm/slub.c)
管理粒度 页(Page),通常为4KB,以及2的幂次方个页。 对象(Object),大小可以从几个字节到几千字节。
主要目标 管理整个物理内存,对抗外部碎片 在伙伴系统之上,为小对象提供内存,对抗内部碎片
关系 基础提供者。Slab分配器是伙伴系统的客户,它向伙伴系统申请”Slabs”(由页组成)。 上层消费者。它将从伙伴系统获得的页进行精细切分,以满足小内存请求。
使用接口 alloc_pages(), free_pages() kmalloc(), kfree(), kmem_cache_alloc()

对比 vmalloc()

特性 伙伴系统 (alloc_pages()/kmalloc()) vmalloc()
内存连续性 返回的内存是物理上连续的 返回的内存是虚拟地址上连续,但物理上不一定连续
实现方式 直接从物理内存空闲链表中分配。 分别从伙伴系统申请多个不连续的物理页面,然后在内核的虚拟地址空间中建立一段连续的页表映射,将这些物理页面映射到连续的虚拟地址上。
性能 。访问速度快,因为它通常位于内核直接映射区域,没有额外的页表转换开销。 较低。每次访问都可能需要通过页表转换,可能导致TLB未命中,开销较大。
大小限制 受物理内存碎片化程度限制,通常难以分配非常大的块(如几十MB)。 可以分配非常大的内存区域(可达GB级别),只要总的空闲物理内存足够。
使用场景 DMA缓冲区,性能敏感的数据结构,所有常规的内核内存分配。 需要一个非常大的、逻辑上连续的内存缓冲区,但又不要求物理上连续的场景,例如为某些内核模块或数据结构分配临时的大缓冲区。应尽量避免使用。

pcp 每个 CPU 的页面集

在 Linux 内核中,PCPPer-CPU Pageset 的缩写,表示每个 CPU 的页面集。它是内存管理子系统的一部分,用于优化页面分配和释放的性能。


PCP 的作用

  1. 减少锁争用

    • 内存页面的分配和释放是频繁的操作。如果每次都需要访问全局的内存管理结构(如 zone),会导致锁争用,降低性能。
    • PCP 通过为每个 CPU 提供一个本地的页面缓存,减少了对全局锁的依赖,从而提高了并发性能。
  2. 提升性能

    • PCP 缓存了一部分页面,允许页面分配和释放在 CPU 本地完成,避免频繁访问全局内存结构。
    • 这种本地化的设计减少了跨 CPU 的内存访问延迟。
  3. 批量操作

    • PCP 支持批量分配和释放页面。例如,当 PCP 缓存中的页面不足时,会一次性从全局 zone 中获取一批页面;释放时也会将多余的页面批量返还给全局 zone
    • 这种批量操作进一步减少了对全局结构的访问频率。

zone_pcp_init 的作用

zone_pcp_init 函数用于初始化与 zone 相关的 PCP 数据结构。它在内存初始化阶段调用,为每个内存区域(zone)设置默认的 PCP 配置。

关键字段

  • zone->per_cpu_pageset
    • 指向 PCP 数据结构的指针,用于存储每个 CPU 的页面集。
    • 在初始化阶段,指向 boot_pageset,这是一个临时的 PCP 数据结构。
  • zone->pageset_high_minzone->pageset_high_max
    • 配置 PCP 的页面缓存上限。
  • zone->pageset_batch
    • 配置批量操作的页面数量。

逻辑

  • 如果 zone 是有效的(通过 populated_zone 检查),会打印调试信息,包括 zone 的名称、页面数量和批量大小。

PCP 的使用场景

  1. 页面分配
    • 当某个 CPU 需要分配页面时,优先从本地的 PCP 缓存中获取页面。如果缓存中没有足够的页面,则从全局 zone 中获取。
  2. 页面释放
    • 页面释放时,优先将页面放入本地的 PCP 缓存。如果缓存已满,则将多余的页面返还给全局 zone
  3. NUMA 支持
    • 在 NUMA 系统中,每个节点的 zone 都有自己的 PCP,进一步优化了内存访问的本地性。

总结

PCP 是 Linux 内核内存管理中的一个重要机制,通过为每个 CPU 提供本地化的页面缓存,减少了全局锁争用和跨 CPU 的内存访问延迟,从而显著提升了页面分配和释放的性能。zone_pcp_init 函数负责初始化与 zone 相关的 PCP 数据结构,为系统的高效内存管理奠定了基础。

include/linux/gfp.h

gfp_zone 确定目标内存区域(zone)所在

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
/*
* 物理地址区修饰符(参见 linux/mmzone.h - 低 4 位)
*
* 不要对这些设置任何条件。如有必要,请修改不带下划线的定义,并始终如一地使用它们。此处的定义可用于位比较。
*/
#define __GFP_DMA ((__force gfp_t)___GFP_DMA)
#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM)
#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32)
#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */
#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)


#define GFP_ZONE_TABLE ( \
(ZONE_NORMAL << 0 * GFP_ZONES_SHIFT) \
| (OPT_ZONE_DMA << ___GFP_DMA * GFP_ZONES_SHIFT) \
| (OPT_ZONE_HIGHMEM << ___GFP_HIGHMEM * GFP_ZONES_SHIFT) \
| (OPT_ZONE_DMA32 << ___GFP_DMA32 * GFP_ZONES_SHIFT) \
| (ZONE_NORMAL << ___GFP_MOVABLE * GFP_ZONES_SHIFT) \
| (OPT_ZONE_DMA << (___GFP_MOVABLE | ___GFP_DMA) * GFP_ZONES_SHIFT) \
| (ZONE_MOVABLE << (___GFP_MOVABLE | ___GFP_HIGHMEM) * GFP_ZONES_SHIFT)\
| (OPT_ZONE_DMA32 << (___GFP_MOVABLE | ___GFP_DMA32) * GFP_ZONES_SHIFT)\
)

static inline enum zone_type gfp_zone(gfp_t flags)
{
enum zone_type z;
int bit = (__force int) (flags & GFP_ZONEMASK);

z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) &
((1 << GFP_ZONES_SHIFT) - 1);
VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1);
return z;
}

__get_free_pages 分配页面

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
struct page *__alloc_pages_noprof(gfp_t gfp, unsigned int order,
int preferred_nid, nodemask_t *nodemask)
{
struct page *page;

page = __alloc_frozen_pages_noprof(gfp, order, preferred_nid, nodemask);
if (page)
set_page_refcounted(page);
return page;
}
EXPORT_SYMBOL(__alloc_pages_noprof);

/*=
* Allocate pages, preferring the node given as nid. The node must be valid and
* online. For more general interface, see alloc_pages_node().
*/
static inline struct page *
__alloc_pages_node_noprof(int nid, gfp_t gfp_mask, unsigned int order)
{
VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES);
warn_if_node_offline(nid, gfp_mask);

return __alloc_pages_noprof(gfp_mask, order, nid, NULL);
}

/*
* 分配页面,优先考虑给定为nid的节点。当nid == NUMA_NO_NODE时,优先考虑当前CPU的最近节点。否则节点必须是有效且在线的。
*/
static inline struct page *alloc_pages_node_noprof(int nid, gfp_t gfp_mask,
unsigned int order)
{
if (nid == NUMA_NO_NODE)
nid = numa_mem_id();

return __alloc_pages_node_noprof(nid, gfp_mask, order);
}

static inline struct page *alloc_pages_noprof(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_node_noprof(numa_node_id(), gfp_mask, order);
}

/*
* 常用的帮助函数。切勿与 __GFP_HIGHMEM 一起使用,因为返回的地址无法表示高内存页面。如果需要访问高内存,使用 alloc_pages 然后使用 kmap。
*/
unsigned long get_free_pages_noprof(gfp_t gfp_mask, unsigned int order)
{
struct page *page;

page = alloc_pages_noprof(gfp_mask & ~__GFP_HIGHMEM, order);
if (!page)
return 0;
return (unsigned long) page_address(page);
}
EXPORT_SYMBOL(get_free_pages_noprof);

#define __get_free_pages(...) alloc_hooks(get_free_pages_noprof(__VA_ARGS__))

include/linux/vmstat.h

__mod_zone_page_state 修改区域页面状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline void zone_page_state_add(long x, struct zone *zone,
enum zone_stat_item item)
{
atomic_long_add(x, &zone->vm_stat[item]);
atomic_long_add(x, &vm_zone_stat[item]);
}

/*
* 我们不在单个处理器配置中维护差异。
* 这些函数直接修改区域和全局计数器。
*/
static inline void __mod_zone_page_state(struct zone *zone,
enum zone_stat_item item, long delta)
{
zone_page_state_add(delta, zone, item);
}

include/linux/page-flags.h

pageflags

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
/*
* 不要直接使用 pageflags。 使用 PageFoo 宏。
* Page flags 字段分为两部分,主标志区域从低位向上延伸,字段区域从高位向下延伸。
*
* |FIELD |... |FLAGS |
* N-1 ^ 0
* (NR_PAGEFLAGS)
* 字段区域保留给字段映射区域、节点(用于 NUMA)和 SPARSEMEM 部分(用于需要部分 ID 的 SPARSEMEM 变体,如 SPARSEMEM_EXTREME 与 !SPARSEMEM_VMEMMAP)。
*/
enum pageflags {
PG_locked, /* 页面被锁定。不要操作。 */
PG_writeback, /* 页面正在写回 */
PG_referenced, /* 页面被引用 */
PG_uptodate, /* 页面是最新的 */
PG_dirty, /* 页面已被修改 */
PG_lru, /* 页面在LRU链表中 */
PG_head, /* 必须是第6位 */
PG_waiters, /* 页面有等待者,请检查其等待队列。必须是第7位,并且与“PG_locked”在同一个字节中 */
PG_active, /* 页面是活动的 */
PG_workingset, /* 页面属于工作集 */
PG_owner_priv_1, /* 所有者使用。如果是页面缓存,文件系统可能会使用 */
PG_owner_2, /* 所有者使用。如果是页面缓存,文件系统可能会使用 */
PG_arch_1, /* 架构特定的标志 */
PG_reserved, /* 特殊页面,通常不应被触碰 */
PG_private, /* 如果是页面缓存,包含文件系统私有数据 */
PG_private_2, /* 如果是页面缓存,包含文件系统辅助数据 */
PG_reclaim, /* 应尽快回收 */
PG_swapbacked, /* 页面由RAM/交换支持 */
PG_unevictable, /* 页面是“不可驱逐的” */
PG_dropbehind, /* 在IO完成时丢弃页面 */
#ifdef CONFIG_MMU
PG_mlocked, /* 页面被vma锁定 */
#endif
#ifdef CONFIG_MEMORY_FAILURE
PG_hwpoison, /* 硬件损坏的页面。不要操作 */
#endif
#if defined(CONFIG_PAGE_IDLE_FLAG) && defined(CONFIG_64BIT)
PG_young, /* 页面最近被访问 */
PG_idle, /* 页面处于空闲状态 */
#endif
#ifdef CONFIG_ARCH_USES_PG_ARCH_2
PG_arch_2, /* 架构特定的标志 */
#endif
#ifdef CONFIG_ARCH_USES_PG_ARCH_3
PG_arch_3, /* 架构特定的标志 */
#endif
__NR_PAGEFLAGS, /* 页面标志的总数 */

PG_readahead = PG_reclaim, /* 预读页面 */

/* 匿名内存(和shmem) */
PG_swapcache = PG_owner_priv_1, /* 交换页面:swp_entry_t在private字段中 */
/* 一些文件系统 */
PG_checked = PG_owner_priv_1, /* 页面已检查 */

/*
* 根据匿名folio映射到页面表的方式(例如,头页面的单个PMD/PUD/CONT映射与PTE映射的THP),
* PG_anon_exclusive可能仅设置在头页面或匿名folio的尾页面上。目前,我们仅期望它在PTE映射的THP的尾页面上设置。
*/
PG_anon_exclusive = PG_owner_2,

/*
* 如果folio中的所有缓冲区头都已映射,则设置。
* 不使用缓冲区头的文件系统可以将其用于自己的目的。
*/
PG_mappedtodisk = PG_owner_2,

/* FS-Cache使用两个页面位来维护本地缓存状态。
* 当这些页面属于netfs的inode时,这些位会被设置。
*/
PG_fscache = PG_private_2, /* 页面由缓存支持 */

/* XEN */
/* 在Xen中作为只读页表页面固定。 */
PG_pinned = PG_owner_priv_1,
/* 作为域保存的一部分固定(参见xen_mm_pin_all())。 */
PG_savepinned = PG_dirty,
/* 具有另一个(外部)域页面的授权映射。 */
PG_foreign = PG_owner_priv_1,
/* 由swiotlb-xen重新映射。 */
PG_xen_remapped = PG_owner_priv_1,

/* 非LRU隔离的可移动页面 */
PG_isolated = PG_reclaim,

/* 仅对伙伴系统中的页面有效。用于跟踪已报告的页面 */
PG_reported = PG_uptodate,

#ifdef CONFIG_MEMORY_HOTPLUG
/* 用于自托管内存映射页面 */
PG_vmemmap_self_hosted = PG_owner_priv_1,
#endif

/*
* 仅对复合页面有效的标志。存储在第一个尾页面的flags字段中。
* 不能使用前8个标志或任何标记为PF_ANY的标志。
*/

/* folio中至少有一个页面设置了hwpoison标志 */
PG_has_hwpoisoned = PG_active,
PG_large_rmappable = PG_workingset, /* 匿名或文件支持 */
PG_partially_mapped = PG_reclaim, /* 被标识为部分映射 */
};

FOLIO_FLAG 操作 Folio(页簇)中的标志位

  • Folio 是内核中用于管理多个连续页面的抽象概念,旨在提高内存管理的效率。
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
//返回 Folio 中指定页面的标志位的只读指针
//指向 struct folio 的指针,表示需要操作的页簇
static const unsigned long *const_folio_flags(const struct folio *folio,
unsigned n)
{
const struct page *page = &folio->page;

VM_BUG_ON_PGFLAGS(page->compound_head & 1, page);
VM_BUG_ON_PGFLAGS(n > 0 && !test_bit(PG_head, &page->flags), page);
return &page[n].flags;
}

static unsigned long *folio_flags(struct folio *folio, unsigned n)
{
struct page *page = &folio->page;
//检查页面是否是复合页面的尾页面(Tail Page)。如果是,则触发调试错误
VM_BUG_ON_PGFLAGS(page->compound_head & 1, page);
//索引 n 大于 0 且页面不是复合页面的头页面(Head Page),触发调试错误
VM_BUG_ON_PGFLAGS(n > 0 && !test_bit(PG_head, &page->flags), page);
return &page[n].flags;
}

#define FOLIO_TEST_FLAG(name, page) \
static __always_inline bool folio_test_##name(const struct folio *folio) \
{ return test_bit(PG_##name, const_folio_flags(folio, page)); }

#define FOLIO_SET_FLAG(name, page) \
static __always_inline void folio_set_##name(struct folio *folio) \
{ set_bit(PG_##name, folio_flags(folio, page)); }

#define FOLIO_CLEAR_FLAG(name, page) \
static __always_inline void folio_clear_##name(struct folio *folio) \
{ clear_bit(PG_##name, folio_flags(folio, page)); }


#define FOLIO_FLAG(name, page) \
FOLIO_TEST_FLAG(name, page) \
FOLIO_SET_FLAG(name, page) \
FOLIO_CLEAR_FLAG(name, page)

Page Flags 页面标志(Page Flags)相关的策略

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
static inline const struct page *page_fixed_fake_head(const struct page *page)
{
return page;
}

static __always_inline int page_is_fake_head(const struct page *page)
{
return page_fixed_fake_head(page) != page;
}

//该函数返回给定页面的头页面地址。如果页面是复合页面的一部分,则返回复合页面的头页面;如果页面是普通页面,则返回其自身。
static __always_inline unsigned long _compound_head(const struct page *page)
{
unsigned long head = READ_ONCE(page->compound_head);

if (unlikely(head & 1)) //表明页面是复合页面的一部分
return head - 1; //返回复合页面的头页面地址
//page_fixed_fake_head 是一个辅助函数,用于处理特殊情况,例如伪造的页面头
return (unsigned long)page_fixed_fake_head(page); //返回页面本身的地址
}

#define compound_head(page) ((typeof(page))_compound_head(page))

//检查页面是否是复合页面(Compound Page)的尾页面(Tail Page)
static __always_inline int PageTail(const struct page *page)
{
//如果最低位为 1,则表示该页面是尾页面 检查页面是否是伪造的头页面
return READ_ONCE(page->compound_head) & 1 || page_is_fake_head(page);
}
//检查页面是否是复合页面的一部分
static __always_inline int PageCompound(const struct page *page)
{
return test_bit(PG_head, &page->flags) || //如果标志位被设置,表示页面是复合页面的头页面
READ_ONCE(page->compound_head) & 1; //如果最低位为 1,则表示该页面是复合页面的一部分
}

#define PAGE_POISON_PATTERN -1l //-1l 是一个长整型的全 1 值(即所有位都为 1
//定义一个特殊的模式值,用于标记页面为“损坏”或未初始化。
static inline int PagePoisoned(const struct page *page)
{
return READ_ONCE(page->flags) == PAGE_POISON_PATTERN;
}

static __always_inline int PageHead(const struct page *page)
{
PF_POISONED_CHECK(page);
return test_bit(PG_head, &page->flags) && !page_is_fake_head(page);
}

/*
* PF_POISONED_CHECK:检查页面是否被标记为“损坏”(poisoned)或未初始化。
* PF_ANY:页面标志适用于普通页面、小页面、复合页面的头页面和尾页面。
* PF_HEAD:对于复合页面,页面标志的操作仅应用于头页面。
* PF_NO_TAIL:页面标志的修改只能在普通页面或复合页面的头页面上进行,但检查可以在尾页面上进行。
* PF_NO_COMPOUND:页面标志不适用于复合页面。
* PF_SECOND:页面标志存储在复合页面的第一个尾页面中。
*/
#define PF_POISONED_CHECK(page) ({ \
//检查页面是否有 PagePoisoned 标志。如果页面被标记为损坏,则触发调试错误
VM_BUG_ON_PGFLAGS(PagePoisoned(page), page); \
page; })
#define PF_ANY(page, enforce) PF_POISONED_CHECK(page)
#define PF_HEAD(page, enforce) PF_POISONED_CHECK(compound_head(page)) //使用 compound_head 获取复合页面的头页面
#define PF_NO_TAIL(page, enforce) ({ \ //禁止在尾页面上修改页面标志,但允许检查
VM_BUG_ON_PGFLAGS(enforce && PageTail(page), page); \ //如果 enforce 为真且页面是尾页面(PageTail),则触发调试错误。
PF_POISONED_CHECK(compound_head(page)); })
#define PF_NO_COMPOUND(page, enforce) ({ \ //页面标志不适用于复合页面
VM_BUG_ON_PGFLAGS(enforce && PageCompound(page), page); \ //如果 enforce 为真且页面是复合页面(PageCompound),则触发调试错误
PF_POISONED_CHECK(page); })
#define PF_SECOND(page, enforce) ({ \ //页面标志存储在复合页面的第一个尾页面中
VM_BUG_ON_PGFLAGS(!PageHead(page), page); \ //如果页面不是头页面(!PageHead),则触发调试错误。
PF_POISONED_CHECK(&page[1]); }) //检查复合页面的第一个尾页面(&page[1])是否有效

PAGEFLAG

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define TESTPAGEFLAG(uname, lname, policy)				\
// Folio(页簇)相关的辅助宏,用于扩展标志的功能。
FOLIO_TEST_FLAG(lname, FOLIO_##policy) \
//生成的函数名
static __always_inline int Page##uname(const struct page *page) \
{ return test_bit(PG_##lname, &policy(page, 0)->flags); } //检查 policy(page, 0)->flags 中对应的位是否被设置

#define SETPAGEFLAG(uname, lname, policy) \
FOLIO_SET_FLAG(lname, FOLIO_##policy) \
static __always_inline void SetPage##uname(struct page *page) \
{ set_bit(PG_##lname, &policy(page, 1)->flags); }

#define CLEARPAGEFLAG(uname, lname, policy) \
FOLIO_CLEAR_FLAG(lname, FOLIO_##policy) \
static __always_inline void ClearPage##uname(struct page *page) \
{ clear_bit(PG_##lname, &policy(page, 1)->flags); }


//为指定的页面标志生成测试、设置和清除操作
#define PAGEFLAG(uname, lname, policy) \
TESTPAGEFLAG(uname, lname, policy) \
SETPAGEFLAG(uname, lname, policy) \
CLEARPAGEFLAG(uname, lname, policy)

PAGEFLAG Reserved 宏定义

1
2
3
4
//标志的名称	标志的内部字段名称	该标志不适用于复合页面
PAGEFLAG(Reserved, reserved, PF_NO_COMPOUND)
__CLEARPAGEFLAG(Reserved, reserved, PF_NO_COMPOUND) //用于清除页面的 Reserved 标志
__SETPAGEFLAG(Reserved, reserved, PF_NO_COMPOUND) //设置页面的 Reserved 标志

page_folio 将 struct page 转换为 struct folio

  • 在 Linux 内核中,struct page 是用于描述单个物理内存页的核心数据结构。然而,随着内存管理需求的增加,内核引入了 struct folio,它表示一组连续的物理页,旨在减少对单个页的操作开销并提高内存管理的效率
1
2
3
4
5
6
7
8
9
10
11
12
/**
* page_folio - 从页面转换为作品集。
* @p:页面。
*
* 每一页都是作品集的一部分。 不能在 NULL 指针上调用此函数。
*
* 上下文:@page上不需要引用或锁定。 如果调用方没有引用,则此调用可能会与作品集拆分赛跑,因此在获得作品集的引用后,它应重新检查作品集仍包含此页面。返回:包含此页面的作品集。
*/
#define page_folio(p) (_Generic((p), \
const struct page *: (const struct folio *)_compound_head(p), \
struct page *: (struct folio *)_compound_head(p)))

PAGE_TYPE_OPS page_type 操作

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
#define FOLIO_TYPE_OPS(lname, fname)					\
static __always_inline bool folio_test_##fname(const struct folio *folio) \
{ \
return data_race(folio->page.page_type >> 24) == PGTY_##lname; \
} \
static __always_inline void __folio_set_##fname(struct folio *folio) \
{ \
if (folio_test_##fname(folio)) \
return; \
VM_BUG_ON_FOLIO(data_race(folio->page.page_type) != UINT_MAX, \
folio); \
folio->page.page_type = (unsigned int)PGTY_##lname << 24; \
} \
static __always_inline void __folio_clear_##fname(struct folio *folio) \
{ \
if (folio->page.page_type == UINT_MAX) \
return; \
VM_BUG_ON_FOLIO(!folio_test_##fname(folio), folio); \
folio->page.page_type = UINT_MAX; \
}

#define PAGE_TYPE_OPS(uname, lname, fname) \
FOLIO_TYPE_OPS(lname, fname) \
//检查页面是否属于指定的类型
static __always_inline int Page##uname(const struct page *page) \
{ \
//检查 page->page_type 的高 8 位(page->page_type >> 24)是否等于 PGTY_##lname 来判断页面类型
return data_race(page->page_type >> 24) == PGTY_##lname; \
} \
static __always_inline void __SetPage##uname(struct page *page) \
{ \
//页面已经是目标类型,则直接返回
if (Page##uname(page)) \
return; \
//表示页面未被初始化或处于非法状态),触发调试错误
VM_BUG_ON_PAGE(data_race(page->page_type) != UINT_MAX, page); \
//将页面类型设置为 PGTY_##lname 的高 8 位
page->page_type = (unsigned int)PGTY_##lname << 24; \
} \
static __always_inline void __ClearPage##uname(struct page *page) \
{ \
if (page->page_type == UINT_MAX) \
return; \
VM_BUG_ON_PAGE(!Page##uname(page), page); \
page->page_type = UINT_MAX; \
}

/*
* PageBuddy() indicates that the page is free and in the buddy system
* (see mm/page_alloc.c).
*/
PAGE_TYPE_OPS(Buddy, buddy, buddy)

mm/internal.h

find_buddy_page_pfn 查找伙伴页面

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
/* - 检查两个页面是否可以作为伙伴合并
- 合并条件:
1. 伙伴页面未被标记为“保护页”且在伙伴系统中:
- page_is_guard(buddy) 检查页面是否为保护页。
- PageBuddy(buddy) 检查页面是否在伙伴系统中。
2. 伙伴页面与当前页面的阶数(order)相同:
- buddy_order(buddy) 获取伙伴页面的阶数。
3. 伙伴页面与当前页面属于同一个内存区域(zone):
- page_zone_id(page) 和 page_zone_id(buddy) 检查页面的内存区域 ID。
4. 伙伴页面的引用计数为 0:
- page_count(buddy) 确保伙伴页面未被其他进程或内核模块使用。 */
static inline bool page_is_buddy(struct page *page, struct page *buddy,
unsigned int order)
{
if (!page_is_guard(buddy) && !PageBuddy(buddy))
return false;

if (buddy_order(buddy) != order)
return false;

/*
* zone check is done late to avoid uselessly calculating
* zone/node ids for pages that could never merge.
*/
if (page_zone_id(page) != page_zone_id(buddy))
return false;

VM_BUG_ON_PAGE(page_count(buddy) != 0, buddy);

return true;
}

/* 功能:根据页面帧号(PFN)和阶数(order)计算伙伴页面的帧号。
实现原理:
使用公式 B2 = B1 ^ (1 << O) 计算伙伴页面的帧号。
例如,对于帧号为 8 的页面,其阶数为 1 的伙伴页面帧号为 8 ^ (1 << 1) = 10。
返回值:伙伴页面的帧号。
用途:快速定位伙伴页面,便于后续的合并操作 */
static inline unsigned long
__find_buddy_pfn(unsigned long page_pfn, unsigned int order)
{
return page_pfn ^ (1 << order);
}

/*
* Find the buddy of @page and validate it.
* @page: The input page
* @pfn: The pfn of the page, it saves a call to page_to_pfn() when the
* function is used in the performance-critical __free_one_page().
* @order: The order of the page
* @buddy_pfn: The output pointer to the buddy pfn, it also saves a call to
* page_to_pfn().
*
* The found buddy can be a non PageBuddy, out of @page's zone, or its order is
* not the same as @page. The validation is necessary before use it.
*
* Return: the found buddy page or NULL if not found.
*/
static inline struct page *find_buddy_page_pfn(struct page *page,
unsigned long pfn, unsigned int order, unsigned long *buddy_pfn)
{
unsigned long __buddy_pfn = __find_buddy_pfn(pfn, order);
struct page *buddy;

buddy = page + (__buddy_pfn - pfn);
if (buddy_pfn)
*buddy_pfn = __buddy_pfn;

if (page_is_buddy(page, buddy, order))
return buddy;
return NULL;
}

__alloc_frozen_pages 分配冻结页面

1
2
#define __alloc_frozen_pages(...) \
alloc_hooks(__alloc_frozen_pages_noprof(__VA_ARGS__))

include/linux/page_ref.h

page_ref_add_unless 尝试增加页面(struct page)的引用计数,但仅在引用计数不等于指定的值 u 时执行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static inline bool page_ref_add_unless(struct page *page, int nr, int u)
{
bool ret = false;

rcu_read_lock();
/*避免写入正在重新映射的 VMEMMAP 区域*/
/* 检查页面是否可写 true */
if (page_count_writable(page, u))
/* 尝试将页面的引用计数增加 nr
仅当引用计数不等于 u 时,操作才会成功*/
ret = atomic_add_unless(&page->_refcount, nr, u);
rcu_read_unlock();

/* 启用了 page_ref_mod_unless 跟踪点,
调用 __page_ref_mod_unless 记录引用计数的修改操作 */
if (page_ref_tracepoint_active(page_ref_mod_unless))
__page_ref_mod_unless(page, nr, ret);
return ret;
}

folio_ref_add_unless 尝试增加 folio(页面的抽象表示)的引用计数

1
2
3
4
5
6
7
static inline bool folio_ref_add_unless(struct folio *folio, int nr, int u)
{
/* folio:表示页面的抽象结构,通常用于管理页面缓存或内存对象。
nr:要增加的引用计数值。
u:条件值,表示引用计数的限制 */
return page_ref_add_unless(&folio->page, nr, u);
}

folio_try_get 尝试增加 folio(页面的抽象表示)的引用计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* folio_try_get - 尝试增加作品集的 refcount。
* @folio:对开页。
*
* 如果还没有对作品集的引用,则可以尝试使用此函数获取一个。
* 例如,如果发现指向作品集的指针后作品集已被释放,或者作品集因拆分或迁移而被冻结,则可能会失败。
*
* 返回:如果引用计数成功递增,则为 True。
*/
static inline bool folio_try_get(struct folio *folio)
{
/* 引用计数当前为 0(表示页面已被释放或冻结),操作将失败 */
return folio_ref_add_unless(folio, 1, 0);
}

page_ref_dec_and_test 减少页面(struct page)的引用计数,并检查是否已减少到零

1
2
3
4
5
6
7
8
static inline int page_ref_dec_and_test(struct page *page)
{
int ret = atomic_dec_and_test(&page->_refcount);

if (page_ref_tracepoint_active(page_ref_mod_and_test))
__page_ref_mod_and_test(page, -1, ret);
return ret;
}

include/linux/mm.h

folio_put 减少 folio(页面的抽象表示)的引用计数

  • 当引用计数减少到零时,页面的内存会被释放回页面分配器(page allocator),以供其他内存分配使用。该函数在 Linux 内核中用于管理页面的生命周期,确保内存资源的高效回收。
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
/*
* 删除 ref,如果 refcount 降至零(页面没有用户),则返回 true
*/
static inline int put_page_testzero(struct page *page)
{
VM_BUG_ON_PAGE(page_ref_count(page) == 0, page);
return page_ref_dec_and_test(page);
}

static inline int folio_put_testzero(struct folio *folio)
{
return put_page_testzero(&folio->page);
}

/**
* folio_put - 递减作品集上的引用计数。
* @folio:对开页。
*
* 如果作品集的引用计数达到零,则内存将被释放回页面分配器,并可能立即被另一个分配使用。 请勿在调用 folio_put() 后访问内存或结构作品集,除非您可以确定它不是最后一个引用。
*
* 上下文:可以在进程或中断上下文中调用,但不能在 NMI 中调用
*上下文。 可以在持有 spinlock 时调用。
*/
static inline void folio_put(struct folio *folio)
{
/* 调用 folio_put_testzero 减少 folio 的引用计数,并检查引用计数是否已减少到零。 */
if (folio_put_testzero(folio))
__folio_put(folio);
}

include/linux/pagemap.h

wake_page_match 检查等待页面队列中的页面是否与给定的键匹配

1
2
3
4
5
6
7
8
9
10
11
12
static inline bool wake_page_match(struct wait_page_queue *wait_page,
struct wait_page_key *key)
{
if (wait_page->folio != key->folio)
return false;
key->page_match = 1;

if (wait_page->bit_nr != key->bit_nr)
return false;

return true;
}

mm/page_alloc.c

zone_pcp_init 每个 CPU 的页面集区域初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__meminit void zone_pcp_init(struct zone *zone)
{
/*
* per CPU 子系统此时未启动。下面的代码依赖于链接器的能力,
* 以提供 per cpu 变量到 per cpu 区域中的 (static) 偏移量。
*/
zone->per_cpu_pageset = &boot_pageset;
zone->per_cpu_zonestats = &boot_zonestats;
zone->pageset_high_min = BOOT_PAGESET_HIGH;
zone->pageset_high_max = BOOT_PAGESET_HIGH;
zone->pageset_batch = BOOT_PAGESET_BATCH;
/* //Returns true if a zone has memory
static inline bool populated_zone(struct zone *zone)
{
return zone->present_pages;
} */
if (populated_zone(zone))
pr_debug(" %s zone: %lu pages, LIFO batch:%u\n", zone->name,
zone->present_pages, zone_batchsize(zone));
}

set_pfnblock_flags_mask 为 pageblock_nr_pages 个页面块设置请求的标志组

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
/**
* set_pfnblock_flags_mask - 为 pageblock_nr_pages 个页面块设置请求的标志组
* @page:感兴趣的块内的页面
* @flags:要设置的标志
* @pfn:目标页面框架编号
* @mask:调用方感兴趣的位掩码
*/
void set_pfnblock_flags_mask(struct page *page, unsigned long flags,
unsigned long pfn,
unsigned long mask)
{
unsigned long *bitmap;
unsigned long bitidx, word_bitidx;
unsigned long word;

BUILD_BUG_ON(NR_PAGEBLOCK_BITS != 4);
BUILD_BUG_ON(MIGRATE_TYPES > (1 << PB_migratetype_bits));

bitmap = get_pageblock_bitmap(page, pfn);
bitidx = pfn_to_bitidx(page, pfn);
word_bitidx = bitidx / BITS_PER_LONG;
bitidx &= (BITS_PER_LONG-1);

VM_BUG_ON_PAGE(!zone_spans_pfn(page_zone(page), pfn), page);

mask <<= bitidx;
flags <<= bitidx;

word = READ_ONCE(bitmap[word_bitidx]);
do {
} while (!try_cmpxchg(&bitmap[word_bitidx], &word, (word & ~mask) | flags));
}

set_pageblock_migratetype 设置页面块的迁移类型

1
2
3
4
5
6
7
8
9
void set_pageblock_migratetype(struct page *page, int migratetype)
{
if (unlikely(page_group_by_mobility_disabled &&
migratetype < MIGRATE_PCPTYPES))
migratetype = MIGRATE_UNMOVABLE;

set_pfnblock_flags_mask(page, (unsigned long)migratetype,
page_to_pfn(page), MIGRATETYPE_MASK);
}

__build_all_zonelists 初始化 zonelists

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
/*
* 构建分配回退区域列表。
*
* 将节点的所有填充区域添加到区域列表。
*/
static int build_zonerefs_node(pg_data_t *pgdat, struct zoneref *zonerefs)
{
struct zone *zone;
enum zone_type zone_type = MAX_NR_ZONES; //2
int nr_zones = 0;

do {
zone_type--;
zone = pgdat->node_zones + zone_type;
if (populated_zone(zone)) { // zone->present_pages;
zoneref_set_zone(zone, &zonerefs[nr_zones++]);
check_highest_zone(zone_type);
}
} while (zone_type);

return nr_zones;
}

static void build_zonelists(pg_data_t *pgdat)
{
struct zoneref *zonerefs;
int nr_zones;

zonerefs = pgdat->node_zonelists[ZONELIST_FALLBACK]._zonerefs;
nr_zones = build_zonerefs_node(pgdat, zonerefs);
zonerefs += nr_zones;

zonerefs->zone = NULL;
zonerefs->zone_idx = 0;
}

static void __build_all_zonelists(void *data)
{
int nid;
int __maybe_unused cpu;
pg_data_t *self = data;
unsigned long flags;

/*
* 必须使用 irqsave 获取 zonelist_update_seq,因为可以使用 GFP_ATOMIC 从 IRQ 调用读取器。
*/
write_seqlock_irqsave(&zonelist_update_seq, flags);
/*
* 此外,请禁用同步 printk() 以防止任何 printk() 尝试持有 port->lock,
* 因为其他 CPU 上的 tty_insert_flip_string_and_push_buffer() 可能会在持有 port->lock 的情况下调用 kmalloc(GFP_ATOMIC | __GFP_NOWARN)。
*/
printk_deferred_enter();

/*
*此节点已热添加,尚不存在内存。 所以只构建 zonelist 就可以了 - 不需要接触其他节点。
*/
if (self && !node_online(self->node_id)) {
build_zonelists(self);
} else {
/*
* 所有可能的节点都预先分配了 pgdat free_area_init
*/
for_each_node(nid) {
pg_data_t *pgdat = NODE_DATA(nid); //&contig_page_data

build_zonelists(pgdat);
}
}

printk_deferred_exit();
write_sequnlock_irqrestore(&zonelist_update_seq, flags);
}

per_cpu_pages_init 初始化每个 CPU 的页面集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void per_cpu_pages_init(struct per_cpu_pages *pcp, struct per_cpu_zonestat *pzstats)
{
int pindex;

memset(pcp, 0, sizeof(*pcp));
memset(pzstats, 0, sizeof(*pzstats));

spin_lock_init(&pcp->lock);
for (pindex = 0; pindex < NR_PCP_LISTS; pindex++)
INIT_LIST_HEAD(&pcp->lists[pindex]);

/*
* Set batch and high values safe for a boot pageset. A true percpu
* pageset's initialization will update them subsequently. Here we don't
* need to be as careful as pageset_update() as nobody can access the
* pageset yet.
*/
pcp->high_min = BOOT_PAGESET_HIGH;
pcp->high_max = BOOT_PAGESET_HIGH;
pcp->batch = BOOT_PAGESET_BATCH;
pcp->free_count = 0;
}

build_all_zonelists_init 初始化所有 zonelists

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 noinline void __init
build_all_zonelists_init(void)
{
int cpu;

__build_all_zonelists(NULL);

/*
* Initialize the boot_pagesets that are going to be used
* for bootstrapping processors. The real pagesets for
* each zone will be allocated later when the per cpu
* allocator is available.
*
* boot_pagesets are used also for bootstrapping offline
* cpus if the system is already booted because the pagesets
* are needed to initialize allocators on a specific cpu too.
* F.e. the percpu allocator needs the page allocator which
* needs the percpu allocator in order to allocate its pagesets
* (a chicken-egg dilemma).
*/
for_each_possible_cpu(cpu)
per_cpu_pages_init(&per_cpu(boot_pageset, cpu), &per_cpu(boot_zonestats, cpu));

mminit_verify_zonelist();
cpuset_init_current_mems_allowed();
}

build_all_zonelists 初始化 zonelist

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
/*
* 除非 system_state == SYSTEM_BOOTING。
*
* __ref由于调用__init注释的帮助程序 build_all_zonelists_init [受 SYSTEM_BOOTING 保护]。
*/
void __ref build_all_zonelists(pg_data_t *pgdat)
{
unsigned long vm_total_pages;

if (system_state == SYSTEM_BOOTING) {
build_all_zonelists_init();
} else {
__build_all_zonelists(pgdat);
/* cpuset 刷新例程应在此处 */
}
/* 获取所有区域中超出高水印的可用页面数. */
vm_total_pages = nr_free_zone_pages(gfp_zone(GFP_HIGHUSER_MOVABLE));
/*
* 如果系统中的页数太少,无法使该机制正常工作,则禁用按移动分组。
* 按区域检查会更准确,但成本更高。此检查是在 memory-hotadd 上进行的,
* 因此系统可以在禁用移动性的情况下启动,并在以后启用它 */
if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))
page_group_by_mobility_disabled = 1;
else
page_group_by_mobility_disabled = 0;

pr_info("Built %u zonelists, mobility grouping %s. Total pages: %ld\n",
nr_online_nodes,
str_off_on(page_group_by_mobility_disabled),
vm_total_pages);
#ifdef CONFIG_NUMA
pr_info("Policy zone: %s\n", zone_names[policy_zone]);
#endif
}

free_pages_prepare 准备释放页面

  • 用于在释放物理内存页之前执行一系列的检查和准备工作
  • 对即将释放的内存页进行检查、标记清理和初始化,以确保这些页可以安全地重新纳入内存管理系统。它处理了多种特殊情况,例如复合页(compound pages)、硬件内存错误(HWPoison)、内存锁定(mlocked)等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__always_inline bool free_pages_prepare(struct page *page,
unsigned int order)
{
int bad = 0;
bool skip_kasan_poison = should_skip_kasan_poison(page); //跳过 KASAN(Kernel Address Sanitizer)标记
bool init = want_init_on_free(); //判断是否需要在释放时初始化内存
bool compound = PageCompound(page); //检查页是否是复合页(compound page)
struct folio *folio = page_folio(page); //将 struct page 转换为 struct folio

VM_BUG_ON_PAGE(PageTail(page), page); //验证页是否是尾页(tail page),如果是,则触发内核调试错误
//证复合页(compound page)的 order 是否与预期值一致。如果不一致,则触发内核调试错误
VM_BUG_ON_PAGE(compound && compound_order(page) != order, page);

page->flags &= ~PAGE_FLAGS_CHECK_AT_PREP;

return true;
}

__del_page_from_free_list 从空闲列表中删除页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static inline void __del_page_from_free_list(struct page *page, struct zone *zone,
unsigned int order, int migratetype)
{
int nr_pages = 1 << order;
//确保页面的迁移类型与传入的 migratetype 参数一致
VM_WARN_ONCE(get_pageblock_migratetype(page) != migratetype,
"page type is %lu, passed migratetype is %d (nr=%d)\n",
get_pageblock_migratetype(page), migratetype, nr_pages);

/*清除报告状态并更新报告页数*/
if (page_reported(page))
__ClearPageReported(page);

list_del(&page->buddy_list); //将页面从其所在的伙伴系统空闲列表中移除
__ClearPageBuddy(page); //清除页面的 "Buddy" 标志,表示该页面不再属于伙伴系统的空闲页面
set_page_private(page, 0); //清除页面的私有数据字段,确保页面不再携带任何伙伴系统相关的元数据
zone->free_area[order].nr_free--;

if (order >= pageblock_order && !is_migrate_isolate(migratetype))
__mod_zone_page_state(zone, NR_FREE_PAGES_BLOCKS, -nr_pages);
}

__add_to_free_list 将页面添加到空闲列表

  • 将页面块添加到伙伴系统(Buddy System)的空闲列表中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 用于不在其他列表中的页面 */
static inline void __add_to_free_list(struct page *page, struct zone *zone,
unsigned int order, int migratetype,
bool tail)
{
struct free_area *area = &zone->free_area[order];
int nr_pages = 1 << order;
//确保页面的迁移类型与传入的 migratetype 参数一致。
VM_WARN_ONCE(get_pageblock_migratetype(page) != migratetype,
"page type is %lu, passed migratetype is %d (nr=%d)\n",
get_pageblock_migratetype(page), migratetype, nr_pages);

if (tail)
list_add_tail(&page->buddy_list, &area->free_list[migratetype]);
else
list_add(&page->buddy_list, &area->free_list[migratetype]);
area->nr_free++;

if (order >= pageblock_order && !is_migrate_isolate(migratetype))
__mod_zone_page_state(zone, NR_FREE_PAGES_BLOCKS, nr_pages);
}

set_buddy_order 设置页面的伙伴顺序

1
2
3
4
5
6
7
8
9
10
static inline void set_page_private(struct page *page, unsigned long private)
{
page->private = private;
}

static inline void set_buddy_order(struct page *page, unsigned int order)
{
set_page_private(page, order);
__SetPageBuddy(page);
}

__free_one_page 释放页面

  • 实现了一个用于伙伴系统(buddy system)分配器的页面释放函数 __free_one_page。伙伴系统是一种高效的内存管理机制,通过将内存划分为不同大小的块(以 2 的幂次为单位),并在释放时合并相邻的空闲块来减少内存碎片
  • 将一个页面块释放回伙伴系统,并根据需要合并相邻的空闲块(称为“伙伴”)以形成更大的块。它还负责更新内存区域的元数据和迁移类型(migratetype),并通知相关子系统
  • 伙伴系统的基本概念
  1. 伙伴关系:
    • 在伙伴系统中,每个内存块都有一个“伙伴”,即与其大小相同且地址连续的块。如果两个伙伴块都空闲,它们可以合并为一个更大的块。
  2. 分配与释放:
    • 当分配一个小块时,可能需要将一个大块拆分为多个小块。当释放一个块时,如果其伙伴也空闲,则触发合并操作。
  3. 页面标记:
    • 每个页面块的大小(order)记录在 page_private(page) 字段中。
    • 空闲页面块通过 PageBuddy 标志标记
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
//更新内存区域(zone)中空闲页面的统计信息。它根据页面的迁移类型(migratetype)调整相关的计数器,以确保内存管理系统的元数据保持一致。
static inline void account_freepages(struct zone *zone, int nr_pages,
int migratetype)
{
lockdep_assert_held(&zone->lock);
//隔离页面
if (is_migrate_isolate(migratetype))
return;

__mod_zone_page_state(zone, NR_FREE_PAGES, nr_pages);

if (is_migrate_cma(migratetype)) //CMA 页面计数更新
__mod_zone_page_state(zone, NR_FREE_CMA_PAGES, nr_pages);
else if (is_migrate_highatomic(migratetype)) //高原子性页面计数更新
WRITE_ONCE(zone->nr_free_highatomic,
zone->nr_free_highatomic + nr_pages);
}

static inline void __free_one_page(struct page *page,
unsigned long pfn,
struct zone *zone, unsigned int order,
int migratetype, fpi_t fpi_flags)
{
struct capture_control *capc = task_capc(zone);
unsigned long buddy_pfn = 0;
unsigned long combined_pfn;
struct page *buddy;
bool to_tail;

VM_BUG_ON(!zone_is_initialized(zone)); //验证内存区域是否已初始化
//检查页面标志是否符合预期
VM_BUG_ON_PAGE(page->flags & PAGE_FLAGS_CHECK_AT_PREP, page);
//验证迁移类型(migratetype)是否有效
VM_BUG_ON(migratetype == -1);
//确保页框号(pfn)按照块大小对齐
VM_BUG_ON_PAGE(pfn & ((1 << order) - 1), page);
VM_BUG_ON_PAGE(bad_range(zone, page), page);
//更新内存区域的空闲页面计数,记录释放的页面数量
account_freepages(zone, 1 << order, migratetype);
//合并循环
while (order < MAX_PAGE_ORDER) {
int buddy_mt = migratetype;

//在循环中查找当前块的伙伴块(buddy
buddy = find_buddy_page_pfn(page, pfn, order, &buddy_pfn);
if (!buddy)
goto done_merging;
//防止跨迁移类型的合并
//如果块的大小超过页面块的粒度
if (unlikely(order >= pageblock_order)) {
buddy_mt = get_pfnblock_migratetype(buddy, buddy_pfn);
//检查迁移类型是否匹配 防止不同迁移类型的块合并,以避免内存管理中的不一致
if (migratetype != buddy_mt &&
(!migratetype_is_mergeable(migratetype) ||
!migratetype_is_mergeable(buddy_mt)))
goto done_merging;
}

//处理守护页(guard page)
if (page_is_guard(buddy))
//如果伙伴块是守护页(guard page),清除其守护标志
clear_page_guard(zone, buddy, order);
else
//否则,从空闲列表中移除伙伴块
__del_page_from_free_list(buddy, zone, order, buddy_mt);
//如果伙伴块的迁移类型与当前块不匹配,更新伙伴块的迁移类型
if (unlikely(buddy_mt != migratetype)) {
set_pageblock_migratetype(buddy, migratetype);
}
//如果伙伴块存在且可以合并,则将两个块合并为更大的块,并更新页框号(pfn)和块大小(order)
combined_pfn = buddy_pfn & pfn;
page = page + (combined_pfn - pfn);
pfn = combined_pfn;
order++;
}

done_merging:
//设置最终合并后的块大小(order
set_buddy_order(page, order);

if (fpi_flags & FPI_TO_TAIL)
to_tail = true;
else if (is_shuffle_order(order))
to_tail = shuffle_pick_tail();
else
to_tail = buddy_merge_likely(pfn, buddy_pfn, page, order);
//根据策略(如是否放置到尾部)将块加入空闲列表
__add_to_free_list(page, zone, order, migratetype, to_tail);

/* 通知页面报告子系统 */
if (!(fpi_flags & FPI_SKIP_REPORT_NOTIFY))
page_reporting_notify_free(order); //通知页面报告子系统,释放了指定大小的页面块
}

split_large_buddy 将多块自由页面拆分为单独的页面块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*将多块自由页面拆分为单独的页面块。 */
static void split_large_buddy(struct zone *zone, struct page *page,
unsigned long pfn, int order, fpi_t fpi)
{
unsigned long end = pfn + (1 << order); //计算页块的结束地址
//检查页框号(pfn)是否按照页块大小对齐
VM_WARN_ON_ONCE(!IS_ALIGNED(pfn, 1 << order));
//检查页是否已被标记为伙伴页(PageBuddy)。如果是,触发内核警告,因为调用者应该已经从空闲列表中移除了该页
VM_WARN_ON_ONCE(PageBuddy(page));
//调整拆分的最大粒度
if (order > pageblock_order)
order = pageblock_order;
//循环拆分页块
do {
//调用 get_pfnblock_migratetype 获取当前页块的迁移类型(migratetype)
int mt = get_pfnblock_migratetype(page, pfn);
//调用 __free_one_page 将当前页块释放到伙伴系统中
__free_one_page(page, pfn, zone, order, mt, fpi);
pfn += 1 << order;
if (pfn == end)
break;
page = pfn_to_page(pfn);
} while (1);
}

free_one_page 将指定的内存页块释放到其所属的内存区域(zone)中

  • 将指定的内存页块释放到其所属的内存区域(zone)中
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 add_page_to_zone_llist(struct zone *zone, struct page *page,
unsigned int order)
{
/* Remember the order */
page->order = order;
/* Add the page to the free list */
llist_add(&page->pcp_llist, &zone->trylock_free_pages);
}

static void free_one_page(struct zone *zone, struct page *page,
unsigned long pfn, unsigned int order,
fpi_t fpi_flags)
{
struct llist_head *llhead;
unsigned long flags;
//尝试获取自旋锁
if (!spin_trylock_irqsave(&zone->lock, flags)) {
//获取失败且标志位包含 FPI_TRYLOCK
if (unlikely(fpi_flags & FPI_TRYLOCK)) {
//将页块加入延迟释放队列 zone->trylock_free_pages
add_page_to_zone_llist(zone, page, order);
return;
}
//如果未设置 FPI_TRYLOCK,则使用阻塞式的 spin_lock_irqsave 获取锁
spin_lock_irqsave(&zone->lock, flags);
}

/* 锁定成功。处理延迟的页面. */
llhead = &zone->trylock_free_pages;
//检查延迟释放队列是否非空, 并且当前未设置 FPI_TRYLOCK 标志
if (unlikely(!llist_empty(llhead) && !(fpi_flags & FPI_TRYLOCK))) {
struct llist_node *llnode;
struct page *p, *tmp;
//将所有延迟释放的页块从队列中取出
llnode = llist_del_all(llhead);
llist_for_each_entry_safe(p, tmp, llnode, pcp_llist) {
unsigned int p_order = p->order;
//调用 split_large_buddy 将它们释放到伙伴系统
split_large_buddy(zone, p, page_to_pfn(p), p_order, fpi_flags);
__count_vm_events(PGFREE, 1 << p_order); //使用 __count_vm_events 记录释放的页块数量
}
}
//释放当前页块
split_large_buddy(zone, page, pfn, order, fpi_flags);
spin_unlock_irqrestore(&zone->lock, flags); //释放自旋锁,恢复中断状态

__count_vm_events(PGFREE, 1 << order); //使用 __count_vm_events 记录释放的页块数量
}

__free_pages_ok 释放页面

1
2
3
4
5
6
7
8
9
static void __free_pages_ok(struct page *page, unsigned int order,
fpi_t fpi_flags)
{
unsigned long pfn = page_to_pfn(page);
struct zone *zone = page_zone(page);

if (free_pages_prepare(page, order))
free_one_page(zone, page, pfn, order, fpi_flags);
}

__free_pages_core 释放页面

  • 释放从 page 开始的一组物理内存页,并根据内存上下文(context)执行不同的初始化和管理操作。它通过设置页的引用计数、清除标志位以及更新内存管理元数据,将这些页重新标记为可用。
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
void __meminit __free_pages_core(struct page *page, unsigned int order,
enum meminit_context context)
{
unsigned int nr_pages = 1 << order; //需要释放的页数
struct page *p = page;
unsigned int loop;

/*
* 初始化 memmap 时,__init_single_page() 将所有页面的 refcount 设置为 1 (“allocated”/“not free”)。
* 我们必须将所有相关页面的 refcount 设置为 0。
*
* 请注意,热插拔内存页初始化为 PageOffline()。从 memblock 中释放的页面可能会被标记为保留。
*/
if (IS_ENABLED(CONFIG_MEMORY_HOTPLUG) &&
unlikely(context == MEMINIT_HOTPLUG)) { //处理内存热插拔场景

} else { //处理普通内存释放场景
//清除 PageReserved 标志,并将页的引用计数设置为 0
for (loop = 0; loop < nr_pages; loop++, p++) {
__ClearPageReserved(p);
set_page_count(p, 0);
}

/* 更新页所在区域(zone)的受管理页计数 */
atomic_long_add(nr_pages, &page_zone(page)->managed_pages);
}
//处理未接受的内存
if (page_contains_unaccepted(page, order)) {
}

/*
* 绕过 PCP 并将新页面直接放在尾部,主要与内存内联相关。
*/
//将页块释放到伙伴系统(buddy system)
//使用 FPI_TO_TAIL 标志将页块直接放置到伙伴系统的尾部,这在内存上线(memory onlining)场景中特别重要
__free_pages_ok(page, order, FPI_TO_TAIL);
}

prepare_alloc_pages 准备分配页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static inline bool prepare_alloc_pages(gfp_t gfp_mask, unsigned int order,
int preferred_nid, nodemask_t *nodemask,
struct alloc_context *ac, gfp_t *alloc_gfp,
unsigned int *alloc_flags)
{
ac->highest_zoneidx = gfp_zone(gfp_mask); //根据 gfp_mask 确定最高的内存区域索引(如 DMA、Normal 或 HighMem)
//contig_page_data->node_zonelists
ac->zonelist = node_zonelist(preferred_nid, gfp_mask); //根据首选的 NUMA 节点(preferred_nid)和分配标志获取节点的区域列表
ac->nodemask = nodemask;
ac->migratetype = gfp_migratetype(gfp_mask); //根据 gfp_mask 确定迁移类型(如不可移动、可回收等)

/* 仅在快速路径中完成脏区平衡*/
ac->spread_dirty_pages = (gfp_mask & __GFP_WRITE); //如果分配标志包含 __GFP_WRITE,启用脏页分布,确保写操作的内存分配均匀分布在内存区域中

/*
* 首选区域用于统计,但至关重要的是,它也用作 zonelist 迭代器的起点。对于忽略内存策略的分配,它可能会被重置。
*/
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask); //设置 preferred_zoneref,指向分配上下文中的首选区域引用,用于统计和区域列表迭代

return true;
}

alloc_flags_nofragment 确定内存分配时是否需要设置 ALLOC_NOFRAGMENT 标志,以避免内存碎片化

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
int defrag_mode;

/*
* 将 ZONE_DMA32 作为适合用于避免碎片化的区域的限制是微妙的。
* 如果首选区域是 HIGHMEM,则过早使用较低区域可能会导致比碎片更严重的 LOWMEM 压力问题。
* 如果下一个区域是 ZONE_DMA 个区域,则它可能太小了。只有分散分配以避免 Normal 和 DMA32 区域之间出现碎片才有意义。
*/
static inline unsigned int
alloc_flags_nofragment(struct zone *zone, gfp_t gfp_mask)
{
unsigned int alloc_flags;

/*
* __GFP_KSWAPD_RECLAIM 假定与保存分支的 ALLOC_KSWAPD 相同。
*/
alloc_flags = (__force int) (gfp_mask & __GFP_KSWAPD_RECLAIM);
//这表示系统当前处于主动防止碎片化的模式,无需进一步检查
if (defrag_mode) {
alloc_flags |= ALLOC_NOFRAGMENT;
return alloc_flags;
}

return alloc_flags;
}

__zone_watermark_ok 检查内存区域(zone)是否满足分配需求的水位线条件

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
/*
* 如果免费基页高于 'mark',则返回 true。对
于高阶检查,它将返回 true,表示达到 order-0 水印,并且至少有一个合适大小的空闲页面。
现在检查可避免在没有空闲页面时使用 zone lock 来签入分配路径。
*/
bool __zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark,
int highest_zoneidx, unsigned int alloc_flags,
long free_pages)
{
long min = mark;
int o;

/* free_pages 可能会变为负数 - 没关系 */
free_pages -= __zone_watermark_unusable_free(z, order, alloc_flags); //计算当前区域中不可用的页面数量(如保留页面

if (unlikely(alloc_flags & ALLOC_RESERVES)) { // ALLOC_RESERVES,表示分配操作可以使用某些保留页面
/*
* 允许访问最低水位线的 50%
*/
if (alloc_flags & ALLOC_MIN_RESERVE) {
min -= min / 2;

/*
* 允许访问更多的保留页面。
*/
if (alloc_flags & ALLOC_NON_BLOCK)
min -= min / 4;
}

/*
* 进一步降低水位线,允许 OOM(内存不足)受害者尝试分配页面
*/
if (alloc_flags & ALLOC_OOM)
min -= min / 2;
}

/*
* 检查当前区域的空闲页面是否高于最低水位线(min)加上低内存保留值(lowmem_reserve)
*/
if (free_pages <= min + z->lowmem_reserve[highest_zoneidx])
return false;

/* 如果这是 order-0 请求,则水印没问题*/
if (!order)
return true;

/* For a high-order request, check at least one suitable page is free */
for (o = order; o < NR_PAGE_ORDERS; o++) {
struct free_area *area = &z->free_area[o];
int mt;

if (!area->nr_free)
continue;

for (mt = 0; mt < MIGRATE_PCPTYPES; mt++) {
if (!free_area_empty(area, mt))
return true;
}

#ifdef CONFIG_CMA
if ((alloc_flags & ALLOC_CMA) &&
!free_area_empty(area, MIGRATE_CMA)) {
return true;
}
#endif
if ((alloc_flags & (ALLOC_HIGHATOMIC|ALLOC_OOM)) &&
!free_area_empty(area, MIGRATE_HIGHATOMIC)) {
return true;
}
}
return false;
}

zone_watermark_fast 快速检查区域水位线

  • 通过快速路径检查当前内存区域是否有足够的空闲页面满足分配需求。如果快速路径检查失败,则需要更复杂的逻辑来计算保留页面和其他条件
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
static inline unsigned long zone_page_state(struct zone *zone,
enum zone_stat_item item)
{
long x = atomic_long_read(&zone->vm_stat[item]); //获取当前区域的页面状态统计信息(如空闲页面数、保留页面数等)
return x;
}


static inline bool zone_watermark_fast(struct zone *z, unsigned int order,
unsigned long mark, int highest_zoneidx,
unsigned int alloc_flags, gfp_t gfp_mask)
{
long free_pages;

free_pages = zone_page_state(z, NR_FREE_PAGES); //获取当前区域的空闲页面数量

/*
*如果分配阶数为 0(order-0),即分配单个页面,尝试快速路径检查
*/
if (!order) {
long usable_free;
long reserved;

usable_free = free_pages;
//调用 __zone_watermark_unusable_free 计算当前区域中不可用的页面数量(如高原子分配保留页面)
reserved = __zone_watermark_unusable_free(z, 0, alloc_flags);

usable_free -= min(usable_free, reserved);
//如果可用页面数大于目标水位线(mark)加上低内存保留值(lowmem_reserve)
if (usable_free > mark + z->lowmem_reserve[highest_zoneidx])
return true;
}
//调用完整水位线检查
if (__zone_watermark_ok(z, order, mark, highest_zoneidx, alloc_flags,
free_pages))
return true;

/*
* 在检查最小水印时,忽略 __GFP_HIGH order-0 分配的水印提升。最小水位线是忽略 boosting 的点,因此当低于低水位线时,kswapd 会被唤醒。
*/
if (unlikely(!order && (alloc_flags & ALLOC_MIN_RESERVE) && z->watermark_boost
&& ((alloc_flags & ALLOC_WMARK_MASK) == WMARK_MIN))) {
mark = z->_watermark[WMARK_MIN];
return __zone_watermark_ok(z, order, mark, highest_zoneidx,
alloc_flags, free_pages);
}

return false;
}

pcp_lock 每个 CPU 的页面缓存列表锁

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
#if defined(CONFIG_SMP) || defined(CONFIG_PREEMPT_RT)
/*
* 在 SMP 上,spin_trylock 就足够了。
* 在 PREEMPT_RT 上,spin_trylock 在 SMP 和 UP 上都是等效的。
*/
#define pcp_trylock_prepare(flags) do { } while (0)
#define pcp_trylock_finish(flag) do { } while (0)
#else

/* UP spin_trylock 总是成功的,因此请禁用 IRQ 以防止重入。 */
#define pcp_trylock_prepare(flags) local_irq_save(flags)
#define pcp_trylock_finish(flags) local_irq_restore(flags)
#endif

/*
* 锁定 pcp 需要 PCP 查找,后跟旋转锁。为避免迁移导致锁定错误的 PCP 和远程内存
* 可能已分配,将任务固定到 CPU 以进行查找锁定。
* preempt_disable 用于 !RT 的 Tim migrate_disable 的
* migrate_disable 用于 RT,因为否则 RT 自旋锁的使用会受到干扰,并且高优先级任务无法抢占分配器。
*/
#ifndef CONFIG_PREEMPT_RT
#define pcpu_task_pin() preempt_disable()
#define pcpu_task_unpin() preempt_enable()
#else
#define pcpu_task_pin() migrate_disable()
#define pcpu_task_unpin() migrate_enable()
#endif

/*
* 查找的通用帮助程序和带有嵌入式自旋锁的 per-cpu 变量。
* 返回值应与等效的解锁助手一起使用。
*/
#define pcpu_spin_lock(type, member, ptr) \
({ \
type *_ret; \
pcpu_task_pin(); \
_ret = this_cpu_ptr(ptr); \
spin_lock(&_ret->member); \
_ret; \
})

#define pcpu_spin_trylock(type, member, ptr) \
({ \
type *_ret; \
pcpu_task_pin(); \
_ret = this_cpu_ptr(ptr); \
if (!spin_trylock(&_ret->member)) { \
pcpu_task_unpin(); \
_ret = NULL; \
} \
_ret; \
})

#define pcpu_spin_unlock(member, ptr) \
({ \
spin_unlock(&ptr->member); \
pcpu_task_unpin(); \
})

/*struct per_cpu_pages 特定的帮助程序. */
#define pcp_spin_lock(ptr) \
pcpu_spin_lock(struct per_cpu_pages, lock, ptr)

#define pcp_spin_trylock(ptr) \
pcpu_spin_trylock(struct per_cpu_pages, lock, ptr)

#define pcp_spin_unlock(ptr) \
pcpu_spin_unlock(lock, ptr)

expand 用于将较大的页面块拆分为较小的页面块

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
/*
* 此处的细分顺序对于 IO 子系统至关重要。
* 请不要在没有充分理由和回归测试的情况下更改此顺序。
* 具体来说,由于对大内存块进行了细分,因此较小块的交付顺序取决于它们在此函数中的细分顺序。
* 根据实证测试,这是影响页面交付到 IO 子系统的顺序的主要因素,并且通过考虑包含由一系列小分配作用的单个大内存块的伙伴系统的行为,
* 也可以证明这一点。此行为是 sglist 合并成功的关键因素。
*/
static inline unsigned int expand(struct zone *zone, struct page *page, int low,
int high, int migratetype)
{
unsigned int size = 1 << high;
unsigned int nr_added = 0;
//循环逐步将页面块从 high 阶数拆分为更小的阶数,直到达到目标阶数 low
while (high > low) {
//每次循环将页面块的阶数减少 1,并将页面块的大小减半
high--;
size >>= 1;
VM_BUG_ON_PAGE(bad_range(zone, &page[size]), &page[size]);
//将拆分出的页面块添加到伙伴系统的空闲列表
__add_to_free_list(&page[size], zone, high, migratetype, false);
set_buddy_order(&page[size], high); //设置页面块的阶数,标记其属于伙伴系统
nr_added += size;
}

return nr_added; //记录添加到空闲列表中的页面总数
}

page_del_and_expand 将页面从空闲列表中删除并扩展

1
2
3
4
5
6
7
8
9
10
static __always_inline void page_del_and_expand(struct zone *zone,
struct page *page, int low,
int high, int migratetype)
{
int nr_pages = 1 << high;

__del_page_from_free_list(page, zone, high, migratetype);
nr_pages -= expand(zone, page, low, high, migratetype); //将页面块从当前阶数(high)拆分为目标阶数(low
account_freepages(zone, -nr_pages, migratetype); //更新内存区域的空闲页面统计信息
}

__rmqueue_smallest 从伙伴系统中移除最小的可用页面

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
static inline struct page *get_page_from_free_area(struct free_area *area,
int migratetype)
{
/* 从链表中获取第一个元素。如果链表为空,则返回 NULL
&area->free_list[migratetype]:表示从 free_area 中获取指定迁移类型的空闲列表。
struct page:指定链表中存储的元素类型。
buddy_list:表示 struct page 中的链表成员,用于链接空闲页面。
*/
return list_first_entry_or_null(&area->free_list[migratetype],
struct page, buddy_list);
}

/*
* 浏览给定 migratetype 的可用列表,并从可用列表中删除最小的可用页面
*/
static __always_inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
int migratetype)
{
unsigned int current_order;
struct free_area *area;
struct page *page;

/* 在首选列表中找到适当大小的页面 */
for (current_order = order; current_order < NR_PAGE_ORDERS; ++current_order) {
area = &(zone->free_area[current_order]);
page = get_page_from_free_area(area, migratetype); //从当前空闲区域中尝试获取一个符合迁移类型的页面
if (!page)
continue;

page_del_and_expand(zone, page, order, current_order, //将其从空闲列表中移除
migratetype);
trace_mm_page_alloc_zone_locked(page, order, migratetype,
pcp_allowed_order(order) &&
migratetype < MIGRATE_PCPTYPES);
return page;
}

return NULL;
}

__rmqueue 从伙伴系统(buddy allocator)中移除一个页面

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
/*
* 执行从 buddy 分配器中删除元素的艰苦工作。
* 在已持有 zone->lock 的情况下打电话给我。

zone:页面所属的内存区域。
order:分配的页面阶数(2^order 表示分配的页面数)。
migratetype:页面的迁移类型(如不可移动、可回收等)。
alloc_flags:分配标志,用于控制分配逻辑。
mode:分配模式,表示当前分配的策略(如正常分配、CMA 回退等)。
*/
static __always_inline struct page *
__rmqueue(struct zone *zone, unsigned int order, int migratetype,
unsigned int alloc_flags, enum rmqueue_mode *mode)
{
struct page *page;
/*
* 首先尝试请求的 migratetype 的空闲列表,然后尝试碎片风险级别增加的回退模式。
*
* 回退逻辑成本高昂,并且 rmqueue_bulk() 在持有 zone->lock 的情况下循环调用,这意味着空闲列表不受任何外部更改的影响。
* 请记住在 * 模式下我们找到付费污垢的地方,以节省我们下次通话的搜索。
*/
switch (*mode) {
case RMQUEUE_NORMAL: //正常分配模式
//从请求的迁移类型的空闲列表中分配页面
page = __rmqueue_smallest(zone, order, migratetype); //查找最小的合适页面块
if (page)
return page;
fallthrough;
case RMQUEUE_CMA: //尝试从 CMA 区域分配页面
if (alloc_flags & ALLOC_CMA) {
page = __rmqueue_cma_fallback(zone, order);
if (page) {
*mode = RMQUEUE_CMA;
return page;
}
}
fallthrough;
case RMQUEUE_CLAIM:
//调用 __rmqueue_claim 从其他迁移类型的空闲列表中分配页面
page = __rmqueue_claim(zone, order, migratetype, alloc_flags);
if (page) {
/* Replenished preferred freelist, back to normal mode. */
*mode = RMQUEUE_NORMAL;
return page;
}
fallthrough;
case RMQUEUE_STEAL:
if (!(alloc_flags & ALLOC_NOFRAGMENT)) {
//调用 __rmqueue_steal 从其他迁移类型中“窃取”页面
page = __rmqueue_steal(zone, order, migratetype);
if (page) {
*mode = RMQUEUE_STEAL;
return page;
}
}
}
return NULL;
}

rmqueue_bulk 从 Buddy 分配器中获取指定数量的元素

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
/*
* 从 Buddy 分配器中获取指定数量的元素,所有这些元素都在一次锁定下,以提高效率。 将它们添加到提供的列表中。返回放置在 *list 中的新页面数。

zone:页面所属的内存区域。
order:分配的页面阶数(2^order 表示分配的页面数)。
count:需要分配的页面数量。
list:调用者提供的链表,用于存储分配的页面。
migratetype:页面的迁移类型(如不可移动、可回收等)。
alloc_flags:分配标志,用于控制分配逻辑。
*/
static int rmqueue_bulk(struct zone *zone, unsigned int order,
unsigned long count, struct list_head *list,
int migratetype, unsigned int alloc_flags)
{
enum rmqueue_mode rmqm = RMQUEUE_NORMAL;
unsigned long flags;
int i;

if (unlikely(alloc_flags & ALLOC_TRYLOCK)) {
if (!spin_trylock_irqsave(&zone->lock, flags))
return 0;
} else {
spin_lock_irqsave(&zone->lock, flags);
}
for (i = 0; i < count; ++i) {
struct page *page = __rmqueue(zone, order, migratetype,
alloc_flags, &rmqm);
if (unlikely(page == NULL))
break;

/*
* expand() 返回的拆分好友页面在此处按物理页面顺序接收。
* 该页面将添加到调用方列表的尾部。从调用者的角度来看,在某些情况下,链表是按页码排序的。
* 这对于可以从 head 向前方向的 IO 设备非常有用,因此也可以按物理页面顺序进行。
* 这对于如果物理页排序正确,则可以合并 IO 请求的 IO 设备非常有用。
*/
list_add_tail(&page->pcp_list, list);
}
spin_unlock_irqrestore(&zone->lock, flags);

return i;
}

nr_pcp_alloc 计算每个 CPU 的页面缓存列表中需要分配的页面数量

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
static int nr_pcp_alloc(struct per_cpu_pages *pcp, struct zone *zone, int order)
{
int high, base_batch, batch, max_nr_alloc;
int high_max, high_min;

base_batch = READ_ONCE(pcp->batch); //批量大小
high_min = READ_ONCE(pcp->high_min);
high_max = READ_ONCE(pcp->high_max);
//将当前高水位线(pcp->high)限制在 high_min 和 high_max 之间,确保其值合法
high = pcp->high = clamp(pcp->high, high_min, high_max);

/* 检查 PCP 是否已禁用或启动页面集 */
if (unlikely(high < base_batch))
return 1; //表示 PCP 被禁用或处于引导页面集(boot pageset)状态

if (order)
batch = base_batch;
else
batch = (base_batch << pcp->alloc_factor);

/*
* If we had larger pcp->high, we could avoid to allocate from
* zone.
*/
if (high_min != high_max && !test_bit(ZONE_BELOW_HIGH, &zone->flags))
high = pcp->high = min(high + batch, high_max);

if (!order) {
max_nr_alloc = max(high - pcp->count - base_batch, base_batch);
/*
* Double the number of pages allocated each time there is
* subsequent allocation of order-0 pages without any freeing.
*/
if (batch <= max_nr_alloc &&
pcp->alloc_factor < CONFIG_PCP_BATCH_SCALE_MAX)
pcp->alloc_factor++;
batch = min(batch, max_nr_alloc);
}

/*
* Scale batch relative to order if batch implies free pages
* can be stored on the PCP. Batch can be 1 for small zones or
* for boot pagesets which should never store free pages as
* the pages may belong to arbitrary zones.
*/
if (batch > 1)
batch = max(batch >> order, 2);

return batch;
}

__rmqueue_pcplist 从每个 CPU 的页面缓存列表中移除页面

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
/* 从per-cpu链表移除page时,调用者必须保护链表 
zone:页面所属的内存区域(zone)。
order:分配的页面阶数(2^order 表示分配的页面数)。
migratetype:页面的迁移类型(如不可移动、可回收等)。
alloc_flags:分配器的内部标志,用于控制分配逻辑。
pcp:指向 per-CPU 页面缓存的结构体,管理当前 CPU 的页面缓存。
list:指向 PCP 中的页面链表。
*/
static inline
struct page *__rmqueue_pcplist(struct zone *zone, unsigned int order,
int migratetype,
unsigned int alloc_flags,
struct per_cpu_pages *pcp,
struct list_head *list)
{
struct page *page;

do {
if (list_empty(list)) {
int batch = nr_pcp_alloc(pcp, zone, order); //计算需要从伙伴系统批量分配的页面数量(batch)
int alloced;

alloced = rmqueue_bulk(zone, order, //从伙伴系统中批量分配页面,并将其添加到 PCP 列表中
batch, list,
migratetype, alloc_flags);

pcp->count += alloced << order; //更新 PCP 的页面计数(pcp->count),表示当前 PCP 中的页面总数
if (unlikely(list_empty(list)))
return NULL;
}

page = list_first_entry(list, struct page, pcp_list);
list_del(&page->pcp_list);
pcp->count -= 1 << order;
} while (check_new_pages(page, order)); // 对分配的页面进行校验

return page;
}

rmqueue_pcplist 从每个 CPU 的页面缓存列表中尝试分配页面

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
/* 从 per-cpu 列表中锁定和删除页面 
referred_zone:首选的内存区域,用于统计目的。
zone:实际尝试分配页面的内存区域。
order:分配的页面阶数(2^order 表示分配的页面数)。
migratetype:页面的迁移类型(如不可移动、可回收等)。
alloc_flags:分配器的内部标志,用于控制分配逻辑。
*/
static struct page *rmqueue_pcplist(struct zone *preferred_zone,
struct zone *zone, unsigned int order,
int migratetype, unsigned int alloc_flags)
{
struct per_cpu_pages *pcp;
struct list_head *list;
struct page *page;
unsigned long __maybe_unused UP_flags;

/* spin_trylock 可能会因并行 drain 或 IRQ 重入而失败。 */
pcp_trylock_prepare(UP_flags);
//尝试获取每个 CPU 的页面缓存列表
pcp = pcp_spin_trylock(zone->per_cpu_pageset);
if (!pcp) {
pcp_trylock_finish(UP_flags);
return NULL;
}

/*
* 在分配时,减少批量释放的页面数。
* 参见 nr_pcp_free(),其中 free_factor 对于后续的释放会增加。
*/
pcp->free_count >>= 1;
list = &pcp->lists[order_to_pindex(migratetype, order)]; //根据页面的迁移类型和阶数,选择对应的 per-CPU 页面缓存列表
page = __rmqueue_pcplist(zone, order, migratetype, alloc_flags, pcp, list); // 从缓存列表中移除并返回一个页面
pcp_spin_unlock(pcp);
pcp_trylock_finish(UP_flags);
if (page) {
__count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order); //调用 __count_zid_vm_events 记录页面分配事件
zone_statistics(preferred_zone, zone, 1); //用 zone_statistics 更新内存区域的统计信息
}
return page;
}

rmqueue_buddy 从伙伴系统中分配页面

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
static __always_inline
struct page *rmqueue_buddy(struct zone *preferred_zone, struct zone *zone,
unsigned int order, unsigned int alloc_flags,
int migratetype)
{
struct page *page;
unsigned long flags;

do {
page = NULL;
if (unlikely(alloc_flags & ALLOC_TRYLOCK)) {
if (!spin_trylock_irqsave(&zone->lock, flags))
return NULL;
} else {
spin_lock_irqsave(&zone->lock, flags);
}
if (alloc_flags & ALLOC_HIGHATOMIC)
page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
if (!page) {
enum rmqueue_mode rmqm = RMQUEUE_NORMAL;

page = __rmqueue(zone, order, migratetype, alloc_flags, &rmqm);

/*
* If the allocation fails, allow OOM handling and
* order-0 (atomic) allocs access to HIGHATOMIC
* reserves as failing now is worse than failing a
* high-order atomic allocation in the future.
*/
if (!page && (alloc_flags & (ALLOC_OOM|ALLOC_NON_BLOCK)))
page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);

if (!page) {
spin_unlock_irqrestore(&zone->lock, flags);
return NULL;
}
}
spin_unlock_irqrestore(&zone->lock, flags);
} while (check_new_pages(page, order));

__count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order);
zone_statistics(preferred_zone, zone, 1);

return page;
}

rmqueue 从空闲列表中获取页面

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
static inline bool pcp_allowed_order(unsigned int order)
{
if (order <= PAGE_ALLOC_COSTLY_ORDER) //<= 3
return true;

return false;
}

/*
* 不要使用 KMSAN 检测 rmqueue()。此函数可以调用
* __msan_poison_alloca() 通过调用 set_pfnblock_flags_mask() 来实现。
* 如果 __msan_poison_alloca() 尝试为堆栈库分配页面,它可能会再次调用 rmqueue(),这将导致死锁。

preferred_zone:首选的内存区域。
zone:实际尝试分配页面的内存区域。
order:分配的页面阶数(2^order 表示分配的页面数)。
gfp_flags:分配标志,指定分配行为。
alloc_flags:分配器的内部标志,用于控制分配逻辑。
migratetype:页面的迁移类型(如不可移动、可回收等)。
*/
__no_sanitize_memory
static inline
struct page *rmqueue(struct zone *preferred_zone,
struct zone *zone, unsigned int order,
gfp_t gfp_flags, unsigned int alloc_flags,
int migratetype)
{
struct page *page;
//优先从 pcplist 分配
if (likely(pcp_allowed_order(order))) {
//调用 rmqueue_pcplist 从每个 CPU 的页面缓存列表中尝试分配页面
page = rmqueue_pcplist(preferred_zone, zone, order,
migratetype, alloc_flags);
if (likely(page))
goto out;
}
//free_low_memory_core_early时将内存都放入伙伴系统中
//如果 pcplist 分配失败,调用 rmqueue_buddy 从伙伴系统中分配页面。
//伙伴系统是一种内存管理机制,通过合并和拆分页面块来管理内存
page = rmqueue_buddy(preferred_zone, zone, order, alloc_flags,
migratetype);

out:
if ((alloc_flags & ALLOC_KSWAPD) &&
unlikely(test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags))) {
clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags);
wakeup_kswapd(zone, 0, 0, zone_idx(zone));
}
//检查分配的页面是否超出区域范围(bad_range),如果超出则触发调试错误
VM_BUG_ON_PAGE(page && bad_range(zone, page), page);
return page;
}

post_alloc_hook 分配后钩子函数

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
/*
* 只能由新分配的页面上的页面分配器调用。
*/
static inline void set_page_pfmemalloc(struct page *page)
{
page->lru.next = (void *)BIT(1);
}

static inline void clear_page_pfmemalloc(struct page *page)
{
page->lru.next = NULL;
}

inline void post_alloc_hook(struct page *page, unsigned int order,
gfp_t gfp_flags)
{
bool init = !want_init_on_free() && want_init_on_alloc(gfp_flags) &&
!should_skip_init(gfp_flags);
bool zero_tags = init && (gfp_flags & __GFP_ZEROTAGS);
int i;

set_page_private(page, 0); //清除页面的私有数据

/*
* 如果内存标签应归零(仅当内存也应初始化时才会发生)。
*/
if (zero_tags) {
/* 初始化内存和内存标签。 */
for (i = 0; i != 1 << order; ++i)
tag_clear_highpage(page + i);

/* 请注意,内存是由上面的循环初始化的。 */
init = false;
}

/* 如果内存仍未初始化,请立即初始化它。 */
if (init)
kernel_init_pages(page, 1 << order);
}

prep_new_page 准备新分配的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void prep_new_page(struct page *page, unsigned int order, gfp_t gfp_flags,
unsigned int alloc_flags)
{
post_alloc_hook(page, order, gfp_flags); //对页面执行分配后的初始化操作
//如果页面的阶数大于 0(order > 0),并且分配标志包含 __GFP_COMP
if (order && (gfp_flags & __GFP_COMP)) //初始化复合页面
prep_compound_page(page, order);

/*
* pfmemalloc 标记的作用是指示页面分配是为了释放更多内存,
* 调用者应避免将该页面用于非内存回收的用途
*/
if (alloc_flags & ALLOC_NO_WATERMARKS) //表示页面分配突破了内存水位限制
set_page_pfmemalloc(page);
else
clear_page_pfmemalloc(page); //清除该标记
}

get_page_from_freelist 从空闲列表中获取页面

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
/*
* get_page_from_freelist 遍历 zonelist 尝试分配一个页面。
*/
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
const struct alloc_context *ac)
{
struct zoneref *z;
struct zone *zone;
struct pglist_data *last_pgdat = NULL;
bool last_pgdat_dirty_ok = false;
bool no_fallback;

retry:
/*
* 扫描区域列表,寻找具有足够空闲空间的区域。
* 另请参见 kernel/cgroup/cpuset.c 中的 cpuset_node_allowed() 注释。
*/
no_fallback = alloc_flags & ALLOC_NOFRAGMENT; //是否允许回退到可能导致内存碎片化的分配
z = ac->preferred_zoneref;
//遍历 zonelist 中的所有区域(zone),从首选区域开始
for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx,
ac->nodemask) {
struct page *page;
unsigned long mark;

/*
* 在分配页面缓存页面进行写入时,我们希望从其脏限制内的节点获取该页面,这样,没有单个节点持有的脏页面超过其全局允许的脏页面的比例份额。
* 脏限制考虑了节点的 lowmem 保留和高 watermark,因此 kswapd 应该能够平衡它,而不必从其 LRU 列表中写入页面。
*
* XXX:目前,在进入回收之前,允许分配可能超过慢速路径中每个节点的脏限制(spread_dirty_pages未设置),
* 这在 NUMA 设置中允许的节点一起不够大而无法达到全局限制时非常重要。 这些情况的正确解决方法需要了解脏限制和 flusher 线程中的节点。
*/
if (ac->spread_dirty_pages) {
//查当前节点是否在其脏页限制内。
if (last_pgdat != zone->zone_pgdat) {
last_pgdat = zone->zone_pgdat;
last_pgdat_dirty_ok = node_dirty_ok(zone->zone_pgdat);
}

if (!last_pgdat_dirty_ok)
continue;
}

/*
* 检测免费页数是否低于高水位线。 如果是这样,我们将减少 free path 中的 pcp->high 和空闲 PCP 页面,以减少页面过早回收的可能性。
* 在此处进行检测是为了避免在较热的自由路径中执行此作。
*/
if (test_bit(ZONE_BELOW_HIGH, &zone->flags))
goto check_alloc_wmark;

mark = high_wmark_pages(zone);
//检查当前区域的空闲页面是否高于高水位线(high_wmark_pages)
//order = 0,表示分配单个页面,允许执行快速路径
if (zone_watermark_fast(zone, order, mark,
ac->highest_zoneidx, alloc_flags,
gfp_mask))
goto try_this_zone;
else
set_bit(ZONE_BELOW_HIGH, &zone->flags);

check_alloc_wmark:
//计算当前区域的目标水位线(mark)
mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
//使用 zone_watermark_fast 检查当前区域的空闲页面是否高于目标水位线
//如果低于水位线,表示当前区域可能无法满足分配需求,进入后续处理逻辑
if (!zone_watermark_fast(zone, order, mark,
ac->highest_zoneidx, alloc_flags,
gfp_mask)) {
int ret;

/* Checked here to keep the fast path fast */
BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
//分配标志中包含 ALLOC_NO_WATERMARKS,跳过水位线检查,直接尝试分配页面
if (alloc_flags & ALLOC_NO_WATERMARKS)
goto try_this_zone;
//节点回收机制
//节点回收(node_reclaim)未启用,或者当前区域不允许回收,跳过当前区域
if (!node_reclaim_enabled() ||
!zone_allows_reclaim(zonelist_zone(ac->preferred_zoneref), zone))
continue;

ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
switch (ret) {
case NODE_RECLAIM_NOSCAN: //未扫描当前节点,跳过
/* did not scan */
continue;
case NODE_RECLAIM_FULL: //扫描完成但无法回收页面,跳过
/* scanned but unreclaimable */
continue;
default:
/* did we reclaim enough */
//默认情况:如果回收后区域的水位线满足分配条件
if (zone_watermark_ok(zone, order, mark,
ac->highest_zoneidx, alloc_flags))
goto try_this_zone;

continue;
}
}

try_this_zone:
//页面分配尝试

//从当前区域的空闲页面队列中分配页面
page = rmqueue(zonelist_zone(ac->preferred_zoneref), zone, order,
gfp_mask, alloc_flags, ac->migratetype);
if (page) {
prep_new_page(page, order, gfp_mask, alloc_flags); //初始化页面

if (unlikely(alloc_flags & ALLOC_HIGHATOMIC)) //包含 ALLOC_HIGHATOMIC,表示这是一个高阶原子分配
reserve_highatomic_pageblock(page, order, zone); // 保留页面块以供未来使用

return page;
}
}

return NULL;
}

__alloc_pages_slowpath

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
tatic inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
struct alloc_context *ac)
{
bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
bool can_compact = gfp_compaction_allowed(gfp_mask);
bool nofail = gfp_mask & __GFP_NOFAIL;
const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
struct page *page = NULL;
unsigned int alloc_flags;
unsigned long did_some_progress;
enum compact_priority compact_priority;
enum compact_result compact_result;
int compaction_retries;
int no_progress_loops;
unsigned int cpuset_mems_cookie;
unsigned int zonelist_iter_cookie;
int reserve_flags;

if (unlikely(nofail)) {
/*
* We most definitely don't want callers attempting to
* allocate greater than order-1 page units with __GFP_NOFAIL.
*/
WARN_ON_ONCE(order > 1);
/*
* Also we don't support __GFP_NOFAIL without __GFP_DIRECT_RECLAIM,
* otherwise, we may result in lockup.
*/
WARN_ON_ONCE(!can_direct_reclaim);
/*
* PF_MEMALLOC request from this context is rather bizarre
* because we cannot reclaim anything and only can loop waiting
* for somebody to do a work for us.
*/
WARN_ON_ONCE(current->flags & PF_MEMALLOC);
}

restart:
compaction_retries = 0;
no_progress_loops = 0;
compact_result = COMPACT_SKIPPED;
compact_priority = DEF_COMPACT_PRIORITY;
cpuset_mems_cookie = read_mems_allowed_begin();
zonelist_iter_cookie = zonelist_iter_begin();

/*
* The fast path uses conservative alloc_flags to succeed only until
* kswapd needs to be woken up, and to avoid the cost of setting up
* alloc_flags precisely. So we do that now.
*/
alloc_flags = gfp_to_alloc_flags(gfp_mask, order);

/*
* 我们需要重新计算 zonelist 迭代器的起点
* 因为我们可能在快速路径中使用了不同的 nodemask,或者
* 有一个 cpuset 修改,我们正在重试 - 否则我们会
* 最终可能会无休止地迭代不符合条件的区域。
*/
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
if (!zonelist_zone(ac->preferred_zoneref))
goto nopage;

/*
* Check for insane configurations where the cpuset doesn't contain
* any suitable zone to satisfy the request - e.g. non-movable
* GFP_HIGHUSER allocations from MOVABLE nodes only.
*/
if (cpusets_insane_config() && (gfp_mask & __GFP_HARDWALL)) {
struct zoneref *z = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx,
&cpuset_current_mems_allowed);
if (!zonelist_zone(z))
goto nopage;
}

if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);

/*
* The adjusted alloc_flags might result in immediate success, so try
* that first
*/
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)
goto got_pg;

/*
* For costly allocations, try direct compaction first, as it's likely
* that we have enough base pages and don't need to reclaim. For non-
* movable high-order allocations, do that as well, as compaction will
* try prevent permanent fragmentation by migrating from blocks of the
* same migratetype.
* Don't try this for allocations that are allowed to ignore
* watermarks, as the ALLOC_NO_WATERMARKS attempt didn't yet happen.
*/
if (can_direct_reclaim && can_compact &&
(costly_order ||
(order > 0 && ac->migratetype != MIGRATE_MOVABLE))
&& !gfp_pfmemalloc_allowed(gfp_mask)) {
page = __alloc_pages_direct_compact(gfp_mask, order,
alloc_flags, ac,
INIT_COMPACT_PRIORITY,
&compact_result);
if (page)
goto got_pg;

/*
* Checks for costly allocations with __GFP_NORETRY, which
* includes some THP page fault allocations
*/
if (costly_order && (gfp_mask & __GFP_NORETRY)) {
/*
* If allocating entire pageblock(s) and compaction
* failed because all zones are below low watermarks
* or is prohibited because it recently failed at this
* order, fail immediately unless the allocator has
* requested compaction and reclaim retry.
*
* Reclaim is
* - potentially very expensive because zones are far
* below their low watermarks or this is part of very
* bursty high order allocations,
* - not guaranteed to help because isolate_freepages()
* may not iterate over freed pages as part of its
* linear scan, and
* - unlikely to make entire pageblocks free on its
* own.
*/
if (compact_result == COMPACT_SKIPPED ||
compact_result == COMPACT_DEFERRED)
goto nopage;

/*
* Looks like reclaim/compaction is worth trying, but
* sync compaction could be very expensive, so keep
* using async compaction.
*/
compact_priority = INIT_COMPACT_PRIORITY;
}
}

retry:
/*
* Deal with possible cpuset update races or zonelist updates to avoid
* infinite retries.
*/
if (check_retry_cpuset(cpuset_mems_cookie, ac) ||
check_retry_zonelist(zonelist_iter_cookie))
goto restart;

/* Ensure kswapd doesn't accidentally go to sleep as long as we loop */
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);

reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
if (reserve_flags)
alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, reserve_flags) |
(alloc_flags & ALLOC_KSWAPD);

/*
* Reset the nodemask and zonelist iterators if memory policies can be
* ignored. These allocations are high priority and system rather than
* user oriented.
*/
if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
ac->nodemask = NULL;
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
}

/* Attempt with potentially adjusted zonelist and alloc_flags */
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)
goto got_pg;

/* Caller is not willing to reclaim, we can't balance anything */
if (!can_direct_reclaim)
goto nopage;

/* Avoid recursion of direct reclaim */
if (current->flags & PF_MEMALLOC)
goto nopage;

/* Try direct reclaim and then allocating */
page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
&did_some_progress);
if (page)
goto got_pg;

/* Try direct compaction and then allocating */
page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
compact_priority, &compact_result);
if (page)
goto got_pg;

/* Do not loop if specifically requested */
if (gfp_mask & __GFP_NORETRY)
goto nopage;

/*
* Do not retry costly high order allocations unless they are
* __GFP_RETRY_MAYFAIL and we can compact
*/
if (costly_order && (!can_compact ||
!(gfp_mask & __GFP_RETRY_MAYFAIL)))
goto nopage;

if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
did_some_progress > 0, &no_progress_loops))
goto retry;

/*
* It doesn't make any sense to retry for the compaction if the order-0
* reclaim is not able to make any progress because the current
* implementation of the compaction depends on the sufficient amount
* of free memory (see __compaction_suitable)
*/
if (did_some_progress > 0 && can_compact &&
should_compact_retry(ac, order, alloc_flags,
compact_result, &compact_priority,
&compaction_retries))
goto retry;

/* Reclaim/compaction failed to prevent the fallback */
if (defrag_mode && (alloc_flags & ALLOC_NOFRAGMENT)) {
alloc_flags &= ~ALLOC_NOFRAGMENT;
goto retry;
}

/*
* Deal with possible cpuset update races or zonelist updates to avoid
* a unnecessary OOM kill.
*/
if (check_retry_cpuset(cpuset_mems_cookie, ac) ||
check_retry_zonelist(zonelist_iter_cookie))
goto restart;

/* Reclaim has failed us, start killing things */
page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
if (page)
goto got_pg;

/* Avoid allocations with no watermarks from looping endlessly */
if (tsk_is_oom_victim(current) &&
(alloc_flags & ALLOC_OOM ||
(gfp_mask & __GFP_NOMEMALLOC)))
goto nopage;

/* Retry as long as the OOM killer is making progress */
if (did_some_progress) {
no_progress_loops = 0;
goto retry;
}

nopage:
/*
* Deal with possible cpuset update races or zonelist updates to avoid
* a unnecessary OOM kill.
*/
if (check_retry_cpuset(cpuset_mems_cookie, ac) ||
check_retry_zonelist(zonelist_iter_cookie))
goto restart;

/*
* Make sure that __GFP_NOFAIL request doesn't leak out and make sure
* we always retry
*/
if (unlikely(nofail)) {
/*
* Lacking direct_reclaim we can't do anything to reclaim memory,
* we disregard these unreasonable nofail requests and still
* return NULL
*/
if (!can_direct_reclaim)
goto fail;

/*
* Help non-failing allocations by giving some access to memory
* reserves normally used for high priority non-blocking
* allocations but do not use ALLOC_NO_WATERMARKS because this
* could deplete whole memory reserves which would just make
* the situation worse.
*/
page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_MIN_RESERVE, ac);
if (page)
goto got_pg;

cond_resched();
goto retry;
}
fail:
warn_alloc(gfp_mask, ac->nodemask,
"page allocation failure: order:%u", order);
got_pg:
return page;
}

__alloc_frozen_pages_noprof 分配冻结页面

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
/*
* 将每个任务的 gfp 上下文应用于给定的分配标志。
* PF_MEMALLOC_NOIO 意味着 GFP_NOIO
* PF_MEMALLOC_NOFS 意味着 GFP_NOFS
* PF_MEMALLOC_PIN 暗示 !GFP_MOVABLE
*/
static inline gfp_t current_gfp_context(gfp_t flags)
{
unsigned int pflags = READ_ONCE(current->flags);
/* PF_MEMALLOC_NOIO:禁止 I/O 操作。
PF_MEMALLOC_NOFS:禁止文件系统操作。
PF_MEMALLOC_PIN:禁止使用可移动内存区域。 */
if (unlikely(pflags & (PF_MEMALLOC_NOIO | PF_MEMALLOC_NOFS | PF_MEMALLOC_PIN))) {
/*
* NOIO 意味着 NOIO 和 NOFS,它是一个较弱的上下文,因此请始终确保它优先
*/
//如果任务标志包含 PF_MEMALLOC_NOIO,清除分配标志中的 __GFP_IO 和 __GFP_FS,禁止 I/O 和文件系统操作
if (pflags & PF_MEMALLOC_NOIO)
flags &= ~(__GFP_IO | __GFP_FS);
else if (pflags & PF_MEMALLOC_NOFS) //如果任务标志包含 PF_MEMALLOC_NOFS,仅清除分配标志中的 __GFP_FS,禁止文件系统操作
flags &= ~__GFP_FS;

if (pflags & PF_MEMALLOC_PIN) //果任务标志包含 PF_MEMALLOC_PIN,清除分配标志中的 __GFP_MOVABLE,禁止使用可移动内存区域
flags &= ~__GFP_MOVABLE;
}
return flags;
}

/*
*这是分区伙伴分配的 “心脏”。
*/
struct page *__alloc_frozen_pages_noprof(gfp_t gfp, unsigned int order,
int preferred_nid, nodemask_t *nodemask)
{
struct page *page;
unsigned int alloc_flags = ALLOC_WMARK_LOW; //低水位线分配
gfp_t alloc_gfp; /* 实际用于分配的 gfp_t*/
struct alloc_context ac = { }; //存储分配上下文信息

/*
* 在几个地方,我们假设订单价值是合理的,因此如果请求越界,请尽早退出。
*/
if (WARN_ON_ONCE_GFP(order > MAX_PAGE_ORDER, gfp)) //检查 order 是否超出允许的最大值 MAX_PAGE_ORDER 10
return NULL;

gfp &= gfp_allowed_mask;
/*
* 应用范围分配约束。这主要是关于GFP_NOFS 或 GFP_NOIO 必须为来自特定上下文的所有分配请求继承memalloc_no{fs,io}_{save,restore}。PF_MEMALLOC_PIN确保在分配期间不使用可移动区域。
* 调用 current_gfp_context 应用上下文约束(如 GFP_NOFS 和 GFP_NOIO),并更新 gfp
*/
gfp = current_gfp_context(gfp);
alloc_gfp = gfp;
//准备分配上下文,包括设置首选区域和分配标志
if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac,
&alloc_gfp, &alloc_flags))
return NULL;

/*
* 防止内存碎片化
* 为分配标志添加防止内存碎片化的约束,确保在尝试所有本地区域之前不回退到可能导致碎片化的类型。
*/
/* static inline struct zone *zonelist_zone(struct zoneref *zoneref)
{
return zoneref->zone;
} */
alloc_flags |= alloc_flags_nofragment(zonelist_zone(ac.preferred_zoneref), gfp); //alloc_flags |= (gfp_mask & __GFP_KSWAPD_RECLAIM)

/*快速路径分配 第一次分配尝试*/
page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac); //从空闲列表中尝试快速分配页
if (likely(page))
goto out;
//慢速路径分配
alloc_gfp = gfp;
ac.spread_dirty_pages = false;

/*
* 如果原始节点掩码可能被 &cpuset_current_mems_allowed 替换,请恢复它以优化快速路径尝试。
*/
ac.nodemask = nodemask; //恢复原始的 nodemask,并禁用脏页分布

page = __alloc_pages_slowpath(alloc_gfp, order, &ac);

out:
return page;
}

zone_set_pageset_high_and_batch 根据区域的大小,为区域的所有每 CPU 页面集计算并设置新的高值和批处理值

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
/*
* 根据区域的大小,为区域的所有每 CPU 页面集计算并设置新的高值和批处理值。
*/
static void zone_set_pageset_high_and_batch(struct zone *zone, int cpu_online)
{
int new_high_min, new_high_max, new_batch;

new_batch = max(1, zone_batchsize(zone));
if (percpu_pagelist_high_fraction) {
new_high_min = zone_highsize(zone, new_batch, cpu_online,
percpu_pagelist_high_fraction);
/*
* PCP high is tuned manually, disable auto-tuning via
* setting high_min and high_max to the manual value.
*/
new_high_max = new_high_min;
} else {
new_high_min = zone_highsize(zone, new_batch, cpu_online, 0);
new_high_max = zone_highsize(zone, new_batch, cpu_online,
MIN_PERCPU_PAGELIST_HIGH_FRACTION);
}

if (zone->pageset_high_min == new_high_min &&
zone->pageset_high_max == new_high_max &&
zone->pageset_batch == new_batch)
return;

zone->pageset_high_min = new_high_min;
zone->pageset_high_max = new_high_max;
zone->pageset_batch = new_batch;

__zone_set_pageset_high_and_batch(zone, new_high_min, new_high_max,
new_batch);
}

setup_zone_pageset 设置每个区域的页面集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __meminit setup_zone_pageset(struct zone *zone)
{
int cpu;

/* 尺寸可能为 0 上!SMP & & !NUMA */
if (sizeof(struct per_cpu_zonestat) > 0)
zone->per_cpu_zonestats = alloc_percpu(struct per_cpu_zonestat);

zone->per_cpu_pageset = alloc_percpu(struct per_cpu_pages);
for_each_possible_cpu(cpu) {
struct per_cpu_pages *pcp;
struct per_cpu_zonestat *pzstats;

pcp = per_cpu_ptr(zone->per_cpu_pageset, cpu);
pzstats = per_cpu_ptr(zone->per_cpu_zonestats, cpu);
per_cpu_pages_init(pcp, pzstats);
}

zone_set_pageset_high_and_batch(zone, 0);
}

setup_per_cpu_pageset 设置每个 CPU 的页面集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 分配每个 CPU 的页面集并初始化它们。
* 在此次调用之前,只有启动页面集可用。
*/
void __init setup_per_cpu_pageset(void)
{
struct pglist_data *pgdat;
struct zone *zone;
int __maybe_unused cpu;

for_each_populated_zone(zone)
setup_zone_pageset(zone);

for_each_online_pgdat(pgdat)
pgdat->per_cpu_nodestats =
alloc_percpu(struct per_cpu_nodestat);
}

sysctl_lowmem_reserve_ratio: 定义内存区域之间保护比率的内核参数

此代码片段定义了一个名为sysctl_lowmem_reserve_ratio的全局数组。这个数组是Linux内核内存管理的一个关键可调参数 (tunable), 它为setup_per_zone_lowmem_reserve函数提供了核心的”保护比率”。数组中的每一个值都规定了, 当一个来自较高内存区域(如ZONE_NORMAL)的内存分配请求试图在较低内存区域(如ZONE_DMA)中分配内存时, 应该在该较低区域中保留多少内存, 以防止其被完全耗尽。

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
/*
* 注释中给出的示例, 解释了在一个具有1GB内存的PC架构上, 当 lowmem_reserve_ratio 分别为 256 和 32 时的效果.
* 这种情况通常有 ZONE_DMA(<16MB), ZONE_NORMAL(<896MB), 和 ZONE_HIGHMEM(>896MB) 三个区域.
* - 当一个来自 ZONE_NORMAL 的分配请求试图在 ZONE_DMA 中分配时, 它会保留 ZONE_DMA 内存的 1/256.
* - 当一个来自 ZONE_HIGHMEM 的分配请求试图在 ZONE_NORMAL 中分配时, 它会保留 ZONE_NORMAL 内存的 1/32.
* - 当一个来自 ZONE_HIGHMEM 的分配请求试图在 ZONE_DMA 中分配时, 它会保留 ZONE_DMA 内存的 1/256.
* 这个机制确保了较低的内存区域总是有一些余量, 以服务那些只能在低地址分配的请求(如某些老旧设备的DMA).
*/

/*
* 定义一个静态整型数组, 数组大小为 MAX_NR_ZONES (系统中可能的最大内存区域数量).
* 这个数组可以通过 sysctl 接口在运行时进行调整.
* 它为每个内存区域(由数组索引代表)存储一个保护比率.
*/
static int sysctl_lowmem_reserve_ratio[MAX_NR_ZONES] = {
/*
* #ifdef 是一个C预处理器指令. 下面的条目只有在内核编译时定义了相应的配置宏时才会被包含进来.
*/
#ifdef CONFIG_ZONE_DMA
/*
* 如果内核配置了 CONFIG_ZONE_DMA (通常用于x86架构上ISA设备的<16MB内存区域),
* 则为 ZONE_DMA 设置保护比率为 256.
* 这是一个非常高的保护比率(即保留 1/256), 因为 ZONE_DMA 非常宝贵.
* STM32平台通常不使用此配置.
*/
[ZONE_DMA] = 256,
#endif
#ifdef CONFIG_ZONE_DMA32
/*
* 如果内核配置了 CONFIG_ZONE_DMA32 (通常用于x86_64架构上<4GB的内存区域),
* 则为 ZONE_DMA32 设置保护比率为 256.
* STM32平台不使用此配置.
*/
[ZONE_DMA32] = 256,
#endif
/*
* 为 ZONE_NORMAL (普通内存区域) 设置保护比率为 32.
* 这意味着当来自 ZONE_HIGHMEM 的分配请求试图在 ZONE_NORMAL 中分配时,
* 内核会尝试在 ZONE_NORMAL 中保留其大小的 1/32.
* 在只有一个 ZONE_NORMAL 的STM32系统上, 这个值虽然存在, 但没有实际作用.
*/
[ZONE_NORMAL] = 32,
#ifdef CONFIG_HIGHMEM
/*
* 如果内核配置了 CONFIG_HIGHMEM (用于32位系统上访问超过~1GB的内存),
* 则为 ZONE_HIGHMEM 设置保护比率为 0.
* 比率为0意味着 ZONE_HIGHMEM 不需要为任何来自更高区域(不存在)的分配请求保留内存.
* STM32平台通常不启用 HIGHMEM.
*/
[ZONE_HIGHMEM] = 0,
#endif
/*
* 为 ZONE_MOVABLE (可移动页面区域) 设置保护比率为 0.
* 可移动区域专门用于存放用户空间页面等可以随意移动的内容, 它不需要保护自己不被侵占.
*/
[ZONE_MOVABLE] = 0,
};

calculate_totalreserve_pages: 计算内核保留的总页数

此函数的核心作用是计算一个全局的内核参数 totalreserve_pages。这个值代表了系统中所有内存区域(Zone)的”保留页数”总和, 它综合了每个区域的高水位标记 (high_watermark) 和为防止低层内存被高层内存分配耗尽而设置的低内存保留区 (lowmem_reserve)。这个总保留页数是内核衡量整体内存压力的一个关键指标, 主要供OOM (Out-Of-Memory) Killer用以判断系统是否真正处于危险的低内存状态。

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
/*
* calculate_totalreserve_pages - 当 sysctl_lowmem_reserve_ratio 或 min_free_kbytes 改变时被调用.
*/
static void calculate_totalreserve_pages(void)
{
/*
* pgdat: 一个指向 struct pglist_data 的指针, 代表一个内存节点.
* reserve_pages: 一个无符号长整型变量, 用于累加所有节点的总保留页数.
* i, j: 枚举类型 zone_type 的变量, 用作循环计数器.
*/
struct pglist_data *pgdat;
unsigned long reserve_pages = 0;
enum zone_type i, j;

/*
* 遍历系统中所有在线的内存节点. 在STM32这种UMA(统一内存访问)架构上, 这个循环只会执行一次.
*/
for_each_online_pgdat(pgdat) {

/*
* 将当前节点的总保留页数清零, 准备重新计算.
*/
pgdat->totalreserve_pages = 0;

/*
* 遍历节点中的所有内存区域. 在只有一个ZONE_NORMAL的STM32上, 这个循环也只会执行一次.
*/
for (i = 0; i < MAX_NR_ZONES; i++) {
/*
* 获取当前区域i的zone结构体指针.
*/
struct zone *zone = pgdat->node_zones + i;
/*
* max: 一个长整型变量, 用于寻找当前区域i中最大的lowmem_reserve值.
*/
long max = 0;
/*
* 获取当前区域可管理的总页数.
*/
unsigned long managed_pages = zone_managed_pages(zone);

/* 在这个区域中, 寻找有效且最大的 lowmem_reserve 值 */
/*
* 遍历从区域i到最高的区域j.
* 在只有一个ZONE_NORMAL的STM32上, 这个循环只会对 j=i=ZONE_NORMAL 执行一次.
*/
for (j = i; j < MAX_NR_ZONES; j++) {
/*
* 比较并找出最大的保留值. 因为在STM32上lowmem_reserve[j]为0, 所以max将保持为0.
*/
if (zone->lowmem_reserve[j] > max)
max = zone->lowmem_reserve[j];
}

/* 我们将高水位标记也视作保留页. */
/*
* 将高水位标记 (WMARK_HIGH) 的页数加到max上.
* 在STM32上, 这行代码实际上等价于 max = high_wmark_pages(zone).
*/
max += high_wmark_pages(zone);

/*
* 一个安全检查: 保留的页数不能超过区域本身的总页数.
*/
if (max > managed_pages)
max = managed_pages;

/*
* 将计算出的max值累加到当前节点的总保留页数上.
*/
pgdat->totalreserve_pages += max;

/*
* 将max值也累加到全局的总保留页数上.
*/
reserve_pages += max;
}
}
/*
* 将最终计算出的全局保留页数赋给全局变量 totalreserve_pages.
*/
totalreserve_pages = reserve_pages;
/*
* trace_mm_calculate_totalreserve_pages 是一个追踪点, 用于调试内存管理子系统.
*/
trace_mm_calculate_totalreserve_pages(totalreserve_pages);
}

calculate_min_free_kbytes: 计算内核应保留的最小空闲内存

此函数的作用是为Linux内核计算一个至关重要的参数:min_free_kbytes。这个参数定义了内核将尽力维持的系统最小空闲内存量, 它是一个安全基线, 用于防止系统在内存压力下完全耗尽内存而崩溃。此函数采用非线性的平方根公式进行计算, 以便在小内存和大内存系统上都能得出一个合理的值。

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
/*
* 初始化 min_free_kbytes.
*
* 对于小内存机器, 我们希望它小一些 (最小128k). 对于大内存机器, 我们希望它大一些 (最大256MB).
* 但它不是线性的, 因为网络带宽并不会随着机器内存大小线性增长. 我们使用
*
* min_free_kbytes = 4 * sqrt(lowmem_kbytes), 为了更好的精度, 实现为:
* min_free_kbytes = sqrt(lowmem_kbytes * 16)
*
* 这将得出如下结果:
*
* 16MB: 512k
* 32MB: 724k
* 64MB: 1024k
* 128MB: 1448k
* 256MB: 2048k
* 512MB: 2896k
* 1024MB: 4096k
* 2048MB: 5792k
* 4096MB: 8192k
* 8192MB: 11584k
* 16384MB: 16384k
*/
void calculate_min_free_kbytes(void)
{
/*
* lowmem_kbytes: 一个无符号长整型变量, 用于存储系统低端内存的总大小 (以KB为单位).
* 在32位的STM32平台上, 所有可用内存都属于低端内存.
*/
unsigned long lowmem_kbytes;
/*
* new_min_free_kbytes: 一个整型变量, 用于存储根据公式新计算出的 min_free_kbytes 值.
*/
int new_min_free_kbytes;

/*
* 调用 nr_free_buffer_pages() 获取系统当前可用于分配的总页数,
* 然后乘以每页的KB数 (PAGE_SIZE >> 10, 即 PAGE_SIZE / 1024),
* 从而得到系统可用内存的总KB数.
*/
lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);
/*
* 这是核心计算公式. 调用 int_sqrt() 函数计算整数平方根.
* 这个公式等价于 4 * sqrt(lowmem_kbytes).
*/
new_min_free_kbytes = int_sqrt(lowmem_kbytes * 16);

/*
* 检查新计算出的值是否大于用户在内核启动命令行中通过 'min_free_kbytes=' 参数设定的值(user_min_free_kbytes).
* 内核尊重用户设定的值, 只要它不低于自动计算出的值 (即, 用户可以要求更高的保留值, 但不能要求更低的).
*/
if (new_min_free_kbytes > user_min_free_kbytes)
/*
* 如果自动计算的值更优, 则使用 clamp() 宏将其限制在一个安全范围内.
* 最终值被限制在 128KB (对非常小的系统) 和 262144KB (即256MB, 对非常大的系统) 之间.
* 然后将这个最终值赋给全局变量 min_free_kbytes.
*/
min_free_kbytes = clamp(new_min_free_kbytes, 128, 262144);
else
/*
* 如果用户设定的值更高, 则保留用户设定的值, 并打印一条警告信息,
* 告知开发者内核为何没有更新 min_free_kbytes 的值.
*/
pr_warn("min_free_kbytes is not updated to %d because user defined value %d is preferred\n",
new_min_free_kbytes, user_min_free_kbytes);

}

setup_per_zone_wmarks: 为每个内存区域(Zone)设置水位标记

此函数的核心作用是为内核中的每一个内存区域(Zone)计算并设置其minlow、和high三个”水位标记” (watermarks)。这些水位标记是内存管理子系统的关键阈值。当一个区域的空闲内存数量下降到不同的水位线以下时, 内核会触发相应的对策, 从最温和的后台回收, 到最激进的阻塞分配请求, 从而确保系统在内存压力下仍能保持稳定, 防止因内存耗尽而崩溃。

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/*
* 这是一个内部函数, 执行实际的水位标记设置逻辑.
*/
static void __setup_per_zone_wmarks(void)
{
/*
* 将全局的 min_free_kbytes (单位KB) 转换为页数, 存入 pages_min.
* 这是所有区域 WMARK_MIN 的总基准.
*/
unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10);
/*
* lowmem_pages: 用于统计所有"低端内存"区域的总页数.
*/
unsigned long lowmem_pages = 0;
struct zone *zone;
unsigned long flags;

/* 计算所有 !ZONE_HIGHMEM 和 !ZONE_MOVABLE 区域的总页数 */
/*
* 遍历系统中的每一个内存区域 (zone).
* 在STM32H750上, 通常只有一个 ZONE_NORMAL.
*/
for_each_zone(zone) {
/*
* is_highmem: 检查是否为高端内存区 (32位系统上大于~1GB的内存). STM32不适用.
* ZONE_MOVABLE: 是指专门用于可移动页面的区域. STM32通常不使用.
* 因此, 在STM32上, 这个条件为真, lowmem_pages 会累加 ZONE_NORMAL 的总页数.
*/
if (!is_highmem(zone) && zone_idx(zone) != ZONE_MOVABLE)
lowmem_pages += zone_managed_pages(zone);
}

/*
* 再次遍历每一个内存区域, 为它们分别设置水位标记.
*/
for_each_zone(zone) {
u64 tmp;

/*
* 对当前区域的锁 zone->lock 进行加锁, 并保存中断状态到 flags.
* 这是为了保护下面对 _watermark 数组的修改操作, 防止在单核系统上被中断或抢占打断.
*/
spin_lock_irqsave(&zone->lock, flags);
/*
* 这是一个按比例分配的核心计算.
* 它将全局的 pages_min, 按照当前区域的页数(zone_managed_pages)占总低端内存页数(lowmem_pages)的比例,
* 分配给当前区域.
* 在只有一个 ZONE_NORMAL 的STM32上, 这个比例是1, 所以 tmp 的值就等于 pages_min.
*/
tmp = (u64)pages_min * zone_managed_pages(zone);
tmp = div64_ul(tmp, lowmem_pages);

/*
* 这个 if 分支用于处理高端内存区或可移动内存区, 在STM32上通常不执行.
* 它为这些特殊区域设置一个较小的、固定的 WMARK_MIN, 因为常规内存分配很少使用它们.
*/
if (is_highmem(zone) || zone_idx(zone) == ZONE_MOVABLE) {
unsigned long min_pages;

min_pages = zone_managed_pages(zone) / 1024;
min_pages = clamp(min_pages, SWAP_CLUSTER_MAX, 128UL);
zone->_watermark[WMARK_MIN] = min_pages;
} else {
/*
* 对于低端内存区域 (STM32的ZONE_NORMAL会进入此分支),
* 将 WMARK_MIN 设置为上面按比例计算出的 tmp 值.
* WMARK_MIN 是最低水位, 低于此水位, 内存分配将会同步等待回收.
*/
zone->_watermark[WMARK_MIN] = tmp;
}

/*
* 计算 kswapd 水位标记之间的距离(即 WMARK_LOW 与 WMARK_MIN 的差值).
* 这个距离决定了页面回收的"缓冲带"大小.
* 取下面两个值的较大者:
* 1. tmp >> 2: WMARK_MIN 的 1/4, 提供一个基础的缓冲量.
* 2. mult_frac(...): 区域总大小的一个小比例 (由 watermark_scale_factor 决定, 默认为0.1%).
* 这样做可以确保在小内存系统上也有一个最小的缓冲带.
*/
tmp = max_t(u64, tmp >> 2,
mult_frac(zone_managed_pages(zone),
watermark_scale_factor, 10000));

/* watermark_boost 用于在压力下动态提升水位, 此处初始化为0. */
zone->watermark_boost = 0;
/*
* WMARK_LOW = WMARK_MIN + 缓冲带.
* 当空闲内存低于 WMARK_LOW 时, 内核会唤醒kswapd线程来异步回收页面.
*/
zone->_watermark[WMARK_LOW] = min_wmark_pages(zone) + tmp;
/*
* WMARK_HIGH = WMARK_LOW + 缓冲带.
* WMARK_HIGH 是一个更宽松的阈值, kswapd一旦被唤醒, 会尝试回收到这个水位以上再重新休眠.
*/
zone->_watermark[WMARK_HIGH] = low_wmark_pages(zone) + tmp;
/* WMARK_PROMO 用于页面晋升, 这里设置为更高一级. */
zone->_watermark[WMARK_PROMO] = high_wmark_pages(zone) + tmp;

/* trace_mm_setup_per_zone_wmarks 是一个追踪点, 用于调试. */
trace_mm_setup_per_zone_wmarks(zone);

/*
* 解锁区域锁, 并恢复之前保存的中断状态.
*/
spin_unlock_irqrestore(&zone->lock, flags);
}

/*
* 更新 totalreserve_pages, 这是一个全局变量, 累加了所有区域的 WMARK_MIN,
* 主要供OOM(Out-Of-Memory) killer判断系统状况时使用.
*/
calculate_totalreserve_pages();
}

/**
* setup_per_zone_wmarks - 当 min_free_kbytes 改变或内存被热插拔时调用.
*
* 确保每个区域的 watermark[min,low,high] 值相对于 min_free_kbytes 被正确设置.
*/
void setup_per_zone_wmarks(void)
{
struct zone *zone;
/*
* 使用 DEFINE_SPINLOCK 静态定义一个自旋锁.
* 这个锁用于确保整个 __setup_per_zone_wmarks 函数的执行是原子操作, 防止并发调用.
*/
static DEFINE_SPINLOCK(lock);

/* 加锁 */
spin_lock(&lock);
/* 调用内部核心函数执行实际工作 */
__setup_per_zone_wmarks();
/* 解锁 */
spin_unlock(&lock);

/*
* 水位标记的大小已经改变, 所以更新 per-cpu 的页面缓存批次(batch)和高位限制(high limits),
* 否则这些限制可能变得不合适.
*/
for_each_zone(zone)
zone_pcp_update(zone, 0);
}

setup_per_zone_lowmem_reserve: 设置每个内存区域的低内存保留区

此函数的核心作用是为内核中的每个内存区域(Zone)设置一个保护机制, 称为”低内存保留区” (lowmem_reserve)。这个机制的目的是防止来自较高内存区域的分配请求, 耗尽较低内存区域的内存。它通过在每个较低的区域中”保留”一部分页面, 使这些页面对于那些本可以由较高区域满足的分配请求变得不可用, 从而确保即使在内存压力下, 每个区域也能为真正需要它的分配请求保留一些空间。

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
/*
* setup_per_zone_lowmem_reserve - 当 sysctl_lowmem_reserve_ratio 改变时被调用.
* 确保每个区域都有一个正确的页面保留值, 以便在一次成功的 __alloc_pages() 调用后,
* 该区域中仍有足够数量的页面剩余.
*/
static void setup_per_zone_lowmem_reserve(void)
{
/*
* pgdat: 一个指向 struct pglist_data 的指针, 代表一个内存节点. 在STM32这种UMA架构上, 只有一个内存节点.
* i, j: 枚举类型 zone_type 的变量, 用作循环计数器来遍历内存区域.
*/
struct pglist_data *pgdat;
enum zone_type i, j;

/*
* 遍历系统中所有在线的内存节点. 对于STM32, 这个循环只会执行一次.
*/
for_each_online_pgdat(pgdat) {
/*
* 外层循环: 遍历所有可能的内存区域, 直到倒数第二个.
* i 代表那个需要被"保护"的、较低的内存区域.
*/
for (i = 0; i < MAX_NR_ZONES - 1; i++) {
/*
* 获取区域 i 的 zone 结构体指针.
*/
struct zone *zone = &pgdat->node_zones[i];
/*
* 获取为区域 i 配置的保护比率. 这是一个可以通过 sysctl 在运行时调整的全局数组.
*/
int ratio = sysctl_lowmem_reserve_ratio[i];
/*
* 如果保护比率为0或者当前区域本身没有可管理的内存页, 那么就不需要设置保留区.
*/
bool clear = !ratio || !zone_managed_pages(zone);
/*
* managed_pages: 一个累加器, 用于计算所有比 i 更高的区域的总页数.
*/
unsigned long managed_pages = 0;

/*
* 内层循环: 遍历所有比 i 更高的内存区域.
* j 代表那个可能会"侵占"区域 i 内存的、较高的内存区域.
* 在只有一个 ZONE_NORMAL 的STM32系统上, 这个循环体永远不会被执行.
*/
for (j = i + 1; j < MAX_NR_ZONES; j++) {
/*
* 获取上层区域 j 的 zone 结构体指针.
*/
struct zone *upper_zone = &pgdat->node_zones[j];

/*
* 累加更高区域的页数.
*/
managed_pages += zone_managed_pages(upper_zone);

/*
* 如果 clear 标志为真, 就将保留值设为0.
*/
if (clear)
zone->lowmem_reserve[j] = 0;
else
/*
* 核心逻辑: 设置区域 i 为防止区域 j 的分配而保留的页数.
* 保留的页数 = (所有高于i且不高于j的区域的总页数) / 保护比率.
* 这意味着, 目标分配区域 j 越高, 在低区域 i 中需要保留的页面就越多.
*/
zone->lowmem_reserve[j] = managed_pages / ratio;
/*
* trace_mm_setup_per_zone_lowmem_reserve 是一个追踪点, 用于调试.
*/
trace_mm_setup_per_zone_lowmem_reserve(zone, upper_zone,
zone->lowmem_reserve[j]);
}
}
}

/*
* 在更新了各个区域的保留值之后, 调用 calculate_totalreserve_pages.
* 这个函数会重新计算一个全局的 totalreserve_pages 值, 该值是所有区域的 WMARK_MIN 和
* lowmem_reserve 的总和, 主要供OOM(Out-Of-Memory) killer判断系统状况时使用.
*/
calculate_totalreserve_pages();
}

init_per_zone_wmark_min: 初始化每个内存区域的水位标记

此函数的核心作用是根据系统中可用的总内存量, 计算并设置内核内存管理子系统的关键阈值——“水位标记” (watermarks)。这些水位标记是内存压力的指示器, 当空闲内存量下降到不同水位时, 会触发内核采取不同的行动 (如启动页面回收、阻塞分配请求等), 以确保系统的稳定运行, 防止因内存耗尽而崩溃。

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
/*
* int __meminit init_per_zone_wmark_min(void)
*
* 这是一个内存子系统的初始化函数.
* __meminit 属性表明该函数仅在内核启动时执行, 其占用的内存可在初始化后被回收.
* @return: 总是返回0, 表示成功.
*/
int __meminit init_per_zone_wmark_min(void)
{
/*
* 调用 calculate_min_free_kbytes() 函数.
* 此函数会根据系统总内存量, 计算出一个合理的内核应保留的最小空闲内存值(min_free_kbytes), 作为一个全局基准.
* 在STM32平台上, 它会查看可用的SRAM总量, 并据此设定一个安全阈值.
*/
calculate_min_free_kbytes();
/*
* 调用 setup_per_zone_wmarks() 函数.
* 这是本函数的核心. 它会遍历系统中的每一个内存区域(zone), 并基于上面计算出的全局 min_free_kbytes 值,
* 为每个区域设置三个关键的水位标记: wmark_min, wmark_low, 和 wmark_high.
* 当一个区域的空闲内存低于 wmark_low 时, 内核的页面回收守护进程(kswapd)会被唤醒.
*/
setup_per_zone_wmarks();
/*
* 调用 refresh_zone_stat_thresholds() 函数.
* 这个函数刷新用于更新 /proc/vmstat 中统计信息的阈值.
* 为了降低开销, 某些内存统计信息只有在变化量超过一定阈值时才会被更新. 此调用会根据新的水位标记调整这些阈值.
*/
refresh_zone_stat_thresholds();
/*
* 调用 setup_per_zone_lowmem_reserve() 函数.
* 此函数为每个内存区域设置一个"低内存保留区". 这是一个安全机制,
* 它确保即使在内存严重不足时, 内核为执行关键操作(如内存回收)而发起的特权内存分配请求也能够成功.
*/
setup_per_zone_lowmem_reserve();

/*
* #ifdef CONFIG_NUMA 是一个预处理指令. 下面的代码块仅在内核配置了CONFIG_NUMA时才会被编译.
* NUMA (非统一内存访问架构) 是用于多CPU插槽的服务器的技术.
* STM32H750是单核、统一内存架构的微控制器, 不会启用此配置.
* 因此, 下面的代码在为STM32编译的内核中不存在.
*/
#ifdef CONFIG_NUMA
setup_min_unmapped_ratio();
setup_min_slab_ratio();
#endif

/*
* 调用 khugepaged_min_free_kbytes_update() 函数.
* 此函数用于更新透明大页(Transparent Huge Pages, THP)守护进程(khugepaged)所需的最小空闲内存阈值.
* 透明大页是依赖于MMU的性能优化技术.
* 在无MMU的STM32平台上, CONFIG_TRANSPARENT_HUGEPAGE 会被禁用,
* 因此这个函数调用通常是一个空操作, 不会产生任何实际代码.
*/
khugepaged_min_free_kbytes_update();

/*
* 返回0, 表示初始化成功.
*/
return 0;
}
/*
* 使用 postcore_initcall 宏注册 init_per_zone_wmark_min 函数.
* 这确保了它在内核的核心组件(如内存区域本身)初始化之后, 但在大多数设备驱动开始大量消耗内存之前被调用.
* 这是一个执行内存管理策略初始化的恰当时机.
*/
postcore_initcall(init_per_zone_wmark_min)