Linux 内存管理深度剖析(一):物理内存组织与伙伴分配系统

物理内存是操作系统最基础的资源之一,而 Linux 内核的内存管理子系统正是围绕着如何高效、可靠地组织与分配这些物理页帧展开的。本文基于 Linux 6.4-rc1 源码,系统性地剖析物理内存的组织模型、zone 水位机制、伙伴分配器的核心算法,以及 Per-CPU 页帧缓存、GFP 标志体系和 OOM Killer 的工作原理,并给出实用的诊断方法。


一、物理内存模型

1.1 从硬件拓扑到内核抽象

现代服务器普遍采用 NUMA(Non-Uniform Memory Access) 架构:多个处理器插槽各自拥有本地内存,访问本地内存的延迟远低于访问远端节点的内存。Linux 内核用三层抽象来映射这一硬件现实:

1
2
3
NUMA Node (pg_data_t)
└── Zone (struct zone)
└── Page (struct page)

每个 NUMA 节点对应一个 pg_data_t(即 pglist_data),节点内的物理内存按地址范围和硬件约束划分为若干 Zone,每个物理页帧对应一个 struct page

1.2 NUMA 节点:struct pglist_data

pg_data_t 是节点级别的核心数据结构,定义于 include/linux/mmzone.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct pglist_data {
/* 本节点的所有 zone,按类型索引 */
struct zone node_zones[MAX_NR_ZONES];

/* 包含所有节点所有 zone 的有序列表,用于跨节点 fallback */
struct zonelist node_zonelists[MAX_ZONELISTS];

int nr_zones; /* 本节点实际存在的 zone 数量 */

unsigned long node_start_pfn; /* 本节点起始物理页帧号 */
unsigned long node_present_pages; /* 物理页总数 */
unsigned long node_spanned_pages; /* 含空洞的总跨度 */
int node_id; /* NUMA 节点 ID */

wait_queue_head_t kswapd_wait; /* kswapd 等待队列 */
struct task_struct *kswapd; /* 本节点的内存回收守护线程 */
int kswapd_order;
enum zone_type kswapd_highest_zoneidx;
...
} pg_data_t;

关键字段解析:

  • node_zones[]:仅包含本节点各 zone。分配器直接通过数组下标(ZONE_DMAZONE_NORMAL 等枚举值)访问。
  • node_zonelists[]zonelist 是一个有序的 zone 列表,按分配优先级排列。ZONELIST_FALLBACK 包含全系统所有可用 zone,当本节点内存不足时,分配器沿此列表向其他节点 fallback。ZONELIST_NOFALLBACK(仅 NUMA 下存在)用于 __GFP_THISNODE,强制只在本节点分配。
  • kswapd:每个节点有一个 kswapd 内核线程,专门负责异步回收该节点的内存。当某个 zone 的空闲页跌破 low watermark 时唤醒它。

在 UMA(单 NUMA 节点)系统上,内核只有一个全局的 contig_page_dataNODE_DATA(0) 直接指向它,编译期优化掉了所有 NUMA 间接寻址。

1.3 内存区域:struct zone

struct zone 是分配器的核心工作单元,关键字段如下(include/linux/mmzone.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct zone {
/* watermark 数组,通过 *_wmark_pages(zone) 宏访问 */
unsigned long _watermark[NR_WMARK];
unsigned long watermark_boost;

unsigned long nr_reserved_highatomic;

/* 每 CPU 页集 */
struct per_cpu_pages __percpu *per_cpu_pageset;

/* zone 起始 PFN */
unsigned long zone_start_pfn;

/* 由伙伴系统管理的页数(present - reserved) */
atomic_long_t managed_pages;
unsigned long spanned_pages; /* 含空洞 */
unsigned long present_pages; /* 实际物理页 */

/* 核心:各阶 free_area,MAX_ORDER+1 个 */
struct free_area free_area[MAX_ORDER + 1];

spinlock_t lock; /* 保护 free_area */
...
} ____cacheline_internodealigned_in_smp;

几个关键计数的关系:

1
2
3
spanned_pages = zone_end_pfn - zone_start_pfn   (含物理空洞)
present_pages = spanned_pages - holes (真实物理页)
managed_pages = present_pages - reserved_pages (buddy 管理的页)

managed_pages 是 watermark 计算和可用内存统计的基准,reserved_pages 包含 bootmem 分配器使用的页、memmap 本身等内核保留页。

1.4 空闲区域:struct free_area 与迁移类型

伙伴系统按 分配阶(order) 组织空闲页,每个 order 对应 2^order 个连续页帧。MAX_ORDER 默认为 10,即最大单次分配 1024 页(4MB,x86-64 下),MAX_ORDER + 1 共 11 个阶。

1
2
3
4
5
/* include/linux/mmzone.h */
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};

每个 free_area 包含 MIGRATE_TYPES 条链表,分别对应不同的迁移类型。这是 Linux 应对内存碎片化的核心机制:将具有相同迁移特性的页聚集在一起,减少不可移动页散落在可移动页块中导致的碎片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* include/linux/mmzone.h */
enum migratetype {
MIGRATE_UNMOVABLE, /* 不可移动:内核数据结构、模块 */
MIGRATE_MOVABLE, /* 可移动:用户空间匿名页、文件缓存 */
MIGRATE_RECLAIMABLE, /* 可回收:slab 缓存(SLAB_RECLAIM_ACCOUNT) */
MIGRATE_PCPTYPES, /* PCP 链表使用上述三种类型 */
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, /* 高阶原子分配预留 */
#ifdef CONFIG_CMA
MIGRATE_CMA, /* CMA 专用:只允许可移动页分配 */
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE, /* 隔离中,不可分配 */
#endif
MIGRATE_TYPES
};

MIGRATE_HIGHATOMIC 是一个专为 order > 0 的原子分配(如网络驱动中断路径)预留的池,通过 nr_reserved_highatomic 控制其大小。

Fallback 顺序:当目标迁移类型的 free_list 为空时,__rmqueue_fallback() 按预定顺序回退:

1
2
3
4
5
6
/* mm/page_alloc.c */
static int fallbacks[MIGRATE_TYPES][MIGRATE_PCPTYPES - 1] = {
[MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE },
[MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE },
[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE },
};

当从其他迁移类型窃取页块时,内核会将整个页块的迁移类型重新标记,以保持页块内迁移类型的一致性,减少长期碎片化。

1.5 物理页描述符:struct page

系统中每个物理页帧都有一个对应的 struct page,存储在 vmemmap 区域中(SPARSEMEM 内存模型下)。struct page 设计极为精巧,大量使用 union 以减少内存占用(include/linux/mm_types.h):

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
struct page {
unsigned long flags; /* 页标志位:zone/node 编码 + PG_locked/PG_dirty 等 */

union {
struct { /* 页缓存页与匿名页 */
union {
struct list_head lru; /* LRU 链表节点 */
struct list_head buddy_list; /* 在 buddy 空闲链表中 */
struct list_head pcp_list; /* 在 PCP 链表中 */
};
struct address_space *mapping; /* 文件页:指向 address_space
* 匿名页:指向 anon_vma(低位置1) */
union {
pgoff_t index; /* 在 mapping 中的偏移 */
unsigned long share;
};
unsigned long private; /* 若 PageBuddy,存储 order;
* 若 PagePrivate,存储 buffer_heads */
};
struct { /* 复合页尾页 */
unsigned long compound_head; /* bit 0 置 1 标识尾页 */
};
struct rcu_head rcu_head;
/* ... 其他用途 ... */
};

union {
atomic_t _mapcount; /* 被多少个页表项映射,-1 表示未映射 */
unsigned int page_type;
};

atomic_t _refcount; /* 引用计数,0 表示空闲 */

#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif
} _struct_page_alignment;

几个核心字段的语义:

字段 含义
flags 高位编码 section/node/zone 信息,低位为 PG_* 状态标志
_refcount 页的引用计数,alloc_pages() 返回时为 1,put_page() 减至 0 则释放
_mapcount 用户态页表映射次数,-1 表示未被任何页表映射
mapping 文件页指向 address_space,匿名页指向 anon_vma(bit 0 置 1 区分)
index 文件页在 page cache 中的偏移;匿名页在 vma 内的偏移
private PageBuddy 时存 order,PagePrivate 时存 buffer_head 指针
buddy_list 页在 buddy free_list 上时的链表节点
pcp_list 页在 Per-CPU 链表上时的链表节点

flags 字段的布局(以 64 位系统为例):

1
| SECTION bits | NODE bits | ZONE bits | LAST_CPUPID bits | ... | PG_* flag bits |

通过 page_zonenum(page)page_to_nid(page) 等宏可以从 flags 字段直接读取 zone 和 node 编号,无需额外存储。


二、内存区域(Zone)

2.1 Zone 类型与地址范围

Linux 将物理内存按用途和硬件约束划分为若干 zone(include/linux/mmzone.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA, /* 历史遗留:x86 下 0~16MB,供 ISA DMA 设备使用 */
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32, /* x86-64:0~4GB,供 32 位 DMA 地址空间设备使用 */
#endif
ZONE_NORMAL, /* 内核直接映射的普通内存(x86-64:4GB~ 全部内存) */
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM, /* 32 位系统:高于 896MB 需动态映射的内存 */
#endif
ZONE_MOVABLE, /* 软件虚拟 zone:仅存放可移动页,便于内存热插拔 */
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE, /* 持久内存(PMEM)、GPU HBM 等设备内存 */
#endif
__MAX_NR_ZONES
};

在典型的 x86-64 系统上:

Zone 地址范围 用途
ZONE_DMA 0 ~ 16 MB 旧式 ISA 总线 DMA
ZONE_DMA32 0 ~ 4 GB 32 位寻址 DMA 设备
ZONE_NORMAL 4 GB ~ 全部 内核直接映射
ZONE_MOVABLE 配置决定 热插拔/THP 专用

ZONE_MOVABLE 并不对应特定的物理地址范围,而是一个软件抽象:通过 kernelcore=movablecore= 内核参数,将高端内存的一部分划为只允许可移动分配的区域,从而保证内存热拔出时可以将这段内存中的页全部迁移走。

2.2 Watermark 水位机制

每个 zone 有三条水位线,加上一个动态 boost 值:

1
2
3
4
5
6
7
8
9
10
11
12
/* include/linux/mmzone.h */
enum zone_watermarks {
WMARK_MIN, /* 最低水位 */
WMARK_LOW, /* 低水位 */
WMARK_HIGH, /* 高水位 */
WMARK_PROMO, /* 提升水位(MGLRU 使用) */
NR_WMARK
};

#define min_wmark_pages(z) (z->_watermark[WMARK_MIN] + z->watermark_boost)
#define low_wmark_pages(z) (z->_watermark[WMARK_LOW] + z->watermark_boost)
#define high_wmark_pages(z) (z->_watermark[WMARK_HIGH] + z->watermark_boost)

水位的触发逻辑:

1
2
3
4
空闲页 > high_wmark  → 充裕,kswapd 休眠
空闲页 < low_wmark → 唤醒 kswapd,后台异步回收至 high_wmark
空闲页 < min_wmark → 触发直接回收(direct reclaim),分配路径阻塞
空闲页 < min/2 → GFP_ATOMIC 类紧急分配仍可通过(访问 atomic reserves)

水位由 vm.min_free_kbytes sysctl 决定(min watermark),low 和 high 通过比例推算。watermark_boost 是当检测到碎片化时动态抬升的临时增量,用于提前触发压缩,消除碎片。

2.3 zone_watermark_ok 实现

分配器在每次从 zone 取页前,都会调用 watermark 检查函数(mm/page_alloc.c):

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

/* 减去不可用的保留页(highatomic、lowmem_reserve 等) */
free_pages -= __zone_watermark_unusable_free(z, order, alloc_flags);

/* GFP_ATOMIC/__GFP_HIGH 可访问更多保留,OOM victim 可访问最多 */
if (unlikely(alloc_flags & ALLOC_RESERVES)) {
if (alloc_flags & ALLOC_MIN_RESERVE) {
min -= min / 2;
if (alloc_flags & ALLOC_NON_BLOCK)
min -= min / 4;
}
if (alloc_flags & ALLOC_OOM)
min -= min / 2;
}

/* order-0 检查:空闲页需高于 mark + lowmem_reserve */
if (free_pages <= min + z->lowmem_reserve[highest_zoneidx])
return false;

if (!order)
return true;

/* 高阶请求:检查是否有合适的连续空闲块 */
for (o = order; o <= MAX_ORDER; 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;
}
...
}
return false;
}

bool zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark,
int highest_zoneidx, unsigned int alloc_flags)
{
return __zone_watermark_ok(z, order, mark, highest_zoneidx, alloc_flags,
zone_page_state(z, NR_FREE_PAGES));
}

lowmem_reserve[] 是一个二维保留机制:低 zone(如 DMA)为高 zone 的分配请求保留一定数量的页面,防止高 zone 内存耗尽后 DMA 分配也失败。


三、伙伴分配系统(Buddy System)

3.1 算法原理

伙伴系统(Buddy Allocator)由 Knuth 提出,Linux 将其用于管理物理页帧。核心思想:

  1. 内存按 2^n 页的块管理,共分 MAX_ORDER + 1(11)个阶。
  2. 分配 order=k 的块时,若 k 阶无空闲,则将 k+1 阶的块分裂(split)成两个 k 阶的”伙伴”,取一个返回,另一个挂入 k 阶链表。
  3. 释放时,检查对应地址的”伙伴”是否也空闲,若是则合并(merge)成高一阶的块,递归向上合并。

伙伴关系通过 PFN 的 bit-k 来判断:若 pfn ^ buddy_pfn == (1 << order),则互为伙伴。

3.2 分配入口:__alloc_pages

__alloc_pages 是整个物理内存分配的核心入口(mm/page_alloc.c):

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
/*
* This is the 'heart' of the zoned buddy allocator.
*/
struct page *__alloc_pages(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;
struct alloc_context ac = { };

if (WARN_ON_ONCE_GFP(order > MAX_ORDER, gfp))
return NULL;

gfp &= gfp_allowed_mask;
gfp = current_gfp_context(gfp); /* 继承 current 的 GFP 约束 */
alloc_gfp = gfp;
if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac,
&alloc_gfp, &alloc_flags))
return NULL;

alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp);

/* ===== Fast path:尝试直接从 freelist 分配 ===== */
page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
if (likely(page))
goto out;

/* ===== Slow path:内存回收、压缩、OOM ===== */
alloc_gfp = gfp;
ac.spread_dirty_pages = false;
ac.nodemask = nodemask;
page = __alloc_pages_slowpath(alloc_gfp, order, &ac);

out:
...
trace_mm_page_alloc(page, order, alloc_gfp, ac.migratetype);
return page;
}
EXPORT_SYMBOL(__alloc_pages);

Fast path 使用 ALLOC_WMARK_LOW,即要求空闲页高于 low watermark,这是正常情况下的快速分配。

Slow path __alloc_pages_slowpath 会依次尝试:

  1. 唤醒 kswapd,再次尝试分配(放宽至 min watermark)
  2. 直接内存回收(__alloc_pages_direct_reclaim
  3. 内存压缩(__alloc_pages_direct_compact
  4. OOM Killer(out_of_memory
  5. 无限重试(若设置了 __GFP_NOFAIL

3.3 get_page_from_freelist:遍历 zonelist

get_page_from_freelist 遍历 zonelist,对每个 zone 执行 watermark 检查,通过则调用 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
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;
...

retry:
no_fallback = alloc_flags & ALLOC_NOFRAGMENT;
z = ac->preferred_zoneref;

/* 遍历 zonelist(含跨 NUMA 节点 fallback) */
for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx,
ac->nodemask) {
unsigned long mark;
...

/* 选取对应水位 */
mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);

/* 水位检查 */
if (!zone_watermark_fast(zone, order, mark,
ac->highest_zoneidx, alloc_flags, gfp_mask)) {
/* 水位不足:尝试 node_reclaim 或 continue */
...
continue;
}

try_this_zone:
/* 真正取页 */
page = rmqueue(ac->preferred_zoneref->zone, zone, order,
gfp_mask, alloc_flags, ac->migratetype);
if (page) {
prep_new_page(page, order, gfp_mask, alloc_flags);
return page;
}
}
return NULL;
}

ALLOC_NOFRAGMENT 标志用于 fast path 的第一轮:只从本地节点分配,禁止跨节点 fallback,以避免碎片化。若失败,则去掉该标志 retry,允许跨节点分配。

3.4 rmqueue 与 __rmqueue_smallest

rmqueue 是 zone 级别的分配函数,优先尝试 PCP,否则调用 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
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;

if (likely(pcp_allowed_order(order))) {
/* order 0~3(含 THP)尝试 PCP 快速路径 */
if (!IS_ENABLED(CONFIG_CMA) || alloc_flags & ALLOC_CMA ||
migratetype != MIGRATE_MOVABLE) {
page = rmqueue_pcplist(preferred_zone, zone, order,
migratetype, alloc_flags);
if (likely(page))
goto out;
}
}

/* PCP 未命中,走 buddy 分配 */
page = rmqueue_buddy(preferred_zone, zone, order,
alloc_flags, migratetype);
out:
...
return page;
}

rmqueue_buddy 最终调用 __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
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;

/* 从目标 order 开始向上查找,直到找到非空链表 */
for (current_order = order; current_order <= MAX_ORDER; ++current_order) {
area = &(zone->free_area[current_order]);
page = get_page_from_free_area(area, migratetype);
if (!page)
continue;

/* 找到:从链表中摘除 */
del_page_from_free_list(page, zone, current_order);

/* 分裂多余阶,将下半部分挂回低阶链表 */
expand(zone, page, order, current_order, migratetype);

set_pcppage_migratetype(page, migratetype);
return page;
}
return NULL;
}

expand() 函数实现页分裂(buddy splitting):若分配 order=2 但找到 order=5 的块,则将剩余的 order=4、order=3、order=2 的”伙伴”分别挂入对应链表。

3.5 页面释放与合并:__free_one_page

释放时,__free_pagesfree_unref_page(order 0 或小阶)→ __free_one_page(合并逻辑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static inline void __free_one_page(struct page *page,
unsigned long pfn, struct zone *zone,
unsigned int order, int migratetype, fpi_t fpi_flags)
{
unsigned long buddy_pfn = 0;
unsigned long combined_pfn;
struct page *buddy;

if (likely(!is_migrate_isolate(migratetype)))
__mod_zone_freepage_state(zone, 1 << order, migratetype);

/* 循环向上合并,直到无法继续或达到 MAX_ORDER */
while (order < MAX_ORDER) {
buddy = find_buddy_page_pfn(page, pfn, order, &buddy_pfn);
if (!buddy)
goto done_merging;

/* 检查跨 pageblock 边界的合并限制 */
if (unlikely(order >= pageblock_order)) {
int buddy_mt = get_pageblock_migratetype(buddy);
if (migratetype != buddy_mt && ...)
goto done_merging;
}

/* 将伙伴从其链表中摘除 */
del_page_from_free_list(buddy, zone, order);

/* 合并:取两者 PFN 的较小值作为合并后块的起始 */
combined_pfn = buddy_pfn & pfn;
page = page + (combined_pfn - pfn);
pfn = combined_pfn;
order++; /* 升阶 */
}

done_merging:
set_buddy_order(page, order); /* 将 order 存入 page->private */

/* 决定是插入链表头还是尾(影响 cache 热度) */
to_tail = buddy_merge_likely(pfn, buddy_pfn, page, order);
if (to_tail)
add_to_free_list_tail(page, zone, order, migratetype);
else
add_to_free_list(page, zone, order, migratetype);

page_reporting_notify_free(order);
}

合并时取 buddy_pfn & pfn 是因为伙伴对的 PFN 仅在 bit-order 处不同,AND 后得到对齐的合并块起始地址。set_buddy_order 将 order 写入 page->private,通过 PageBuddy 标志区分该页是否在 buddy 链表中。


四、Per-CPU Page Frame Cache(PCP)

4.1 设计动机

全局的 zone->lock 是伙伴分配器的主要竞争点。在多核系统上,频繁的 order-0 分配(如网络收包、进程创建)若每次都竞争 zone 锁,将极大影响吞吐量。PCP(Per-CPU Pages) 是为此设计的无锁快速路径:每个 CPU 维护一个私有的小型页池,分配与释放优先从中操作,仅在池枯竭或过满时才批量与全局 buddy 交互。

4.2 struct per_cpu_pages

1
2
3
4
5
6
7
8
9
10
11
12
13
/* include/linux/mmzone.h */
struct per_cpu_pages {
spinlock_t lock; /* 保护 lists,通常用 pcp_spin_trylock 无竞争获取 */
int count; /* 池中总页数 */
int high; /* 高水位:超出则批量还回 buddy */
int batch; /* 批量补充/清空的页数基准 */
short free_factor; /* 批量清空时的缩放因子(越多释放越多) */
#ifdef CONFIG_NUMA
short expire; /* 远端 PCP 的 drain 计数器 */
#endif
/* 按 migratetype + order 的组合索引的链表数组 */
struct list_head lists[NR_PCP_LISTS];
} ____cacheline_aligned_in_smp;

NR_PCP_LISTS 的计算:

1
2
3
4
#define NR_LOWORDER_PCP_LISTS (MIGRATE_PCPTYPES * (PAGE_ALLOC_COSTLY_ORDER + 1))
/* 3 种迁移类型 × (order 0~3) = 12 个链表 */
#define NR_PCP_LISTS (NR_LOWORDER_PCP_LISTS + NR_PCP_THP)
/* 再加 1 个 THP 链表(若启用) */

4.3 rmqueue_pcplist:PCP 快速分配

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

pcp_trylock_prepare(UP_flags);
pcp = pcp_spin_trylock(zone->per_cpu_pageset);
if (!pcp) {
pcp_trylock_finish(UP_flags);
return NULL; /* trylock 失败则回退到 buddy */
}

pcp->free_factor >>= 1; /* 分配时降低批量清空因子 */

/* 按 order+migratetype 组合找到对应链表 */
list = &pcp->lists[order_to_pindex(migratetype, order)];
page = __rmqueue_pcplist(zone, order, migratetype,
alloc_flags, pcp, list);
pcp_spin_unlock(pcp);
pcp_trylock_finish(UP_flags);
...
return page;
}

当 PCP 链表为空时,__rmqueue_pcplist 调用 rmqueue_bulk 批量从 buddy 取 batch 个页补充,再返回第一个。

4.4 free_unref_page:PCP 优先释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void free_unref_page(struct page *page, unsigned int order)
{
struct per_cpu_pages *pcp;
struct zone *zone;
unsigned long pfn = page_to_pfn(page);
int migratetype;

if (!free_unref_page_prepare(page, pfn, order))
return;

/* ISOLATE 类型直接走 buddy,其余归入 PCP */
migratetype = get_pcppage_migratetype(page);
...

pcp = pcp_spin_trylock(zone->per_cpu_pageset);
if (pcp) {
free_unref_page_commit(zone, pcp, page, migratetype, order);
pcp_spin_unlock(pcp);
} else {
/* 无法获取 PCP 锁,直接释放到 buddy */
free_one_page(zone, page, pfn, order, migratetype, FPI_NONE);
}
}

free_unref_page_commit 将页挂入 PCP 链表,若 pcp->count >= high,则调用 free_pcppages_bulk 批量将 nr_pcp_free() 个页归还给 buddy。批量大小受 free_factor 调节:连续释放时 free_factor 增大,清空的页更多,避免 PCP 频繁超高水位。


五、内存分配标志(GFP Flags)

5.1 标志体系概览

GFP(Get Free Pages)标志是调用者与分配器的”合约”,描述分配的约束和行为。定义在 include/linux/gfp_types.h

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
/* Zone 选择标志(互斥,最多一个)*/
#define __GFP_DMA ((__force gfp_t)0x01u) /* 从 ZONE_DMA 分配 */
#define __GFP_HIGHMEM ((__force gfp_t)0x02u) /* 允许 ZONE_HIGHMEM */
#define __GFP_DMA32 ((__force gfp_t)0x04u) /* 从 ZONE_DMA32 分配 */
#define __GFP_MOVABLE ((__force gfp_t)0x08u) /* 允许 ZONE_MOVABLE */

/* 迁移类型暗示 */
#define __GFP_RECLAIMABLE ((__force gfp_t)0x10u) /* 可回收(slab)*/

/* 水位/保留访问控制 */
#define __GFP_HIGH ((__force gfp_t)0x20u) /* 可访问紧急保留 */
#define __GFP_MEMALLOC ((__force gfp_t)0x20000u) /* 访问全部内存保留 */
#define __GFP_NOMEMALLOC ((__force gfp_t)0x80000u) /* 禁止访问保留 */

/* 回收控制 */
#define __GFP_IO ((__force gfp_t)0x40u) /* 允许启动物理 IO */
#define __GFP_FS ((__force gfp_t)0x80u) /* 允许调用 FS 层 */
#define __GFP_DIRECT_RECLAIM ((__force gfp_t)0x400u) /* 允许直接回收 */
#define __GFP_KSWAPD_RECLAIM ((__force gfp_t)0x800u) /* 可唤醒 kswapd */
#define __GFP_RECLAIM ((__force gfp_t)(0x400u|0x800u)) /* 直接+kswapd */

/* 重试策略 */
#define __GFP_NORETRY ((__force gfp_t)0x10000u) /* 轻量重试,可失败 */
#define __GFP_RETRY_MAYFAIL ((__force gfp_t)0x4000u) /* 多次重试,可失败 */
#define __GFP_NOFAIL ((__force gfp_t)0x8000u) /* 无限重试,不可失败 */

/* 行为修饰 */
#define __GFP_NOWARN ((__force gfp_t)0x2000u) /* 失败时不打印警告 */
#define __GFP_ZERO ((__force gfp_t)0x100u) /* 返回清零的页 */
#define __GFP_COMP ((__force gfp_t)0x40000u) /* 复合页元数据 */

5.2 常用复合标志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* include/linux/gfp_types.h */

/* 内核常规分配:可睡眠,允许 IO 和 FS,允许回收 */
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS)

/* 原子分配:不可睡眠,访问紧急保留,可唤醒 kswapd */
#define GFP_ATOMIC (__GFP_HIGH | __GFP_KSWAPD_RECLAIM)

/* 用户空间页:可睡眠,enforce cpuset 限制 */
#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)

/* 用户空间可移动页(LRU 页的典型分配) */
#define GFP_HIGHUSER_MOVABLE (GFP_USER | __GFP_HIGHMEM | __GFP_MOVABLE | ...)

/* DMA 分配(历史遗留,从 ZONE_DMA 分配) */
#define GFP_DMA __GFP_DMA

/* 不启动 IO 的回收(适用于 IO 路径本身的内存分配) */
#define GFP_NOIO (__GFP_RECLAIM)

/* 不调用 FS 的回收(适用于文件系统持锁时的分配) */
#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO)

5.3 GFP_KERNEL vs GFP_ATOMIC 使用场景

GFP_KERNEL 是内核中最常用的分配标志,适用于:

  • 进程上下文,可以睡眠等待内存
  • 无持有自旋锁(持锁时不可睡眠)
  • 不在中断处理程序或 softirq 中

典型场景:kmalloc(size, GFP_KERNEL)alloc_pages(GFP_KERNEL, 0)

GFP_ATOMIC 适用于不可睡眠的上下文:

  • 硬中断(ISR)、软中断(softirq/tasklet)
  • 持有自旋锁(spin_lock 而非 mutex)
  • 持有 RCU 读锁
  • 不允许任何阻塞操作的原子上下文

典型场景:网络驱动 rx 中断路径分配 sk_buff、定时器回调中的临时缓冲区。

GFP_ATOMICGFP_KERNEL 的分配成功率更低(因为不能回收),且分配失败时内核不会打印 WARN,调用者必须处理返回 NULL 的情况。

__GFP_NOFAIL 告诉内核无论如何都必须成功(无限重试),只应用于真正无法处理失败的关键路径,且必须配合 GFP_KERNEL(可睡眠),对 order > 1 的分配使用会触发 WARN。

__GFP_NOWARN 抑制分配失败时的 WARN 日志,适用于内核明确准备了 fallback 路径的场合(如 THP 分配失败后回退到普通页)。


六、内存分配失败与 OOM Killer

6.1 OOM 触发路径

__alloc_pages_slowpath 经过直接回收、内存压缩等所有手段后仍无法获取足够内存,且分配标志允许 OOM(没有 __GFP_NORETRY 且 order <= PAGE_ALLOC_COSTLY_ORDER),则调用 out_of_memory

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
/* mm/oom_kill.c */
bool out_of_memory(struct oom_control *oc)
{
unsigned long freed = 0;

if (oom_killer_disabled)
return false;

/* 先通知 OOM notifier,若有回调释放了内存则不必杀进程 */
if (!is_memcg_oom(oc)) {
blocking_notifier_call_chain(&oom_notify_list, 0, &freed);
if (freed > 0 && !is_sysrq_oom(oc))
return true;
}

/* 若当前进程即将退出,标记为 OOM victim 快速回收 */
if (task_will_free_mem(current)) {
mark_oom_victim(current);
queue_oom_reaper(current);
return true;
}

/* GFP_NOFS 路径不进行 OOM(避免 FS 持锁死锁) */
if (oc->gfp_mask && !(oc->gfp_mask & __GFP_FS) && !is_memcg_oom(oc))
return true;

oc->constraint = constrained_alloc(oc);
check_panic_on_oom(oc);

/* sysctl_oom_kill_allocating_task:直接杀当前进程 */
if (!is_memcg_oom(oc) && sysctl_oom_kill_allocating_task && ...) {
oc->chosen = current;
oom_kill_process(oc, "Out of memory (oom_kill_allocating_task)");
return true;
}

/* 选择最"坏"的进程 */
select_bad_process(oc);
if (!oc->chosen) {
dump_header(oc, NULL);
pr_warn("Out of memory and no killable processes...\n");
if (!is_sysrq_oom(oc) && !is_memcg_oom(oc))
panic("System is deadlocked on memory\n");
}

if (oc->chosen && oc->chosen != (void *)-1UL)
oom_kill_process(oc, "Out of memory");
return !!oc->chosen;
}

6.2 oom_badness:选择受害进程

select_bad_process 遍历所有进程,调用 oom_badness 计算每个进程的”坏分”,选择最高分者:

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
long oom_badness(struct task_struct *p, unsigned long totalpages)
{
long points;
long adj;

/* 内核线程和 init 进程不杀 */
if (oom_unkillable_task(p))
return LONG_MIN;

p = find_lock_task_mm(p);
if (!p)
return LONG_MIN;

adj = (long)p->signal->oom_score_adj;
/* OOM_SCORE_ADJ_MIN(-1000)的进程不可杀 */
if (adj == OOM_SCORE_ADJ_MIN || ...)
return LONG_MIN;

/* 基础分 = RSS + swap + 页表 */
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
mm_pgtables_bytes(p->mm) / PAGE_SIZE;
task_unlock(p);

/* oom_score_adj 在 [-1000, +1000] 范围调整最终得分 */
adj *= totalpages / 1000;
points += adj;

return points;
}

oom_score_adj 可由用户空间写入 /proc/<pid>/oom_score_adj:设为 +1000 意味着优先被杀,设为 -1000 则完全豁免。系统管理程序(如 systemd)、关键守护进程(如 sshd)通常将自己设为负值保护。

6.3 oom_kill_process:执行终止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void oom_kill_process(struct oom_control *oc, const char *message)
{
struct task_struct *victim = oc->chosen;

/* 若进程已在退出,直接标记为 OOM victim 让其快速退出 */
task_lock(victim);
if (task_will_free_mem(victim)) {
mark_oom_victim(victim);
queue_oom_reaper(victim);
task_unlock(victim);
put_task_struct(victim);
return;
}

/* 打印 OOM 报告(含内存快照),限速 */
if (__ratelimit(&oom_rs))
dump_header(oc, victim);

__oom_kill_process(victim, message);
/* 若有 memcg,还需杀死 memcg 内其他成员 */
...
}

__oom_kill_process 向受害进程发送 SIGKILL,并通过 OOM Reaper 机制(异步线程)快速回收其内存,无需等待进程完全退出。


七、诊断方法

7.1 /proc/buddyinfo

显示各节点各 zone 从 order-0 到 order-10 的空闲页块数:

1
2
3
4
$ cat /proc/buddyinfo
Node 0, zone DMA 1 0 1 0 2 1 1 0 1 1 3
Node 0, zone DMA32 186 190 86 42 12 5 4 2 1 1 22
Node 0, zone Normal 6543 3280 1754 893 452 220 108 43 19 8 113

每列对应 order 0~10 的空闲块数。高 order 块数多表示内存碎片化较轻。若 order-10 长期为 0 而 order-0 大量堆积,说明存在严重碎片化,THP 分配将频繁失败。

7.2 /proc/zoneinfo

显示每个 zone 的详细统计,包括 watermark、页统计等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat /proc/zoneinfo
Node 0, zone Normal
pages free 123456
min 8192
low 10240
high 12288
spanned 2621440
present 2621440
managed 2568192
cma 0
nr_free_pages 123456
nr_zone_inactive_anon 45678
nr_zone_active_anon 89012
...

min/low/high 即三条水位线(页数)。managed 是 buddy 管理的总页数,用于 watermark 百分比计算。

7.3 /proc/meminfo 关键字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cat /proc/meminfo
MemTotal: 65536000 kB # 总物理内存
MemFree: 8192000 kB # 当前空闲(buddy 空闲)
MemAvailable: 32768000 kB # 应用可用估算(含可回收缓存)
Buffers: 512000 kB # 块设备 buffer cache
Cached: 16384000 kB # 文件 page cache
SwapCached: 102400 kB # 已换出但仍在 swap cache 的页
Active: 24576000 kB # 活跃 LRU 页(难以回收)
Inactive: 16384000 kB # 不活跃 LRU 页(易于回收)
Slab: 1024000 kB # slab 分配器使用总量
SReclaimable: 768000 kB # 可回收 slab(dentries/inodes)
SUnreclaim: 256000 kB # 不可回收 slab(内核数据结构)
PageTables: 102400 kB # 页表占用内存
HugePages_Total: 0 # 预分配大页总数
AnonHugePages: 4096000 kB # THP 匿名大页

MemAvailable 是比 MemFree 更实用的指标,它估算了在不触发 swap 的情况下应用可以分配的内存量(包括可回收的 page cache 和 slab)。

7.4 /proc/pagetypeinfo

显示各 zone 各迁移类型的页块分布:

1
2
3
4
5
6
7
8
9
10
$ cat /proc/pagetypeinfo
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 ...
Node 0, zone DMA, type Unmovable 1 0 1 ...
Node 0, zone DMA, type Movable 0 0 0 ...
Node 0, zone DMA32, type Unmovable 23 12 7 ...
Node 0, zone DMA32, type Movable 163 178 79 ...
Node 0, zone DMA32, type Reclaimable 0 0 0 ...

可以观察各迁移类型的碎片情况。若 Unmovable 类型的高阶空闲块很多,说明不可移动页对 Movable 区域的侵占(fallback stealing)较少。

7.5 bpftrace 追踪分配路径

利用 bpftrace 动态追踪 __alloc_pages 的调用情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 追踪所有分配请求,按 order 统计
bpftrace -e '
kprobe:__alloc_pages {
@orders[arg1] = count();
}
interval:s:5 {
print(@orders);
clear(@orders);
}'

# 追踪分配失败(返回 NULL)
bpftrace -e '
kretprobe:__alloc_pages /retval == 0/ {
@failures = count();
printf("alloc failed: gfp=%x order=%d\n",
*((gfp_t*)arg0), (int)arg1);
}'

7.6 perf 统计分配频率

1
2
3
4
5
6
7
8
9
10
# 统计 5 秒内的页分配/释放事件数
perf stat -e kmem:mm_page_alloc,kmem:mm_page_free \
-a sleep 5

# 按调用栈分析高频分配来源
perf record -e kmem:mm_page_alloc -ag sleep 10
perf report --sort=dso,symbol

# 查看 PCP 批量补充频率(间接反映 order-0 分配压力)
perf stat -e kmem:mm_page_alloc_zone_locked -a sleep 5

mm_page_alloc_zone_locked 事件在 PCP 为空触发 rmqueue_bulk 时记录,其频率高说明 PCP 命中率低,order-0 分配压力大,可以考虑调大 vm.percpu_pagelist_high_fraction


总结

本文从 NUMA 节点的 pg_data_t、zone 的 struct zone、物理页的 struct page 三层数据结构出发,系统介绍了 Linux 物理内存的组织方式。伙伴分配系统通过按迁移类型分类的多阶空闲链表,以 O(log N) 的时间复杂度实现了对物理页的分配与合并。Per-CPU 页帧缓存在 order-0 分配的快速路径上消除了锁竞争瓶颈。GFP 标志体系为不同上下文的调用者提供了精细化的行为控制。OOM Killer 作为最后防线,通过 oom_badness 的启发式算法尽可能地选择”代价最小”的受害者以恢复系统运转。

理解这些机制是进行内核内存调优、分析内存泄漏与碎片化问题的基础。下一篇文章将深入虚拟内存管理:页表结构、缺页处理与 mmap 的实现机制。


参考源文件

  • include/linux/mmzone.h — zone/pgdat/free_area/per_cpu_pages 数据结构
  • include/linux/mm_types.h — struct page/folio/vm_area_struct
  • include/linux/gfp_types.h — GFP 标志定义
  • include/linux/gfp.h — GFP 组合标志与分配 API
  • mm/page_alloc.c — 伙伴分配器核心:__alloc_pagesget_page_from_freelistrmqueue__rmqueue_smallest__free_one_pagermqueue_pcplistfree_unref_page
  • mm/oom_kill.c — OOM Killer:out_of_memoryoom_badnessoom_kill_process

本文源码基于 Linux 6.4-rc1(commit ac9a78681b92),部分实现细节在不同版本间可能有所差异。