首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
Linux内核内存分配实战:伙伴系统、kmalloc与GFP标志位
📅 2026/10/10 16:12:56
✍️ 爱科研究院
👁 阅读 3,247
聊到linux内核内存分配很多第一次写内核模块的人会觉得不过是从系统里“借一块地址”真正踩过坑之后才发现这块地址背后的门道远比想象中多。你可能遇到过这样的现场明明模块逻辑没问题加载时却因为一个 kmalloc 直接 panic或者业务高峰期内核日志刷出一屏 page allocation failurefree 还剩十几个 G偏偏就是分不出连续页。这组内容既是给刚开始接触内核驱动开发的同学画一张内存分配的整体地图也是给正在线上排查分配失败问题的人一份对照清单。我会从物理页、伙伴系统、slab/slub 这一整套底层路径讲起再对比 kmalloc、vmalloc、kzalloc 这些常用接口的取舍说清楚 GFP 标志位背后的真实含义最后分享几份我平时排查问题必看的台账和踩坑记录。内容偏实践经验尽量把每一个选择背后的原因讲明白不搞云里雾里的概念堆砌。1. 先绕一遍内存分配的顶层路径从物理页到虚拟地址1.1 节点、区域和伙伴系统底层那张地图现代内核把物理内存按架构拆成节点node每个节点再按用途拆成多个区域zone。64 位系统里最常见的是 DMA、DMA32、Normal部分平台还会配置 Movable 区域。每个 zone 内部由伙伴系统管理页块这是最底层的分配器它的最小单位是一个物理页更大的内存块按 2 的幂次组织成 order。order 表示一次拿到连续页的规模order0 是单页order1 是两页order3 是 8 页以此类推。之所以要把内存拆成 zone 再按 order 管理是因为不同设备对物理内存地址范围有硬性要求。比如传统 DMA 设备只能访问低位地址如果伙伴系统不维护“哪个 zone 有哪些可用页”驱动根本无法判断要访问的内存是否合法。页块的 order 设计则是为了平衡分配速度和碎片控制找指定 order 的空闲块可以直接查链表不必遍历内存块也可以按需拆分或合并尽量避免小块碎片。看到这里读者应该先建立“到底在对谁说话”的认知普通驱动调接口其实是在和某个 zone 的伙伴系统打交道。当我们说内存不够时通常不是总内存不够而是某个节点、某个 zone、某个 order 上不够。这个意识非常重要因为它决定了后续所有排查的方向。1.2 一次 kmalloc 调用在底层做了什么看似简单的 kmalloc 并不是直接跑到伙伴系统去拿页。绝大多数小对象会走 slab 或 slub 分配器slab 先一次性向伙伴系统申请一批页块再把页块切成多个固定大小的对象放在缓存里随取随用。这样做的核心收益有两个一是避免每个小对象都触发伙伴系统级别的页操作分配速度大大提升二是对象大小固定释放后能马上复用缓存局部性也好得多。当一个对象大小超过 slab 的常用尺寸类kmalloc 内部可能直接向伙伴系统请求更高 order 的页块。所以即便你调用 kmalloc(32 * 1024, GFP_KERNEL)它也有可能真的去拿 order3 的连续页这时候能否成功就不只看系统总空闲内存还得看运行的那个 zone 里是否存在足够多连续的空闲页。这里要特别提醒一点slab/slub 的缓存在不同 CPU 上还有 per-cpu 队列分配时会先查本 CPU 的 freelist命中就直接返回避免跨 CPU 锁竞争。这也是为什么内核里有些对象要强调 per-cpu 设计的原因——内存分配本身在热路径上是有开销的减少竞争比单纯多申请点内存更值钱。新手容易忽略这层以为分配函数只是个“取号机”其实它内部做了大量层析缓存。1.3 为什么会“明明还有内存却分配失败”最常见的困惑是free 命令显示还剩十几个 G可内核就是报分配失败。原因在于 free 看到的是系统范围内的空闲统计而内核实际要请求的是某个 zone 里、大于等于某 order 的连续页块。如果这些空闲页分散在千千万万个 order0 的单页里就无法满足 order3 的连续块请求。这就是碎片化。伙伴系统还会在水位上做文章。每个 zone 都维护着 min、low、high 三档水位线当空闲页低于 low 时后台的 kswapd 会被唤醒去回收页面低于 min 时除非请求本身带有紧急标志否则普通分配会被拒之门外。水位线预留的内存是为了应付原子分配和系统关键路径不能把系统内存用到一页不剩。所以分配失败的判定条件比多数人想象得要严不是“没有可用页”而是“没有可用的、符合 order 要求的连续页且当前水位不支持紧急回收”。2. kmalloc / vmalloc / kzalloc 的选择逻辑先看连续性和大小2.1 kmalloc 默认够用但别把它当无限仓库在绝大多数内核代码里kmalloc 就是默认选择。它返回的虚拟地址位于线性映射区虚拟地址连续对应的物理内存也连续cache 友好释放也简单。很多新手以为 kmalloc 只能分配一点小内存其实通过 slub 尺寸类单次分配也能覆盖不少中间量级。但我不建议把它当无限仓库原因有两个一是它返回物理连续内存对大块请求来说碎片化风险和分配失败概率会明显上升二是它要维护额外数据结构频繁分配释放对性能不友好。如果对象需要一个固定大小的结构体kzalloc kmalloc 清零往往更合适尤其适合那些不希望残留敏感数据的内核对象。清零不是“风格好看”而是避免使用未初始化内存带来的不确定性——内核代码里出现“无法复现的乱值”很多都源于某处没有初始化就直接读。写驱动的经验告诉我能用 kzalloc 的地方尽量不要裸用 kmalloc省的那点清零时间通常会在排查时加倍还回来。2.2 vmalloc 接下“大块且不追求性能”的活vmalloc 和 kmalloc 最大的区别是它只保证虚拟地址连续不保证物理连续。内核会为它单独建立 vm_area 并做页表映射每次分配都要更新内核页表因此速度比 kmalloc 慢不少。它适合分配较大的缓冲区或者数量大、不频繁操作的内存例如某些模块把几 MB 的配置表整体拉起来。做出选择时要看三个问题第一这块内存要不要做 DMADMA 基本必须物理连续那就得用 dma_alloc_coherent 之类的接口而不是 vmalloc。第二访问频率高不高如果是热路径上的频繁读写vmalloc 的页表开销和可能的 TLB 抖动会让性能打折。第三单次大小有多大几十 KB 以内通常 kmalloc 也能接没必要一上来就 vmalloc。注意释放也有对应规则。kmalloc/kzalloc 对应 kfreevmalloc 对应 vfree。用混了轻则无法释放麻烦的是某些路径上用了 kvfree 之类的兼容释放函数它内部会判断地址到底属于哪种分配。这属于特殊情况常规代码里还是老老实实配对别偷懒。2.3 生命周期跟着设备走devm_ 系列写 platform_driver 或设备驱动时很多分配的生命周期其实和设备绑定。设备 probe 成功分配remove 时释放中间的 error path 一多漏释放的概率不低。devm_kzalloc 这类 managed 资源接口会把内存注册进设备的资源列表设备一旦注销框架会自动释放省掉大量手工配对代码也让 error path 简单很多。但也要注意用了 devm_ 以后不要再去手工 kfree否则可能导致双重释放。把自动释放当成兜底而不是随意双保险。如果你的模块释放时机和设备生命周期不完全一致就别硬套 devm_老老实实手工管理更省心。总而言之选接口不是看谁名字长而是看这个对象的生命周期归谁管以及它是否需要物理连续、是否能接受慢分配。2.4 一张接口选型对照表接口物理连续性典型用途释放函数主要约束kmalloc / kzalloc连续小对象、结构体、缓冲区kfree大块时碎片风险高vmalloc不连续大数据块、低频访问vfree速度慢不支持 DMAdevm_kzalloc连续跟随设备生命周期自动释放不要手动释放alloc_pages连续直接拿页块free_pages / __free_pages需要自己管理高阶页dma_alloc_coherent连续DMA 缓冲区dma_free_coherent受设备 DMA mask 约束3. GFP 标志位就是应急等级决定你能否睡眠3.1 GFP_KERNEL 的睡眠代价GFP 是 get free page 的缩写但这串标志位真正决定的是“当前这个分配行为有多大的操作权限”。GFP_KERNEL 允许睡眠、允许触发页面回收、允许做文件系统写回操作这也是普通进程上下文里最常用的模式。它看起来很顺手代价是一旦内存紧张调用者可能长时间睡在分配路径里等 kswapd 回收完内存再继续。如果你所在的代码路径不允许睡眠比如持锁、中断、底半部、原子上下文那 GFP_KERNEL 就是在自掘坟墓。很多 panic 日志里的 sleeping function called from invalid context 就是这么来的。分配函数尝试调用可能睡眠的回收例程内核检查上下文后发现当前处于原子状态直接打报告。这不是内存真的不够而是你用错了应急等级。3.2 什么时候必须换成 GFP_ATOMIC中断处理、软中断、spinlock 临界区、RCU 读侧临界区这类路径分配不能睡眠。这时应该用 GFP_ATOMIC。GFP_ATOMIC 不会调用可能睡眠的回收路径会优先尝试使用系统为紧急情况预留的少量内存因此成功率比普通路径低但至少能保证调用者不会因为睡眠检查而 panic。这里有个实际心得在原子环境下不要把某个大块的分配硬塞进去能提前准备就提前准备。比如网络驱动收包路径里的 skb很多做法是在预先分配好的缓存池里取而不是每次都在软中断里去调 GFP_ATOMIC因为原子分配失败率实在不低热路径上反复失败会影响业务吞吐。如果你不得不这么做至少要把失败分支写清楚别拿 BUG_ON(!ptr) 这种粗暴方式对待一个本来就可能失败的调用。3.3 其他常被误用的标志除了最常用的两类还有几个标志位值得了解。__GFP_ZERO 表示分配后清零kzalloc 就是封装了它__GFP_HIGH 表示这是高优先级请求可以动用紧急保留内存__GFP_NOFAIL 则是“不许失败”的决死请求分配器会反复重试直到成功看上去很美好但它可能长时间霸占 CPU 并让整个系统卡顿滥用的话比内存不足本身更危险。另外一个容易被忽略的概念是加在标志位里的行为要求比如是否参与回收、是否允许写 IO。这些都需要对照当前请求的上下文来判断。经验法则所有分配前先把代码路径的上下文写清楚——可不可以睡眠、持有什么锁、有没有原子性要求——再倒推该用什么 GFP。我见过不少驱动把 GFP_KERNEL 当成固定默认值到处用不出事纯属运气好。4. 内存不足的真实信号碎片化、水位线和 OOM 日志4.1 order 请求和连续页碎片的关系回到最核心的分配单位。伙伴系统里每个 zone 都维护着一张按 order 划分的空闲链表。想要分配一个 order3 的块系统会先查 order-3 链表如果没有就向上找更大的块拆出来给你。反过来释放时如果相邻块也空闲会尝试合并回去。这套机制保证分配速度很快但也意味着如果内存里到处是无人认领的 order0 单页系统就凑不出一个高阶连续块。碎片化在实际运维里非常常见。最典型的是业务模块反复分配、释放大小不一的缓冲区时间一长每个 zone 里高阶块越来越少最终表现为“明明 free 还有十几个 G但 kmalloc(32K) 就是失败”。给大家一个直观比喻一整块豆腐被切成一万个小方块之后你想再拿一块完整的豆腐不是因为有豆腐就行而是得先有人把碎块重新拼起来。内存规整compaction干的其实就是重新拼豆腐的活把可移动页搬走腾出连续空间。4.2 怎么从 dmesg 里读出诊断信息当一次页面分配失败时内核不会沉默它会往内核日志里打印一屏信息。开头通常包含 page allocation failure: order:3, mode:0x... (GFP_KERNEL) 之类的字样。看到这里先不要急着慌只需要抓住三样东西请求的 order 是多少、请求的 GFP 模式是什么、哪个调用点发出的。紧跟其后的 Mem-Info 段会把每个 zone 的水位、free 页数、活动非活动页、页表页数都列出来同时给出 buddyinfo 形态的空闲页块分布。这块信息很多但最值得看的还是对应 zone 的 free 页块分布——如果低 order 列很满、高 order 列全 0那基本可以坐实碎片化如果连低 order 都不足那才是真正的整体内存紧张。日志末尾的调用栈能帮你定位到是哪一行代码触发了分配这一步在做内核开发时等同于案发现场。4.3 一次高并发场景的分配失败复盘我以前调试过一个包处理模块业务高峰期日志里开始零星出现 order3 分配失败系统层面空闲内存却还剩十几个 G。一开始怀疑是水位线调太低后来用 buddyinfo 看现场Normal zone 里 order0、order1 的块非常多order3 只剩两个order4 以上全空。这完全不是缺内存而是长时间高并发的 8K/32K 缓冲区分配释放把连续内存打碎成了单页块。当时做了三件事第一把热路上那种“每次现分配 32KB 再释放”的写法改成预分配对象池从源头上减少对高阶连续页的依赖第二临时echo 1 /proc/sys/vm/compact_memory触发一次内存规整缓解燃眉之急并观察 buddyinfo 恢复情况第三把需要 8 页连续空间的大对象换成更小的分片设计。做完后分配失败日志彻底消失。这个案例最想说明的一点是内存分配问题往往不是加内存能解决的碎片化治理和分配策略调整才是久药。5. 三份必读台账buddyinfo、slabinfo 与 kmemleak5.1 朗读 /proc/buddyinfo 的碎片化信号/proc/buddyinfo 每一行是一个 zone后面的数字从左到右分别代表 order0 到 order10 的空闲块数量。想判断碎片程度不需要看全部列重点看两个信息低 order 列是不是爆满以及高 order 列是不是长期为 0。比如这样一串Node 0, zone Normal 2048 1024 512 0 0 0 0 0 0 0 0order3 是 0说明所有空闲页都碎成了小块此时任何 order3 的分配都可能失败。如果还伴随业务日志里的分配失败报告基本可以确认碎片是主因。反过来如果某一天 order3 到 order5 从 0 涨回来了说明内存规整或自然合并在起作用。实际习惯我一般在持续压测或上线前会抓一份 buddyinfo 存档等到出现分配失败时再对比一次。两次之间“高 order 列从有到无”的变化比单看一次快照更能说明问题。5.2 从 /proc/slabinfo 捕捉对象泄漏/proc/slabinfo 列出所有 slab/slub 缓存字段包含对象大小、已用对象、空闲对象、slab 总数等。排查内存泄漏时找一个和自己业务相关的 kmem_cache 条目看 active 对象数量是不是持续上涨。如果一个对象分配后显然会被释放但它的 active_objs 只涨不降那大概率是 release 路径漏了 free。常见误区是只看 RSS 或 free但内核对象泄漏往往表现为某个 kmem_cache 的大小在 /proc/meminfo 的 Slab 字段里异常增长而不是用户态 RSS 变化。所以遇到内核模块疑似泄漏先开 slabtop 按对象量排序再盯着自己的缓存类型看趋势往往比猜代码更快。5.3 用 kmemleak 抓“忘了释放”的现场kmemleak 是内核自带的内存泄漏检测器核心思路是持续扫描内存中的指针引用发现没有被任何地方引用的已分配对象就报告为潜在泄漏。使用它比较简单内核开启 CONFIG_DEBUG_KMEMLEAK挂载 debugfs 后通过几个命令控制扫描并在报告里读出泄漏点。典型流程是先清空旧报告再重复触发几次可能有问题的路径然后手动 scan。如果报告里出现 unreferenced object 且调用栈指向你的模块代码那就是漏释放现场。有几个细节要注意kmemleak 的扫描有开销别在业务高峰期一直开着它可能误报因为某些对象是通过物理地址或间接结构引用的不会被普通指针遍历识别所以报告结果必须结合代码分析不能无脑信。5.4 一个基于代码的泄漏定位流程如果不想在正式环境开 kmemleak也可以用最笨但可靠的办法给模块加上一套分配计数。在分配入口用 atomic_inc 计数释放路径 atomic_dec卸载模块时打印计数器的最终值。反复加载卸载并观察计数是否归零能很快暴露在哪条路径上漏了配对。这个办法不依赖任何专用调试器普通内核模块就能做也适合作为长期回归检查的一部分。static atomic_t mybuf_alloc_cnt ATOMIC_INIT(0); void *mybuf_alloc(size_t size) { void *ptr kmalloc(size, GFP_KERNEL); if (ptr) atomic_inc(mybuf_alloc_cnt); return ptr; } void mybuf_free(void *ptr) { if (ptr) { kfree(ptr); atomic_dec(mybuf_alloc_cnt); } }真正排查泄漏时我习惯把三份台账合起来看先看 /proc/meminfo 里的 Slab 字段有没有异常上涨再看 /proc/slabinfo 定位到具体缓存最后用 kmemleak 或计数方法拿到调用栈。完整链路走一遍基本能把 90% 的内核对象泄漏问题钉死。6. 我用内核内存分配时踩过的坑与保留习惯6.1 中断上下文里用 GFP_KERNEL一碰就 panic我有一次在软中断收包路径里图省事直接抄了 GFP_KERNEL。结果压测一到中高负载日志就刷 BUG: sleeping function called from invalid context堆栈指向 slub 分配器。排查的时候发现第一反应不是内存不够而是上下文不匹配。把调用里的标志位改成 GFP_ATOMIC并把热路径上的分配改成预分配池之后问题消失。这个教训告诉我每个分配接口在调用前先把当前的上下文和锁条件写在注释里比任何技巧都管用。6.2 小需求也去申请 order2 大页纯粹给伙伴系统添乱另一个让我印象很深的坑是某次优化为了“方便”把一个本来只需要一个页的数据缓冲改成了 alloc_pages(order2)想着“多给点没关系”。实际上连续页是非常宝贵的资源频繁的小块高阶分配会加速碎片化还会让其他需要连续内存的模块跟着遭殃。正确做法是需求多大就申请多大如果业务确实需要更大的连续内存应该一次性做资源规划而不是在每次请求里碰运气。写内存分配代码时“克制”是很重要的素质。6.3 分配释放不配对泄漏往往藏得很深用过 devm_ 系列后有人会在 probe 失败路径里顺手再 kfree 一次结果导致二次释放也有人把 kmalloc 得到的内存用 vfree 去释放行为完全不可预期。还有一种情况是分配函数用的是 kmem_cache_alloc释放时却图省事写 kfreeslab 元数据直接乱套。我的做法很简单任何一种手动分配函数入口处就在注释里标清“由谁在什么时机释放”并规定释放接口必须配对。对于生命周期和设备绑定的资源放心交给 devm_不要再二次释放。这套纪律短期看是牺牲了一点灵活长期看能省掉大量低级 crash。6.4 我长期保留的检查习惯最后分享几个我一直在用的检查习惯一每次写新的内核分配点之前先回答三个问题——这里能不能睡眠、有没有持锁、内存要求多大二上线前的压测阶段每隔一段时间抓一次 buddyinfo 和 slabinfo积累基线数据三改完释放相关代码后用 kmemleak 做一轮扫描再配合分配计数验证四对确实需要频繁分配的模块优先做预分配池或内存池而不是反复向 slub 要对象。这几个习惯不一定立竿见影但能避免大部分内存分配层面的地雷。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/10 16:12:56
几十个自媒体账号怎么集中管理不用反复登录|矩阵账号统一管控方案
2026/10/10 16:07:55
真实驾驶场景疲劳行为识别数据集:COCO格式7类状态标注
2026/10/10 16:07:55
给PL/0增加%和^:运算符优先级与结合性的完整实现指南
2026/10/10 16:58:08
鸿蒙化适配实战:weather_pack迁移与MethodChannel桥接全解析
2026/10/10 16:58:08
Flutter鸿蒙化适配:binary_tree二叉树搜索优化实践
2026/10/10 16:58:08
NodeJS+微信小程序智慧城市项目实战:从环境搭建到接口联调全流程
2026/10/10 16:58:08
C盘爆满怎么办?不碰分区表也能腾出几十G的清理与扩容指南
2026/10/10 16:58:08
Flutter适配OpenHarmony实战:FAQ模块跨端迁移与踩坑全记录
2026/10/10 16:53:05
华为 Mate 90 端侧跑 30B 大模型:手机 AI 开始不用联网了
2026/10/10 0:03:38
工业软件标准化路线图:国产替代的落地施工图
2026/10/10 0:03:38
VCMI安卓版实操指南:原生运行英雄无敌3的3步技术落地
2026/10/10 0:03:38
稀疏多通道盲反褶积的MATLAB算法实现与参数调优
2026/10/10 3:42:06
Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化
2026/10/10 3:42:01
多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系
2026/10/10 3:41:58
hindsight:面向LLM应用的事后可观测性工程实践
2026/10/10 3:41:56
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/10 3:41:54
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/9 11:36:17
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)