当我们谈论 Linux 内存管理时,大部分注意力都给了用户空间的堆和栈。但在内核里,有一套完全不同的机制——今天聊聊 Slab。

一、为什么内核需要自己的内存分配器?
想象一下这个场景:内核每天要处理成千上万次进程创建、文件系统操作、网络包收发。每次创建进程时,内核需要为 task_struct 分配内存;每次打开文件,需要分配 file 结构体。这些操作非常频繁。如果每次都像用户空间那样 malloc(),会面临几个尴尬的问题:首先,内核可不能随便睡大觉。用户空间的程序睡了就睡了,内核必须随叫随到,分配内存必须快,不能等。其次,内核没有虚拟内存那套奢侈的东西——没有页表,没有 Swapping,什么都必须精打细算。最后,那些常用的数据结构(比如 task_struct)大小固定不说,还被频繁创建销毁,如果每次都从伙伴系统要内存,那效率简直没法看。
二、Slab 的老前辈:SLAB、SLUB、SLOB 三兄弟
SLAB 是最早的实现,1994 年诞生于 SunOS。它思路清晰:给每种对象类型(比如 task_struct)建一个"仓库",仓库里放满了预先分配好的对象。问题是——太复杂了,代码量吓死人。后来,Linux 的大佬们一合计,算了算了另外写一个吧。SLUB 应运而生,它是现在内核的默认选项。SLUB 甩掉了 SLAB 那些花里胡哨的东西,代码简洁,性能还好。SLOB 是个特例,专门给嵌入式小内存设备用的,代码最简,但功能也最少。
三、SLUB 是怎么干活的?
3.1 核心数据结构
SLUB 的核心是三个东西:kmem_cache、slab、object。kmem_cache 就像是每种对象的"说明书"。它告诉你这个对象有多大、怎么初始化、放在哪个 CPU 本地仓库里。一个 kmem_cache 下面管着多个 slab。每个 slab 相当于一个容器,里面装了一堆 object。这些 slab 从伙伴系统那里成批拿来内存页,然后切成小块。如图所示,一个 4KB 的页面可以被切成 16 个 256 字节的 object(剩下的空间用作 metadata)。3.2 分配流程
当你调用 kmem_cache_alloc 时,SLUB 怎么找空闲对象?它会先看当前 CPU 的本地仓库有没有货。有的话直接拿,速度最快——不需要加锁。如果本地仓库空了,就去当前 node 的共享仓库看看。如果也没有,就只能从伙伴系统要新的页面了。这就像去超市买水——先看家里冰箱有没有(有就直接喝),没有再去楼下小卖部(快一些),还不行就去远处大超市(慢但肯定有)。3.3 释放流程
释放对象比分配简单。kmem_cache_free 直接把对象扔回原来的 slab 就完事了。如果一个 slab 里的对象全空了,SLUB 可能会把页面还给伙伴系统——但不是马上还,而是等"时机成熟"。
四、那些你可能没听过的黑科技
4.1 着色:让 CPU 缓存更友好
我们知道 CPU 有三级缓存(L1/L2/L3)。问题是,如果所有 object 都从同一个地址开始,那么同一类型的 object 就会映射到缓存的同一行——这叫缓存冲突,严重影响性能。简单说,给每个 slab 安排一个"偏移量"。第一个 slab 从地址 0 开始,第二个从 16 字节偏移开始,第三个从 32 字节开始......这样,同类型对象就错开了,缓存利用率 up up。4.2 CPU 本地缓存
刚才说了,分配时先看本地仓库。这其实就是利用了空间局部性——刚分配的对象很可能马上就会用到。每个 CPU 有自己的"小仓库",互不干扰,分配时根本不用加锁。这可是并发程序梦寐以求的东西。4.3 对象初始化
有些对象分配后需要初始化(比如把结构体字段清零)。SLAB/SLUB 支持构造函数,在分配时自动调用。但实际上,内核的大部分对象分配后不需要额外初始化,因为内核代码里通常会主动初始化。所以这个功能用得不多——反而是那些"分配了马上用"的场景,不用构造函数反而更快。
五、怎么查看和调试 SLUB?
5.1 /proc/slabinfo
想知道内核现在有哪些 cache、每个 cache 用了多少内存?直接 cat:slabinfo - version: 2.1name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab>:slabcache 0 0 232 17 1kmalloc-256 1234 1536 256 16 1task_struct 456 512 3328 1 1
你可以看到每种 cache 有多少对象、占多大空间。5.2 slub_debug
echo debug > /sys/kernel/debug/slab/slab_common
echo "slub_debug=F" > /sys/kernel/debug/slab/kmalloc-256
六、实战:看看常见对象的 cache
6.1 查看 task_struct
cat /proc/slabinfo | grep task_struct输出:task_struct 120 128 5632 1 2
这说明系统当前有 128 个 task_struct 对象(预留槽位),每个占 5632 字节。6.2 查看 "kmalloc"
内核的 kmalloc() 实际上是一堆预定义的 cache:cat /proc/slabinfo | grep kmalloc
你会看到 kmalloc-8、kmalloc-16、kmalloc-32......一直到 kmalloc-2M。每次你调用 kmalloc(100),内核会自动帮你选一个最合适的 cache。
七、性能优化的那些事儿
7.1 减少分配频率
最好的优化是不分配。如果你需要频繁创建某种结构体,可以考虑:7.2 选对 cache
kmalloc(300) 和 kmalloc(512) 看似差别不大,但在内核里可是两个不同的 cache。尽量选合适的尺寸,别"浪费"太多空间。7.3 关注 hit/miss 比率
通过 /proc/slabinfo 里的 active_objs / num_objs 可以大致看出命中率。如果一个 cache 始终是满的,说明对象一直被占用,没问题。但如果频繁有新对象进来又出去,说明可能需要调整。
八、总结
SLUB 可能不是内核最"酷"的部分,但绝对是最重要 的部分之一。它就像一个高效的仓库管理员,默默处理着每一次内存请求,让内核能快速响应各种操作。理解 SLUB,不仅能帮你更好地调试内核问题,还能让你对"为什么内核这么快"有更深的认识。好了,今天就聊到这儿。如果你想深入,可以看看内核源码 mm/slub.c——虽然代码量不小,但逻辑其实很清晰。