Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:每个物理页帧一个描述符。它不是「一页内存」,而是「这一页的元数据」;页本身在别处(直接映射区)。
定义 3.7.1 · zone 与水位

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 数组、保留区)。三者依次变小,差值能告诉你「有多少内存被内核自己吃掉了」。

定义 3.7.2 · page 与 pfn

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 低位为 1mapping 指向 anon_vma(去掉低位)
页缓存页page->mapping 非空且低位为 0mapping 指向 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 向下转。

关于 struct page 的四个误解
  1. 「page->index 总是页号」:只有页缓存页才是「文件内页偏移」;匿名页的 index 常被复用为其他含义(如 swap_entry 的偏移)。
  2. 「_refcount 是映射数」:_refcount 是引用计数(谁持有这个页),_mapcount 才是被多少个 PTE 映射。两者都为 0 才能释放(还有 pagecache 的引用)。
  3. 「PageAnon 看 mapping 是否为空」:匿名页的 mapping 非空(指向 anon_vma),只是低位为 1。空 mapping 的是「不在页缓存也不在 anon_vma」的页,如 slab、页表、驱动私有页。
  4. 「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 链表为空:

  1. 去 order-4 找,找到一块 16 页;
  2. 把它分裂成两个 8 页的「伙伴」,一个返回,一个挂到 order-3 链表;
  3. 如果 order-4 也空,继续往上找,直到 order-10;
  4. 全空才走慢路径(回收/规整/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 相同、且迁移类型兼容。第三点是抗碎片的核心。

定义 3.7.3 · 迁移类型(migratetype)

每个页块有一个迁移类型,描述「这块内存能不能搬走」:

类型 含义 典型来源
MIGRATE_UNMOVABLE 不可迁移,位置固定 内核数据结构、页表、DMA 缓冲
MIGRATE_MOVABLE 可通过更新页表搬走 用户匿名页、页缓存
MIGRATE_RECLAIMABLE 可通过回收释放 dentry/inode 缓存
MIGRATE_CMA 连续内存预留区 摄像头、GPU 大缓冲
MIGRATE_ISOLATE 被隔离,不参与分配 内存热插拔、离线
MIGRATE_HIGHATOMIC 给原子分配预留 GFP_ATOMIC 的应急池

分配时优先从同类型链表取;取不到才降级(fallback) 到其他类型。降级会把不可迁移的页混进可迁移的页块,造成碎片——这就是 /proc/pagetypeinfo 里「类型分布」值得看的原因。

例 3.7.1 · 一次 order-2 分配的全过程

假设 zone 的 order-2 与 order-3 链表都空,order-4 有一块 pfn=0x100 的 16 页。

  1. __rmqueue_smallest(zone, order=2, migratetype) 从 order 2 找到 MAX_PAGE_ORDER-1,在 order 4 命中;
  2. 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,另一个返回;
  3. 返回 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 分桶 */
};

分配路径变成:

  1. rmqueue_pcplist() 先看本地 pcp 链表,有就直接摘一个,不碰 zone->lock;
  2. 没有就 rmqueue_bulk() 一次从伙伴系统拿 batch 个(默认几十个),补进 pcp;
  3. 释放时 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 不破坏「连续内存」

pcp 缓存的是单页(order 0)与少量高阶块,且按迁移类型分桶。它让「分配一页」变成无锁操作,代价是这些页暂时不在伙伴系统里,/proc/buddyinfo 的 order-0 计数会偏低。高 order 分配不走 pcp(rmqueue 直接查 free_area),所以大块连续性不受影响。

3.7.5 分配路径与 GFP 标志

定义 3.7.4 · 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()。慢路径依次尝试:

  1. wake_all_kswapds() 唤醒后台回收;
  2. __alloc_pages_direct_reclaim() 直接回收(当前进程自己扫 LRU,见 3.10);
  3. __alloc_pages_direct_compact() 内存规整(把可迁移页搬走来拼出连续块);
  4. __alloc_pages_may_oom() 触发 OOM killer;
  5. 若 __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) 只对低端内存有效。

分配页时的六个错误
  1. 不检查返回值:GFP_KERNEL 也可能失败(OOM 前的最后尝试),GFP_ATOMIC 更容易失败。解引用 NULL 是最常见的内核崩溃。
  2. 在中断上下文用 GFP_KERNEL:会触发 BUG: sleeping function called from invalid context,或悄悄死锁。中断里只能 GFP_ATOMIC/GFP_NOWAIT。
  3. order 超过 MAX_PAGE_ORDER-1:alloc_pages 会返回 NULL 并打印警告。要更大块用 CMA 或 vmalloc(虚拟连续)。
  4. 把 __get_free_pages 的返回值传给 kfree:kfree 只认 kmalloc/kzalloc 的指针。页分配要用 free_pages。
  5. __GFP_NOFAIL 滥用:它会让分配无限重试,在内存耗尽时把进程卡死(甚至在原子上下文里死锁)。只在「失败就无法继续、且没有退路」的地方用。
  6. 忘了 __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 与大页有希望。
  • DMA zone 的数字很小:正常,它只有 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估算的「还能给应用多少」含可回收的页缓存
Slabslab 占用SReclaimable 部分可回收
PageTables页表占用大映射/多进程时显著
VmallocUsedvmalloc 区已用含内核栈(CONFIG_VMAP_STACK)
CmaTotal/CmaFreeCMA 预留区摄像头/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_idNUMA 节点号启动时
kswapd_waitkswapd 睡眠的等待队列回收路径唤醒
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_pfnzone 起始 pfnspanned 的起点
managed_pages交给伙伴系统的页数(扣保留区)水位按它算
spanned_pages覆盖页数(含空洞)/proc/zoneinfo 的 spanned
present_pages实际存在页数present
cma_pagesCMA 预留页数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 缓存十几万页。

zone 字段的五个坑
  1. 把 present_pages 当可用内存:它包含保留区与 vmemmap,实际可分配看 managed_pages。
  2. 用 zone->vm_stat[NR_FREE_PAGES] 直接读:并发下要用 zone_page_state()(带近似与稳定性处理),直接读可能看到撕裂的值。
  3. 以为 lowmem_reserve 是「预留内存」:它只是分配时的检查阈值,不预留实际页;真正预留看 memblock_reserve/CMA。
  4. 在 64 位上看 ZONE_HIGHMEM:x86-64 没有高端内存,ZONE_HIGHMEM 不存在;代码里 #ifdef CONFIG_HIGHMEM 才有效。
  5. 把 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;
}

三个细节:

  1. 从最高阶往下找:高阶块更稀缺,但拿大块来切能减少「把小块留给大分配」的碎片;6.8 的策略是「优先 fallback 到高阶」。
  2. find_suitable_fallback() 检查目标迁移类型能否接受这个块;MIGRATE_HIGHATOMIC 是特殊保留池,普通分配不能动。
  3. 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。

伙伴系统源码的六个坑
  1. alloc_pages() 返回的页没有清零:__GFP_ZERO 才会清零;alloc_page() 返回的页可能含上一个用户的数据。
  2. 把 page->private 当自己的字段:空闲时它存 order,分配后可能被页缓存/驱动复用;驱动要用自己的结构,不要占用 private。
  3. 释放时 order 传错:__free_pages(page, 1) 释放一个原本 order-0 的页,会把相邻页也卷进空闲链表,静默破坏伙伴系统;必须用分配时的 order。
  4. __free_pages() 后继续用 page 指针:页可能立刻被另一个 CPU 分配走;释放后不要再触碰。
  5. 以为 zone->lock 保护所有字段:它保护 free_area 与部分计数,不保护 LRU(那是 lru_lock);混用会死锁。
  6. 在 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_UNMOVABLE0内核结构、页表、DMA 缓冲否否
MIGRATE_MOVABLE1用户匿名页、页缓存是是(页缓存)
MIGRATE_RECLAIMABLE2dentry/inode、可回收 slab否是
MIGRATE_PCPTYPES3pcp 只管理前三种——
MIGRATE_HIGHATOMIC3GFP_ATOMIC 高阶预留否否
MIGRATE_CMA4CMA 预留区是(特殊)否
MIGRATE_ISOLATE5热插拔/离线隔离——

MIGRATE_PCPTYPES 不是一种真实类型,而是「pcp 链表只覆盖前三种」的哨兵;MIGRATE_HIGHATOMIC 与它同值但在 zone 的 free_area 里单独一档。MIGRATE_ISOLATE 的页不在任何空闲链表里,分配器看不到。

fallback 矩阵

当目标类型没有空闲块时,按「兼容性」顺序降级。6.8 的 fallbacks[][3] 大致是:

请求类型第 1 选择第 2 选择第 3 选择
UNMOVABLERECLAIMABLEMOVABLE—
MOVABLERECLAIMABLEUNMOVABLE—
RECLAIMABLEUNMOVABLEMOVABLE—
CMAMOVABLE——
HIGHATOMIC不 fallback(保留池)——

这张表的设计原则:

  • 不可迁移的分配可以借用可回收/可迁移的块,因为「不可迁移」只是说它自己不能搬,不要求周围干净;
  • 可迁移的分配优先借用可回收,再借不可迁移;借用不可迁移会把可迁移区污染,所以放最后;
  • CMA 只能借 MOVABLE,因为 CMA 区本身要求「随时能腾出来」,不可迁移页会让 CMA 分配失败。

find_suitable_fallback() 还会检查「这个 order 的块是否值得偷」:如果 pageblock 里空闲页比例太低,偷过来也拼不出连续块,就跳过。

steal:把整个 pageblock 改类型

steal_suitable_fallback() 做两件事:

  1. 如果 pageblock 内空闲页足够多(can_steal),把整个 pageblock 的迁移类型改成请求类型;
  2. 把该 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 或调整工作负载。

迁移类型的六个坑
  1. 以为迁移类型是每页属性:它是 pageblock 属性;一个 4 KiB 页的「可迁移性」由它所在的 2 MiB block 决定。
  2. 把 MIGRATE_HIGHATOMIC 当普通类型:它是 GFP_ATOMIC 的保留池,普通分配不能 fallback 进去,否则原子分配在关键时刻会失败。
  3. /proc/pagetypeinfo 的 order 列从 1 开始数:第一列是 order 0(1 页),不是 order 1。
  4. 以为 CMA 能被随便借用:CMA 只接受 MOVABLE 的 fallback,且借用后要能通过迁移/回收腾出;不可迁移页进去会把 CMA 废掉。
  5. MIGRATE_ISOLATE 的页会计入 MemFree:不会,隔离页已从空闲链表摘除;但 MemFree 的统计口径可能短暂不一致。
  6. 调 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_OOMOOM 前的最后一次尝试
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 缓存里的页会「消失」在统计外。

水位与 pcp 的六个坑
  1. MemFree 低但 buddyinfo 高:差额在 pcp;echo 1 > /proc/sys/vm/drop_caches 不会 drain pcp,CPU 离线才会。
  2. 把 min_free_kbytes 调得越大越好:太大会让系统频繁回收、可用内存变少;按 sqrt 默认值起调,观察 pgscan 再动。
  3. watermark_scale_factor 与 min_free_kbytes 重复调:两者都抬高水位,叠加会让 kswapd 过度活跃;一次只调一个。
  4. ALLOC_NO_WATERMARKS 被普通分配用:只有 PF_MEMALLOC 才该用;普通路径用了会绕过安全垫,导致回收器自己分配失败。
  5. pcp 的 high 越大越好:缓存太多会让 MemFree 虚高、伙伴系统拿不到页;percpu_pagelist_high_fraction 调小可缓解。
  6. 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_* 看趋势。
例 3.7.1 · 「空闲 1.2 GiB,THP 却只有 30%」

某服务 /proc/meminfo 显示 MemFree 1.2 GiB,但 AnonHugePages 只占匿名页的 30%。三步定位:

  1. cat /proc/buddyinfo 看到 order-9 只有个位数,/proc/pagetypeinfo 显示 Unmovable 的 order-0 很多——碎片严重;
  2. grep compact_ /proc/vmstat 看到 compact_stall 很大、compact_fail 接近 compact_stall——规整也拼不出 2 MiB;
  3. 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->lock3.7.4
选对 GFP 标志中断用 GFP_ATOMIC,用户页可迁移3.7.5
读懂 buddyinfo 的 11 列判断碎片程度3.7.7
说出 zone/pglist_data 关键字段三个页数与水位的关系3.7.9
走通 rmqueue/expand/free合并条件与 fallback3.7.10
解释 pageblock 与迁移类型为什么是 2 MiB 粒度3.7.11
算出一个 zone 的水位min/low/high 与 min_free_kbytes3.7.12
制造并解释 order-9 失败buddyinfo/pagetypeinfo/page_owner3.7.13
即时自测 · 3.7

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 是什么、缺页异常的四条分支各自做什么。