上一篇把物理内存分了页,这篇讲页怎么分给使用者.核心是 buddy 分配器:一个用"伙伴关系"换零碎片合并的七十岁老算法,今天仍是内核页面分配的唯一正门.alloc_pages 一族怎么用,GFP 标志在说什么,buddyinfo 的每一列怎么读,这篇全程拿实机的数字对账,包括一次我自己的算式翻车.
这篇回答几件事:
- buddy 算法的核心机制,为什么合并是免费的
- order 的含义,buddyinfo 每列怎么读
- GFP 那串标志到底在修饰什么
- 快路径和 per-CPU 缓存在省什么
先摆算法.buddy 管理的不是单页,是 2 的幂次页块:order-n 的块是 2^n 页,4KB 起步,order-10 封顶(4MB).每个 order 一条自由链,挂在 zone 上.分配 order-n:链上有现成的就拿,没有就找 order-n+1 的块,劈成两半,一半给请求,另一半挂回 order-n 的链 — 递归向上,最多劈到 order-10.释放是镜像:块回来,内核算出它的"伙伴" — 同阶且物理地址按两倍块大小对齐配对的那个兄弟,兄弟也空闲就合并成高一阶的块,再找高一阶的伙伴,递归到顶.伙伴地址怎么算有个位运算的小聪明:块首地址异或块大小,就是伙伴首地址.
这个设计的妙处:任意时刻的碎片状态都能精确复原,合并不需要扫描,算个异或就知道对象在哪.代价:只能分配 2 的幂,要 3 页得拿 4 页.所以 buddy 只伺候"页级"的请求,比页小的碎片需求全部交给下一层的 slab(下篇),两层配合,buddy 的形状劣势被消化掉.
读表实验./proc/buddyinfo 每个 zone 一行,列号就是 order:
Node 0, zone DMA 144 42 110 164 101 48 30 18 9 4 423
ord4 ord5
order-4 有 101 个 64KB 块,order-5 有 48 个 128KB 块.现在上模块抓 64 个 order-4(共 4MB),前后对比:
BEFORE: ... 164 101 48 30 18 9 4 423
AFTER: ... 164 53 40 30 18 9 4 423
FREED: ... 164 101 48 30 18 9 4 423
三个时刻一列列对账,全是算术:order-4 列 101 变 53,净少 48;但请求是 64 个,差 16 哪来的?看 order-5 列,48 变 40,拆了 8 块,每块劈两半恰好产出 16 个 order-4.64 = 101 库存里的 48 + 拆出来的 16,完美闭环.释放后两列精确复原 — 高阶块被逐级合并回去,buddy 的"零成本复原"看得见.顺便自首一个翻车:模块里算 KB 的表达式优先级写错,日志打出"每块 0 KB",被 buddyinfo 的列变化当场打脸,尺寸换算老老实实手做:order-n 块大小 = 4KB 左移 n.
GFP 标志是分配的"行为合同",不是选项菜单.__GFP 主要分三组修饰:能不能睡(GFP_KERNEL 可以,它会唤醒回收和换出, GFP_ATOMIC 禁睡,中断和持锁上下文唯一选择,不惜代价从预留里抠);从哪拿(zone 降级链, __GFP_HIGHMEM 允许高端内存, __GFP_DMA 限定低区);拿不到怎么办(__GFP_NORETRY 不重试, __GFP_RETRY_MAYFAIL 有上限地重试,默认的 GFP_KERNEL 是"不死心",会一路触发回收压缩直到 OOM).写驱动最常错的就一条:中断里用 GFP_KERNEL — 它可能睡,轻则 WARN(taint),重则死锁.记住口诀:进程上下文 KERNEL,原子上下文 ATOMIC,别的一律想清楚再写.
快路径最后讲.buddy 的 zone 锁是全 zone 一把,多核抢同一 zone 会排队.解法 per-CPU pageset:每个 CPU 一个小时货栈,囤一批单页,order-0 的分配和释放走自己的货栈,不碰 zone 锁,只有货栈空了或退还大块才去慢路径.这就是为什么单页分配的快路径只有几纳秒.zoneinfo 里每 CPU 一段的 counters 就是这些货栈的存货.代价是 NUMA 场景的轻微漂移:本 CPU 货栈里的页可能来自别的 node,严格 NUMA 绑定的工作流会绕开 pageset 直接走慢路径.
最后留几个容易忽略的细节:
- order-10 是 MAX_ORDER-1,4MB 封顶;想要 8MB 连续物理内存,标准 API 拿不到,要么 CMA,要么修内核 — 这是巨型页(HugeTLB)要预留的原因之一.
- __free_pages 释放时 order 必须和分配时一致,错了要么泄漏要么把别人的页当你的放了,都是灾难.
- alloc_pages 失败返回 NULL,查它和查 kmalloc 一样是义务;GFP_ATOMIC 的预留就是为"最后关头还能分出来"设计的,日常把它当正常分配用会掏空预留.
- buddyinfo 最后一列巨大(板上 423 个 order-10)不代表内存充裕,要看总空闲和碎片分布;order-0..3 很多而高阶稀少,就是典型外碎片,大块分配会失败.
- 碎片整理(compaction)就是为这个状态准备的:把可移动页搬家腾出高阶块,配合 __GFP_DIRECT_RECLAIM 在分配路径上同步干,/proc/pagetypeinfo 能看到每阶按迁移类型的分布.
- 连续物理内存对 DMA 是刚需(老设备不带 scatter-gather),现代驱动尽量用 scatterlist + IOMMU 摆脱对 buddy 大块的依赖,这是趋势.
- 模块卸载前记得页全部归还,泄漏的页在 /proc/vmstat 的 pgfree 对不上时才会现形,kmemleak 是另一条排查路(如果编了).