6.5 使用 slabinfo 及其配套工具
工具箱里的新家伙
到现在为止,我们一直是在内核「内部」打转——看内核日志,解读 panic 信息,或者对着 hex dump 发呆。这当然很重要,但有时候,你需要像个抽离的观察者那样,站在外面往里看。
这就需要一把趁手的「外窥镜」。
你可能会问,/proc/slabinfo 那个文件我们不是提过好几次了吗?是的,但直接读那个文件就像在读一长串没有逗号的句子——信息全在那里,但人类读起来太累了。你需要一个能把这些数据结构化、甚至帮你分析出来的工具。
这个工具就是 slabinfo。
有点反直觉的是,这个虽然是用户空间程序,但它其实就住在内核源码树里:
tools/vm/slabinfo.c
编译它非常简单,不需要折腾复杂的 Kbuild 系统。只要进入源码树的 tools/vm 目录,直接敲 make 就行了。编译出来是个二进制文件,为了方便调用,你可以把它软链接到 /usr/bin/slabinfo。
$ ls -l $(which slabinfo)
lrwxrwxrwx 1 root root 71 Nov 20 16:26 /usr/bin/slabinfo ->
<...>/linux-5.10.60/tools/vm/slabinfo
在开始之前,先跑一下 -h(或者 --help)。你会看到一大坨参数。别被吓到了,很多参数其实是针对特定场景的。下图展示的是 5.10.60 内核版本的所有参数(Figure 6.6):
(Figure 6.6 – The help screen of the kernel slabinfo utility)
有几点需要你心里有数:
- 默认行为:
slabinfo默认只显示有数据的 slab 缓存(这等同于带上了-l参数)。如果你想看那些空的缓存,运行slabinfo -e。 - 前提条件:不是所有参数都能随手用。大部分功能需要内核开启了
CONFIG_SLUB_DEBUG=y。好消息是,大多数发行版内核默认都开了这个。还有些功能需要你在内核启动参数里通过slub_debug传进去特定的标志。 - 权限:你必须是 root。
先来一把「体检」
让我们先不接任何参数跑一次,看看它能吐出什么。
这里我们只看表头和一个具体的例子(kmalloc-32 缓存):
$ sudo slabinfo |head -n1
Name Objects Objsize Space Slabs/Part/Cpu O/S O %Fr %Ef Flg
$ sudo slabinfo | grep "^kmalloc-32"
kmalloc-32 35072 32 1.1M 224/0/50 128 0 0 100
$
这行输出有点挤,我们把它拆开揉碎了看。每一列都是这块缓存的「体检指标」:
- Name: 缓存的名字(这里是
kmalloc-32)。 - Objects: 当前分配了多少个对象(35072 个)。
- Objsize: 单个对象的大小(32 字节,这很符合预期)。
- Space: 这些对象占用的总内核内存空间(大约 1.1MB,基本就是
Objects * Objsize)。 - Slabs/Part/Cpu: 这是一个三元组,分别代表满的 slab 数量、部分空的 slab 数量 和 每 CPU slab 数量。这个比例能告诉你系统的碎片化程度。
- O/S (Objects per Slab): 每个 slab 能装多少个对象(128 个)。
- O (Order): 向页分配器申请内存的阶数。0 代表 1 页($2^0$),1 代表 2 页($2^1$),以此类推。这里如果是 0,说明是用单页来承载这些对象的。
- %Fr: 空闲内存的百分比。
- %Ef: 有效内存使用率百分比。
- Flg: 标志位。这里显示了该缓存启用的特殊属性(下面会细说)。
如果你想看这行打印到底是怎么生成的,可以直接去看源码: https://elixir.bootlin.com/linux/v5.10.60/source/tools/vm/slabinfo.c#L640
关于最右侧的 Flg 标志位,它们代表了 slab 缓存的各种属性,含义如下:
- *: 存在别名
- d: DMA 内存
- A: 硬件缓存行对齐
- P: Poisoned(已投毒,用于检测未初始化访问)
- a: 激活了回收统计
- Z: Red Zoned(红区,用于检测越界)
- F: Sanity checks on(开启健全性检查)
- U: User tracking(记录谁分配了它)
- T: Traced(正在被跟踪)
另外,如果你加了 -D(Display active)选项,输出的列格式会发生变化,展示更详细(但也更宽)的信息。
谁是内存大户?
这时候一个常见的工程问题冒出来了:在这么多 slab 缓存里,到底谁占用了最多的内核内存?
slabinfo 提供了两种思路来回答这个问题:
- 用
-B参数,它会显示占用的字节数,方便你手工排序。 - 更简单的方法,直接用
-S参数。它会让slabinfo按照占用空间从大到小排序,并且自动带上人类可读的单位(KB, MB 等)。
下图展示了用 -S 查看占用内存前 10 名的 slab 缓存(Figure 6.7):
(Figure 6.7 – The top 10 slab caches sorted by total kernel memory space taken)
你可以看到,通常排名靠前的都是通用的 kmalloc-* 或者是某些高频内核数据结构。
这里有个很有意思的插曲:-U(Show unreclaimable slabs only,只显示不可回收的 slab)这个选项的诞生,完全是因为一次真实的系统恐慌。
故事是这样的:系统的不可回收 slab 内存占用一度逼近 100%,导致 OOM Killer(内存溢出杀手)甚至找不到候选进程来杀,最后直接 panic。为了以后排查这种绝境,开发者提交了一个补丁,不仅让 slabinfo 能显示这些顽固分子,也让 OOM Killer 的代码路径能打印它们。这个补丁在 4.15 内核合入。你可以感受一下这个 commit 的沉重感:
https://github.com/torvalds/linux/commit/7ad3f188aac15772c97523dc4ca3e8e5b6294b9c
浪费去哪了?(内部碎片)
接下来是 -L(sort by loss,按损失排序)选项。这个「损失」(Loss),其实换个词你会更熟悉——内部碎片。
slab 层采用的是「最佳拟合」模型。当你请求 100 字节时,内核没法给你切 100 字节出来,它会把你塞进 kmalloc-128 的缓存里(因为 96 装不下)。
结果是:你请求了 100 字节,实际占用了 128 字节。
那 28 字节的差价,就是所谓的 Loss 或 Waste。
运行 sudo slabinfo -L |head,你会看到按照浪费量降序排列的缓存列表(看第四列 Loss)。这对于评估系统的内存效率非常有帮助——有时候你会发现,某些结构体稍微优化一下大小,就能省下惊人的碎片空间。
深挖单个缓存
如果你发现某个缓存看起来很可疑,或者只是单纯想深入研究它,-r(report,报告)选项是你的好帮手。
默认情况下,它会吐出所有缓存的详细统计。更实用的做法是配合正则表达式。比如,只想看虚拟内存相关的缓存:
sudo slabinfo -r vm.*
这会显示所有匹配 vm.* 模式的缓存详情。如果之前启用了 SLUB 调试标志(比如 U),这里甚至能看到是谁、在哪里 alloc/free 了这些对象。
有时候你会看到一个名字很生疏的缓存,不知道它是干嘛的。这时 -a(或 --aliases)选项就派上用场了,它会显示这个缓存是哪个内核对象的别名,帮你揭开面纱。
想看全局概览?-T 选项会显示所有缓存的汇总快照:有多少个缓存、活跃多少、总共用了多少内存等。如果你觉得还不够过瘾,-X 选项会提供扩展版的详细信息(Figure 6.8)。
(Figure 6.8 – Screenshot showing extended summary information via slabinfo -X)
最后,如果想看所有缓存(包括空的),用 -z(zero)参数。
也就是这个参数,能救你的命
这里有两个专门用于调试的选项:-d 和 -v。
它们都有一个硬性前提:你的系统必须带 slub_debug 内核参数启动,而且这个参数得是非空值。
让我们先看点直观的。当你用 slub_debug=FZPU 启动后,你会发现所有 slab 缓存的标志位里,哪怕之前什么都没有,现在也至少挂着 PZFU 这几个字母(Figure 6.9)。
(Figure 6.9 – Partial screenshot – the focus is on the SLUB debug flags being set)
这表示内核已经强制给所有缓存加上了这些调试属性。
关于 -d 选项:
- 单独运行
slabinfo -d会把调试关掉。这很不直观,对吧?但这和内核参数slub_debug的行为是一致的。 - 如果你想打开某些调试标志,需要显式传参,比如
--debug=fzput。
这背后的黑盒机制其实很简单:当你执行 --debug=fzput 时,slabinfo 工具(作为 root)会去写 /sys/kernel/slab/<slabname>/ 下面的对应伪文件,把它们置为 1。
具体映射关系如下:
f|F-> 写入/sys/kernel/slab/<slabname>/sanity_checksz|Z-> 写入/sys/kernel/slab/<slabname>/red_zone- 以此类推...
干脏活的代码在这里: https://elixir.bootlin.com/linux/v5.10.60/source/tools/vm/slabinfo.c#L717
然后是 -v(validate,验证)选项。这个选项会强制 SLUB 遍历指定缓存里的所有对象,检查元数据的有效性。一旦发现不对劲,它会把诊断信息直接喷到内核日志里。
这和你之前看到的那种「内核自己报错」的格式是一模一样的。实际上,slabinfo -v 的本质就是往 /sys/kernel/slab/<slabcache>/validate 里写个 1。
这对于排查现网的怀疑有内存破坏的系统非常有用——主动触发一次全盘扫描,看看有没有藏在深处的炸弹。
顺便提一句,内核源码里甚至还有一个 slabinfo-gnuplot.sh 脚本,能把 slab 的运行情况画成图。想玩可视化的话,可以去读一下内核文档里的说明:
Documentation/vm/slub.txt (Extended slabinfo mode and plotting section)
那个老伙计:/proc/slabinfo
当然,内核本身就通过 procfs 暴露了这些信息,即 /proc/slabinfo。这实际上也是 slabtop 等工具的数据源。同样需要 root 权限读。
它的格式非常详细(甚至有点啰嗦):
$ sudo head -n2 /proc/slabinfo
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> :
tunables <limit> <batchcount> <sharedfactor> :
slabdata <active_slabs> <num_slabs> <sharedavail>
下图是实际数据的一瞥(Figure 6.10):
(Figure 6.10 – A screenshot showing some data from /proc/slabinfo)
关于怎么解读这些列,man slabinfo(5) 写得很清楚。不过要注意,手册页稍微有点老了,有些统计信息主要是为了老式的 SLAB 分配器设计的。
还有个老朋友 vmstat。加上 -m 参数,它也能显示 slab 统计。它本质上也是读 /proc/slabinfo。
sudo vmstat -m
像看 top 一样看 slab:slabtop
既然有 top 看 CPU,那有没有看 slab 的 top?
当然有,它就叫 slabtop。
它的用法和 top 几乎一模一样,实时刷新,默认按对象数量排序(你可以用 -s 改变排序字段)。它也是基于 /proc/slabinfo 的数据,所以需要 root。
跑起来之后,你会发现除了那些特定的内核结构体缓存,小尺寸的通用缓存(kmalloc-* 系列)通常是系统的劳模,被调用得最频繁。
更前沿的视角:slabratetop
最后,是一个比较新的成员——基于 eBPF 的 slabratetop(你的系统上可能叫 slabratetop-bpfcc)。
前面提到的工具大多是「存量统计」,而 slabratetop 展示的是「增量速率」。它实时显示内核 slab 缓存的分配速率(每秒多少次)和总字节数,刷新频率默认是一秒。
它内部是通过跟踪 kmem_cache_alloc 这个关键的内核 API 来实现统计的。
比如,想以 5 秒为间隔,看 3 次采样:
sudo slabratetop-bpfcc 5 3
这对分析系统的动态行为非常有帮助,能让你看到哪块内存正处于「高并发分配」的状态。
实战演练:谁在吃我的内存?
好了,工具箱打开了,怎么用它们来解决实际问题?
最常见的一个问题就是:内存去哪了?
这分两层:用户空间和内核空间。
如果是用户空间进程,用 smem、ps 或者直接读 /proc/*/status 就能搞定。比如想找物理内存(RSS)占用前 10 的进程:
grep -r "^VmRSS" /proc/*/status |sed 's/kB$//'|sort -t: -k3n |tail
但如果你怀疑是内核在偷吃内存,特别是 slab 内存,这就有点意思了。我们来推演一个排查场景:
- 第一步:用
slabratetop找出哪个缓存的分配频率或占用字节数最反常。 - 第二步:用动态 kprobes 抓取内核栈,看看具体是哪条代码路径在疯狂分配这个缓存。
我们来实战一下。
首先,跑一下 slabratetop(或者 slabratetop-bpfcc):
sudo slabratetop-bpfcc
[...]
CACHE ALLOCS BYTES
names_cache 18 78336
vm_area_struct 176 46464
...
在这段输出里(Figure 6.11 上半部分),我看到一个叫 vm_area_struct 的缓存分配频率特别高(每秒 176 次)。这很可疑。
(Figure 6.11 – Output from slabinfo and slabratetop)
现在的问题是:谁在分配 vm_area_struct?
我们知道,内核分配特定缓存对象,最终都要调用 kmem_cache_alloc()。如果我们能实时看到这个函数的内核栈,不就知道是谁调用的了吗?
这就轮到 kprobe 出场了(回顾一下第 4 章的内容)。我们可以用 kprobe-perf 脚本来抓取 kmem_cache_alloc 的调用栈。
但有个细节:kmem_cache_alloc 的第一个参数是指向 struct kmem_cache 的指针。我们怎么知道这究竟是哪个缓存的指针呢?
在 x86_64 上,第一个参数通过 RDI 寄存器传递。而 struct kmem_cache 结构体里,偏移量 96 字节的地方就是 name 成员(缓存的名字)。
所以我们的命令可以这么写:
sudo kprobe-perf -s 'p:kmem_cache_alloc name=+0(+96(%di)):string'
这会打印所有 slab 分配的栈。为了只看我们关心的 vm_area_struct,用 grep 过滤一下:
sudo kprobe-perf -s 'p:kmem_cache_alloc name=+0(+96(%di)):string' | grep -A10 "name=.*vm_area_struct"
你会得到类似下面的输出(Figure 6.11 下半部分):
(Figure 6.11 – Kernel stack trace showing the call path to kmem_cache_alloc)
这栈非常清晰:
sys_brk() --> do_brk() --> do_brk_flags() --> vm_area_alloc() --> kmem_cache_alloc()
sys_brk 是用户空间 brk() 系统调用的入口,通常用于扩展堆内存。
内核为了管理进程的内存区域,需要维护 VMA(Virtual Memory Area)结构。每当需要新建或扩展一个内存映射时,内核就得从 vm_area_struct 这个专有缓存里分配一个对象。
这就是这一连串分配的源头。
如果你去翻源码,能在 mm/mmap.c 里找到这一行:
https://elixir.bootlin.com/linux/v5.10.60/source/mm/mmap.c#L3110
看到这里,你应该对系统的内存行为有了一种「透视」感。
Security Tip
最后,虽然稍微跑题一点,但这很重要:安全。
为了保证 slab 内存在分配时和释放时的内容都被彻底擦除(防止信息泄露),你可以在内核启动参数里加上:
init_on_alloc=1 init_on_free=1
这会有性能损耗,但对于高安全要求的场景,这是值得的。 参考一下 Kernel Self Protection Project 的建议: https://kernsec.org/wiki/index.php/Kernel_Self_Protection_Project/Recommended_Settings