TaterLi 个人博客

[内核学习]物理页面分配

上一篇把物理内存分了页,这篇讲页怎么分给使用者.核心是 buddy 分配器:一个用"伙伴关系"换零碎片合并的七十岁老算法,今天仍是内核页面分配的唯一正门.alloc_pages 一族怎么用,GFP 标志在说什么,buddyinfo 的每一列怎么读,这篇全程拿实机的数字对账,包括一次我自己的算式翻车.

这篇回答几件事:

先摆算法.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 直接走慢路径.

最后留几个容易忽略的细节:

退出移动版