Linux内核内存分配全链路:伙伴系统、SLUB与GFP实战排查

发布时间:2026/10/8 10:26:59
Linux内核内存分配全链路:伙伴系统、SLUB与GFP实战排查 做内核开发和底层排障的人几乎都经历过这种时刻业务代码反复 review 没问题可用内存却肉眼可见地往下掉系统在某个凌晨突然卡死dmesg 里只有孤零零一行page allocation failure: order:3, mode:0x1000c2(GFP_KERNEL)。没经验的人会对着这行字发懵有经验的人会立刻意识到这是内核最底层那套内存分配体系在向你求救。Linux 内核内存分配表面上就是 kmalloc、alloc_pages、vmalloc 这几个函数但背后是物理页管理、伙伴系统、SLUB 缓存、zone 水位、GFP 语义、内存回收和 OOM 判定一整条链路。任何一个环节出问题表现都是内存不够但根因可能完全不同可能是碎片化导致分配不到连续页可能是某个驱动长期泄漏也可能是 GFP 标志位在错误的上下文里用坏了。这篇文章就把这条链路从头到尾捋一遍不讲试卷上的空泛概念只讲排障和开发时真正要用的那套东西。适合正在啃内核源码的人、写内核模块和驱动的工程师也包括被线上 OOM 和内存抖动折磨的运维同学——这套知识同样是内核面试题的常客理解之后比背八股管用得多。1. 先搞清一件事内核态要内存和用户态完全不是一回事1.1 所有进程共享同一片内核地址空间普通用户进程之间虚拟地址空间是隔离的——进程 A 的地址 0x7f001000 和进程 B 的地址 0x7f001000 互不相干这是 MMU 加页表的功劳。但内核地址空间是完全不同的玩法它是一段所有进程共享的全局地址空间。每个进程切换进来看到的内核部分都是一样的。这意味着什么意味着内核里分配出来的虚拟地址天然就是全局的不存在仅属于某个进程的内核地址这种说法。理解这一点你才能理解为什么内核内存不能像用户态那样自由换页、回收和按进程记账。这也带来一个实用的排查视角当你怀疑某个内核模块泄漏内存时没法像用户态那样轻易按进程回收因为泄漏的内存挂在全局地址空间里除非模块主动释放或者卸载否则谁都拿不回来。很多线上内存缓慢增长的问题最后都定位到这种全局内存被某模块悄悄吃掉的路径上。1.2 物理内存不是拿来即用zone、直接映射与高端内存内核把物理内存按用途划分成若干 zone。以最常见的 x86_64 为例有 ZONE_DMA头 16MB老设备 DMA 要用、ZONE_DMA324GB 以下、ZONE_NORMAL4GB 到可用物理内存上限和 ZONE_MOVABLE专门给可迁移页使用的区域内存热插拔和碎片整理都靠它。32 位时代还有一个 ZONE_HIGHMEM因为 32 位内核地址空间总共只有 4GB物理内存超过这个数之后装不下只能动态建立映射性能差而且用起来麻烦64 位普及后它基本退出了历史舞台。这里最关键的机制是直接映射线性映射64 位内核在启动早期就把几乎全部物理内存线性映射到一个固定的内核虚拟地址范围phys 加个常量偏移就是 virt。page_address()能直接拿到物理页对应的虚拟地址靠的就是这套映射。所以内核里物理页和内核虚拟地址在正常路径下几乎是一一对应的而用户态 malloc 返回的地址背后经过两层以上页表映射物理页可能根本还没分配。下次有人问你为什么内核分配比用户态分配快直接映射就是答案的核心。1.3 为什么内核自己的内存不能换出用户态内存可以换页、可以按需分配缺页异常会去把页填上。内核自己用的内存一般不行——它运行的很多路径中断、自旋锁临界区、page fault 修复路径本身不能再触发复杂的内存管理行为否则会死锁或者无限递归。所以内核内存基本都是常驻的一旦分配出来就钉在物理内存里直到主动释放。内核栈、进程描述符、页表本身、内核模块代码全是不能换出的。这也是内核内存紧张时系统表现通常比用户态 OOM 更硬朗、更突然的原因。写驱动的人如果习惯了用户态 malloc 那套随便分配、不行就多要点的思维进了内核很容易翻车。内核分配内存是你主动向一个资源有限的全局池子里伸手不仅要说明要多少还要说明打算怎么用GFP 标志位优先级不同系统给的待遇完全不同。这一层认知是这个领域所有技巧的地基。2. 三层分配器各管一摊伙伴系统、SLUB 和 vmalloc2.1 伙伴系统物理内存的批发商物理内存管理的核心是伙伴系统buddy allocator。它管理的是页这个粒度——最小单位是一页通常 4KB并且按 2 的幂次把空闲页块组织成链表order 0 是单页order 1 是两页order 3 是 8 页32KB一直到 MAX_ORDER常见 11即最高 order 104MB为止。分配时要 8 页就从 order 3 的链表里拿如果 order 3 空了就从 order 4 拆成两个 order 3一个给你一个挂回链表。释放时则反过来先看相邻块是不是空闲如果是就合并成更大的块这就是伙伴合并。别小看这个设计它解决的是外部碎片问题的经典手段。但有代价它只能分配 2 的幂次大小的连续页你想要的 12 页它不会直接给你 12 页而是给你 16 页order 4浪费 4 页。更严重的是如果长时间运行系统里空闲页被分散在不同 order 上你要 8 页连续页order 3但所有内存都碎成单页order 0这时候就算 MemFree 显示还有几个 GBalloc_pages 照样失败——这就是经典的分配失败但内存充足场景。现代内核还为 order 0 的单页分配加了 per-CPU pageset每个 CPU 都维护一小批常用页单页分配只需要从本地链表拿不用碰全局锁这也是为什么单页分配能这么快。判断碎片化的第一手资料是/proc/buddyinfoNode 0, zone Normal 1262 1452 733 421 212 99 44 16 4 1 0从左到右是 order 0 到 order 10 的空闲块数量。如果低 order 数量巨大、高 order 数量很少甚至为 0说明内存正在碎片化而不是真正不够。2.2 SLUB为小对象准备的零售柜台如果所有分配都走伙伴系统那每分配几十字节就得弄一页浪费率接近 99%所以内核在伙伴系统之上又做了 slab 分配器。从 2.6.23 开始 x86 平台的默认实现是 SLUB它把内存组织成一个个 slab 缓存cache每个缓存的服务对象大小固定比如 kmalloc-64 就专门分配 64 字节的块。kmalloc 背后就是一组预置的通用缓存按幂次从小到大排列8、16、32、64……直到数 MB。你 kmalloc(100) 拿到的实际是 kmalloc-128 这个缓存里的一个对象多余空间不可避免这是所有 slab 类分配器的共性。SLUB 的关键设计是 per-CPU 空闲链表和 partial 链表当前 CPU 释放的对象会优先回到本地链表下次分配直接复用所以同一个高频分配/释放模式里对象大概率还在同一 CPU 上cache 命中率很高。还有个细节你写驱动时一定会遇到kmalloc 出来的内存地址是内核虚拟地址而且因为底层是连续物理页它天然满足大部分 DMA 要求前提是 zone 合适。但如果对象超过了一个页SLUB 底层会退化为从伙伴系统直接分配多个页kmalloc(20000) 底层拿的往往就是 order 3 的整页32KB slab。内核还会为某些高频对象开独立缓存task_struct、inode、dentry、skb 这些都有自己的 cache通过/proc/slabinfo能看到每个缓存的对象数量和空闲情况这也是排查慢速泄漏的重要窗口之一。2.3 vmalloc允许零散的第三种选择伙伴系统要求物理连续SLUB 又基于页。但有些场景真的很尴尬比如需要一块几 MB 的缓冲区又并不在意物理连续。这时候用 vmalloc它通过修改内核页表把若干不连续的物理页映射成一段连续的内核虚拟地址。代价是每次访问都要走多级页表而且 TLB 缓存效果差性能明显低于直接映射区所以高性能路径几乎不用它。vmalloc 适合的场景很典型内核模块加载模块代码放 vmalloc 区、某些大型驱动缓冲区、以及需要大块映射的调试接口。判断一个地址是不是 vmalloc 区看/proc/vmallocinfo就能查。很多初学者会犯一个错希望在 vmalloc 得到的地址上直接做 DMA结果 IOMMU 或设备不支持分散聚合直接崩。判断标准其实很简单——先问自己这块内存是给 CPU 频繁访问用的还是要给设备 DMA前者可以 vmalloc后者老老实实走 kmalloc 或 alloc_pages。2.4 分配 API 选型一张能直接抄的决策表把上面三层分配器落到具体 API 选择上我常用的判断逻辑是需求推荐 API底层机制注意点单个小对象几 KB 以内kmallocSLUB 缓存速度快有最小区块浪费需要多页但不做 DMAvmalloc页表动态映射性能低于连续映射别放热路径需要连续物理页交给 DMAalloc_pages dma_map伙伴系统大块分配有碎片失败风险设备驱动常用的 DMA 缓冲dma_alloc_coherent 系列CMA / 伙伴系统优先自动处理一致性映射需要大块连续内存的嵌入式多媒体CMA 预留 dma_alloc_coherentCMA配置 cma 内核参数选错 API 的结果通常不是立刻报错而是性能损耗或诡异崩溃这两样都比报错难查得多。我在实际工作中见过最典型的案例驱动工程师图省事把整个驱动的一处视频缓冲从 kmalloc 改成了 vmalloc功能一点都不受影响但 CPU 占用直接上涨了 40%因为每帧数据都在热路径上访问 vmalloc 地址TLB 疯狂失效。选型这件事真不是能跑就行这么简单。3. GFP 标志位每次分配请求都要写清楚的请求单3.1 核心标志位逐个拆解GFPGet Free Pages标志位本质是一份行为说明书告诉内存管理代码这次分配允许做什么、优先级多高、从哪里拿。最常用的三个标志位含义什么时候用GFP_KERNEL常规分配允许睡眠可以触发内存回收/文件系统 IO进程上下文大部分驱动路径GFP_ATOMIC原子分配不允许睡眠只能动用已有空闲和紧急储备中断、软中断、自旋锁临界区GFP_NOWAIT不等待不做直接回收拿不到就返回 NULL不希望分配路径阻塞的场合除了这三个宏观语义还有一组修饰性的__GFP_前缀标志__GFP_ZERO要求清零、__GFP_HIGH允许动用系统的紧急保留内存、__GFP_NOFAIL表示不许失败内核会死等代价很大别乱用、__GFP_RETRY_MAYFAIL和__GFP_NORETRY控制失败时尝试的力度。GFP_DMA / GFP_DMA32 则指定只能从对应 zone 分配老声卡驱动、USB 等场景会用到。3.2 上下文决定你能不能睡这是新手翻车重灾区在中断上下文或自旋锁保护的区域里绝对不能调用会睡眠的分配函数。为什么因为内核内存回收路径direct reclaim可能需要睡眠——要等 IO、要写回脏页、要等待锁这些动作在原子上下文里根本无法安全执行。你如果在一个 spin_lock 里写了kmalloc(..., GFP_KERNEL)运气好系统只是爆出scheduling while atomic警告然后 panic运气不好就是各种诡异锁死排查起来特别痛苦。反过来进程上下文里用 GFP_ATOMIC 虽然不会崩但会白白浪费大量内存——原子分配拿不到就失败而且不能用很多回收手段长期频繁原子分配等于在慢性耗尽系统的紧急储备。所以原则是能睡则睡不能睡必须显式表态。写驱动时一个很好的自查习惯就是往上数三层函数有没有持 spinlock、有没有在 hardirq/softirq 里有就 GFP_ATOMIC 或干脆提前分配。3.3 代码里最常见的三种分配模式贴三个实际常用的分配模式直接抄着改就行。/* 模式一进程上下文常规内核缓冲区 */ void *buf kmalloc(PAGE_SIZE * 4, GFP_KERNEL); if (!buf) return -ENOMEM; ... kfree(buf);/* 模式二中断/软中断里必须原子分配 */ /* 注意中断里别分配大块内存小对象 GFP_ATOMIC 才是正路 */ struct my_event *ev kmem_cache_alloc(my_event_cache, GFP_ATOMIC); if (!ev) { /* 记录失败计数交给 kworker 后续处理别硬撑 */ atomic_inc(my_drv-alloc_fail); return -ENOMEM; }/* 模式三需要物理连续且可能较大的 DMA 缓冲区 */ /* 在支持 CMA 的平台用 dma_alloc_coherent 系列更省心 */ struct page *page alloc_pages(GFP_KERNEL | __GFP_ZERO, get_order(64 * 1024)); if (!page) return -ENOMEM; void *addr page_address(page); ... __free_pages(page, get_order(64 * 1024));第三个模式里get_order()是标准宏计算 2 的幂次 order别自己拿 log2 手算边界情况会出错。3.4 还有两个容易被忽略的防递归标志GFP_NOFS 和 GFP_NOIO 很少被人提起但在文件系统和块设备层几乎是保命级的。你在持有某个文件系统内部锁时分配内存如果内核为了凑内存去触发回收回收路径又去操作同一个文件系统就会死锁——这是经典的 reclaim 递归死锁。GFP_NOFS 告诉回收机制别碰文件系统GFP_NOIO 更严格连块设备 IO 都别碰。XFS、ext4 内部大量使用这两个标志。我在排查一个 ext4 卡死问题时最后就是发现某个新加的内核模块在 FS 上下文中用了 GFP_KERNEL导致回收路径和事务提交抢同一把锁。值得记住只要你的代码可能在文件系统或 IO 栈里执行给分配加重一点约束性能损失很小但能避免一次通宵排障。4. 分配失败时别急着骂内核一条完整的排查链路4.1 第一步分清是真缺内存还是碎片化先看/proc/meminfo重点不是 MemFree而是 MemAvailable——这是预估还能安全分配多少内存的指标Cached 里的页大多可以被回收所以 MemFree 很低但 MemAvailable 很高是正常现象。然后立刻看/proc/buddyinfo和/proc/pagetypeinfo。如果低 order 空闲块堆积、目标 order 的块为 0且 MemAvailable 不低那基本可以断定是碎片化问题。碎片化的处置手段分两步。第一是直接触发压缩echo 1 /proc/sys/vm/compact_memory内核会尝试用可迁移页把零散页挤到一起有时当场就能让高 order 分配恢复。第二是长期防控确认 CONFIG_COMPACTION 是否打开发行版一般默认开某些嵌入式内核会把它裁掉那高 order 分配失败会变得更频繁。另外可以观察碎片是不是被某个进程长期持有的不可迁移页卡住的——ZONE_MOVABLE 就是为这个问题设计的启用合适的内存迁移策略后情况会明显改善。4.2 第二步用 tracepoint 精准抓到每次 alloc 的调用现场如果统计分析定位不到问题就得靠内核事件跟踪。ftrace 的 kmem 事件可以直接记录每个分配请求的调用栈我最常用的组合cd /sys/kernel/debug/tracing # 开启 kmalloc 分配事件同时记录 stack trace echo kmalloc set_event echo 1 options/stacktrace echo 1 tracing_on # 跑一段时间或者复现问题后关闭 echo 0 tracing_on cat trace /tmp/alloc_trace.txt如果某个调用路径疯狂分配trace 里会出现大量重复栈一搜就能看到是哪个模块哪个函数在刷。如果怀疑大块连续页分配失败可以把事件换成 mm_page_alloc并过滤 order 字段echo mm_page_alloc set_event echo order 2 events/kmem/mm_page_alloc/filterfilter 语法在 ftrace 里支持简单比较。我建议写驱动调驱动前先试一晚上 trace很多奇怪的内存问题其实都是某个路径在疯调分配函数导致的看到 stack trace 一切都清楚了。4.3 第三步泄漏的定位靠 kmemleak 和 page owner如果分配量不大但持续增长那更多是泄漏。slab 层面的泄漏内核提供了 kmemleak内核配置要打开 CONFIG_DEBUG_KMEMLEAK然后mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/kmemleakkmemleak 会周期性扫描内核分配的内存找出没有被引用的分配把它对应的调用栈打印出来——虽然偶尔误报但对定位驱动泄漏非常好用。物理页层面的泄漏alloc_pages 拿了不还用 page owner 更靠谱。需要 CONFIG_PAGE_OWNER 编译进内核启动参数加page_owneron之后用cat /sys/kernel/debug/page_owner /tmp/page_owner.txt它会列出每个物理页的分配调用栈和状态统计哪个栈分配了最多未释放页一目了然。这个功能性能开销不小生产环境可以关掉排查期专门开一轮。4.4 OOM 之后从系统的最后记忆里找线索真到了 OOM内核会调用 OOM Killer。它按 oom_score 选进程杀掉评分逻辑主要看进程的 RSS、页表等占用还有 oom_score_adjsystemd 的重要进程会调低分数。dmesg 里的 OOM 信息会打印各进程的占用排行Out of memory: Killed process 3526 (java) total-vm:8388608kB, anon-rss:4194304kB, file-rss:0kB看到这行别急着骂 OOM Killer 杀错了关注两个东西第一是 MemInfo 里 Slab 和 PageTables 有没有异常增长第二是触发 OOM 的分配请求 order 是多少。如果是高 order 分配触发、MemFree 还很大那多半又是碎片化或者 CMA 区域被占满杀进程只是治标不治本。此时正确操作是查/proc/pagetypeinfo的 Unmovable 页有没有堆积以及 CMA 区域谁占用了cat /proc/pagetypeinfo ls /sys/kernel/debug/cma/ cat /sys/kernel/debug/cma/区域名/usage有一种情况我碰到过两次CMA 区域被驱动长期占用不释放多媒体设备想用大块连续内存时分配失败系统误判为 OOM。这时候杀掉业务进程并不会让 CMA 内存回来必须找到那个霸占 CMA 的驱动。所以定位问题永远要从谁在用内存出发而不是从系统杀了谁出发。5. 调参与经验让内核内存分配更可靠5.1 值得动的几个 sysctl 参数先声明内核内存参数不是调得越多越好多数情况下默认值已经是全局最优。真正值得日常关注的就三四个参数作用我的建议vm.min_free_kbytes保证每个 zone 至少保留多少空闲页供原子分配和紧急恢复物理内存 4GB 左右设 2~4MB 足够别设太大否则白白浪费可回收内存vm.watermark_scale_factor控制回收水位线与 min 的倍数关系默认 10高压力场景调大一点比如 50提前回收减少直接回收抖动vm.vfs_cache_pressure控制 inode/dentry 缓存回收倾向默认 100只读为主的服务器可以调低到 50减少不必要的元数据缓存抖动vm.zone_reclaim_mode控制 NUMA 节点内回收行为默认 0多路服务器遇上内存分配偏斜时值得研究别盲目开 1min_free_kbytes 是最容易踩坑的很多教程让人把它调到 1% 物理内存结果内存明明很多系统却频繁进入 direct reclaimCPU 飙高。它的本质不是缓冲大小而是水位基线系统在低于水位时会提前回收。调得过高等于把大量页锁在可回收但不给用的状态。除了 sysctl针对 slab 还有个非常实用的开发期参数slub_debug。如果怀疑 slab 越界、重复释放等问题可以在内核启动参数里加slub_debugFZPUF 表示开启 sanity checks、Z 表示在对象前后加 redzone 检测越界、P 表示填充特定字节以便发现未初始化或已释放后使用、U 表示记录分配/释放调用栈。生产环境千万别开性能影响很大但开发测试阶段开着很多坑在复现阶段就能炸出来而不是等客户环境半夜宕机。5.2 嵌入式场景下的特殊考虑我在嵌入式 Linux 上踩过的坑比服务器更多。嵌入式内核经常被裁剪CONFIG_COMPACTION 被关掉、CMA region 大小设置不合理、内存总共就 256MB一个驱动错用 GFP_KERNEL 分配几 MB就能让整个系统的内存状况变得极其脆弱。嵌入式项目如果要用大量连续物理内存ISP、GPU、音视频编解码器都干这事我的建议是第一确认 CONFIG_CMA 和 DMA_CMA 是打开的通过cma内核参数预留足够 CMA 区域。第二驱动里别直接alloc_pages(GFP_KERNEL, get_order(8MB))改用dma_alloc_coherent系列函数它能自动从 CMA 或通用页里选路。第三把 CMA 使用情况监控放进日常工作——CMA 被占满的表现非常隐蔽不是 OOM而是某次分配突然失败业务侧看起来像偶发性故障。5.3 我实际踩过的三个坑希望你别再踩第一个坑在一个持有自旋锁的路径里用了 GFP_KERNEL。症状不是立刻崩溃而是某些场景下随机的 scheduling while atomic复现率极低。最后是靠打开 CONFIG_DEBUG_ATOMIC_SLEEP 并在测试机上跑了一整晚 stress 才暴露的。从那以后我写驱动有个习惯每个要分配内存的函数第一行注释就写明本函数可能被调用的上下文真出问题先看注释对不对。第二个坑把 vmalloc 的地址当成普通内核地址在热路径上高频访问。驱动里申请了一块 vmalloc 内存却在每帧数据处理时反复读写性能直接掉了 40%因为 TLB 命中率太差。后来改成在驱动 init 阶段一次性用 kmalloc 分配性能恢复正常。内核路径上快和省vmalloc 只能保一个。第三个坑调试内存泄漏时前半天只盯着 meminfo 和 page_owner漏看了/proc/slabinfo。当时某个内核模块反复创建设备节点和 skbslab 里的活性对象数量暴涨最后是通过/proc/slabinfo第三列 active_objs 的异常增长才锁定泄漏模块的。很多人一查内存问题就只看 MemFree这是不对的slab 里的增长比物理页泄漏更常见先扫一眼 slabinfo 能省一半排查时间。最后分享一个让我少接不少半夜电话的小技巧把 dmesg 里的page allocation failure关键字专门收集起来做告警。这类信息有个规律——第一次出现往往只是预兆真正宕机是很多次失败叠加之后才发生的。早一步看到早一步去查是谁在刷分配失败比等到 OOM 之后再抢救要从容得多。内核内存分配这套东西说复杂确实复杂但只要你把上下文、GFP、分配器选型这条线想清楚大多数问题都能在第一次报警时就掐灭在源头。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询