news 2026/10/8 11:13:48

Linux内核内存分配机制全解析:伙伴系统、slab与vmalloc实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内核内存分配机制全解析:伙伴系统、slab与vmalloc实践指南

1. 先搞清楚内核内存分配到底在解决什么问题

做内核开发和嵌入式Linux的老哥,应该都有过这样的经历:用户态程序内存不够了,malloc一个NULL回来,你能清晰感受到问题出在哪。但内核不一样,内存分配失败的后果往往不是返回个NULL就完了——可能是内核panic、宕机,或者整个系统完全卡死。所以在Linux内核里,内存分配从来不是"有就分,没有就报错"这么简单,而是一套有多层缓存、多种分配策略、大量优化手段的复杂体系。

这篇文章主要面向三类读者:一是正在啃内核源码的初学者,想弄明白kmalloc、vmalloc、slab这些概念到底怎么回事;二是做嵌入式Linux或驱动开发的工程师,经常要跟内存分配打交道,但又说不清底层机制;三是做底层性能优化的运维,遇到内存问题只知道看free命令,却不知道内核内部发生了什么。文章会从整体设计思路讲起,把物理页分配、slab缓存、vmalloc这些核心机制拆开揉碎,最后再结合我实际排查过的内存问题说一些经验教训。

内核的内存分配之所以难学,跟它的设计目标有直接关系。用户态的内存分配可以只考虑"够不够用",但内核必须同时照顾到性能、碎片控制、DMA和设备访问需求、NUMA拓扑的局部性等等。举个例子,你在驱动里分配一个DMA缓冲区,如果随便用kmalloc去分,得到的物理内存可能因为页表映射关系没法直接给设备用;你在高优先级中断上下文里想分内存,如果代码里去睡眠等待内存回收,那整个系统就死给你看。这些约束条件叠加起来,才有了今天我们看到的这套复杂的分配体系。

2. 内核内存分配的分层架构与设计思路

2.1 三层结构:伙伴系统、slab缓存、vmalloc区域

内核的内存分配从宏观上看是分层的。最底层是伙伴系统(Buddy System),它管理的是物理页帧(Page Frame),以2的幂次方为单位分配连续的物理页。往上走,slab缓存层基于伙伴系统分配来的大块内存做细粒度管理,专门服务那些频繁创建和销毁的内核对象,比如task_struct、inode、dentry这些。最上层是vmalloc区域,它分配的是地址空间连续但物理页不连续的内存,适用于那些需要大块内存但对物理连续性不敏感的场景。

这三层各自解决不同层面的问题。伙伴系统解决的是物理页管理的根本问题——如何快速找到合适的连续物理页;slab层解决的是"对象重复创建导致的开销"问题;vmalloc层解决的是"物理碎片化导致大块内存分配失败"的问题。理解这个层级关系,你就明白为什么内核里会有这么多不同的内存分配API,它们并不是冗余,而是各有各的适用场景。

2.2 为什么需要NUMA感知和内存策略

现在服务器动辄几十个核、几百GB内存,硬件上基本都是NUMA架构——不同CPU访问不同内存节点的速度不一样。如果内核分配内存时不考虑这个,就可能出现CPU0频繁访问CPU1节点上的内存,性能损耗非常明显。

为了处理NUMA问题,伙伴系统在设计上就考虑了节点(node)和区域(zone)的概念。每个内存节点维护自己的伙伴系统,分配内存时可以通过GFP_THISNODE等标志强制从当前节点分配。内核还提供了numa_node_id()、cpumask等机制来辅助决策。写驱动时如果我们不做特殊处理,内核会默认优先在"当前CPU所在节点"分配内存,这也是很多性能敏感型驱动在初始化时要绑定CPU并分配内存的原因。

2.3 水线和回收机制:内存不是用完了才回收

内核设计了一套水位线(watermark)机制来提前干预内存压力。每个内存区域都有WMARK_MIN、WMARK_LOW、WMARK_HIGH三个水位线,当空闲内存低于WMARK_LOW时,kswapd内核线程会被唤醒开始回收内存;如果内存继续下降触碰到WMARK_MIN,则正常的内存分配路径会进入直接回收(direct reclaim),也就是同步等待内存被回收释放。

这套机制的巧妙之处在于它把"内存快用完"的预警做成了异步和同步两级响应,而不是等内存彻底耗尽了才手忙脚乱。我见过很多内核崩溃的现场,其实早在几分钟前系统就已经进入频繁的直接回收状态了,只是没有监控到。了解这个水位线机制,不仅对读源码有帮助,对排查"系统响应突然变慢"这类问题也很有用。

3. 核心分配机制详解:从伙伴系统到slab

3.1 伙伴系统的order机制与页块分配

伙伴系统把物理内存划分为大小不同的块,每个块包含2的order次方个连续物理页。order=0是一页(通常4KB),order=1是两页(8KB),以此类推,最大order取决于系统配置。分配时,系统会先找恰好能满足需求的最小order块,如果没有,就把大块逐级拆半,直到拆出合适大小的块为止;释放时则检查相邻的"伙伴"块是否也是空闲的,如果是就合并成更大的块。

这里有个很容易踩坑的点:连续物理页并不等于连续虚拟地址。如果你用__get_free_pages这类接口拿到了物理连续的页,但在内核虚拟地址空间里,它们是通过线性映射访问的,地址是连续的(在典型的x86_64和ARM64配置下,内核线性映射区域是恒等映射加上一个偏移)。但如果你在写驱动时用vmalloc,得到的虚拟地址连续、物理地址不连续,这时候如果要给DMA设备用,就可能出问题。我见过有人拿vmalloc的内存直接填DMA描述符,结果设备读到一半数据出错,排查了整整两天。

伙伴系统的实现核心是alloc_pages和free_pages两个路径,前者最终会走到__alloc_pages_nodemask,后者走到free_unref_page或__free_pages。理解代码的关键点是**rmqueue函数**——它负责从指定的order链表中摘块并做拆分合并。读这部分代码时建议先忽略CMA、page_owner、memory cgroup这些附加逻辑,抓主干,不然很容易被绕晕。

3.2 slab/slub分配器的对象缓存机制

直接跟伙伴系统打交道其实挺低效的——每次分配至少是一整页,而且很多内核对象只有几十上百字节,如果每次都从伙伴系统拿一整页,内部碎片会非常严重。于是slab分配器出现了,它的核心思想是:从伙伴系统拿一整页或几页,然后切成大小相等的对象,维护一个对象池。

Linux内核现在默认用的是SLUB分配器,它跟早期的slab相比简化了很多元数据管理,对多核扩展性更好。在SLUB里,每个kmem_cache维护了多个cpu_partial链表,每个CPU核心有自己的部分空闲对象链表,分配时优先从当前CPU的partial链表取对象,这样可以避免跨CPU锁竞争。当partial链表空了,就向对应节点的slab页列表申请新的整页;释放时优先放回当前CPU的partial链表。

写驱动时最常用的kmalloc接口,其实就是若干个预置kmem_cache的封装。kmalloc有128、192、256、512等一系列固定大小档位,选最接近需求且能容纳对象的档位。这里有个隐藏细节:kmalloc(DMA)分配的内存,除了大小要匹配,还要看GFP标志位是否包含__GFP_DMA或__GFP_DMA32,这类内存必须来自低地址区域,因为老式设备只有24位或32位地址线。

3.3 vmalloc与线性映射的边界

vmalloc在内核里是个特殊的存在。它分配的是虚拟地址连续的内存区域,物理页可以分散在各处。它解决了物理内存碎片化导致无法分配大块连续内存的问题,比如要分配一个16MB的缓冲区,物理上可能没有16MB连续页,但vmalloc可以在vmalloc区(通常是内核地址空间的特定区域)映射出16MB连续虚拟地址。

但vmalloc有两个代价:一是TLB压力大,因为vmalloc区的页表项通常是每个页单独建立的,访问这些内存可能频繁刷新TLB;二是性能稍差,vmalloc分配需要处理页表操作和flush,比kmalloc慢不少。所以内核里能用kmalloc的地方尽量不用vmalloc,只有确实需要大块内存才选择vmalloc。

这里补充一个容易混淆的点:kmalloc和vmalloc返回的都是内核虚拟地址,但你没法从指针本身判断它是kmalloc来的还是vmalloc来的。要确认某个地址属于哪个区域,可以查/proc/kallsyms里的vmlist信息,或者在内核里用内核提供的is_vmalloc_addr()函数判断。

3.4 GFP标志位:决定分配行为的开关

GFP标志位是整个分配机制里最容易被忽视但最要命的部分。举例说,GFP_KERNEL允许睡眠、允许进行磁盘IO来回收内存,适合进程上下文;GFP_ATOMIC不允许睡眠,适合中断上下文和自旋锁保护区域;GFP_NOWAIT不等待回收直接失败返回。

我踩过最深的坑是在中断上下文里用了GFP_KERNEL——编译没报错,运行时偶尔出现死锁或者说崩溃,最后查源码才确认是睡眠导致的。所以写代码前先确认自己在什么上下文里,再选择GFP标志位。简单说:中断处理、软中断、自旋锁临界区、原子上下文必须用GFP_ATOMIC,普通进程上下文可以用GFP_KERNEL,但也要考虑是否允许回收内存。

__GFP_ZERO这个标志也很好用,它在分配后自动清零页内容,能避免未初始化内存泄漏敏感数据的风险。__GFP_HIGH则提高分配优先级,让分配过程可以吃掉一些紧急保留页。需要强调的是,GFP_ATOMIC和GFP_KERNEL在实现上的核心区别,其实就是__GFP_ATOMIC标志和__GFP_IO、__GFP_FS这些标志的有无。

4. 实操:如何用工具定位内存分配问题

4.1 从free、/proc/buddyinfo和slabtop看内存分布

很多运维和开发对free -m的理解停留在"看容量"层面,其实free命令展示的数值跟内核内部的分配器状态密切相关。free里的buff/cache大部分是页缓存(page cache)和slab可回收内存,在内存紧张时可以被回收。如果你看到available已经很低,但free还有几百MB,说明系统正在靠可回收缓存硬撑着,这时候性能大概率已经劣化了。

/proc/buddyinfo直接展示每个内存节点的伙伴系统各order空闲块数量。比如某节点order=0的空闲页数量很多,但order=4以上的连续块几乎为0,说明物理内存碎片化严重。此时就算整体空闲内存還够,分配大块连续页也可能失败。

slabtop能实时显示各kmem_cache的对象数量和内存占用,排查slab内存异常增长时非常好用。我遇到过一个dentry缓存无限膨胀的问题——某个文件系统不停地创建临时文件但没unlink,目录项缓存越积越多,最后系统直接内存耗尽。用slabtop看dentry这个cache的占用,一秒就定位到了。

4.2 用tracepoint和bpftrace抓分配失败现场

实际排查内存问题时,光看快照数据往往不够,需要抓动态事件。内核提供了kmem:kmalloc、kmem:kmalloc_node、kmem:kfree等tracepoint,可以统计各调用点的分配次数和大小。我来分享一个实际案例:某驱动模块每次分发数据包时调用kmalloc(16KB, GFP_KERNEL),本来没问题,但在内存碎片严重时频繁失败,导致数据包丢弃。我第一反应是加大分配缓存,但排查后发现问题的根源是内存碎片化后order=2的页经常分配失败,最后解决方案是改用内存池或预分配缓冲池。

bpftrace也可以用,比如追踪某个特定模块的分配调用并打印调用栈:

bpftrace -e 'kprobe:__kmalloc { if (arg2 == 16384) { printf("%s\n", kstack); } }'

这个命令在每次__kmalloc被调用且申请16KB时打印内核调用栈。用这个方法可以快速找出谁在频繁分配大块内存。不过bpftrace需要root权限,且内核需要开启BTF支持,部分发行版默认没开启,需要装linux-tools或bpftrace时确认下环境。

4.3 自己写内核模块验证分配行为

纸上谈兵终觉浅,我建议真正想理解内存分配的人写一个内核模块,用不同GFP标志和不同分配大小做对比实验。比如写一个模块,分别用kmalloc分配1KB、64KB、1MB内存,打印返回值;再用__get_free_pages分配order=0和order=4的页;再用vmalloc分配1MB;最后检查/proc/slabinfo和/proc/vmallocinfo的变化。

这个实验的代码很简单,但能直观感受到三者的差异。比如你会发现kmalloc(64KB)实际上是从order=4的伙伴块里切的,但如果系统碎片严重,可能就直接返回NULL了,而同样大小的vmalloc大概率能成功。多做几次这种实验,你对"什么时候该用哪种分配方式"的判断会准确很多。

4.4 内存压测与回收触发观察

要观察内存回收和OOM行为,可以用stress-ng或直接写个进程不断分配内存,同时用vmstat 1看si和so字段的变化(swap in/out),用dmesg -T看内核有没有触发OOM Killer。做这类测试时建议在虚拟机里做,避免把工作机搞挂。

一个值得观察的实验是:写一个进程,不断malloc并touch内存(写入数据以触发实际物理页分配),同时另一个线程每秒读取/proc/vmstat里的pgscan_kswapd和pgscan_direct字段。你会发现内存触顶前,pgscan_kswapd先增长,这是后台回收;等到内存继续下降,pgscan_direct开始飙升,说明内存分配已经开始同步等待回收了。这个瞬间通常是系统性能暴跌的临界点。

5. 常见问题与排查技巧实录

5.1 kmalloc失败但free还有很多内存?

这种现象几乎每次排查内存问题都会遇到,特别是在嵌入式设备上。原因通常是内存碎片化:虽然总空闲内存不少,但没有足够大的连续物理块。解决思路有几个方向:

  • 用vmalloc替代kmalloc,如果对物理连续性没有硬性要求。

  • 预先分配内存池或DMA缓冲池,在初始化阶段就取走大块内存。

  • 开启内存compaction(内存规整),内核会尝试移动可移动页来凑出大块连续内存,嵌入式内核里可以开启CONFIG_COMPACTION。

  • 如果是嵌入式设备长时间运行导致的碎片,可以考虑定期echo 1到/proc/sys/vm/compact_memory触发手动规整。

5.2 slab内存只涨不降的奇葩场景

遇到过一个奇怪的场景:slab内存不断增长,但系统整体内存和其他cache都正常。排查过程比较曲折,最后发现是某个驱动在每次IO时创建bio结构但没有正确释放,导致bio这个kmem_cache的对象数无限增加。怎么定位的呢?盯/proc/slabinfo里每个cache的active_objs变化,同时对驱动的每个IO操作做计数统计,两者关联起来就水落石出了。

这也提醒一个通用思路:看到slab异常增长,先看是哪个cache在涨,再去代码里找这个cache的创建和销毁逻辑是否成对。

5.3 中断上下文分配导致系统卡死

内核在中断上下文只能调用GFP_ATOMIC,这点很多初学者容易忽略。但即使用了GFP_ATOMIC,中断上下文频繁分配内存也容易因为buddy系统锁竞争导致中断延迟飙升。经验之谈:中断路径尽量零分配,最好在驱动的open或初始化阶段把需要的缓冲区全部预分配好,中断处理里只复用不重新分配。

5.4 内存回收导致IO卡顿

当系统内存紧张时,内核会回收page cache,这会导致文件读写性能剧烈下降,因为缓存被清了,磁盘IO重新变多。这时候看/proc/meminfo里的Dirty和Writeback字段,如果Dirty数值长期很高,说明系统在不断把脏页刷盘,IO系统面临压力。可以适当调高/proc/sys/vm/dirty_ratio和dirty_background_ratio来缓解频繁刷盘,但这只是治标,真正的解决办法还是增加内存。

6. 相关工具与后续学习建议

调试内存问题没有万能钥匙,但有几个工具组合是我长期在用的:/proc/meminfo看总览,/proc/buddyinfo看碎片,slabtop看对象级占用,vmstat看动态变化,dmesg看OOM和recall事件,bpftrace抓调用栈。这套组合基本能覆盖九成以上的内存问题场景。

深入学内核内存分配,我建议按这个顺序读源码:先读mm/page_alloc.c的分配主路径__alloc_pages_nodemask,再读mm/slub.c的slab_alloc_node,再看mm/vmalloc.c的__vmalloc_node_range。这三个文件对应本文提到的三层机制,抓主干逻辑,把order链表的拆分合并、CPU partial链表的周转、vmalloc区的红黑树映射这几个核心概念理清就够了。初学者不用把每个宏定义都搞清楚,会把自己心态搞崩掉。

在我个人的实践里,Linux内核内存分配最好的学习方法不是看文档,而是做实验、翻源码、踩坑。建议备一台虚拟机,专门用来做内存压测和内核模块实验,遇到问题大胆用crash工具或gdb去分析vmcore。内核内存分配的复杂度确实高,但一旦理解了三层结构和GFP标志位这两个核心框架,再复杂的现象也能逐渐拆解成几个基础规则的综合作用。后面如果你们在实战中遇到具体的内存问题,欢迎来交流具体的定位思路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 11:12:29

HarmonyOS NEXT端侧大模型部署:五大工程决策与内存功耗优化实践

1. 为什么要在 HarmonyOS NEXT 上跑大模型,而不是调云端 API先把结论摆在前面:在 HarmonyOS NEXT 上接入开源大模型,绝大多数团队真正要解决的不是“能不能跑起来”,而是“跑起来之后,端侧算力、内存、功耗、包体积这四…

作者头像 李华
网站建设 2026/10/8 11:11:32

AI旅游Agent技术栈拆解:从对话到支付的全链路工程实践

1. 为什么我要拆这个 AI 旅游 Agent 的技术栈 去年下半年开始,身边做旅游、做本地生活、做 SaaS 的朋友几乎都在问同一件事:能不能做一个 AI 旅游 Agent,用户说一句"帮我安排五一去成都三天,预算三千,带老人"…

作者头像 李华
网站建设 2026/10/8 11:10:55

AI漫剧量产全攻略:零基础用AI工具做短视频副业赚钱

先聊个让我很意外的现象:我一个完全不会画画、连PS都不太熟的朋友,靠着AI漫剧这个形式,两个月做出了三条数据还不错的短剧视频。他用的工具全是免费或廉价方案,流程就是网上东拼西凑学来的。这件事让我意识到,AI漫剧可…

作者头像 李华
网站建设 2026/10/8 11:10:15

开源雷达周刊:每周精选10个能跑通的自动化工具

1. 为什么我要做这个开源雷达周刊 先说清楚这个周刊到底是个什么东西。简单讲,它是我每周花几个小时,把过去七天里在开源社区里冒出来的、跟自动化沾边的工具筛一遍,挑出十个真正能跑起来、能解决具体问题的项目,然后整理成一份可…

作者头像 李华
网站建设 2026/10/8 11:08:19

text-to-cad实战指南:从自然语言到参数化CAD模型的落地流程

上个月朋友让我帮忙弄一个传感器支架,微信里就甩来一句话:「6061 铝,L 型,底边四个孔,立边一个 M8 螺纹孔,总高 60。」这句话放到十年前,够我开软件画半小时;放到现在,te…

作者头像 李华