3.7 物理内存管理
内核怎么知道「哪些物理地址能用、哪些是设备的」?一个 4 KiB 的页在 `struct page` 里只占 64 字节,它怎么同时表示匿名页、页缓存、slab 对象和复合页?伙伴系统为什么要按 2 的幂分配,合并时怎么找到「兄弟」?`/proc/buddyinfo` 里那 11 个数字各是什么意思,为什么它们能解释「明明有内存却分配不出连续 8 页」?
画出 node → zone → pageblock → page 的层级;说清 `struct page` 每个字段的复用规则;手算一次 order-3 分配的分裂与释放时的合并;读懂 `buddyinfo`/`pagetypeinfo`/`zoneinfo` 并判断碎片程度;写一个模块按阶分配页并验证对齐。
3.6 讲的是「任务放哪个 CPU」。现在往下走一层:任务要内存,内存从哪来?内核自己也要内存——每个 task_struct、每个页表、每个 socket 缓冲区,最终都来自物理页。
物理内存管理要解决三个问题:描述(怎么表示一根内存条上的每一页)、分配(怎么快速给出一块连续物理页)、回收(不够时怎么要回来)。本章讲前两个,回收放到 3.10;虚拟地址与页表放到 3.8;内核自己的小对象分配(slab/vmalloc)放到 3.9。
先给一个数量感:16 GiB 内存、4 KiB 页,有 4194304 个页帧。每个页帧一个 struct page,64 字节,光是描述结构就占 256 MiB,约 1.6% 的内存。这部分内存在启动时由 vmemmap 一次性建好,/proc/meminfo 里看不到它——它已经在 MemTotal 之外被扣掉了。
3.7.1 三层描述:node、zone、page
NUMA node 0 (pg_data_t) NUMA node 1 (pg_data_t)
├── node_zones[] ├── node_zones[]
│ ├── ZONE_DMA (0–16 MiB) │ ├── ZONE_DMA
│ ├── ZONE_DMA32 (0–4 GiB) │ ├── ZONE_DMA32
│ ├── ZONE_NORMAL (其余可直接映射) │ ├── ZONE_NORMAL
│ └── ZONE_MOVABLE (可迁移,热插拔) │ └── ZONE_MOVABLE
├── node_mem_map ──► struct page 数组(vmemmap 里的连续区域)
└── node_distance[](到其他节点的距离,SLIT)
│
▼
struct zone
├── free_area[NR_PAGE_ORDERS] 伙伴系统的空闲链表(按 order 与迁移类型分桶)
├── _watermark[3] MIN / LOW / HIGH 水位
├── per_cpu_pageset 每 CPU 的页框缓存
├── managed_pages / present_pages / spanned_pages
└── vm_stat[] 各状态的页计数(NR_FREE_PAGES 等)
│
▼
struct page(64 B)── 每个物理页帧一个,描述它当前的身份
三层各有分工:
- node:NUMA 的拓扑单位。每个节点有自己的 zone、自己的伙伴系统、自己的水位。UMA 机器只有一个 node 0。
- zone:同一节点内按「物理地址范围与用途」切分。x86-64 上通常只有
ZONE_DMA(0–16 MiB,给老式 ISA DMA)、ZONE_DMA32(0–4 GiB,给 32 位设备)、ZONE_NORMAL(其余);ZONE_HIGHMEM只在 32 位上有;ZONE_MOVABLE用于内存热插拔与巨大页。 - page:每个物理页帧一个描述符。它不是「一页内存」,而是「这一页的元数据」;页本身在别处(直接映射区)。
struct zone 描述一段物理地址范围内的分配器状态。三个水位(_watermark[WMARK_MIN/LOW/HIGH])由 min_free_kbytes 推算:
- MIN:低于它,分配进入慢路径,直接回收;再不行就 OOM。
- LOW:低于它,唤醒
kswapd后台回收。 - HIGH:
kswapd回收到它以上才睡。
水位是「安全垫」:内核不能把内存全分光,否则回收自己都跑不起来(回收要分配临时结构、要发 I/O)。/proc/zoneinfo 的 min/low/high 三行就是它们。
$ cat /proc/zoneinfo | sed -n '1,20p'
Node 0, zone DMA
pages free 3991
min 15
low 18
high 21
spanned 4095
present 3998
managed 3977
Node 0, zone DMA32
pages free 120345
min 2891
low 3613
high 4335
...
spanned 是 zone 覆盖的页帧数(含空洞),present 是实际存在的,managed 是交给伙伴系统管的(扣掉 struct page 数组、保留区)。三者依次变小,差值能告诉你「有多少内存被内核自己吃掉了」。
pfn(page frame number) 是物理页帧号:pfn = phys_addr >> PAGE_SHIFT。struct page 与 pfn 一一对应,pfn_to_page()/page_to_pfn() 在 vmemmap 模型下就是一次数组下标计算。virt_to_page(kaddr) 只对直接映射区有效;page_address(page) 返回页的内核虚拟地址,对高端内存(32 位)可能返回 NULL,需要 kmap_local_page()。
3.7.2 struct page:64 字节里的五种身份
struct page 是内核里最节省(也最难读)的结构之一。它用 union 让同一块内存在不同场景下表示不同东西:
/* include/linux/mm_types.h —— 6.8 节选,已简化 */
struct page {
unsigned long flags; /* 原子标志位:PG_locked/PG_dirty/PG_lru... */
union {
struct { /* 页缓存 / 匿名页 */
struct list_head lru;
struct address_space *mapping; /* 低 2 位是标志 */
pgoff_t index;
unsigned long private;
};
struct { /* network: page_pool */
...
};
struct { /* page_pool(网络栈) */
unsigned long pp_magic;
struct page_pool *pp;
unsigned long _pp_mapping_pad;
unsigned long dma_addr;
atomic_long_t pp_ref_count;
};
struct { /* compound page 的尾页 */
unsigned long compound_head; /* 尾页指向首页,bit0=1 */
};
struct { /* ZONE_DEVICE */
struct dev_pagemap *pgmap;
void *zone_device_data;
};
struct rcu_head rcu_head;
struct { /* 页表页 */
unsigned long _pt_pad_1;
pgtable_t pmd_huge_pte;
unsigned long _pt_pad_2;
union { struct mm_struct *pt_mm; atomic_t pt_frag_refcount; };
spinlock_t *ptl;
};
};
union {
atomic_t _mapcount; /* 被多少个 PTE 映射 */
unsigned int page_type; /* 非映射页的类型(slab/table) */
};
atomic_t _refcount; /* 引用计数 */
...
};
读这个结构的关键是先判断页的身份,再读对应字段:
| 身份 | 判断方法 | 关键字段 |
|---|---|---|
| 匿名页 | PageAnon(page),即 mapping 低位为 1 | mapping 指向 anon_vma(去掉低位) |
| 页缓存页 | page->mapping 非空且低位为 0 | mapping 指向 address_space,index 是文件内偏移 |
| slab 页 | PageSlab(page) | 元数据在 struct slab(mm/slab.h),用 page_slab(page) 取;不在 struct page 里 |
| 复合页首页 | PageHead(page) | 元数据在 struct folio,用 page_folio(page) 取;compound_head 只在尾页 |
| 复合页尾页 | PageTail(page) | compound_head 指向首页 |
| 页表页 | PageTable(page) | pt_mm、ptl |
| 空闲页 | 在伙伴系统链表里 | lru 当链表节点,private 存 order |
mapping 的低位编码是理解匿名页的钥匙:
/* include/linux/page-flags.h —— 6.8 */
#define PAGE_MAPPING_ANON 0x1
#define PAGE_MAPPING_MOVABLE 0x2
#define PAGE_MAPPING_FLAGS (PAGE_MAPPING_ANON | PAGE_MAPPING_MOVABLE)
一个匿名页的 mapping 指向 anon_vma,但指针按至少 4 字节对齐,低 2 位一定是 0,正好空出来做标志。PageAnon() 就是取低位;取 anon_vma 时要 mapping & ~PAGE_MAPPING_FLAGS。这种「指针低位当标志」的技巧在内核里到处都是(dentry、socket、slab 都有),读代码时看到 & ~3 或 | 1 要立刻想到它。
struct folio 是 5.16 之后逐步引入的包装:它把「一个复合页的首页」单独建模,避免每次操作大页都要先 compound_head()。6.8 里 folio 已经渗透到页缓存、回收、rmap 的接口,但 struct page 仍是底层表示。读 6.8 的 mm/ 代码,要习惯 folio 与 page 混用:page_folio(page) 向上转,&folio->page 向下转。
- 「
page->index总是页号」:只有页缓存页才是「文件内页偏移」;匿名页的index常被复用为其他含义(如swap_entry的偏移)。 - 「
_refcount是映射数」:_refcount是引用计数(谁持有这个页),_mapcount才是被多少个 PTE 映射。两者都为 0 才能释放(还有pagecache的引用)。 - 「
PageAnon看mapping是否为空」:匿名页的mapping非空(指向anon_vma),只是低位为 1。空mapping的是「不在页缓存也不在 anon_vma」的页,如 slab、页表、驱动私有页。 - 「
struct page一定是 64 字节」:x86-64 默认配置下是;开了CONFIG_MEMCG的struct page仍是 64 B(union 塞进了 memcg 数据),但某些调试配置会变大。用pahole -C page vmlinux确认。
3.7.3 伙伴系统:2 的幂、分裂与合并
伙伴系统(buddy allocator)只做一件事:管理 2^order 个连续页。order 0 是 1 页,order 10 是 1024 页(4 MiB)。
/* include/linux/mmzone.h —— 6.8 节选 */
struct zone {
...
struct free_area free_area[NR_PAGE_ORDERS]; /* MAX_ORDER = 10 */
...
};
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
分配 order-3(8 页)时,如果 order-3 链表为空:
- 去 order-4 找,找到一块 16 页;
- 把它分裂成两个 8 页的「伙伴」,一个返回,一个挂到 order-3 链表;
- 如果 order-4 也空,继续往上找,直到 order-10;
- 全空才走慢路径(回收/规整/OOM)。
释放时反过来:把页块挂回对应 order,然后检查它的伙伴是否也空闲;是就合并成 order+1,继续往上查,直到不能合并。
伙伴的地址有一个漂亮的性质:order 为 n 的块,其伙伴的 pfn 是 pfn ^ (1 << n)。因为两个伙伴的地址只差第 n 位,异或就能互相得到。
/* mm/page_alloc.c: __free_one_page() —— 6.8 节选,已简化 */
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;
unsigned long combined_pfn;
struct page *buddy;
bool to_tail;
while (order < MAX_PAGE_ORDER - 1) {
buddy_pfn = __find_buddy_pfn(pfn, order);
buddy = page + (buddy_pfn - pfn);
if (!page_is_buddy(page, buddy, order))
break;
/* 伙伴也空闲:从链表摘掉,合并成更高阶 */
list_del(&buddy->lru);
zone->free_area[order].nr_free--;
...
pfn = combined_pfn = min(pfn, buddy_pfn);
page = pfn_to_page(pfn);
order++;
}
...
list_add(&page->lru, &zone->free_area[order].free_list[migratetype]);
zone->free_area[order].nr_free++;
}
page_is_buddy() 检查三件事:伙伴在同一个 zone、order 相同、且迁移类型兼容。第三点是抗碎片的核心。
每个页块有一个迁移类型,描述「这块内存能不能搬走」:
| 类型 | 含义 | 典型来源 |
|---|---|---|
MIGRATE_UNMOVABLE |
不可迁移,位置固定 | 内核数据结构、页表、DMA 缓冲 |
MIGRATE_MOVABLE |
可通过更新页表搬走 | 用户匿名页、页缓存 |
MIGRATE_RECLAIMABLE |
可通过回收释放 | dentry/inode 缓存 |
MIGRATE_CMA |
连续内存预留区 | 摄像头、GPU 大缓冲 |
MIGRATE_ISOLATE |
被隔离,不参与分配 | 内存热插拔、离线 |
MIGRATE_HIGHATOMIC |
给原子分配预留 | GFP_ATOMIC 的应急池 |
分配时优先从同类型链表取;取不到才降级(fallback) 到其他类型。降级会把不可迁移的页混进可迁移的页块,造成碎片——这就是 /proc/pagetypeinfo 里「类型分布」值得看的原因。
假设 zone 的 order-2 与 order-3 链表都空,order-4 有一块 pfn=0x100 的 16 页。
__rmqueue_smallest(zone, order=2, migratetype)从 order 2 找到 MAX_PAGE_ORDER-1,在 order 4 命中;expand(zone, page, low=2, high=4, mt)循环三次:order 4 分裂成两个 order-3(pfn 0x100 与 0x108),一个挂回 order-3,另一个继续分裂;order 3 再分裂成两个 order-2(0x100 与 0x104),一个挂回 order-2,另一个返回;- 返回 pfn=0x100 的页,
free_area[2].nr_free加一(挂回的那块)。
释放时:__free_one_page(0x100, order=2) 找到伙伴 0x104;若它也空闲,合并成 order-3 的 0x100;再看伙伴 0x108,若空闲,合并成 order-4;继续看 0x110……直到遇到不空闲的伙伴或到 MAX_PAGE_ORDER-1。整个过程是 O(MAX_PAGE_ORDER),与内存大小无关。
MAX_PAGE_ORDER(旧名 MAX_ORDER)在 6.4 之前是 11,之后改成 10(最大块从 8 MiB 降到 4 MiB);6.8 的数组大小用 NR_PAGE_ORDERS,上限判断用 MAX_PAGE_ORDER。这个改动的原因之一是:order-10 的块在长时间运行的系统上几乎不可能自然形成,维护 11 个链表只是在浪费扫描时间。需要更大连续内存的场景(大页、CMA、驱动 DMA)走 CMA 或 alloc_contig_range(),不依赖伙伴系统。
3.7.4 per-CPU 页框缓存:绕过 zone->lock
伙伴系统的每个 zone 有一把 zone->lock。如果每次 alloc_pages(GFP_KERNEL, 0) 都去抢它,多核机器上这把锁会成为瓶颈。解法是每 CPU 一个小缓存:
/* include/linux/mmzone.h —— 6.8 节选,已简化 */
struct per_cpu_pages {
spinlock_t lock;
int count; /* 缓存里的页数 */
int high; /* 高于它就开始批量归还 */
int batch; /* 一次从伙伴系统拿/还多少 */
short free_factor;
struct list_head lists[NR_PCP_LISTS]; /* 按 order 与 migratetype 分桶 */
};
分配路径变成:
rmqueue_pcplist()先看本地 pcp 链表,有就直接摘一个,不碰 zone->lock;- 没有就
rmqueue_bulk()一次从伙伴系统拿batch个(默认几十个),补进 pcp; - 释放时
free_unref_page()挂回 pcp;count > high时free_pcppages_bulk()批量还回伙伴系统。
high 与 batch 由 zone_batchsize() 与 CPU 数推导,/proc/sys/vm/percpu_pagelist_high_fraction 可以调整(值越小,缓存越少、越容易还回)。CPU 热插拔时 drain_pages() 会把该 CPU 的 pcp 全部倒回伙伴系统,否则这些页就“丢”在离线 CPU 上了。
pcp 缓存的是单页(order 0)与少量高阶块,且按迁移类型分桶。它让「分配一页」变成无锁操作,代价是这些页暂时不在伙伴系统里,/proc/buddyinfo 的 order-0 计数会偏低。高 order 分配不走 pcp(rmqueue 直接查 free_area),所以大块连续性不受影响。
3.7.5 分配路径与 GFP 标志
gfp_t 是「分配约束」的位掩码,最重要的几位:
| 标志 | 含义 | 常见组合 |
|---|---|---|
__GFP_RECLAIM |
允许回收(直接回收) | GFP_KERNEL 含它 |
__GFP_IO / __GFP_FS |
允许发起 I/O / 调用文件系统 | GFP_KERNEL 含两者 |
__GFP_ATOMIC |
不允许睡眠,只能从应急池拿 | GFP_ATOMIC |
__GFP_HIGH |
可动用紧急保留页 | GFP_ATOMIC 含它 |
__GFP_ZERO |
返回前清零 | GFP_KERNEL | __GFP_ZERO |
__GFP_MOVABLE |
页可迁移(用户页) | GFP_HIGHUSER_MOVABLE |
__GFP_NOFAIL |
不允许失败,一直重试 | 极少数场景,慎用 |
__GFP_ACCOUNT |
计入 memcg | GFP_KERNEL_ACCOUNT |
GFP_KERNEL = __GFP_RECLAIM | __GFP_IO | __GFP_FS;GFP_ATOMIC = __GFP_HIGH | __GFP_ATOMIC | __GFP_KSWAPD_RECLAIM。
alloc_pages() 分快慢两条路:
/* mm/page_alloc.c: __alloc_pages() —— 6.8 节选,已简化 */
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 = gfp;
page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
if (likely(page))
goto out;
/* 慢路径:唤醒 kswapd、直接回收、规整、重试,最后 OOM */
page = __alloc_pages_slowpath(alloc_gfp, order, &ac);
out:
return page;
}
get_page_from_freelist() 遍历 zonelist(本节点优先,再按距离降级),对每个 zone 做 zone_watermark_fast() 检查,通过就 rmqueue()。慢路径依次尝试:
wake_all_kswapds()唤醒后台回收;__alloc_pages_direct_reclaim()直接回收(当前进程自己扫 LRU,见 3.10);__alloc_pages_direct_compact()内存规整(把可迁移页搬走来拼出连续块);__alloc_pages_may_oom()触发 OOM killer;- 若
__GFP_NOFAIL,回到第 1 步。
几个常用封装:
| 接口 | 返回 | 说明 |
|---|---|---|
alloc_pages(gfp, order) | struct page * | 通用,返回页描述符 |
alloc_page(gfp) | struct page * | order 0 的简写 |
__get_free_pages(gfp, order) | unsigned long | 返回直接映射区虚拟地址 |
__get_free_page(gfp) | unsigned long | 同上,order 0 |
free_pages(addr, order) | — | 配 __get_free_pages |
__free_pages(page, order) | — | 配 alloc_pages |
alloc_pages_exact(size, gfp) | void * | 按字节分配,释放用 free_pages_exact() |
__get_free_pages() 只在直接映射区有效,且要求 order < MAX_PAGE_ORDER。它返回的地址可以用 virt_to_page() 转回 struct page;反过来 page_address(page) 只对低端内存有效。
- 不检查返回值:
GFP_KERNEL也可能失败(OOM 前的最后尝试),GFP_ATOMIC更容易失败。解引用NULL是最常见的内核崩溃。 - 在中断上下文用
GFP_KERNEL:会触发BUG: sleeping function called from invalid context,或悄悄死锁。中断里只能GFP_ATOMIC/GFP_NOWAIT。 - order 超过
MAX_PAGE_ORDER-1:alloc_pages会返回NULL并打印警告。要更大块用 CMA 或vmalloc(虚拟连续)。 - 把
__get_free_pages的返回值传给kfree:kfree只认kmalloc/kzalloc的指针。页分配要用free_pages。 __GFP_NOFAIL滥用:它会让分配无限重试,在内存耗尽时把进程卡死(甚至在原子上下文里死锁)。只在「失败就无法继续、且没有退路」的地方用。- 忘了
__GFP_ZERO就假设页是干净的:伙伴系统返回的页可能是别人用过的,含敏感数据。要清零就显式加标志,别自己memset(编译器可能优化掉)。
3.7.6 启动时的内存初始化:从 memblock 到伙伴系统
伙伴系统不是一开始就存在的。启动早期用的是 memblock(一个简单的「已用/可用区间」链表),因为此时还没有 struct page 数组,也没有 zone:
setup_arch()
→ e820__memory_setup() 固件内存地图 → e820_table
→ memblock_add() 可用区间登记
→ memblock_reserve() 内核镜像、initrd、ACPI、页表保留
→ paging_init()
→ sparse_init() / vmemmap_populate() 建 struct page 数组
→ free_area_init() 建 node/zone,算水位
→ memblock_free_all() 把剩余内存交给伙伴系统
free_area_init() 里的关键计算:
zone->spanned_pages:zone 覆盖的页帧数(按物理地址范围算);zone->present_pages:实际有内存的页帧数(按 e820 的 usable 区间算);zone->managed_pages:扣掉struct page数组、保留区之后,真正交给伙伴系统的;- 水位:
min_free_kbytes的平方根关系,按 zone 大小分摊。
MemTotal 与 MemFree 的差值里,有一部分是 struct page 数组(vmemmap)、页表、slab、内核代码段——这些在 MemTotal 里根本没算,所以「MemTotal + 各种占用 ≠ 物理内存」是正常的。
3.7.7 观测:buddyinfo、pagetypeinfo、zoneinfo
$ cat /proc/buddyinfo
Node 0, zone DMA 1 1 1 0 2 1 1 0 1 1 3
Node 0, zone DMA32 1105 661 298 120 45 18 7 3 1 0 1
Node 0, zone Normal 24567 12003 5012 1801 402 101 22 5 1 0 0
每行 11 个数字,对应 order 0 到 order 10 的空闲块个数。读法:
- order 0 很大、高阶为 0:内存碎片化,只能满足单页分配,大块连续分配(DMA、hugepage)会失败。
- order 9/10 有数:还有 2–4 MiB 的连续块,DMA 与大页有希望。
DMAzone 的数字很小:正常,它只有 16 MiB。
/proc/pagetypeinfo 更细,按迁移类型分开:
Free pages count per migrate type at order 0 1 2 3 ...
Node 0, zone Normal, type Unmovable 1234 567 234 89 ...
Node 0, zone Normal, type Movable 20000 9000 4000 1500 ...
Node 0, zone Normal, type Reclaimable 3000 1200 400 100 ...
Node 0, zone Normal, type HighAtomic 12 3 1 0 ...
如果 Unmovable 的 order-0 很多而高阶为 0,同时 Movable 还有高阶块,说明「不可迁移的页把可迁移区切碎了」。这正是 3.10 里内存规整(compaction)要解决的问题。
/proc/meminfo 里与物理页管理直接相关的字段:
| 字段 | 含义 | 关注点 |
|---|---|---|
MemTotal | 内核可管理的物理内存 | 已扣掉固件保留、内核镜像 |
MemFree | 伙伴系统空闲页 | 不含 pcp 缓存里的页 |
MemAvailable | 估算的「还能给应用多少」 | 含可回收的页缓存 |
Slab | slab 占用 | SReclaimable 部分可回收 |
PageTables | 页表占用 | 大映射/多进程时显著 |
VmallocUsed | vmalloc 区已用 | 含内核栈(CONFIG_VMAP_STACK) |
CmaTotal/CmaFree | CMA 预留区 | 摄像头/GPU |
3.7.8 实验:按阶分配与对齐验证
下面的模块按 order 0–9 各分配一块,打印页数、起始 pfn 与是否按 order 对齐,然后全部释放。它验证两件事:伙伴系统返回的块物理地址按大小对齐;大阶分配在碎片化时可能失败。
// buddy_demo.c — 按阶分配物理页并验证对齐(Linux 6.8)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gfp.h>
#include <linux/mm.h>
#define MAX_DEMO_ORDER 10
static struct page *demo_pages[MAX_DEMO_ORDER];
static int __init buddy_demo_init(void)
{
unsigned int order;
for (order = 0; order < MAX_DEMO_ORDER; order++) {
struct page *p = alloc_pages(GFP_KERNEL | __GFP_ZERO, order);
unsigned long pfn;
if (!p) {
pr_info("buddy_demo: order %u FAILED (fragmentation?)\n", order);
continue;
}
demo_pages[order] = p;
pfn = page_to_pfn(p);
pr_info("buddy_demo: order=%u pages=%u pfn=%#lx aligned=%s\n",
order, 1U << order, pfn,
(pfn & ((1UL << order) - 1)) ? "NO" : "yes");
}
return 0;
}
static void __exit buddy_demo_exit(void)
{
unsigned int order;
for (order = 0; order < MAX_DEMO_ORDER; order++) {
if (demo_pages[order]) {
__free_pages(demo_pages[order], order);
demo_pages[order] = NULL;
}
}
pr_info("buddy_demo: all freed\n");
}
module_init(buddy_demo_init);
module_exit(buddy_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("allocate pages order by order and check alignment");
在 512 MiB 的 QEMU 里加载,预期 order 0–9 全部成功,aligned=yes;如果先跑一个吃掉大半内存的程序再加载,order 8/9 可能失败。这个实验比读文档更能说明「碎片」是什么:不是没有内存,是没有连续内存。
用户态读 buddyinfo 的小程序:
/* buddyinfo.c — 把 /proc/buddyinfo 画成按阶的柱状图
* 编译: cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -O2 -o buddyinfo buddyinfo.c
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
int main(void)
{
FILE *fp = fopen("/proc/buddyinfo", "r");
char line[512];
if (!fp) { perror("/proc/buddyinfo"); return 1; }
while (fgets(line, sizeof line, fp)) {
char zone[32];
unsigned long n[11];
int node, got;
got = sscanf(line, "Node %d, zone %31s %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu",
&node, zone, &n[0], &n[1], &n[2], &n[3], &n[4], &n[5],
&n[6], &n[7], &n[8], &n[9], &n[10]);
if (got != 13) continue; /* 跳过无法解析的行 */
printf("node %d zone %-8s:", node, zone);
for (int o = 0; o < 11; o++)
printf(" %5lu", n[o]);
printf("\n");
}
fclose(fp);
return 0;
}
输出是一行 11 个数字,和 cat /proc/buddyinfo 一样,但格式统一,方便脚本处理。把 printf 改成按 n[o] * (1UL << o) 算「该阶能提供的总页数」,就能看出「空闲页都集中在哪个阶」。
3.7.9 zone 与 pglist_data:分配器的全部状态
这一节回答:/proc/zoneinfo 里的 pages free、min/low/high、spanned/present/managed 分别存在哪个字段?struct zone 为什么要把 lock 和 lru_lock 用 cache line 隔开?pglist_data 与 zone 是什么关系?
3.7.1 给了 node → zone → page 的层级,但没有展开字段。真正读 mm/page_alloc.c 时,几乎所有判断都落在 zone 的计数器与水位上;读 /proc/zoneinfo 时,每一行都能对应到一个字段。
struct pglist_data:一个 NUMA 节点的全部内存
/* include/linux/mmzone.h —— 6.8 节选,已简化 */
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones;
unsigned long node_start_pfn;
unsigned long node_present_pages;
unsigned long node_spanned_pages;
int node_id;
wait_queue_head_t kswapd_wait;
wait_queue_head_t pfmemalloc_wait;
struct task_struct *kswapd;
struct task_struct *kcompactd;
unsigned long kswapd_max_order;
enum zone_type kswapd_highest_zoneidx;
...
struct lruvec __lruvec;
struct per_cpu_nodestat __percpu *per_cpu_nodestats;
atomic_long_t vm_stat[NR_VM_NODE_STAT_ITEMS];
} pg_data_t;
| 字段 | 含义 | 谁维护 |
|---|---|---|
node_zones[] | 本节点的 zone 数组,ZONE_DMA…ZONE_MOVABLE | 启动时 free_area_init() |
node_zonelists[] | 分配时的 zone 回退顺序(本节点优先,再按距离) | build_zonelists() |
nr_zones | 实际存在的 zone 数 | 启动时 |
node_start_pfn | 节点起始 pfn | 启动时 |
node_present_pages | 实际存在的页数(不含空洞) | 启动时 |
node_spanned_pages | 覆盖的页数(含空洞) | 启动时 |
node_id | NUMA 节点号 | 启动时 |
kswapd_wait | kswapd 睡眠的等待队列 | 回收路径唤醒 |
kswapd | 每节点一个回收线程 | kswapd_run() |
kcompactd | 每节点一个规整线程 | kcompactd_run() |
__lruvec | 本节点的 root lruvec;用 node_lruvec(pgdat) 取(memcg 另有 per-memcg lruvec) | 页回收/激活 |
vm_stat[] | 节点级 vmstat 计数 | mod_node_page_state() |
pglist_data 是「一个 NUMA 节点的分配器上下文」。UMA 机器只有 node 0,但结构一样;NODE_DATA(nid) 就是取这个数组的元素。
struct zone:水位、空闲链表与每 CPU 缓存
/* include/linux/mmzone.h —— 6.8 节选,已简化 */
struct zone {
unsigned long _watermark[NR_WMARK]; /* MIN/LOW/HIGH */
unsigned long watermark_boost;
unsigned long nr_reserved_highatomic;
long lowmem_reserve[MAX_NR_ZONES];
struct pglist_data *zone_pgdat;
struct per_cpu_pages __percpu *per_cpu_pageset;
unsigned long zone_start_pfn;
atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
unsigned long cma_pages;
const char *name;
struct free_area free_area[NR_PAGE_ORDERS];
unsigned long flags;
spinlock_t lock; /* 保护 free_area */
spinlock_t lru_lock; /* 保护 LRU 链表 */
struct lruvec __lruvec;
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
...
} ____cacheline_internodealigned_in_smp;
| 字段 | 含义 | 读法 |
|---|---|---|
_watermark[WMARK_MIN/LOW/HIGH] | 三个水位(页为单位) | /proc/zoneinfo 的 min/low/high |
watermark_boost | 临时抬高水位(碎片/CMA 压力) | 回收器判断时加上 |
nr_reserved_highatomic | 给 GFP_ATOMIC 预留的高阶页数 | /proc/zoneinfo 的 nr_free_highatomic |
lowmem_reserve[] | 低端 zone 对高端分配的保留 | 防止 DMA 区被普通分配吃光 |
zone_pgdat | 所属节点 | 反查 node |
per_cpu_pageset | 每 CPU 的 pcp 缓存(struct per_cpu_pages) | per_cpu_pageset |
zone_start_pfn | zone 起始 pfn | spanned 的起点 |
managed_pages | 交给伙伴系统的页数(扣保留区) | 水位按它算 |
spanned_pages | 覆盖页数(含空洞) | /proc/zoneinfo 的 spanned |
present_pages | 实际存在页数 | present |
cma_pages | CMA 预留页数 | CmaTotal |
free_area[NR_PAGE_ORDERS] | 每个 order 的空闲链表(再按迁移类型分) | /proc/buddyinfo |
lock | 保护 free_area 与 zone 计数 | 高阶分配要拿 |
vm_stat[] | zone 级计数 | NR_FREE_PAGES 等 |
____cacheline_internodealigned_in_smp 让整个 zone 按 cache line 对齐,CACHELINE_PADDING(_pad1_/_pad2_...) 把 lock 等热字段隔到不同 cache line。6.8 的 zone 里没有 lru_lock/lruvec:LRU 锁在 struct lruvec(lruvec->lru_lock),root lruvec 放在 pglist_data.__lruvec,由 node_lruvec() 取。
三个页数:spanned、present、managed
spanned_pages ████████████████████████████████ 按物理地址范围算,含空洞
present_pages ██████████████████████ 实际有内存的页(e820 usable)
managed_pages █████████████████ 扣掉 vmemmap、保留区、CMA 后交给伙伴系统
spanned - present:地址范围里的空洞(MMIO 洞、保留区);present - managed:内核自己吃掉的部分(struct page数组、early reserved、CMA);- 水位按
managed_pages算,而不是present_pages,否则保留区会被算成可用内存。
在 16 GiB 机器上,spanned 可能是 16.5 GiB(含 PCI 洞),present 16 GiB,managed 15.6 GiB。MemTotal 通常接近 managed,但还扣了内核代码/数据与一些 early 分配。
per_cpu_pageset:每个 CPU 的小仓库
/* include/linux/mmzone.h —— 6.8 节选,已简化 */
struct per_cpu_pages {
spinlock_t lock;
int count; /* 当前缓存的页数 */
int high; /* 高于它就批量归还 */
int batch; /* 一次拿/还多少 */
short free_factor; /* 批量释放的因子 */
struct list_head lists[NR_PCP_LISTS]; /* 按 order/migratetype 分桶 */
};
struct per_cpu_pageset {
struct per_cpu_pages pcp;
u8 expire;
...
};
6.8 的 pcp 不再只有「order-0 + 迁移类型」两维,而是 NR_PCP_LISTS 个桶,支持 order-0 到 PCP_ORDER(通常 0,部分配置支持更高阶)与迁移类型的组合。这样 GFP_ATOMIC 的小块分配也能从 pcp 拿,减少对 zone->lock 的争用。
high 与 batch 的默认值由 zone_batchsize() 算:
/* mm/page_alloc.c: zone_batchsize() —— 6.8 节选,简化 */
static int zone_batchsize(struct zone *zone)
{
int batch;
batch = zone->managed_pages / 1024;
if (batch * PAGE_SIZE > 512 * 1024)
batch = (512 * 1024) / PAGE_SIZE;
batch /= 4;
if (batch < 1)
batch = 1;
batch = rounddown_pow_of_two(batch + batch / 2) - 1;
return batch;
}
以 16 GiB 的 Normal zone 为例:managed_pages = 16 GiB / 4 KiB = 4,194,304 页;batch = managed/1024 = 4096 页(16 MiB,< 512 MiB,不触发截断);batch /= 4 得 1024;rounddown_pow_of_two(1024 + 512) - 1 = 1024 - 1 = 1023。high 默认约 batch * 6 = 6138 页(约 24 MiB),不是一个 CPU 缓存十几万页。
- 把
present_pages当可用内存:它包含保留区与 vmemmap,实际可分配看managed_pages。 - 用
zone->vm_stat[NR_FREE_PAGES]直接读:并发下要用zone_page_state()(带近似与稳定性处理),直接读可能看到撕裂的值。 - 以为
lowmem_reserve是「预留内存」:它只是分配时的检查阈值,不预留实际页;真正预留看memblock_reserve/CMA。 - 在 64 位上看
ZONE_HIGHMEM:x86-64 没有高端内存,ZONE_HIGHMEM不存在;代码里#ifdef CONFIG_HIGHMEM才有效。 - 把
watermark_boost当固定值:它会随碎片事件升降,/proc/zoneinfo的boost行能看到;回收判断要用zone->_watermark[i] + zone->watermark_boost。
3.7.10 伙伴系统源码:rmqueue、expand 与 __free_one_page
这一节回答:一次 alloc_pages(GFP_KERNEL, 3) 在 zone->lock 下到底做了哪几步?expand() 为什么从 high 往 low 切?释放时 __free_one_page() 如何判断伙伴「能不能合并」?
3.7.3 给了分裂/合并的示意,这一节把 6.8 的真实函数链补全。注意 6.8 的 __rmqueue() 已经拆成 __rmqueue_smallest() 与 __rmqueue_fallback(),并且多了 rmqueue_bulk() 给 pcp 补货。
进入伙伴系统之前:watermark 与 pcp
__alloc_pages(gfp, order, nid, nodemask)
→ get_page_from_freelist(gfp, order, alloc_flags, ac)
→ for_next_zone_zonelist_nodemask(zone, ...)
→ zone_watermark_fast(zone, order, mark, ac->highest_zoneidx, alloc_flags)
→ rmqueue(zone, order, gfp, alloc_flags)
→ rmqueue_pcplist() /* order == 0 且允许 pcp */
→ __rmqueue() /* order > 0 */
→ __rmqueue_smallest()
→ __rmqueue_fallback()
zone_watermark_fast() 是快路径的水位检查:它先看 zone_page_state(zone, NR_FREE_PAGES) 是否高于 mark,再扣掉 lowmem_reserve 与 nr_reserved_highatomic。只有通过才进 rmqueue();否则整个 zone 跳过,去 zonelist 的下一个 zone。
__rmqueue_smallest:从目标 order 往上找
/* mm/page_alloc.c: __rmqueue_smallest() —— 6.8 节选,简化 */
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 < MAX_PAGE_ORDER; current_order++) {
area = &zone->free_area[current_order];
if (list_empty(&area->free_list[migratetype]))
continue;
page = list_first_entry(&area->free_list[migratetype],
struct page, buddy_list);
list_del(&page->buddy_list);
area->nr_free--;
expand(zone, page, order, current_order, migratetype);
set_page_order(page, order);
return page;
}
return NULL;
}
6.8 的空闲链表节点从 page->lru 改成了 page->buddy_list(struct page 的 union 成员),语义一样:list_first_entry() 摘第一个,expand() 切蛋糕。
expand():
/* mm/page_alloc.c: expand() —— 6.8 节选,简化 */
static inline void expand(struct zone *zone, struct page *page,
int low, int high, int migratetype)
{
unsigned long size = 1 << high;
while (high > low) {
high--;
size >>= 1;
/* 把后半块挂回 high 阶链表 */
list_add(&page[size].buddy_list, &zone->free_area[high].free_list[migratetype]);
zone->free_area[high].nr_free++;
set_page_order(&page[size], high);
}
}
为什么从 high 往下切?因为一块 2^high 页的内存,前半块 2^(high-1) 继续切,后半块 2^(high-1) 挂回。循环结束时,page[0..2^low-1] 返回给调用者,其余部分都在对应阶的空闲链表里。整个过程最多 MAX_ORDER - order 次,通常是常数。
__rmqueue_fallback:同阶没有就降级
如果目标迁移类型在所有 order 都没有空闲块,__rmqueue() 调 __rmqueue_fallback():
/* mm/page_alloc.c: __rmqueue_fallback() —— 6.8 节选,简化 */
static __always_inline struct page *__rmqueue_fallback(struct zone *zone,
int order, int start_migratetype, unsigned int alloc_flags)
{
struct free_area *area;
int current_order;
int migratetype, fallback_mt;
struct page *page;
/* 从最高阶往下找,优先拿大块,减少碎片 */
for (current_order = MAX_PAGE_ORDER - 1; current_order >= order; current_order--) {
area = &zone->free_area[current_order];
for (migratetype = 0; migratetype < MIGRATE_TYPES; migratetype++) {
if (migratetype == start_migratetype)
continue;
if (list_empty(&area->free_list[migratetype]))
continue;
fallback_mt = find_suitable_fallback(area, current_order,
start_migratetype, false, &can_steal);
if (fallback_mt == -1)
continue;
page = list_first_entry(&area->free_list[fallback_mt],
struct page, buddy_list);
list_del(&page->buddy_list);
area->nr_free--;
expand(zone, page, order, current_order, fallback_mt);
...
/* 如果整块被偷走,改 pageblock 的迁移类型 */
if (can_steal)
steal_suitable_fallback(zone, page, alloc_flags,
start_migratetype);
return page;
}
}
return NULL;
}
三个细节:
- 从最高阶往下找:高阶块更稀缺,但拿大块来切能减少「把小块留给大分配」的碎片;6.8 的策略是「优先 fallback 到高阶」。
find_suitable_fallback()检查目标迁移类型能否接受这个块;MIGRATE_HIGHATOMIC是特殊保留池,普通分配不能动。can_steal:如果整个 pageblock 都是同一种迁移类型,fallback 会把整块「偷」过来,改pageblock的迁移类型;如果只是部分空闲,就不改,避免把可迁移区永久污染。
steal_suitable_fallback() 是抗碎片的关键:它尽量让「一个 pageblock 只有一种迁移类型」,这样后续 compaction 才能把整块搬走。偷取有阈值(pageblock_order 内空闲页比例),不是无条件抢。
释放:__free_one_page 的合并条件
/* mm/page_alloc.c: __free_one_page() —— 6.8 节选,简化 */
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;
struct page *buddy;
bool to_tail;
while (order < MAX_PAGE_ORDER - 1) {
buddy_pfn = __find_buddy_pfn(pfn, order);
buddy = page + (buddy_pfn - pfn);
if (!page_is_buddy(page, buddy, order))
break;
/* 伙伴空闲:从链表摘掉,合并到高一阶 */
del_page_from_free_list(buddy, zone, order);
...
pfn = min(pfn, buddy_pfn);
page = pfn_to_page(pfn);
order++;
}
to_tail = buddy_merge_likely(pfn, buddy_pfn, page, order);
if (to_tail)
list_add_tail(&page->buddy_list, &zone->free_area[order].free_list[migratetype]);
else
list_add(&page->buddy_list, &zone->free_area[order].free_list[migratetype]);
zone->free_area[order].nr_free++;
}
page_is_buddy() 检查:
PageBuddy(buddy):伙伴在空闲链表里;page_order(buddy) == order:伙伴的阶相同;page_zone_id(page) == page_zone_id(buddy):同一个 zone(跨 zone 不合并);- 迁移类型兼容(6.8 里由
free_one_page的migratetype与pageblock决定,page_is_buddy本身主要看前三项)。
to_tail 是一个小优化:如果「下一阶的伙伴也可能马上被释放」,就把合并后的块放到链表尾部,让它在分配时更晚被选中,从而提高继续合并的概率。这个启发式减少了「刚合并又被拆开」的抖动。
MAX_ORDER 为什么从 11 变成 10
6.4 把 MAX_ORDER 从 11 降到 10,最大块从 8 MiB 变 4 MiB。原因:
- order-10 及以上的块在长时间运行的系统上极难自然形成,维护第 11 个链表只是浪费扫描;
- 真正需要 >4 MiB 连续内存的场景(大页、CMA、驱动 DMA)应走 CMA/
alloc_contig_range(),而不是依赖伙伴系统; - 减少
free_area数组与zone结构大小,cache 更友好。
代价是「order-10 分配」在 6.8 上直接返回 NULL,驱动如果写死 alloc_pages(GFP_KERNEL, 10),在 6.4+ 会失败。正确做法是用 alloc_pages_exact()/CMA,或检查 MAX_ORDER。
alloc_pages()返回的页没有清零:__GFP_ZERO才会清零;alloc_page()返回的页可能含上一个用户的数据。- 把
page->private当自己的字段:空闲时它存order,分配后可能被页缓存/驱动复用;驱动要用自己的结构,不要占用private。 - 释放时 order 传错:
__free_pages(page, 1)释放一个原本 order-0 的页,会把相邻页也卷进空闲链表,静默破坏伙伴系统;必须用分配时的 order。 __free_pages()后继续用page指针:页可能立刻被另一个 CPU 分配走;释放后不要再触碰。- 以为
zone->lock保护所有字段:它保护free_area与部分计数,不保护 LRU(那是lru_lock);混用会死锁。 - 在
GFP_ATOMIC下期待 fallback:原子分配不会进入直接回收/规整,fallback 也只能从现有空闲块拿,高阶失败率很高。
3.7.11 迁移类型、pageblock 与 fallback 矩阵
这一节回答:迁移类型存在哪里——每个页,还是每个 pageblock?/proc/pagetypeinfo 里同一阶的 Unmovable 与 Movable 数量为什么此消彼长?CMA 为什么能被普通分配「借用」又还回来?
pageblock:迁移类型的最小单位
迁移类型不是每页一个字节,而是每 2 MiB 一个 pageblock:
/* include/linux/pageblock-flags.h —— 6.8 */
enum pageblock_bits {
PB_migrate,
PB_migrate_skip, /* 规整时跳过这个 block */
PB_migrate_end = PB_migrate + 3, /* 3 位存 8 种迁移类型 */
...
};
#define pageblock_order (MAX_PAGE_ORDER - 1) /* 4 KiB 页上是 9 */
#define pageblock_nr_pages (1UL << pageblock_order) /* 512 页 = 2 MiB */
zone->pageblock_flags 是一个位图,每个 pageblock 用 3 位记录迁移类型(get_pfnblock_migratetype()),另有 1 位 PB_migrate_skip 供 compaction 跳过。
为什么是 2 MiB?因为 THP(透明大页)的 order-9 正好是 2 MiB,DMA/CMA 也常以 2 MiB 为粒度;如果 pageblock 太小,迁移类型会频繁变化,fallback 的「偷取」就失去意义。
一个 zone 的 pageblock 视图(每个格子 2 MiB)
┌────────┬────────┬────────┬────────┬────────┐
│ M │ M │ U │ M │ C │ M=Movable U=Unmovable C=CMA
└────────┴────────┴────────┴────────┴────────┘
可迁移 可迁移 不可迁移 可迁移 预留
分配 order-9 时,伙伴系统需要一个完整的 pageblock 且迁移类型匹配;如果只有「可迁移」的 pageblock 有空闲,而请求是 Unmovable,就会触发 fallback/steal。
迁移类型与 pcp 的分桶
| 类型 | 值 | 典型页 | 可迁移 | 可回收 |
|---|---|---|---|---|
MIGRATE_UNMOVABLE | 0 | 内核结构、页表、DMA 缓冲 | 否 | 否 |
MIGRATE_MOVABLE | 1 | 用户匿名页、页缓存 | 是 | 是(页缓存) |
MIGRATE_RECLAIMABLE | 2 | dentry/inode、可回收 slab | 否 | 是 |
MIGRATE_PCPTYPES | 3 | pcp 只管理前三种 | — | — |
MIGRATE_HIGHATOMIC | 3 | GFP_ATOMIC 高阶预留 | 否 | 否 |
MIGRATE_CMA | 4 | CMA 预留区 | 是(特殊) | 否 |
MIGRATE_ISOLATE | 5 | 热插拔/离线隔离 | — | — |
MIGRATE_PCPTYPES 不是一种真实类型,而是「pcp 链表只覆盖前三种」的哨兵;MIGRATE_HIGHATOMIC 与它同值但在 zone 的 free_area 里单独一档。MIGRATE_ISOLATE 的页不在任何空闲链表里,分配器看不到。
fallback 矩阵
当目标类型没有空闲块时,按「兼容性」顺序降级。6.8 的 fallbacks[][3] 大致是:
| 请求类型 | 第 1 选择 | 第 2 选择 | 第 3 选择 |
|---|---|---|---|
UNMOVABLE | RECLAIMABLE | MOVABLE | — |
MOVABLE | RECLAIMABLE | UNMOVABLE | — |
RECLAIMABLE | UNMOVABLE | MOVABLE | — |
CMA | MOVABLE | — | — |
HIGHATOMIC | 不 fallback(保留池) | — | — |
这张表的设计原则:
- 不可迁移的分配可以借用可回收/可迁移的块,因为「不可迁移」只是说它自己不能搬,不要求周围干净;
- 可迁移的分配优先借用可回收,再借不可迁移;借用不可迁移会把可迁移区污染,所以放最后;
- CMA 只能借
MOVABLE,因为 CMA 区本身要求「随时能腾出来」,不可迁移页会让 CMA 分配失败。
find_suitable_fallback() 还会检查「这个 order 的块是否值得偷」:如果 pageblock 里空闲页比例太低,偷过来也拼不出连续块,就跳过。
steal:把整个 pageblock 改类型
steal_suitable_fallback() 做两件事:
- 如果 pageblock 内空闲页足够多(
can_steal),把整个 pageblock 的迁移类型改成请求类型; - 把该 order 的空闲块从原类型链表移到新类型链表。
这样后续分配看到的是一整块「同类型」的 pageblock,而不是「一半 M 一半 U」的碎片。代价是「可迁移区变小」,长期看可能让 THP/CMA 更难;所以内核有 MIGRATE_HIGHATOMIC 保留池、watermark_boost、compaction 来对冲。
/proc/pagetypeinfo 怎么读
Free pages count per migrate type at order 0 1 2 3 ...
Node 0, zone Normal, type Unmovable 1234 567 234 89 ...
Node 0, zone Normal, type Movable 20000 9000 4000 1500 ...
Node 0, zone Normal, type Reclaimable 3000 1200 400 100 ...
Node 0, zone Normal, type HighAtomic 12 3 1 0 ...
Node 0, zone Normal, type CMA 512 0 0 0 ...
Node 0, zone Normal, type Isolate 0 0 0 0 ...
判读:
Unmovable的 order-0 很多、高阶为 0:不可迁移页把 pageblock 切碎,THP/大块 DMA 会失败;Movable还有 order-9:至少有一个完整的 2 MiB pageblock,THP 有希望;CMA的 order-9 为 0 但总量不少:CMA 区被拆散,摄像头/GPU 大缓冲可能失败;Isolate非零:有内存热插拔/离线正在进行,或 compaction 隔离未归还。
/proc/pagetypeinfo 的末尾还有 Number of blocks type ...,统计每种迁移类型的 pageblock 数:
Number of blocks type Unmovable Movable Reclaimable HighAtomic CMA Isolate
Node 0, zone Normal 1200 8000 300 16 64 0
这张表比空闲页数更能反映「碎片结构」:Movable block 很多但 order-9 为空,说明每个 block 都被切过。
外部碎片指数
cat /sys/kernel/debug/extfrag/extfrag_index
# Node 0, zone Normal -1 -1 0.000 0.000 0.000 0.000 0.000 0.000 1.000 1.000 1.000
cat /sys/kernel/debug/extfrag/unusable_index
extfrag_index 的每个数字是「该 order 分配失败的概率估计」(0–1000,归一化后 0–1);-1 表示该 order 不适用。unusable_index 表示「因为碎片而不可用的页比例」。两者持续为 1 时,说明该 order 已经无法从现有 pageblock 拼出来,需要 compaction 或调整工作负载。
- 以为迁移类型是每页属性:它是 pageblock 属性;一个 4 KiB 页的「可迁移性」由它所在的 2 MiB block 决定。
- 把
MIGRATE_HIGHATOMIC当普通类型:它是GFP_ATOMIC的保留池,普通分配不能 fallback 进去,否则原子分配在关键时刻会失败。 /proc/pagetypeinfo的 order 列从 1 开始数:第一列是 order 0(1 页),不是 order 1。- 以为 CMA 能被随便借用:CMA 只接受
MOVABLE的 fallback,且借用后要能通过迁移/回收腾出;不可迁移页进去会把 CMA 废掉。 MIGRATE_ISOLATE的页会计入MemFree:不会,隔离页已从空闲链表摘除;但MemFree的统计口径可能短暂不一致。- 调
vm.min_free_kbytes就能解决碎片:水位只影响「何时回收」,不改变 pageblock 的迁移类型分布;碎片要靠 compaction/CMA/THP 策略。
3.7.12 水位、min_free_kbytes 与 pcp 的边界
这一节回答:min_free_kbytes 到底怎么变成每个 zone 的 min/low/high?为什么 MemFree 很低但系统不回收?pcp 缓存与水位之间是什么关系?
水位的计算公式
启动时 setup_per_zone_wmarks() 按 managed_pages 分摊 min_free_kbytes:
zone->watermark[WMARK_MIN] = min_free_kbytes * zone->managed_pages / total_managed
zone->watermark[WMARK_LOW] = zone->watermark[WMARK_MIN] * 5 / 4
zone->watermark[WMARK_HIGH] = zone->watermark[WMARK_MIN] * 3 / 2
min_free_kbytes 的默认值不是固定 4 MiB,而是按物理内存的平方根估:
min_free_kbytes = sqrt(lowmem_kbytes * 16)
并 clamp 到 [128, 65536] KiB。16 GiB 机器上 lowmem_kbytes ≈ 16*1024*1024,平方根 *4 ≈ 16 MiB;所以 /proc/zoneinfo 里 min 常见几千到几万页(16–64 MiB)。
watermark_scale_factor(/proc/sys/vm/watermark_scale_factor,默认 10,范围 0–1000)会额外拉大 LOW/HIGH 的间距:
low = min + (managed - min) * scale / 10000
high = min + (managed - min) * scale * 3 / 20000
scale 越大,kswapd 越早启动、回收越激进;代价是可用内存变少。数据库/低延迟场景常调到 50–200。
zone_watermark_ok 的判断
/* mm/page_alloc.c —— 6.8 节选,简化 */
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 预留 */
free_pages -= (1 << order) - 1;
if (alloc_flags & ALLOC_HIGH)
min -= min / 2;
if (alloc_flags & ALLOC_HARDER)
min -= min / 4;
if (free_pages <= min + z->lowmem_reserve[highest_zoneidx])
return false;
for (o = order; o < MAX_ORDER; o++) {
if (!z->free_area[o].nr_free)
continue;
...
}
return true;
}
注意 free_pages -= (1 << order) - 1:分配 order-n 需要「连续」的 2^n 页,水位判断要预留「对齐损耗」;这也是为什么大阶分配在高水位边缘更容易失败。
alloc_flags 的常见组合:
| 标志 | 含义 |
|---|---|
ALLOC_WMARK_MIN | 慢路径,只要求 MIN |
ALLOC_WMARK_LOW | 快路径,要求 LOW |
ALLOC_WMARK_HIGH | 更严格,通常用于 GFP_ATOMIC 之外的特殊路径 |
ALLOC_HARDER | 降低水位要求(高优先级/原子分配) |
ALLOC_OOM | OOM 前的最后一次尝试 |
ALLOC_NO_WATERMARKS | 完全跳过水位(PF_MEMALLOC) |
ALLOC_CPUSET | 遵守 cpuset 的内存节点限制 |
PF_MEMALLOC 的任务(kswapd、直接回收者)用 ALLOC_NO_WATERMARKS,否则「回收需要内存、内存需要水位」会死锁。
watermark_boost:临时抬高水位
当 compaction 失败或 CMA 分配失败时,内核调 boost_watermark(),把 zone->watermark_boost 提高,让后续分配更早触发回收/规整,给碎片治理留出空间。它会在下次 setup_per_zone_wmarks() 或回收成功后回落。
/proc/zoneinfo 里能看到:
pages free 120345
boost 0
min 2891
low 3613
high 4335
如果 boost 长期非零,说明系统持续处于碎片压力,compact_stall 通常也在涨。
pcp 与水位:为什么 MemFree 不等于 buddyinfo 总和
MemFree 来自 NR_FREE_PAGES,包含 pcp 缓存里的页;/proc/buddyinfo 只统计 free_area 里的页,不含 pcp。所以:
MemFree ≈ buddyinfo 总页数 + 所有 CPU pcp->count 之和
在 128 CPU 的机器上,每个 CPU 缓存几万页,差值可能到几 GiB。/proc/zoneinfo 的 pages free 是 NR_FREE_PAGES(含 pcp),free_area 的 nr_free 是纯伙伴系统;两者对不上是正常的。
释放路径:
free_unref_page(page)
→ free_unref_page_prepare() /* 检查 refcount、PageBuddy */
→ free_unref_page_commit() /* 挂到 pcp->lists[] */
→ 如果 pcp->count >= pcp->high:
free_pcppages_bulk(zone, pcp->batch, pcp)
drain_pages(cpu) 在 CPU 热插拔下线时把该 CPU 的 pcp 全部倒回伙伴系统;drain_all_pages() 用于内存热插拔/休眠。否则离线 CPU 缓存里的页会「消失」在统计外。
MemFree低但buddyinfo高:差额在 pcp;echo 1 > /proc/sys/vm/drop_caches不会 drain pcp,CPU 离线才会。- 把
min_free_kbytes调得越大越好:太大会让系统频繁回收、可用内存变少;按sqrt默认值起调,观察pgscan再动。 watermark_scale_factor与min_free_kbytes重复调:两者都抬高水位,叠加会让 kswapd 过度活跃;一次只调一个。ALLOC_NO_WATERMARKS被普通分配用:只有PF_MEMALLOC才该用;普通路径用了会绕过安全垫,导致回收器自己分配失败。- pcp 的
high越大越好:缓存太多会让MemFree虚高、伙伴系统拿不到页;percpu_pagelist_high_fraction调小可缓解。 - CPU 热插拔后内存「少了」:离线 CPU 的 pcp 可能没 drain;检查
drain_pages()与CPU_DEAD回调。
3.7.13 实验:制造碎片、读 buddyinfo、用 page_owner 找来源
这一节回答:怎样在一个 512 MiB 的 QEMU 里,用几十行模块代码把 order-9 分配「弄失败」,并用 /proc/buddyinfo、/proc/pagetypeinfo、bpftrace、page_owner 四种证据解释「为什么明明有内存却分配不出连续 2 MiB」?
模块:buddy_frag.c
思路:先拿一块 order-9(2 MiB)再放掉,确保有一个完整的 pageblock;然后分配 512 个 order-0 页,隔一个释放一个,制造「空闲页被已分配页隔开」的棋盘格局;最后再申请 order-9,观察是否失败。
// buddy_frag.c — 制造外部碎片并观察高阶分配(Linux 6.8)
#include <linux/gfp.h>
#include <linux/mm.h>
#include <linux/mmzone.h>
#include <linux/module.h>
#include <linux/nodemask.h>
#include <linux/vmstat.h>
#define MAX_PAGES 1024
static unsigned int nr_pages = 512;
module_param(nr_pages, uint, 0444);
MODULE_PARM_DESC(nr_pages, "order-0 pages to fragment (max 1024)");
static struct page *pages[MAX_PAGES];
static void dump_buddy(const char *tag)
{
int nid, z, order;
for_each_online_node(nid) {
pg_data_t *pgdat = NODE_DATA(nid);
for (z = 0; z < MAX_NR_ZONES; z++) {
struct zone *zone = &pgdat->node_zones[z];
if (!zone->present_pages)
continue;
pr_info("%s: node%d %-8s free=%lu",
tag, nid, zone->name,
zone_page_state(zone, NR_FREE_PAGES));
for (order = 9; order < MAX_PAGE_ORDER; order++)
pr_cont(" order%d=%lu", order,
zone->free_area[order].nr_free);
pr_cont("\n");
}
}
}
static int __init buddy_frag_init(void)
{
struct page *block;
unsigned int i, allocated;
if (nr_pages > MAX_PAGES)
nr_pages = MAX_PAGES;
dump_buddy("before");
/* 先确保有一个完整的 order-9 块,再放掉 */
block = alloc_pages(GFP_KERNEL, 9);
if (!block) {
pr_err("buddy_frag: cannot get an order-9 block\n");
return -ENOMEM;
}
__free_pages(block, 9);
/* 拿 512 个单页,通常来自同一个 pageblock */
for (i = 0; i < nr_pages; i++) {
pages[i] = alloc_pages(GFP_KERNEL | __GFP_ZERO, 0);
if (!pages[i])
break;
}
allocated = i;
pr_info("buddy_frag: allocated %u order-0 pages\n", allocated);
/* 隔一个释放一个:空闲页被已分配页隔开 */
for (i = 1; i < allocated; i += 2) {
__free_pages(pages[i], 0);
pages[i] = NULL;
}
dump_buddy("after-free-odd");
/* 现在再要 order-9:大概率失败,因为 pageblock 已被切碎 */
block = alloc_pages(GFP_KERNEL, 9);
pr_info("buddy_frag: order-9 allocation %s\n", block ? "ok" : "FAILED");
if (block)
__free_pages(block, 9);
return 0;
}
static void __exit buddy_frag_exit(void)
{
unsigned int i;
for (i = 0; i < nr_pages; i++) {
if (pages[i]) {
__free_pages(pages[i], 0);
pages[i] = NULL;
}
}
pr_info("buddy_frag: unloaded\n");
}
module_init(buddy_frag_init);
module_exit(buddy_frag_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("fragment a pageblock and test order-9 allocation");
在 512 MiB 的 QEMU 里:
sudo insmod buddy_frag.ko nr_pages=512
dmesg | tail -12
# before: node0 Normal free=... order9=1
# after-free-odd: node0 Normal free=... order9=0
# order-9 allocation FAILED
如果 order-9 仍然成功,说明分配器从别的 pageblock 找到了块;把 nr_pages 加到 1024,或先在虚机里跑几个吃内存的进程,再试一次。失败不是「模块 bug」,而是「碎片」的定义:空闲页总数够,但没有连续 512 页。
用户态:zoneinfo.c
/* zoneinfo.c — 汇总 /proc/zoneinfo 与 /proc/buddyinfo 的水位/碎片
* 编译: cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 -o zoneinfo zoneinfo.c
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
static void print_buddy(void)
{
FILE *fp = fopen("/proc/buddyinfo", "r");
char line[512];
if (!fp) { perror("/proc/buddyinfo"); return; }
printf("%-6s %-10s %10s %8s %8s %8s\n",
"node", "zone", "free-pages", "o9", "o10", "high%");
while (fgets(line, sizeof line, fp)) {
unsigned long n[11], total = 0, high = 0;
char zone[32];
int node, o, got;
got = sscanf(line, "Node %d, zone %31s %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu",
&node, zone, &n[0], &n[1], &n[2], &n[3], &n[4], &n[5],
&n[6], &n[7], &n[8], &n[9], &n[10]);
if (got != 13)
continue;
for (o = 0; o < 11; o++)
total += n[o] << o;
high = (n[9] << 9) + (n[10] << 10);
printf("%-6d %-10s %10lu %8lu %8lu %7.2f%%\n",
node, zone, total, n[9], n[10],
total ? 100.0 * high / total : 0.0);
}
fclose(fp);
}
int main(void)
{
FILE *fp = fopen("/proc/zoneinfo", "r");
char line[256], zone[32] = "";
int node = -1, in_zone = 0;
unsigned long free_pg = 0, min = 0, low = 0, high = 0, managed = 0;
print_buddy();
if (!fp) { perror("/proc/zoneinfo"); return 1; }
printf("\n%-6s %-10s %10s %8s %8s %8s %10s %8s\n",
"node", "zone", "free", "min", "low", "high", "managed", "free%");
while (fgets(line, sizeof line, fp)) {
int n;
char z[32];
if (sscanf(line, "Node %d, zone %31s", &n, z) == 2) {
if (in_zone && managed)
printf("%-6d %-10s %10lu %8lu %8lu %8lu %10lu %7.2f%%\n",
node, zone, free_pg, min, low, high, managed,
managed ? 100.0 * free_pg / managed : 0.0);
node = n;
snprintf(zone, sizeof zone, "%s", z);
in_zone = 1;
free_pg = min = low = high = managed = 0;
continue;
}
if (!in_zone)
continue;
if (sscanf(line, " pages free %lu", &free_pg) == 1)
continue;
if (sscanf(line, " min %lu", &min) == 1)
continue;
if (sscanf(line, " low %lu", &low) == 1)
continue;
if (sscanf(line, " high %lu", &high) == 1)
continue;
if (sscanf(line, " managed %lu", &managed) == 1)
continue;
}
if (in_zone && managed)
printf("%-6d %-10s %10lu %8lu %8lu %8lu %10lu %7.2f%%\n",
node, zone, free_pg, min, low, high, managed,
managed ? 100.0 * free_pg / managed : 0.0);
fclose(fp);
return 0;
}
预期输出类似:
node zone free-pages o9 o10 high%
0 DMA 3991 0 0 0.00%
0 DMA32 120345 3 0 1.28%
0 Normal 245670 1 0 0.42%
node zone free min low high managed free%
0 DMA 3991 15 18 21 3977 100.35%
0 DMA32 120345 2891 3613 4335 118000 101.99%
0 Normal 245670 18000 22500 27000 240000 102.36%
free% > 100% 是因为 free_pg 包含 pcp 缓存,而 managed 不含;这正好说明「两个统计口径不同」。如果 free% 长期低于 10%,kswapd 会开始工作。
观测:谁在分配、哪个 order、什么迁移类型
# 按 order 与迁移类型统计页分配(5 秒)
sudo bpftrace -e '
tracepoint:kmem:mm_page_alloc
{
@order[args->order] = count();
@mt[args->migratetype] = count();
}
interval:s:5 { print(@order); print(@mt); exit(); }'
# perf 计数(更轻)
sudo perf stat -e kmem:mm_page_alloc -a sleep 2
# 手动触发规整,观察计数器
grep compact_ /proc/vmstat
echo 1 | sudo tee /proc/sys/vm/compact_memory
grep compact_ /proc/vmstat
mm_page_alloc 的 args->migratetype 是 MIGRATE_* 的数值;args->order 是 0–10。@order 集中在 0 说明分配以单页为主;出现 9 说明 THP/大块分配在尝试。
page_owner:这块页是谁分配的
page_owner 在分配时记录调用栈,是排查「谁把内存吃光/谁造成碎片」的利器。需要启动参数 page_owner=on(或 CONFIG_PAGE_OWNER + 运行时开启):
# 启动参数:page_owner=on
cat /sys/kernel/debug/page_owner | head -60
# 只看 order-9 的页
cat /sys/kernel/debug/page_owner | grep -A15 'order 9' | head -40
# 用内核自带工具排序(tools/vm/page_owner_sort)
sudo page_owner_sort /sys/kernel/debug/page_owner sorted.txt
每条记录包含 Page allocated via order N, mask ...、pid、ts 与调用栈。判读:
- 大量 order-0 来自
alloc_pages的驱动,说明驱动在小块分配上很频繁; - order-9 来自
khugepaged或alloc_pages的 THP,说明大页在尝试; - 同一个调用栈持续增长,可能是泄漏;结合
/proc/vmstat的pgalloc_*看趋势。
某服务 /proc/meminfo 显示 MemFree 1.2 GiB,但 AnonHugePages 只占匿名页的 30%。三步定位:
cat /proc/buddyinfo看到 order-9 只有个位数,/proc/pagetypeinfo显示Unmovable的 order-0 很多——碎片严重;grep compact_ /proc/vmstat看到compact_stall很大、compact_fail接近compact_stall——规整也拼不出 2 MiB;page_owner显示大量 order-0 来自某驱动的kmalloc页表/缓冲区——不可迁移页把 pageblock 切碎。
解法不是「加内存」,而是:把驱动的大缓冲改成 dma_alloc_coherent/CMA,减少 order-0 碎片;或对 THP 设 madvise 模式,只给关键区域用大页。
3.7.14 本章出口检查
| 检查项 | 达标线 | 没达标去哪 |
|---|---|---|
| 画出 node→zone→pageblock→page | 能说出每层的作用 | 3.7.1 |
解释 page->mapping 的低位 | ANON=1,取指针要清低位 | 3.7.2 |
| 说清伙伴的分裂与合并 | 伙伴 pfn = pfn ^ (1<<order) | 3.7.3 |
| 解释迁移类型与降级 | 为什么降级会造成碎片 | 3.7.3 |
| 说出 pcp 的作用 | 绕过 zone->lock | 3.7.4 |
| 选对 GFP 标志 | 中断用 GFP_ATOMIC,用户页可迁移 | 3.7.5 |
读懂 buddyinfo 的 11 列 | 判断碎片程度 | 3.7.7 |
| 说出 zone/pglist_data 关键字段 | 三个页数与水位的关系 | 3.7.9 |
| 走通 rmqueue/expand/free | 合并条件与 fallback | 3.7.10 |
| 解释 pageblock 与迁移类型 | 为什么是 2 MiB 粒度 | 3.7.11 |
| 算出一个 zone 的水位 | min/low/high 与 min_free_kbytes | 3.7.12 |
| 制造并解释 order-9 失败 | buddyinfo/pagetypeinfo/page_owner | 3.7.13 |
Q1 一个 zone 的 order-2 链表有 3 块,order-3 有 1 块,现在要分配 order-3,会发生什么?如果 order-3 的 1 块被占用,分配 order-3 又要走哪条路?
Q2 struct page 的 _refcount 为 1、_mapcount 为 0,这个页可能是什么?如果 _mapcount 为 2 呢?
Q3 为什么 alloc_pages(GFP_ATOMIC, 5) 比 alloc_pages(GFP_KERNEL, 5) 更容易失败?
Q4 /proc/buddyinfo 显示 order-0 有几万块,order-9 为 0。此时用户态 mmap 一个 2 MiB 的 THP 会怎样?内核怎么办?
Q5 free_pages(addr, order) 与 __free_pages(page, order) 有什么区别?把 alloc_pages 返回的 page 传给 free_pages 会怎样?
答案
A1 分配 order-3 时,__rmqueue_smallest 先查 order-3 链表,有就直接摘一块返回,不分裂。如果 order-3 为空,会去 order-4 及以上找;找到就 expand() 分裂,把多余的一半挂回低阶链表。若所有高阶都空,走慢路径(回收/规整/OOM)。
A2 _refcount=1, _mapcount=0:被某人持有但没被映射,典型是 slab 页(slab 持有引用)、页表页、驱动私有页,或刚从伙伴系统取出还没映射的页。_mapcount=2:被两个 PTE 映射,典型是 fork 后父子共享的 COW 页,或 MAP_SHARED 的多进程映射。
A3 GFP_ATOMIC 不允许睡眠与回收,只能从 MIGRATE_HIGHATOMIC 应急池或现成的空闲块里拿;高阶块本来就少,应急池更小,所以失败率高。GFP_KERNEL 可以唤醒 kswapd、直接回收、规整,成功率高得多。
A4 THP 的 khugepaged/缺页路径尝试分配 order-9(2 MiB)块,失败后回退到 4 KiB 小页(VM_NOHUGEPAGE 或 thp_fault_fallback 计数加一)。用户态看到的是「大页没生效」,性能略降,功能不受影响。/proc/vmstat 的 thp_fault_fallback 会增长。
A5 free_pages() 接收虚拟地址(__get_free_pages 的返回值),内部 virt_to_page() 再转;__free_pages() 接收 struct page *。把 page 传给 free_pages 会把它当成地址做 virt_to_page,算出完全错误的页,释放别人的内存,通常立刻 panic 或静默损坏。
母本对应
| 母本 | 章节 | 本书落点 | 程度 |
|---|---|---|---|
| 奔跑吧 Linux 内核 | 第 2 章 2.1 物理内存初始化 | 3.7.1、3.7.6 | 完整(node/zone/page、memblock) |
| 奔跑吧 Linux 内核 | 第 2 章 2.3 内核内存的布局图 | 3.7.1 | 精选(zone 与直接映射) |
| 奔跑吧 Linux 内核 | 第 2 章 2.4 分配物理页面 | 3.7.3–3.7.5 | 完整(伙伴系统、pcp、GFP) |
| OSTEP | 第 15–18 章 地址转换、分段、分页 | 3.7.1 | 背景(物理页的概念框架) |
| 奔跑吧 Linux 内核 | 第 2 章 2.11 page 引用计数 | 3.7.2 | 完整(_refcount/_mapcount) |
| 李治军 | 第 6 章 内存管理 | 3.7.6 | 对照(0.11 的 mem_map 与 free_area) |
| 奔跑吧 Linux 内核 | 第 2 章 2.4.2 释放页面 | 3.7.10 | 对照(4.x 的 __free_one_page) |
| 奔跑吧 Linux 内核 | 第 2 章 2.16 内存规整 | 3.7.11、3.7.13 | 背景(pageblock 与迁移) |
下一站:3.8 虚拟内存与页表 把物理页装进地址空间:四级页表怎么走、VMA 是什么、缺页异常的四条分支各自做什么。