Linux伙伴分配器与物理页碎片排查实践

发布时间:2026/10/27 11:35:37

Linux伙伴分配器与物理页碎片排查实践 排查内存问题的这些年我碰到最多的一个误区就是把free命令里那个free数字当成还能用的内存。真正决定一次分配能不能成功的是 Linux 内核内存管理里最底层的那一环——伙伴分配器buddy allocator。它管的是物理页框以 2 的幂次为单位成块发放kmalloc、vmalloc、页缓存、匿名页、甚至内核栈最后都要落到它这里。很多看起来莫名其妙的现象比如机器明明还有几个 G 的空闲内存一个 order-9 的分配却死活失败比如 hugetlb 预留池怎么都凑不齐比如跑久了buddyinfo里 order 0 的数字一路飙高大块数字全是 0——根子都在伙伴分配器的合并与拆分逻辑上。这篇东西不打算把源码从头到尾念一遍那样没人看得下去。我想干的事是把zone-free_area[]这套结构、__rmqueue_smallest的拆分、__free_one_page的合并、迁移类型和 pageblock 这套反碎片机制串成一条线顺便把我自己踩过的坑和排查手法摊开讲。适合已经能看懂 C 代码、想真正搞明白物理页分配行为的人也适合只是想搞清楚为什么我的大页申请不到的运维和调优同学。1. 伙伴分配器在整条内存管理链路上站的是哪一格1.1 先分清页框、页、zone 这三层概念从 CPU 的视角看物理内存是一条从 0 开始的线性地址空间内核把它切成一块块固定大小的小格子x86_64 上默认一格 4KB这叫页框page frame编号叫 PFN。每个页框对应一个struct page描述符几十个字节内核启动时用vmemmap把这些描述符连续摆在一片虚拟地址区里。这一层是物理事实不由任何分配器决定。再往上一层是zone。物理内存不是处处等价的有些区域是老式 ISA 设备 DMA 能碰到的有些区域只有 32 位地址空间能寻址有些是常规区域。内核按可用性把它们分成ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_HIGHMEM32 位才有、ZONE_MOVABLE等。每个 zone 内部再按 NUMA 节点组织。所以你在/proc/zoneinfo里看到的Node 0, zone Normal就是这样一层一层切下来的。伙伴分配器就是挂在 zone 下面的物理页框批发商。它不关心这块内存将来装的是 dentry 还是匿名页它只负责给我一个 order我给你 2^order 个连续的物理页框并保证这些页框在这段时间里归你独占。用完还回来它负责尽量把相邻的小块拼回大块。1.2 为什么必须是连续和2 的幂有人会问虚拟内存不是随便映射吗为什么非要物理连续因为有些东西绕不开物理连续DMA 引擎的散列表长度有限有些老设备只认一段连续缓冲区页表本身要连续否则页表项没法通过一个基址加偏移算出来hugetlb 大页是硬性要求vmalloc虽然虚拟地址连续但它背后的物理页其实是一页一页单独申请的正好反过来印证了物理连续是稀缺资源这件事。至于2 的幂是一个工程上的权衡。块大小只能是 4KB、8KB、16KB…… 一直到MAX_PAGE_ORDER对应的 4MBx86_64 上 order 10 是 4MBorder 11 是 8MB具体看内核版本的MAX_PAGE_ORDER宏。好处是拆分和合并都极其便宜——把 order N 拆成两块 order N-1只需要把后半块的地址算出来挂到链表上不需要搬数据合并同理。代价是内部碎片你要 6KB它给你 8KB多出来的 2KB 浪费掉了。这个代价在 slab 那一层被缓解了。kmalloc-192、kmalloc-4096这些 kmem_cache 就是预先从伙伴分配器手里批发整页然后自己切成小块零售把内部碎片控制在可接受范围内。所以你在slabtop里看到的那些数字背后的物理页都是从伙伴分配器这里来的。1.3 一次alloc_pages走下去会经过哪些关卡我习惯把这条链路背下来因为排查问题时你需要在脑子里单步执行alloc_pages(gfp, order) - alloc_pages_node(nid, gfp, order) - __alloc_pages(gfp, order, preferred_nid, nodemask) - __alloc_pages_noprof() // 快路径 get_page_from_freelist - __alloc_pages_slowpath() // 慢路径回收、压缩、OOM快路径get_page_from_freelist()会对候选 zone 逐个做水位检查zone_watermark_fast通过了才进rmqueue()。水位检查这一步是很多人忽略的你的 zone 里哪怕挂着几万个空闲页只要低于WMARK_LOW快路径就会认为不安全而跳到慢路径去做直接回收。所以空闲内存有和能立刻分配是两回事。rmqueue()再往下分两路__rmqueue_smallest()走同迁移类型的正规路__rmqueue_fallback()在本类型里找不到时去别的迁移类型里借。这两条路的差别直接决定了内存碎片长什么样后面单独讲。提示如果你只想知道分配失败在哪一步最简单粗暴的办法是打开CONFIG_PAGE_OWNER配合page_owneron或者在__alloc_pages_slowpath里加个WARN_ON_ONCE比盯着free的数字猜要靠谱得多。2.free_area数组、struct page标记和 pageblock 位图2.1struct zone里那个free_area[]到底怎么用核心结构不超过十行struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free; }; struct zone { /* ... */ struct free_area free_area[NR_PAGE_ORDERS]; /* ... */ };拆开看。free_area是一个数组下标就是 order长度是NR_PAGE_ORDERS老版本里这个宏叫MAX_ORDER新版内核为了支持运行时调整改成了NR_PAGE_ORDERS读到不同版本代码时别懵。free_area[3]里挂着的每一块都是 2^3 8 个连续页框。nr_free是这个 order 下所有迁移类型加起来的空闲块数注意单位是块不是页。关键的细节在free_list[MIGRATE_TYPES]同一个 order 下空闲块按迁移类型分了多条链表。这就是反碎片机制的骨架。MIGRATE_UNMOVABLE、MIGRATE_MOVABLE、MIGRATE_RECLAIMABLE、MIGRATE_HIGHATOMIC、MIGRATE_CMA、MIGRATE_ISOLATE一个 zone 里每个 order 都有这么几条链表。还有一个容易读错的地方链表的哨兵节点就代表空闲块本身。list_head被内嵌在struct page里page-buddy_list老版本是page-lru所以你从链表上摘下一个节点用list_entry反推出来的就是这块空闲内存的首页描述符。没有额外的元数据节点这个设计很省但也意味着你不能随便把 page 结构里那几个字段挪作他用——它们被伙伴分配器占着。2.2PageBuddy和page_order是怎么存进去的判断一个 page 是不是空闲块用PageBuddy(page)。它的实现随版本变过好几次早期看page-_mapcount的特殊值后来改成page-page_type的一个位。但不管怎么变语义只有一个这个页框当前归属于伙伴分配器。order 存在page-private里读的时候用page_order(page)。这是private字段的兼职之一——同一个字段在 slab 里当slab_cache指针用在页缓存里可能有别的用途所以必须靠PageBuddy这个标记来区分当前处于哪种状态。这两个字段的写入顺序有讲究。释放时先set_buddy_order()再add_to_free_list()分配时反过来先del_page_from_free_list()再改状态。为什么因为链表操作和标记更新中间有可能被打断比如中断抢占的中途状态被别的 CPU 看到顺序反了就有可能让另一个 CPU 看到在链表上但 order 是错的这种状态进而算错伙伴地址。这类 bug 极其难复现但在上游社区的补丁记录里不是没出现过。提示写内核模块调试时别手动去改page-private或page-page_type。哪怕你只是想临时标记一下也可能和PageBuddy的判定冲突后续__free_one_page合并时会直接VM_BUG_ON或者更糟——静默地把别人的页合并进来。2.3 pageblock 位图迁移类型不是记在 page 上的一个很自然的疑问既然块有迁移类型那类型存在哪答案有点反直觉——不存在单个 page 上而是记在更大粒度的 pageblock 上。pageblock_order通常是MAX_PAGE_ORDER - 1x86_64 上就是 order 10一块 4MB。整个 zone 被切成若干个 pageblock每个 pageblock 用一个 3 bit 的字段记录它的迁移类型所有 pageblock 的字段打包成一个位图zone-pageblock_flags。读写接口是get_pfnblock_migratetype()和set_pageblock_migratetype()。为什么不按单页记因为如果每一页都能有独立迁移类型那块就没有意义了——碎片化的本质是相邻的小块类型不一致导致大块合并后类型混乱。按 4MB 一个大格子统一标注等于告诉分配器这片区域的页大多是可以挪走的MOVABLE回收和压缩时就能整片处理效率高得多。这个设计的副作用也要清楚一旦一个 pageblock 被标成MIGRATE_UNMOVABLE哪怕里面大部分页后来都空出来了它也不会自动变回 MOVABLE。set_pageblock_migratetype()只在特定条件下被调用比如__rmqueue_fallback里的偷块逻辑或者内存热插拔、CMA 激活这些路径。所以碎片是有粘性的一旦形成靠自然分配释放很难自己恢复。3. 分配路径逐行拆__rmqueue_smallest干的事比你想的少3.1 水位检查到底在防什么zone_watermark_fast()里面看一眼会觉得很朴素算当前空闲页数和zone-_watermark[WMARK_MIN/LOW/HIGH]比。三个水位的用途不同水位用途触及后的行为WMARK_MIN应急保留池只有带__GFP_HIGH等标志的分配多为原子上下文才能动用WMARK_LOW快路径下限低于它就走慢路径做直接回收WMARK_HIGH回收目标线kswapd被唤醒后要回收到这条线以上才肯睡WMARK_MIN的意义常被误解。它不是留给内核自己用这种含糊说法而是一个硬性约束任何可能睡眠、可能触发回收的普通分配都不许把空闲内存压到 MIN 以下只有那些绝对不能再等、不能睡眠的分配才有资格突破这条线。这是为了防止原子上下文的分配被普通分配饿死。所以当你看到free明明有几百兆kmalloc(GFP_ATOMIC)却开始失败多半是 zone 已经贴到WMARK_MIN了。水位是每个 zone 各自算的不是全局。这意味着一个 NUMA 节点上某个 zone 触底就可能触发整节点的回收和压缩代价远比想象中大。3.2__rmqueue_smallest的循环一个从 order 往上爬的漏斗函数本身短得惊人以 6.x 的形态为例static 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 NR_PAGE_ORDERS; current_order) { area (zone-free_area[current_order]); page get_page_from_free_area(area, migratetype); if (!page) continue; del_page_from_free_list(page, zone, current_order); expand(zone, page, order, current_order, migratetype); set_pages_mt(page, migratetype, current_order - order); return page; } return NULL; }读法是这样的从你要的 order 开始一级一级往上找找到第一块够大的就停。这是最佳适配的反面——它是最小够用优先避免了把 4MB 的块劈成 4KB 用掉的浪费。找到之后调expand()拆分。expand()的逻辑同样简单从high一路往下降到low每次把块的后半部分挂到free_area[high-1]对应的链表上前半部分继续往下拆。注意挂的是后半块——这个选择直接决定了后面合并时伙伴地址的算法。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; if (set_page_guard(zone, page[size], high, migratetype)) continue; add_to_free_list(page[size], zone, high, migratetype); set_buddy_order(page[size], high); } }这里有个set_page_guard的分支值得单独说。开启CONFIG_PAGE_GUARD用于DEBUG_PAGEALLOC一类场景后每页之间会插入不可用的哨兵页把相邻页隔开任何越界写立刻触发缺页。这时哨兵页并不能被正常分配所以expand里遇到它就跳过。这就是为什么开了页保护调试之后buddyinfo里的数字和不开时对不上——我一开始以为是自己代码算错了查了半天才发现是 guard page 在吃块。3.3fallback本类型没块了怎么办__rmqueue_smallest返回 NULL 之后__rmqueue会转去调__rmqueue_fallback()。这一步是碎片化的主要源头值得单独看。每个迁移类型都有一个fallbacks[][]表规定了我没块了按什么顺序去借。大体上MIGRATE_MOVABLE会优先借RECLAIMABLEUNMOVABLE会优先借RECLAIMABLE最后大家都会借到UNMOVABLE。借的行为有两种小借只把目标 order 的那一块从别人的链表上摘下来直接用pageblock 的迁移类型不改。偷整块如果发现整个 pageblock 里这个 order 的块都空闲干脆把整个 pageblockorder 104MB的迁移类型改成自己需要的类型然后整片征用。偷整块的行为听起来霸道其实是对的选择一个 4MB 区域如果里面的空闲块都快成规模了说明原类型的分配需求不旺整片转换成本最低。反过来如果只是零散地借一小块就会在那个 pageblock 里留下一个异类以后合并大块时就成了障碍。提示/proc/pagetypeinfo里能看到每个 zone、每个迁移类型、每个 order 的块数还会单独列出 fallback 的次数。如果你的Unmovable和Movable的分布明显失衡说明系统长期在做跨类型借用碎片化已经在积累了。4. 释放与合并伙伴地址公式的推导与合并循环的边界4.1 为什么pfn ^ (1 order)能算出伙伴这是整个分配器里最漂亮的技巧值得推一遍。前提所有块都是对齐的。一个 order-N 的块它的首页 PFN 必然是 2^N 的整数倍。因为从整片内存开始一路二分拆下来每次拆分后的子块起始地址都保持在 2^N 对齐上。expand()里挂到链表上的后半块地址就是整块起点 2^N同样对齐。在这个前提下一个 order-N 块的 PFN 写成二进制低 N 位全是 0。它的伙伴是谁是和它在同一个 order-(N1) 块里的另一半。这两个块的 PFN 只有第 N 位不同——一个是 0一个是 1。所以static inline unsigned long __find_buddy_pfn(unsigned long page_pfn, unsigned int order) { return page_pfn ^ (1 order); }异或第 N 位正好把 0 翻成 1、1 翻成 0。整个过程不需要查表、不需要额外存储一次异或搞定。和页表那套通过指针翻倍的写法一比就看出好了页表里要从父表项找兄弟项得先算出父表地址再乘 2 加偏移两步运算伙伴分配器因为块本身就是从 0 开始的连续编号一个异或就够了。这也是为什么它被叫伙伴——两个块大小相同、地址相邻、连起来能拼成更大的块就像一对搭档。4.2 合并循环的四个终止条件__free_one_page()的核心是一个 while 循环while (order max_order - 1) { buddy_pfn __find_buddy_pfn(pfn, order); buddy page (buddy_pfn - pfn); if (!page_is_buddy(page, buddy, order)) goto done_merging; /* ... */ del_page_from_free_list(buddy, zone, order); combined_pfn buddy_pfn pfn; page page (combined_pfn - pfn); pfn combined_pfn; order; }合并能继续的条件很严格任何一条不满足就停伙伴必须空闲PageBuddy(buddy)为真。伙伴的 order 必须一致如果 buddy 是 order 3 而你手里是 order 5说明它不是同层伙伴不能合。伙伴必须在同一个 zone 内跨界合并会把 zone 的边界搞乱page_is_buddy里会检查。order 不能顶到上限order max_order - 1这个条件卡得很关键。最后这条最容易被忽略。为什么停在max_order - 1而不是max_order因为pageblock_order通常就是max_order - 1再往上合就会跨越 pageblock 边界。而 pageblock 的迁移类型是独立的——把两个不同类型的 pageblock 合成一个 order-11 的块挂到某一条链表上这块内存的迁移类型归属就说不清了反碎片机制直接被破坏。这个边界是刻意留的不是漏了。4.3 合并后为什么要重新挂链表、为什么不直接改 order有个细节我第一次读的时候没反应过来合并后的块要del_page_from_free_list(buddy)然后add_to_free_list(page, ..., order1)同时set_buddy_order(page, order1)。也就是说伙伴块被摘掉了自己从一个 order 的链表挪到另一个 order 的链表。为什么不原地把两个节点的 order 改大因为伙伴分配器的空闲块信息就是链表节点本身。合并后是一块内存只能有一个代表节点伙伴那个节点的使命结束了必须从链表中移除而自己的节点需要换到更高 order 的链表上去。链表是块存在的唯一凭据不挂链表就等于这块内存没人管。再看combined_pfn buddy_pfn pfn取两个 PFN 的按位与。因为两者只有第 order 位不同与运算的结果就是把那一位清零得到合并后块的起始 PFN。这行代码和前面的异或公式是配套的一个求伙伴一个求合并后起点互为逆运算。提示DEBUG_PAGEALLOC、init_on_free、KASAN 这些选项都会在__free_one_page前后插入检查或清零导致释放路径明显变慢。做性能对比测试时一定确认对照组和实验组的调试开关完全一致否则测出来的差异全来自调试代码本身。5. 碎片是长期演化的结果迁移类型、pageblock 与反碎片5.1 三种主流迁移类型的语义边界判断一个分配该归到哪个类型逻辑在gfp_migratetype()分配标志归属类型典型用途能否回收或迁移无特殊标志MIGRATE_UNMOVABLE内核数据结构、slab、页表大多不能动__GFP_MOVABLEMIGRATE_MOVABLE用户匿名页、页缓存压缩时可整体迁移__GFP_RECLAIMABLEMIGRATE_RECLAIMABLEdentry/inode 缓存可回收但不迁移__GFP_HIGH等原子场景MIGRATE_HIGHATOMIC应急预留专用小块预留这套划分的目的不是分类管理而是让可迁移的东西聚在一起。内存压缩compaction要做的事是把若干 MOVABLE 页挪到别处腾出一大段连续空间。如果可迁移页和不可迁移页犬牙交错压缩跑断腿也腾不出大块——这就是碎片的真实含义。我见过一个很典型的现象某服务跑了一周之后buddyinfo里 order 0 的数量从 3 万涨到 15 万order 9 降到 0但总空闲量没怎么变。这不是内存泄漏是碎片。原因是有个模块高频地在GFP_KERNEL下分配和释放大小不一的缓冲区把 pageblock 打成了筛子。这种问题加内存是没用的因为加进来的内存迟早被同样打碎。5.2 为什么合理的做法是改分配方式而不是调参数碰到碎片问题很多人第一反应是调/proc/sys/vm/min_free_kbytes或者直接上压缩。我的经验是先看pagetypeinfo里各类型的比例再看 fallback 计数别急着调参数。如果Movable的块数在所有 order 上都远小于Unmovable那说明这台机器上分配来源本身就以不可迁移为主压缩能起的作用有限。这时候真正有效的动作是去查那个高频分配、并且生命周期很短的代码路径——比如一个定时器每 10msvmalloc一次再释放或者一个 netlink 缓冲区常年kmalloc大块。把它们改成常驻池或者kmem_cache碎片的产生速度会肉眼可见地降下来。如果Fallbacks计数pagetypeinfo底部那几行增长很快说明分配器在频繁跨类型借块这是碎片正在恶化的明确信号。这时候再看是哪种类型在借谁的块就能定位到是哪类分配在制造压力。5.3 CMA、hugepage 与伙伴分配器的相互拖累CMA连续内存分配器的实现方式很寄生它在启动时预留一片区域平时把这片区域的页框借给MIGRATE_CMA类型的普通分配用等某个驱动真的需要一大段连续内存时再通过迁移把这些页挪走腾出整片。问题在于如果 CMA 区域里被塞进了不可迁移的页迁移就失败了CMA 分配只能报错。所以内核严格限制 CMA 区域里只能放MIGRATE_MOVABLE类型的东西。而ZONE_MOVABLE的定位也类似——它里面的所有页都保证可迁移这样内存热插拔和空洞填充才有可能。透明大页也在这个体系里。申请一个 2MB 大页order 9伙伴分配器必须能找到一整块连续的 order 9 空闲块而且最好在 MOVABLE 类型的链表上。THP 的defrag策略就是在做这件事madvise模式只在用户显式标记的区域尝试always模式到处尝试代价是频繁触发压缩CPU 占用升高。我在高负载服务上见过always模式把 softirq 时间顶上去的情况改成madvise之后曲线就平了。提示判断 THP 是不在努力但失败看/sys/kernel/debug/extfrag/extfrag_index比看buddyinfo更直观它能告诉你当前内存状态下还能不能凑出 order N。这个接口要挂 debugfs 才有。6. 线上排查从几个伪文件到 tracepoint 的完整链路6.1buddyinfo的读法先看形状再看总量cat /proc/buddyinfo # Node 0, zone DMA 1 1 1 0 2 1 1 0 1 1 3 # Node 0, zone DMA32 2813 1654 886 512 210 77 19 8 1 0 0 # Node 0, zone Normal 51200 20480 6011 1200 140 14 0 0 0 0 0一行的列数就是NR_PAGE_ORDERS从 order 0 开始。order 0 那一列数量的含义是4KB 的空闲块有多少个order 10 是4MB 的空闲块有多少个。千万别把数字直接累加当内存量必须乘上 2^order × 4KB。健康的形状是从 order 0 开始逐级递减但不应该断崖式归零。上面这个例子里 order 7 之后全是 0说明连续大块已经完全拿不到了。如果一个系统长期是这个形状hugetlb预留、THP、大块 DMA 缓冲区都会出问题。再看一个反例order 0 数字很小但 order 5 以上很多。这通常意味着系统刚启动不久分配器还没被打碎属于正常状态。6.2 用sysrq抓一次全局快照怀疑内存问题时我第一件事是echo m /proc/sysrq-trigger dmesg -T | tail -200sysrq-m会把每个 zone 的空闲页数、水位、每个 order 的块数一次全打出来还会带上active_anon、inactive_file、slab这些大类。相比一个个读/proc快照能保证数据来自同一时刻不至于出现你看的时候已经变了的尴尬。判断顺序我一般是这样先看Free和三个水位的相对位置再看 slab 占了多少最后看高阶块还剩多少。如果Free远高于high水位但高阶块为 0那就是纯度很高的碎片问题和内存不足无关。6.3 tracepoint抓具体的分配行为/sys/kernel/tracing/events/kmem/下面有mm_page_alloc、mm_page_free、mm_page_alloc_extfrag等事件。第三个特别有用它只在发生跨迁移类型借用时触发记录的字段包含原类型、新类型和 order。cd /sys/kernel/tracing echo 1 events/kmem/mm_page_alloc_extfrag/enable echo 1 tracing_on sleep 60 echo 0 tracing_on grep -c extfrag trace如果一分钟内 extfrag 事件几千条那基本可以断言这台机器正在持续做跨类型借用碎片在积累。接下来把trace里出现的调用栈抓几条出来看看往往能直接锁定到某个驱动或者某个内核子系统的分配路径。另一条路是CONFIG_PAGE_OWNER。开启后page_owneron然后读/sys/kernel/debug/page_owner能打印出每一页是被哪个调用栈分配的。这个东西开销很大只能短时间用但在排查谁把大块拆碎了这类问题时几乎是唯一手段。6.4 参数调优的边界min_free_kbytes不是万能药min_free_kbytes决定WMARK_MIN另外两个水位按low min × 5/4、high min × 3/2的比例算出来。内核启动时会根据内存大小给一个默认值大致是 4 倍的低端内存平方根再用上下限夹住。调大它的直接效果是应急保留池变大快路径更容易判定水位不足而走慢路径。所以调大之后你可能会看到kswapd更频繁地醒来CPU 的 sys 时间轻微上升但好处是原子分配的成功率提高OOM 触发点往后推。调小它的风险则更直接原子上下文的分配会更容易失败某些驱动在这种时候会直接抛错误表现出来就是网卡时不时丢包这类完全不像内存问题的现象。我的经验值是除非有明确的原子分配失败日志否则别去动它。真正需要关注的是watermark_boost——这个机制在内核压缩失败或者长时间回收之后临时抬高水位避免系统反复卡在阈值边缘。它是自动的不需要手工调但如果zoneinfo里看到 boost 长时间不回落说明压缩一直在失败得回头去看碎片那一节。7. 动手验证写个模块把free_area打出来7.1 从模块里读 zone 数据的正确姿势想真正理解这套结构光看不如自己打印一遍。核心代码不复杂但有几个坑#include linux/module.h #include linux/mm.h #include linux/mmzone.h #include linux/vmstat.h static int __init buddy_dump_init(void) { struct zone *z; pg_data_t *pgdat; int nid, ord; for_each_online_node(nid) { pgdat NODE_DATA(nid); for (ord 0; ord NR_PAGE_ORDERS; ord) pr_info(node %d order %d total free blocks: %lu\n, nid, ord, pgdat-node_zones[ZONE_NORMAL].free_area[ord].nr_free); for_each_populated_zone(z) { pr_info(zone %s free pages %ld, min %lu low %lu high %lu\n, zone-name, zone_page_state(z, NR_FREE_PAGES), z-_watermark[WMARK_MIN], z-_watermark[WMARK_LOW], z-_watermark[WMARK_HIGH]); } } return 0; }坑在哪for_each_populated_zone遍历的是全局所有 zone但NODE_DATA(nid)是特定节点的。两个循环混在一起写如果节点不止一个打印出来的归属关系就乱了。要么用for_each_zone(z)走全局要么在节点循环里用pgdat-node_zones[]逐个取别混用。另一个坑是NR_PAGE_ORDERS这个宏在不同版本里名字不一样。老代码是MAX_ORDER且语义是最大 order 加一。在 6.x 上如果你写了order MAX_ORDER编译能过但循环会少跑一轮或者多跑一轮取决于具体版本。写死数字更不可取因为不同架构的NR_PAGE_ORDERS差别很大。编译前记得加MODULE_LICENSE(GPL)否则pr_info之外的符号可能拿不到某些导出符号对非 GPL 模块是屏蔽的。7.2 一个能直观看到拆分的实验在模块里做这么一组动作static int __init buddy_test_init(void) { struct page *p; unsigned long order_before[NR_PAGE_ORDERS]; int i; for (i 0; i NR_PAGE_ORDERS; i) order_before[i] global_zone_page_state(NR_FREE_PAGES); p alloc_pages(GFP_KERNEL, 3); /* 申请 32KBorder 3 */ if (!p) return -ENOMEM; pr_info(allocated order 3 at pfn %lx\n, page_to_pfn(p)); /* 此时 dump 一次 buddyinfo应该能看到 order 3 少了一块 * 而更低的 order 数量上升如果是从更大的块拆下来的 */ __free_pages(p, 3); return 0; }跑之前先cat /proc/buddyinfo记下快照跑之后再cat一次对比。你会观察到两种可能如果原 order 3 链表上有现成的块那就直接拿走其他 order 不变。如果 order 3 空了但 order 4 有货那 order 4 少一块同时 order 3 多出一块被拆下来的后半部分。第二种情况就是expand()在干活。多跑几轮专门在系统比较空闲的机器上试能很直观地感受到总线是往上爬的分配逻辑。反过来试释放连续申请两块 order 3然后先释放后面那块再释放前面那块。观察第一次释放后 order 3 的块数变化第二次释放后 order 3 是不是少了、order 4 是不是多了一块。如果 order 4 没增加那说明两块在物理上不是伙伴——打印 PFN 算一下pfn1 ^ (13)是不是等于pfn2就知道了。7.3 从这个实验里能得到的实操结论我用这组实验给不少人讲过课最后大家普遍有感触的是这几点。第一分配不上的根源经常不在没内存而在没对齐的空闲块。你申请 order 3 失败不代表系统里没有 32KB 空闲内存可能只是没有 32KB连续且对齐的空闲内存。这个区别决定了你该加内存还是该改分配策略。第二池子大小和可用性是两回事。NR_FREE_PAGES只统计页数不统计连续性。两个系统空闲页数完全一样一个能申请 order 9 一个不能这完全正常。第三调试时的观察点要选对。想看分配行为就看buddyinfo想看碎片程度就看extfrag_index想看水位就看zoneinfo。拿错工具会把问题引向错误的方向。我自己就曾经盯着slabtop找了几小时最后发现问题在 pageblock 的迁移类型上和 slab 一点关系都没有。第四别在开启DEBUG_PAGEALLOC的环境里做数字对比。guard page 会吃掉大量块buddyinfo的形状和正常内核完全不同。做实验前先确认dmesg | grep -i page_guard\|pagealloc避免在错误的前提上做结论。最后再补一个我经常用的判断技巧如果一台机器的buddyinfo里 order 0 数量持续增长而高阶块归零同时slab总量稳定那基本可以断定是某个路径在做小块高频分配而不是泄漏。这时候去找那个每毫秒分配一次又立刻释放的代码往往比调任何内核参数都有效。
延伸阅读

更多相关文章

2026/10/27 11:35:37

顶部导航栏滚动变色实现:scroll监听与IntersectionObserver

顶部导航栏这个组件,几乎每个前端都写过,透明底、白字、压在整屏头图上的那种做法,视觉上确实舒服。但这一步也是最容易翻车的地方:滚动之后导航栏还是透明,白色标题压在白色正文上,用户以为导航丢了&#…

2026/10/27 11:30:35

Redis密码保护与安全配置实战指南

1. Redis密码保护的必要性 在Redis的默认配置中,任何能够连接到Redis服务器的客户端都可以无限制地访问所有数据并执行任意命令。这种设计虽然简化了开发初期的使用流程,但在生产环境中却隐藏着巨大的安全隐患。想象一下,如果你的数据库门户大…

2026/10/27 12:31:05

TailwindCSS 背景渐变动效实战与优化

1. TailwindCSS 背景渐变动效实战指南在现代化前端开发中,背景渐变效果已成为提升界面视觉层次的关键手段。TailwindCSS 作为当下最流行的原子化 CSS 框架,其渐变系统却常被开发者低估。本文将深入解析如何突破默认配置限制,实现复杂渐变场景…

2026/10/27 12:31:05

Unity新手系统化学习指南:从零搭建3D交互场景

1. 为什么Unity新手需要系统化学习?三年前我第一次打开Unity时,被网上各种零散教程搞得晕头转向。有人教如何导入模型,有人讲物理引擎,但没人告诉我这些模块之间该如何配合。直到浪费了两周时间后,我才意识到&#xff…

2026/10/27 12:31:05

在线GIF制作工具评测与实用技巧

1. 在线制作GIF闪图的核心价值 GIF闪图作为一种动态图像格式,已经成为现代网络社交中不可或缺的表达方式。相比静态图片,它能通过循环播放的短动画更生动地传递情绪和信息。而在线制作工具的出现,彻底改变了传统需要下载专业软件才能制作GIF的…

2026/10/27 12:31:05

Delphi直连MySQL:libmysql.dll版本适配与原生C API封装指南

简介:本资源是面向Delphi中高级开发者的技术实践包,聚焦于绕过OLE DB、直接调用MySQL原生客户端库(mysql.dll)实现数据库连接与操作的核心方案。适用于需提升数据库访问性能、规避Windows平台中间层依赖、或定制轻量级数据访问层的…

2026/10/27 12:31:05

马尾发型全攻略:从高度定位到高颅顶不塌的实操技巧

说实话,过去我对马尾的印象就是“没时间洗头时的遮羞布”:随便抓一把头发用皮筋一捆,五分钟出门。直到去年开始认真研究ponytail这个造型,才发现它远比想象中有技术含量,甚至值得当作一个完整项目来拆解:同…

2026/10/27 12:26:05

Claude Code输出风格与系统提示词实战指南

1. Claude Code深度解析:Output Styles与系统提示词的实战应用 在AI辅助编程领域,Claude Code正逐渐成为开发者工具箱中的重要成员。作为一名长期使用各类编程助手的开发者,我发现真正发挥其潜力的关键在于掌握Output Styles(输出…

2026/10/26 0:20:20

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/27 9:34:54

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/27 0:59:21

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/27 0:02:28

WorkBuddy_新建任务与工作模式19_“模式选错”的典型代价

“模式选错”的典型代价 摘要 在软件工程中,设计模式、架构模式、并发模式乃至团队协作模式的选择,往往在项目早期就决定了后续的开发效率和维护成本。本文通过多个真实场景,剖析“模式选错”带来的典型代价——包括返工成本、技术债务累积、…

2026/10/27 0:02:28

WorkBuddy_新建任务与工作模式20_新手推荐路径

新手推荐路径 摘要 在 AI 辅助开发工具快速普及的今天,新手面对最多的困惑往往不是"工具够不够强",而是"我该从哪一步开始用"。本文提出一条经过实践验证的三阶段学习路径:先从 Ask(问答式交互)起…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑