3.9 slab、vmalloc 与内核内存分配
内核里一次 kmalloc(64) 要多久,为什么它比 alloc_pages 快一个数量级?SLUB 怎么在一个 4 KiB 页里塞进几十个对象,又怎么知道哪个是空的?kmalloc 与 vmalloc 的边界在哪,为什么驱动里用错会 panic?/proc/slabinfo 里哪几列能看出内存泄漏?
画出对象 → slab → page → buddy 的四层关系;说清 SLUB 的 per-CPU 快路径与慢路径;按「大小、连续性、上下文」选对分配接口;用 slabinfo/slabtop/kmemleak/bpftrace 定位泄漏与热点;写一个带自定义 kmem_cache 的模块并验证 ksize()。
3.7 的伙伴系统最小单位是一页(4 KiB),3.8 的页表与 VMA 也都是页粒度。但内核里大量对象只有几十到几百字节:task_struct 约 9 KiB、struct file 约 256 B、一个 struct dentry 约 192 B、一个网络 sk_buff 的头约 240 B。如果每个对象都占一页,内部碎片会吃掉一半以上的内存;如果自己管理页内切分,又要为每种对象写一套分配器。
slab 分配器就是这层「页内对象池」。它把页切成固定大小的槽位,用空闲链表串起来,并按对象类型缓存(每个类型一个 kmem_cache)。这样分配是「从链表摘一个」,释放是「挂回去」,都没有页级操作。
这一章讲三件事:slab(SLUB)怎么工作、kmalloc/kvmalloc/vmalloc 怎么选、怎么观测。6.8 的一个版本事实要先说:SLAB 分配器在 6.8 被移除,只剩 SLUB(默认)与 SLOB(极小系统)。读 4.x 的书看到 slab.c/slab.h 的三套实现对比,在 6.8 上只剩 slub.c。
3.9.1 三条分配路径:页、slab、vmalloc
| 路径 | 接口 | 物理连续 | 虚拟连续 | 典型大小 | 能否睡眠 |
|---|---|---|---|---|---|
| 页分配器 | alloc_pages/__get_free_pages | 是(2^order 页) | 是(直接映射) | ≥ 4 KiB | GFP_KERNEL 可以,GFP_ATOMIC 不行 |
| slab | kmalloc/kmem_cache_alloc | 页内连续,对象不跨页 | 是 | 8 B – 8 KiB | 同上 |
| vmalloc | vmalloc/vzalloc | 否 | 是 | ≥ 1 页,可到 GiB | 可以(内部可能睡眠) |
三条路径的取舍:
- 要物理连续(DMA、页表、硬件环形缓冲)→
alloc_pages或dma_alloc_coherent; - 要小对象、要快(绝大多数内核结构)→
kmalloc或专用kmem_cache; - 要大块、只要虚拟连续(模块代码、大缓冲区、内核栈)→
vmalloc。
kmalloc 的上限是 KMALLOC_MAX_CACHE_SIZE(4 KiB 页上是 8 KiB)。超过它只能用 alloc_pages(要连续)或 vmalloc(不要连续)。kvmalloc 是「先试 kmalloc,失败再 vmalloc」的折中,适合「通常不大、偶尔很大」的缓冲区。
slab 是「一个或多个物理页组成的、被切成固定大小槽位的容器」。cache(struct kmem_cache)是「同一类对象的 slab 集合 + 分配策略」。每个 cache 有对象大小、对齐、构造/析构函数、per-CPU 缓存。kmalloc 家族用的是一组按大小预设的通用 cache(kmalloc-8、kmalloc-16、…、kmalloc-8k,以及 96/192 这类非 2 的幂尺寸),专用 cache 由 kmem_cache_create() 创建。
3.9.2 SLUB:per-CPU 空闲链表 + 页内槽位
SLUB 的核心数据结构只有三个:
/* mm/slab.h 与 mm/slub.c —— 6.8 节选,已简化 */
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; /* 每 CPU 的快路径 */
slab_flags_t flags;
unsigned long min_partial;
unsigned int size; /* 含元数据的实际槽位大小 */
unsigned int object_size; /* 用户请求的大小 */
unsigned int offset; /* 空闲指针在对象内的偏移 */
struct kmem_cache_node *node[MAX_NUMNODES];
};
struct kmem_cache_cpu {
union {
struct {
void **freelist; /* 当前 slab 的空闲对象链 */
unsigned long tid; /* 事务号,防 ABA */
};
freelist_double_t freelist_double;
};
struct slab *slab; /* 当前正在分配的 slab */
struct slab *partial; /* 本 CPU 的备用 partial 链表 */
};
struct slab {
unsigned long __page_flags;
struct kmem_cache *slab_cache;
union {
struct {
void *freelist; /* 这个 slab 的空闲链 */
unsigned long counters;
...
};
};
unsigned int inuse; /* 已分配对象数 */
unsigned int objects; /* 总对象数 */
unsigned int frozen; /* 是否被某个 CPU 独占 */
};
一个 slab 页的内部布局:
一个 4 KiB 物理页(order-0 slab),cache 的对象大小 128 B
┌────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ obj 0 │ obj 1 │ obj 2 │ obj 3 │ obj 4 │ obj 5 │ obj 6 │ ... │
│ 已分配 │ 空闲 │ 已分配 │ 空闲 │ 空闲 │ 已分配 │ 空闲 │ │
└───┬────┴───┬────┴────────┴───┬────┴───┬────┴────────┴───┬────┴────────┘
│ │ │ │ │
│ └──► freelist ────┘ └──► freelist ────┘
│ (空闲对象的前 8 字节存下一个空闲对象的地址)
└── 用户数据;空闲时这块空间被复用为指针
slab->objects = 32,slab->inuse = 12,slab->freelist 指向 obj 1
读图:空闲对象内部存的是「下一个空闲对象的地址」(freelist 指针),所以对象最小必须是 8 字节(64 位指针大小),且对齐要够。kmem_cache->offset 指明这个指针放在对象开头还是中间(开了 SLAB_TYPESAFE_BY_RCU 或构造器时可能放在中间)。用户拿到的指针指向对象起始,空闲指针被覆盖是安全的——因为对象此时没人用。
分配快路径(无锁):
/* mm/slub.c: slab_alloc_node() —— 6.8 节选,已简化 */
static __fastpath_inline void *slab_alloc_node(struct kmem_cache *s, gfp_t gfpflags,
int node, size_t orig_size,
unsigned long addr)
{
struct kmem_cache_cpu *c;
void *object;
c = raw_cpu_ptr(s->cpu_slab);
object = c->freelist;
if (unlikely(!object || !slab_matches_node(c->slab, node))) {
/* 慢路径:当前 slab 空了或节点不对 */
object = __slab_alloc_node(s, gfpflags, node, addr, c);
} else {
/* 从 per-CPU freelist 摘一个,用 cmpxchg_double 保证原子 */
void *next_object = get_freepointer_safe(s, object);
if (unlikely(!this_cpu_cmpxchg_double(
s->cpu_slab->freelist, s->cpu_slab->tid,
object, tid, next_object, next_tid(tid)))) {
note_cmpxchg_failure("slab_alloc", s, tid);
goto redo; /* 被中断/抢占打断,重试 */
}
prefetch_freepointer(s, next_object);
}
...
return object;
}
三个设计点:
- per-CPU freelist 让分配无锁。每个 CPU 有自己的
kmem_cache_cpu,从本地链表摘对象不需要任何锁;只有本地 slab 用完、需要向 node partial 链表要新 slab 时才拿node->list_lock。 cmpxchg_double防并发。中断可能在「读 freelist」与「写 freelist」之间打断,另一个上下文也来分配同一个对象。SLUB 用 128 位的原子比较交换同时更新freelist与tid,失败就重试。tid是事务号,防止 ABA。- slab 的
frozen状态。被某个 CPU 独占的 slab 是 frozen 的,其他 CPU 不会碰它;本地 freelist 空了,SLUB 先看c->partial,再看 node 的 partial 链表,最后才向伙伴系统要新页(new_slab()→alloc_slab_page())。
释放路径对称:slab_free() 把对象压回本地 freelist(do_slab_free()),如果这个 slab 变空且 CPU 已有别的当前 slab,就 discard_slab() 把它还给伙伴系统。SLUB 不维护「每 slab 的空闲计数」在快路径上,inuse 只在慢路径更新——这是它比 SLAB 快的原因之一。
CONFIG_SLUB_DEBUG 打开后,可以用启动参数 slub_debug=FZPU 开启:F(释放时检查)、Z(红区,检测越界写)、P(毒化,检测 use-after-free)、U(记录分配/释放的调用栈)。代价是每个对象变大、性能下降,只在排查时开。对应的运行时开关在 /sys/kernel/slab/<cache>/ 下。
cache 合并:为什么 slabinfo 里有 aliases
如果每个 kmem_cache_create() 都独占一组 slab,几百个驱动会造出几百个几乎一样的 cache(都是 64 B 对象),内存利用率会很差。SLUB 默认做cache 合并:把「对象大小、对齐、标志都兼容」的 cache 合成一个,多个名字指向同一个底层 cache。
$ ls /sys/kernel/slab/ | grep -c . # 本机有多少个 cache 目录
$ cat /sys/kernel/slab/kmalloc-64/aliases # 合并进来的 cache(地址形式)
合并的代价是调试困难:slub_debug 按底层 cache 开,合并后无法只调试自己的那一个。所以排查内存问题时常加启动参数 slab_nomerge 关掉合并,让每个 cache 独立。反过来,如果 /proc/slabinfo 里出现大量 active_objs=0 的小 cache,说明合并没生效,值得检查。
带构造器(ctor)、SLAB_TYPESAFE_BY_RCU、SLAB_ACCOUNT 的 cache 默认不参与合并,因为它们有额外的生命周期语义。
3.9.3 kmalloc 家族:怎么选、怎么不踩坑
| 接口 | 语义 | 备注 |
|---|---|---|
kmalloc(size, gfp) | 分配 size 字节 | 实际大小按 cache 向上取整 |
kzalloc(size, gfp) | 分配并清零 | 等价 kmalloc + __GFP_ZERO |
kcalloc(n, size, gfp) | 数组,带溢出检查 | 等价 kmalloc_array + 清零 |
kmalloc_array(n, size, gfp) | 数组,带溢出检查 | 失败返回 NULL |
krealloc(p, size, gfp) | 调整大小 | 可能搬移,返回新指针 |
kfree(p) | 释放 | 接受 NULL 与 ZERO_SIZE_PTR |
ksize(p) | 实际分配大小 | 只对 kmalloc 家族的指针有效 |
kmem_cache_alloc(c, gfp) | 从专用 cache 取对象 | 快,且类型安全 |
kmem_cache_zalloc(c, gfp) | 同上并清零 | |
kvmalloc(size, gfp) | 先 kmalloc 后 vmalloc | 释放用 kvfree |
kmalloc 的尺寸会被向上取整到最近的 cache。6.1 起有 kmalloc_size_roundup(size) 可以提前知道会被取整到多少,用于「按实际大小做统计/对齐」的场景。
专用 cache 相比 kmalloc 的三个好处:
- 缓存局部性:同类对象集中,减少 cache line 争用;
- 构造/析构:
kmem_cache_create(..., ctor)可以在分配/释放时初始化; - 可观测:
/proc/slabinfo里单独一行,泄漏一眼可见。
代价是要自己管理生命周期(模块卸载前必须 kmem_cache_destroy(),且此时不能有未释放对象)。
kfree()一个vmalloc()的指针:kfree只认 slab 地址,会算出错误的 slab 并崩溃。vmalloc的指针必须vfree()/kvfree()。ksize()用在vmalloc指针上:未定义。ksize只对 slab 对象有效。kmalloc(0)返回ZERO_SIZE_PTR((void *)16):不是NULL,kfree它是安全的,但解引用会崩。kmalloc_array(n, size)的整数溢出:n * size溢出会分配一小块却按大数组用。用kmalloc_array/kcalloc/struct_size(),它们内部用check_mul_overflow()。- 在原子上下文用
GFP_KERNEL:与 3.7 相同,会睡眠。中断里用GFP_ATOMIC,并接受可能失败。 kmem_cache_destroy()时还有对象未释放:内核会打印警告并泄漏整个 slab;模块卸载路径必须先释放全部对象。krealloc()后继续用旧指针:它可能搬移,旧指针失效。写p = krealloc(p, ...),且失败时p被覆盖成NULL会丢原指针——正确写法是先用临时变量。- 忘了
kfree的错误分支:内核没有 GC,每个goto err都要清理已分配的东西。devm_*系列能自动清理,但只在probe/remove生命周期内有效。
3.9.4 vmalloc:只要虚拟连续
vmalloc 在 vmalloc 区(x86-64 上 0xffffc90000000000 起,32 TiB)找一段空闲虚拟地址,然后逐页从伙伴系统分配物理页,再改内核页表把它们映射上去。物理页可以不连续,虚拟地址连续。
/* mm/vmalloc.c: __vmalloc_area_node() —— 6.8 节选,已简化 */
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
pgprot_t prot, unsigned int page_shift,
int node)
{
...
area->nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
area->pages = kmalloc_array(area->nr_pages, sizeof(*area->pages), GFP_KERNEL);
...
for (i = 0; i < area->nr_pages; i++) {
struct page *page;
...
page = __alloc_pages_node(node, gfp_mask, order);
if (unlikely(!page)) {
/* 失败:把已分配的页全部还回去 */
area->nr_pages = i;
goto fail;
}
area->pages[i] = page;
}
if (vmap_pages_range(addr, addr + get_vm_area_size(area), prot,
area->pages, page_shift) < 0)
goto fail;
...
}
关键点:
- 地址空间是稀缺资源:vmalloc 区 32 TiB 听起来很大,但每次分配至少要一页页表项,碎片化后可能找不到连续虚拟区间(
/proc/vmallocinfo能看)。 - TLB 压力大:每页一个 PTE,没有大页(除非
VM_ALLOW_HUGE_VMAP且架构支持);访问大块 vmalloc 内存的 TLB miss 比直接映射多。 - 分配可能睡眠:
__vmalloc_area_node里kmalloc_array与__alloc_pages_node都可能睡眠,所以vmalloc不能在原子上下文调用。 - 不能用于 DMA:物理不连续,设备无法直接访问(除非 IOMMU 做映射)。
vmalloc 的典型用户:模块代码(module_alloc())、内核栈(CONFIG_VMAP_STACK)、ioremap()、大缓冲区(如 seq_file 的大输出)、bpf_prog 的 JIT 镜像。
/proc/vmallocinfo 列出每一段 vmalloc 区域,是排查「vmalloc 区碎片」与「谁占了大块」的第一手材料:
0xffffc90000000000-0xffffc90000021000 135168 module_alloc+0x.../0x... phys=...
0xffffc90000021000-0xffffc90000022000 4096 vmalloc+0x.../0x... phys=...
0xffffc90000400000-0xffffc90000484000 540672 pcpu_alloc+0x.../0x... phys=...
三列分别是「虚拟地址范围、大小、调用者+物理页信息」。phys= 后面的一串页帧号能看出物理页有多散;ioremap 的条目会显示 ioremap 而不是 vmalloc,且 phys 是设备地址。
vmap() 是另一条路:把已经存在的页映射到虚拟连续区(不分配新页)。它用于「页已经有了,但需要连续虚拟地址」的场景,比如大页的拆分/合并、ioremap 的页表复用。
/* 常见组合 */
void *p = vmalloc(1024 * 1024); /* 1 MiB,物理离散 */
if (!p) return -ENOMEM;
memset(p, 0, 1024 * 1024);
vfree(p);
void *q = kvmalloc(1024 * 1024, GFP_KERNEL); /* 先试 kmalloc(大块通常失败),再 vmalloc */
kvfree(q); /* 必须用 kvfree,不能 kfree */
is_vmalloc_addr(addr) 可以判断一个地址是否落在 vmalloc 区;vmalloc_to_page(addr) 取对应 struct page。这两个函数在驱动里常用于「这个指针该怎么释放/能不能 DMA」的判断。
3.9.5 kmap 与高端内存:一段历史
32 位内核的虚拟地址空间只有 4 GiB,其中内核占 1 GiB(3G/1G 拆分),装不下全部物理内存。于是有了 ZONE_HIGHMEM:高地址的物理页没有固定的内核虚拟地址,用时必须临时映射:
/* 32 位时代的写法 */
void *kaddr = kmap(page); /* 建立临时映射(可能睡眠) */
memcpy(kaddr, src, len);
kunmap(page); /* 撤销 */
x86-64 没有高端内存——直接映射区覆盖全部物理内存,kmap() 退化成「返回 page_address()」。但 kmap_local_page() 仍然存在,因为「禁止迁移/抢占的短时映射」在 64 位上也有用(尤其是 CONFIG_HIGHPTE 与 per-CPU 映射)。新代码用 kmap_local_page()/kunmap_local(),不要用老的 kmap()(它可能睡眠,且在高分子页上语义复杂)。
3.9.6 mempool 与预留:OOM 时也要能前进
块层在内存紧张时必须能分配 bio,否则 I/O 无法进行,回收也就无法进行——死锁。mempool 就是为此设计的「保底池」:
/* 创建一个至少保留 4 个对象的池,底层用 kmem_cache */
mempool_t *pool = mempool_create(4, mempool_alloc_slab, mempool_free_slab, cache);
void *obj = mempool_alloc(pool, GFP_NOIO); /* 池空时用 GFP_NOIO 现分配 */
mempool_free(obj, pool);
mempool_alloc() 的语义:先从池里取;池空则用给定 gfp 现分配;再失败就等待(池的等待队列)。它保证「只要池里还有预留对象,分配就不会失败」,是 GFP_NOIO/GFP_ATOMIC 场景的保险。
其他预留机制:
| 机制 | 用途 |
|---|---|
MIGRATE_HIGHATOMIC | 给 GFP_ATOMIC 预留的高阶页块 |
| CMA | 给摄像头/GPU 预留的连续内存,可被 movable 页临时借用 |
dma_alloc_coherent | 设备 DMA 一致性内存,通常在低端 zone |
gen_pool | 设备侧 SRAM/专用内存的分配器 |
一个完整的 mempool 用例(块层、网络收包这类「不能失败」的路径常见):
/* 用自定义 cache 做底层,池里预留 8 个对象 */
static struct kmem_cache *req_cache;
static mempool_t *req_pool;
static int __init pool_demo_init(void)
{
req_cache = kmem_cache_create("demo_req", sizeof(struct demo_req),
0, SLAB_HWCACHE_ALIGN, NULL);
if (!req_cache)
return -ENOMEM;
req_pool = mempool_create(8, mempool_alloc_slab, mempool_free_slab, req_cache);
if (!req_pool) {
kmem_cache_destroy(req_cache);
return -ENOMEM;
}
return 0;
}
static void __exit pool_demo_exit(void)
{
mempool_destroy(req_pool); /* 先销毁池,再销毁 cache */
kmem_cache_destroy(req_cache);
}
顺序很重要:mempool_destroy() 会把池里预留的对象还回 cache,之后 kmem_cache_destroy() 才能成功;反过来会留下未释放对象并触发警告。
3.9.7 观测:slabinfo、slabtop、kmemleak
$ head -3 /proc/slabinfo
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables ...
kmalloc-64 12345 12672 64 64 1 : tunables ...
六列的含义:
| 列 | 含义 | 怎么用 |
|---|---|---|
active_objs | 已分配对象数 | 持续增长 = 可能泄漏 |
num_objs | 总对象数(含空闲) | 与 active 的差是空闲 |
objsize | 对象大小(含元数据) | 与请求大小的比值看浪费 |
objperslab | 每 slab 多少对象 | 4 KiB / objsize 取整 |
pagesperslab | 每 slab 多少页 | 大对象会 > 1 |
slabdata | 活跃/总 slab 数 | slab 数暴涨也是泄漏信号 |
slabtop 是 slabinfo 的交互版,按内存占用排序:
slabtop -s c -o # 按 cache 大小排序,输出一次
/sys/kernel/slab/<cache>/ 下有每个 cache 的详细计数(objects、slabs、cpu_slabs、partial、aliases),比 slabinfo 更细。
kmemleak 是内核的泄漏检测器(CONFIG_DEBUG_KMEMLEAK):它周期性扫描内存,把「没有任何指针指向的已分配块」标记为泄漏。
mount -t debugfs none /sys/kernel/debug
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak # 列出可疑块与分配调用栈
echo clear > /sys/kernel/debug/kmemleak
它的局限:扫描很慢、会误报(指针存在寄存器或未扫描区域时)、不能检测「还可达但逻辑上泄漏」的对象。生产环境用 KASAN(越界/UAF)与 slub_debug 更常见。
bpftrace 看分配热点:
# 按分配大小统计 kmalloc 次数(10 秒)
bpftrace -e 'tracepoint:kmem:kmalloc { @bytes[args->bytes_alloc] = count(); } interval:s:10 { exit(); }'
# 按调用栈统计 kmem_cache_alloc 的来源(需要栈符号)
bpftrace -e 'tracepoint:kmem:kmem_cache_alloc { @[kstack(5)] = count(); }'
kmem:kmalloc 的 args->bytes_req 是请求大小,args->bytes_alloc 是实际分配大小,两者之差就是 slab 取整的浪费。这个差值在调优「为什么 1 万个 100 字节对象吃了 4 MiB」时很有用。
3.9.8 实验:自定义 cache 与尺寸取整
模块创建自己的 kmem_cache,分配 100 个对象,同时用 kmalloc 分配一串不同大小,打印 ksize() 看取整结果:
// slab_demo.c — 自定义 kmem_cache 与 kmalloc 取整(Linux 6.8)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#define N_OBJ 100
struct demo_obj {
u64 id;
u64 payload[4];
char tag[16];
};
static struct kmem_cache *demo_cache;
static struct demo_obj *objs[N_OBJ];
static int __init slab_demo_init(void)
{
unsigned int i;
size_t sizes[] = { 8, 24, 33, 64, 100, 200, 1000, 4096, 8192 };
void *ptrs[ARRAY_SIZE(sizes)];
demo_cache = kmem_cache_create("demo_obj", sizeof(struct demo_obj),
__alignof__(struct demo_obj),
SLAB_HWCACHE_ALIGN, NULL);
if (!demo_cache)
return -ENOMEM;
for (i = 0; i < N_OBJ; i++) {
objs[i] = kmem_cache_alloc(demo_cache, GFP_KERNEL);
if (!objs[i]) {
pr_err("slab_demo: alloc failed at %u\n", i);
return -ENOMEM;
}
objs[i]->id = i;
}
pr_info("slab_demo: cache object_size=%zu size=%u\n",
sizeof(struct demo_obj), kmem_cache_size(demo_cache));
for (i = 0; i < ARRAY_SIZE(sizes); i++) {
ptrs[i] = kmalloc(sizes[i], GFP_KERNEL);
if (!ptrs[i]) {
pr_err("slab_demo: kmalloc(%zu) failed\n", sizes[i]);
continue;
}
pr_info("slab_demo: kmalloc(%4zu) -> ksize=%4zu\n",
sizes[i], ksize(ptrs[i]));
}
for (i = 0; i < ARRAY_SIZE(sizes); i++)
kfree(ptrs[i]);
return 0;
}
static void __exit slab_demo_exit(void)
{
unsigned int i;
for (i = 0; i < N_OBJ; i++) {
kmem_cache_free(demo_cache, objs[i]);
objs[i] = NULL;
}
kmem_cache_destroy(demo_cache);
pr_info("slab_demo: unloaded\n");
}
预期:sizeof(struct demo_obj) 是 56(8 + 32 + 16),而 kmem_cache_size() 可能因对齐返回 64;kmalloc(33) 的 ksize 是 64(对齐到 kmalloc-64),kmalloc(100) 是 128,kmalloc(1000) 是 1024。kmalloc(8192) 正好是 kmalloc-8k 的上限。
用户态读 slabinfo 并排序:
/* slabtop_lite.c — 按总占用给 slab cache 排序
* 编译: cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -O2 -o slabtop_lite slabtop_lite.c
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct entry { char name[64]; unsigned long objs, objsize, pages; };
static int cmp(const void *a, const void *b)
{
const struct entry *x = a, *y = b;
unsigned long sx = x->objs * x->objsize, sy = y->objs * y->objsize;
return sx < sy ? 1 : (sx > sy ? -1 : 0);
}
int main(void)
{
FILE *fp = fopen("/proc/slabinfo", "r");
struct entry e[512];
char line[256];
int n = 0;
if (!fp) { perror("/proc/slabinfo"); return 1; }
while (fgets(line, sizeof line, fp) && n < 512) {
unsigned long active, total, size, per_slab, pages;
char name[64];
if (line[0] == '#' || line[0] == '\n') continue;
if (sscanf(line, "%63s %lu %lu %lu %lu %lu",
name, &active, &total, &size, &per_slab, &pages) != 6)
continue;
snprintf(e[n].name, sizeof e[n].name, "%s", name);
e[n].objs = total; /* 用 total,反映已占用的 slab */
e[n].objsize = size;
e[n].pages = pages;
n++;
}
fclose(fp);
qsort(e, (size_t)n, sizeof e[0], cmp);
printf("%-20s %10s %8s %12s\n", "cache", "objects", "objsize", "total-KiB");
for (int i = 0; i < n && i < 15; i++)
printf("%-20s %10lu %8lu %12lu\n", e[i].name, e[i].objs,
e[i].objsize, e[i].objs * e[i].objsize / 1024);
return 0;
}
/proc/slabinfo 需要 root 才能读完整内容;普通用户会得到 Permission denied,这是 4.x 之后的安全限制(避免泄露内核堆布局)。
某驱动为每个请求 kmalloc(200) 存一个状态结构,10 万个并发请求。200 向上取整到 kmalloc-256,实际占用 25.6 MB 而不是 20 MB。后来结构体加了一个 u64 字段变成 208 字节,仍然落在 256 的桶里,没有额外代价;但如果再加到 257 字节,就会跳到 kmalloc-512,内存翻倍。
结论:在结构体大小接近 cache 边界时,加字段要算一下桶。pahole -C my_struct vmlinux 看实际大小,/proc/slabinfo 看桶的尺寸,两者一对就知道会落到哪。
3.9.9 SLUB 慢路径:当 per-CPU freelist 空了
这一节回答:c->freelist 为空时,SLUB 按什么顺序找新对象?cpu_partial、node partial、new_slab 三条路各自什么代价?为什么 slabinfo 里 active_objs 不大,num_objs 却很多?
3.9.2 只讲了快路径:从 kmem_cache_cpu->freelist 摘一个对象,cmpxchg_double 成功就返回。真实系统里快路径命中率很高,但一旦 miss,就会进入 __slab_alloc_node(),这条路径决定了 SLUB 的「内存效率」与「延迟长尾」。
先看两张表:cache 与 slab
struct kmem_cache 在 6.8 的字段(include/linux/slub_def.h,已删减):
| 字段 | 类型 | 含义 | 何时变 |
|---|---|---|---|
cpu_slab | struct kmem_cache_cpu __percpu * | 每 CPU 的当前 slab 与 freelist | 每次分配/释放 |
flags | slab_flags_t | SLAB_HWCACHE_ALIGN、SLAB_TYPESAFE_BY_RCU、SLAB_ACCOUNT 等 | 创建后固定 |
min_partial | unsigned long | node partial 链表最少保留的空 slab 数 | 创建时算 |
size | unsigned int | 含元数据的槽位大小(对齐后) | 创建后固定 |
object_size | unsigned int | 用户请求的对象大小 | 创建后固定 |
offset | unsigned int | freepointer 在对象内的偏移 | 创建时定 |
cpu_partial | unsigned int | 每 CPU partial 链表的目标对象数 | 创建时算 |
oo | struct kmem_cache_order_objects | order 与每 slab 对象数打包 | 创建时算 |
node | struct kmem_cache_node *node[MAX_NUMNODES] | 每 NUMA 节点的 partial 链表与计数 | 慢路径 |
random | unsigned int | 开启 CONFIG_SLAB_FREELIST_RANDOM 后的随机种子 | 创建时 |
ctor | void (*)(void *) | 构造器,每次分配新对象时调用 | 创建后固定 |
name | const char * | /proc/slabinfo 与 sysfs 里的名字 | 创建后固定 |
refcount | int | 引用计数,合并/销毁时用 | 创建/销毁 |
struct slab(6.8 中与 struct page 共享内存布局,字段语义如下):
| 字段 | 含义 |
|---|---|
slab_cache | 属于哪个 cache |
slab_list | 挂在 node partial 或 cpu partial 链表上 |
freelist | 这个 slab 的空闲对象链(frozen 时可能为空) |
inuse | 已分配对象数(16 位) |
objects | 总对象数(15 位) |
frozen | 是否被某个 CPU 独占(1 位) |
_refcount | slab 页的引用计数 |
inuse/objects/frozen 被打包进一个 unsigned long counters,用 cmpxchg_double 原子更新。这样「分配一个对象」不需要额外的锁,只要改 freelist 与 inuse。
慢路径的四条分支
__slab_alloc_node(s, gfp, node, addr, c)
├─ c->slab 还有对象,但 freelist 为空(被中断/竞争)→ deactivate_slab()
├─ c->partial 非空 → 取一个 partial slab,设为当前
├─ node partial 非空 → get_partial(),摘一个 slab
└─ 都没有 → new_slab() → allocate_slab() → alloc_pages()
对应源码(mm/slub.c,6.8,简化):
static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c)
{
void *freelist;
struct slab *slab;
slab = c->slab;
if (!slab)
goto new_slab;
/* 当前 slab 的 freelist 为空,但可能还有别的 CPU 释放的对象 */
freelist = get_freelist(s, slab);
if (!freelist) {
c->slab = NULL;
goto new_slab;
}
...
new_slab:
slab = c->partial;
if (slab) {
c->partial = slab->next;
c->slab = slab;
...
}
slab = get_partial(s, node, &pc); /* node partial 链表 */
if (slab)
goto load_freelist;
slab = new_slab(s, gfpflags, node); /* 向伙伴系统要页 */
...
}
get_partial() 先看本 node 的 partial 链表,再按 node 距离降级;new_slab() 调用 allocate_slab(),后者用 alloc_slab_page() 向伙伴系统要 oo_order(oo) 页。新 slab 的 inuse=0、frozen=1,直接成为当前 CPU 的 slab。
cpu partial:介于快路径与 node 之间
如果每次 per-CPU slab 用完都去抢 node->list_lock,多核扩展性会很差。SLUB 用 cpu_partial 做第二层缓存:当前 slab 空了,先把「还有空闲对象」的 slab 挂到 c->partial,攒到 cpu_partial 个再一次性 unfreeze_partials() 还给 node。
/* mm/slub.c: put_cpu_partial() —— 6.8 节选,简化 */
static void put_cpu_partial(struct kmem_cache *s, struct slab *slab, int drain)
{
struct slab *oldslab;
unsigned long flags;
local_lock_irqsave(&s->cpu_slab->lock, flags);
oldslab = this_cpu_read(s->cpu_slab->partial);
if (oldslab) {
if (drain && oldslab->slabs >= s->cpu_partial) {
/* 超过阈值:把整条 partial 链还给 node */
unfreeze_partials(s, oldslab);
oldslab = NULL;
}
}
slab->next = oldslab;
this_cpu_write(s->cpu_slab->partial, slab);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
}
cpu_partial 的目标值由 set_cpu_partial() 根据对象大小算:小对象多缓存(如 30 个 slab),大对象少缓存(如 2 个)。/sys/kernel/slab/<cache>/cpu_partial 可读,slabs_cpu_partial 显示当前每 CPU partial 的页数。
释放路径:free 不一定马上还页
kfree() → slab_free() → do_slab_free():
- 如果对象属于当前 CPU 的 slab,直接压回
c->freelist(快路径); - 否则检查是否是「本 CPU 的 frozen slab」,是就
cmpxchg_double压回; - 都不是,走
__slab_free():拿n->list_lock,inuse--;如果inuse==0且 slab 不在 partial 链表中,discard_slab()把整页还给伙伴系统。
关键点:slab 变空不等于立刻释放。SLUB 会把空 slab 留在 partial 链表里(最多 min_partial 个),以备下次分配;超过 min_partial 才 discard_slab()。这解释了 /proc/slabinfo 里 num_objs 远大于 active_objs:空闲对象还在 slab 里,页没有还给伙伴系统。
min_partial 的默认值由 set_min_partial() 算,通常 5;对小 cache 可能更小。它不是「缓存多少个对象」,而是「缓存多少个空 slab」,调大能减少页分配次数,但会占用更多内存。
sysfs:每个 cache 的体检报告
ls /sys/kernel/slab/kmalloc-64/
# align cpu_partial ctor objects objects_partial objs_per_slab
# order partial poison reclaim_account red_zone sanity_checks
# slab_size slabs slabs_cpu_partial store_user total_objects trace
cat /sys/kernel/slab/kmalloc-64/objects
cat /sys/kernel/slab/kmalloc-64/slabs
cat /sys/kernel/slab/kmalloc-64/objects_partial
cat /sys/kernel/slab/kmalloc-64/cpu_partial
读法:
objects是总对象数(含空闲),slabs * objs_per_slab应接近它;objects_partial是挂在 partial 链表上的对象数,长期很大说明「分配后长期不用」;slabs_cpu_partial是每 CPU partial 的页数,某个 CPU 特别大可能是释放不均衡;order是每个 slab 占2^order页,大对象 order>0。
- 以为
kmem_cache_alloc()一定不睡眠:GFP_KERNEL在慢路径要拿node->list_lock、可能alloc_pages()睡眠;只有GFP_ATOMIC且快路径命中时才不会睡。 - 在中断里用
kmem_cache_alloc(GFP_KERNEL):慢路径睡眠会触发BUG: sleeping function called from invalid context;中断里用GFP_ATOMIC并接受失败。 kmem_cache_destroy()前还有对象:SLUB 会警告kmem_cache_destroy ... called after all objects freed并拒绝销毁,cache 泄漏;卸载路径必须先释放全部对象。- 把
min_partial当成对象缓存数:它是空 slab 数,不是对象数;调大它会增加内存占用,不增加可分配对象。 slabinfo的active_objs等于「正在使用」:它包含已分配但被驱动缓存的对象;真正的泄漏要看「持续增长」而不是绝对值。- 以为
SLAB_TYPESAFE_BY_RCU的 cache 能随便合并:这类 cache 依赖 RCU 宽限期,默认不参与合并;强行合并会让kmem_cache_free后的对象提前复用,破坏 RCU 语义。
3.9.10 kmalloc 的大小类:从 8 字节到 8 KiB 的阶梯
这一节回答:kmalloc(65) 到底落在哪个 cache?为什么有 kmalloc-96 和 kmalloc-192 这种「非 2 的幂」?kmalloc_size_roundup() 在编译期能算出来吗?ksize() 为什么不能用在 vmalloc 指针上?
大小类表
6.8 的通用 kmalloc cache 由 kmalloc_info[] 描述(mm/slab_common.c)。在 4 KiB 页、64 位配置下,常见阶梯是:
| 请求大小范围 | cache 名 | 实际对象大小 | 每 slab 对象数(约) |
|---|---|---|---|
| 1–8 | kmalloc-8 | 8 | 512 |
| 9–16 | kmalloc-16 | 16 | 256 |
| 17–32 | kmalloc-32 | 32 | 128 |
| 33–64 | kmalloc-64 | 64 | 64 |
| 65–96 | kmalloc-96 | 96 | 42 |
| 97–128 | kmalloc-128 | 128 | 32 |
| 129–192 | kmalloc-192 | 192 | 21 |
| 193–256 | kmalloc-256 | 256 | 16 |
| 257–512 | kmalloc-512 | 512 | 8 |
| 513–1024 | kmalloc-1k | 1024 | 4 |
| 1025–2048 | kmalloc-2k | 2048 | 2 |
| 2049–4096 | kmalloc-4k | 4096 | 1 |
| 4097–8192 | kmalloc-8k | 8192 | 1(order-1) |
96 与 192 是为了减少内部碎片加进去的:一个 100 字节的结构,落到 128 会浪费 28 字节(22%),落到 96 的相邻档 128 也一样;但 65–96 这一段用 96 桶,比 128 桶省 25%。192 同理,覆盖 129–192。
kmalloc 的映射不是「查表循环」,而是 kmalloc_index() 的一串 if,编译器会把它优化成位运算/查表:
/* include/linux/slab.h —— 6.8 节选,简化 */
static __always_inline unsigned int kmalloc_index(size_t size)
{
if (!size)
return 0;
if (size <= KMALLOC_MIN_SIZE)
return KMALLOC_SHIFT_LOW; /* 8 字节 */
if (size <= 64)
return 6; /* 64 */
if (size <= 128)
return 7;
if (size <= 256)
return 8;
...
if (size <= 8 * 1024)
return 13; /* 8 KiB */
return -1;
}
KMALLOC_MIN_SIZE 在 64 位上通常是 8(指针大小),所以 kmalloc(1) 也占 8 字节。kmalloc(0) 返回 ZERO_SIZE_PTR((void *)16),不是 NULL;kfree(ZERO_SIZE_PTR) 是安全的,但解引用会崩。
编译期常量折叠
当 size 是编译期常量时,kmalloc() 会走 kmalloc_trace() 的常量版本,__builtin_constant_p(size) 为真,kmalloc_index() 在编译期算出常量,直接调用对应 cache 的 kmem_cache_alloc_trace(),省掉运行时查表。反汇编能看到 mov $cache_addr, %rdi,而不是一串比较。
这也是为什么「用变量 size 调 kmalloc」比「用常量」慢一点:不是慢在分配,而是慢在 kmalloc_slab() 的索引计算与分支预测。热路径上如果 size 固定,写成 kmalloc(sizeof(struct foo), gfp),不要用运行时变量。
kmalloc_size_roundup(size)(6.1+)返回「实际会落在哪个大小类」:
size_t actual = kmalloc_size_roundup(100); /* 128 */
void *p = kmalloc(100, GFP_KERNEL);
pr_info("requested=100 actual=%zu ksize=%zu\n", actual, ksize(p));
actual 与 ksize(p) 通常相等;但开了 CONFIG_SLAB_FREELIST_HARDENED/KASAN 后,ksize() 可能包含 redzone,kmalloc_size_roundup() 只给大小类,不含调试元数据。
ksize 的边界
ksize(p) 的实现是 __ksize():
/* mm/slub.c —— 6.8 节选,简化 */
size_t __ksize(const void *object)
{
struct slab *slab;
struct folio *folio;
if (unlikely(object == ZERO_SIZE_PTR))
return 0;
folio = virt_to_folio(object);
if (unlikely(!folio_test_slab(folio))) {
mem_dump_obj(object);
return 0;
}
slab = folio_slab(folio);
return slab->objects ? slab->size : 0;
}
它对「不是 slab 的指针」会打印 mem_dump_obj 并返回 0,而不是崩。但这只是防御,不代表可以传任意指针:
vmalloc()的指针不在直接映射区,virt_to_folio()会算出错误地址;ksize返回 0 或触发警告;dma_alloc_coherent()的指针可能是 IOMMU 映射的,不是 slab;- 栈指针、全局数组更不行。
所以规则是:ksize() 只用于 kmalloc/kmem_cache_alloc 返回的指针;kvmalloc() 的返回值要用 kvfree(),不要 ksize()。
某网络驱动每个包的状态结构是 184 字节,落在 kmalloc-192,每包 192 字节。加了一个 16 字节的统计字段后变成 200 字节,跳到 kmalloc-256,内存放大 33%。如果每秒 100 万包、每包生命周期 1 秒,常驻内存从 192 MB 涨到 256 MB。
修法不是「把字段删掉」,而是:
- 把统计字段移到 per-CPU 计数器,不放进 per-packet 结构;
- 用
pahole -C pkt_state vmlinux看空洞,重排字段把 200 压回 192; - 如果确实需要 200 字节,考虑专用
kmem_cache(object_size=200,SLUB 会按 200 对齐,可能仍占 256,但可用SLAB_HWCACHE_ALIGN控制)。
3.9.11 vmalloc 的地址空间:红黑树、guard page 与惰性释放
这一节回答:vmalloc(1 MiB) 为什么至少要占 1 MiB + 4 KiB 虚拟地址?内核怎么在 32 TiB 的 vmalloc 区里找一块空洞?vfree() 之后为什么页表不是立刻拆?
vmalloc 与 kmalloc 的根本区别是「物理连续」:kmalloc 从 slab 拿一段物理连续(页内或 order-n 页)内存,vmalloc 只保证虚拟地址连续,物理页一页一页从伙伴系统拿。代价是:
- 每页一个 PTE,TLB 压力大;
- 分配要建页表、刷 TLB,比
kmalloc慢; - 地址空间有限(x86-64 4 级页表约 32 TiB),碎片化后可能找不到连续虚拟区间;
- 不能用于 DMA(设备看物理地址)。
两个结构:vm_struct 与 vmap_area
/* include/linux/vmalloc.h —— 6.8 节选 */
struct vm_struct {
struct vm_struct *next;
void *addr;
unsigned long size;
unsigned long flags; /* VM_ALLOC / VM_IOREMAP / VM_MAP / VM_NO_GUARD */
struct page **pages;
unsigned int nr_pages;
phys_addr_t phys_addr; /* ioremap 用 */
const void *caller;
};
/* include/linux/vmalloc.h —— 6.8 节选 */
struct vmap_area {
unsigned long va_start;
unsigned long va_end;
struct rb_node rb_node; /* 挂在空闲/占用树上 */
struct list_head list; /* 按地址排序的链表 */
union {
unsigned long subtree_max_size; /* 空闲树:子树最大空洞 */
struct vm_struct *vm; /* 占用:对应的 vm_struct */
};
unsigned long flags;
};
两棵树的分工:
vmap_area_root:所有已占用区域,按va_start排序,用于查找「这个地址属于哪个 vmalloc 区」;free_vmap_area_root:所有空闲区域,按va_start排序,并用subtree_max_size做「最大空洞」剪枝,用于快速找到能容纳请求的空洞。
vm_struct 描述「这块虚拟内存是什么」,vmap_area 描述「这段地址空间被谁占了」。一个 vm_struct 对应一个 vmap_area,但 vmap_area 还可能表示纯地址预留(没有 vm)。
分配:__get_vm_area_node 与 alloc_vmap_area
/* mm/vmalloc.c: __get_vm_area_node() —— 6.8 节选,简化 */
static struct vm_struct *__get_vm_area_node(unsigned long size,
unsigned long align, unsigned long shift, unsigned long flags,
unsigned long start, unsigned long end, int node, gfp_t gfp_mask,
const void *caller)
{
struct vmap_area *va;
struct vm_struct *area;
size = PAGE_ALIGN(size);
if (unlikely(!size || size > (ULONG_MAX >> PAGE_SHIFT)))
return NULL;
/* 多分配一页做 guard,防止越界踩到相邻区域 */
if (!(flags & VM_NO_GUARD))
size += PAGE_SIZE;
area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
if (unlikely(!area))
return NULL;
va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
if (IS_ERR(va)) {
kfree(area);
return NULL;
}
setup_vmalloc_vm(area, va, flags, caller);
return area;
}
三个细节:
- guard page:默认多要一页,但不映射。越界写会命中未映射页,触发 page fault 而不是静默踩坏下一个 vmalloc 区。
VM_NO_GUARD用于「确定不会越界」的内核内部映射(如 per-cpu)。 - 对齐:
align可以是 1(页对齐)到PAGE_SIZE的幂;VM_ALLOC的shift支持 huge vmap 的 PMD 对齐。 - 地址范围:
start/end默认VMALLOC_START/VMALLOC_END;ioremap用VMALLOC_START到VMALLOC_END之间的一段,module_alloc用模块区。
alloc_vmap_area() 是核心。6.8 的实现比 4.x 复杂得多(有 free_vmap_cache、vmap_area_lock、lazy_max_pages),但思想仍是「在空闲树里找第一个 va_end - va_start >= size 的空洞」:
/* mm/vmalloc.c: alloc_vmap_area() —— 6.8 节选,极度简化 */
static struct vmap_area *alloc_vmap_area(unsigned long size, unsigned long align,
unsigned long vstart, unsigned long vend, int node, gfp_t gfp_mask)
{
struct vmap_area *va;
unsigned long addr;
va = kmem_cache_alloc(vmap_area_cachep, gfp_mask);
if (!va)
return ERR_PTR(-ENOMEM);
spin_lock(&vmap_area_lock);
addr = __alloc_vmap_area(&free_vmap_area_root, &free_vmap_area_list,
size, align, vstart, vend);
spin_unlock(&vmap_area_lock);
if (addr == vend)
goto overflow;
va->va_start = addr;
va->va_end = addr + size;
va->vm = NULL;
insert_vmap_area(va, &vmap_area_root, &vmap_area_list);
return va;
overflow:
kmem_cache_free(vmap_area_cachep, va);
return ERR_PTR(-EBUSY);
}
__alloc_vmap_area() 用 free_vmap_area_root 的 subtree_max_size 剪枝:如果某棵子树的最大空洞都小于 size,整棵子树跳过。这让查找从 O(n) 降到 O(log n),但最坏情况仍可能扫描很多节点——这就是 vmalloc 区碎片化后分配变慢的原因。
建映射:vmap_pages_range
拿到虚拟区间后,__vmalloc_area_node() 逐页分配物理页,存进 area->pages[],再 vmap_pages_range() 建 PTE:
/* mm/vmalloc.c: __vmalloc_area_node() —— 6.8 节选,简化 */
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
pgprot_t prot, unsigned int page_shift, int node)
{
struct page **pages;
unsigned int nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
unsigned int i;
pages = kmalloc_array(nr_pages, sizeof(*pages), GFP_KERNEL);
...
for (i = 0; i < nr_pages; i++) {
pages[i] = __alloc_pages_node(node, gfp_mask, 0);
if (!pages[i])
goto fail;
}
if (vmap_pages_range(area->addr, area->addr + get_vm_area_size(area),
prot, pages, page_shift) < 0)
goto fail;
area->pages = pages;
area->nr_pages = nr_pages;
return area->addr;
fail:
/* 把已分配的页逐页还回去,再释放 area 与 vmap_area */
...
}
vmap_pages_range() 走 vmap_pages_pte_range(),对每一页 alloc_pte() + set_pte_at()。中间任何一页失败都要回滚:已建的 PTE 要清、已分配的页要 __free_pages()、area->pages 数组要 kfree()。这条错误路径是 vmalloc 代码里最容易泄漏的地方。
释放:vfree 的惰性拆表
vfree() 不是「立刻拆页表、立刻还地址」:
remove_vm_area()把vmap_area从占用树摘掉,挂到vmap_area_root的待释放链表;__free_vmap_area()把地址区间还给free_vmap_area_root;- 页表拆除通过
free_unmap_vmap_area()延迟到vmap_area_lock之外,由purge_vmap_area_lazy()批量做; - 物理页
__free_pages()逐页还伙伴系统。
为什么要惰性?因为拆 PTE 需要 flush_tlb_kernel_range(),这是跨 CPU 的昂贵操作。把多个 vfree() 攒在一起,一次 flush 一大段,比每次 flush 一页快得多。代价是 vfree() 返回后,虚拟地址可能还在 TLB 里短暂存在——但内核不会立刻复用这段地址,所以是安全的。
/proc/vmallocinfo 能看到每段区域的「大小、调用者、物理页」:
0xffffc90000000000-0xffffc90000021000 135168 module_alloc+0x... phys=...
0xffffc90000021000-0xffffc90000022000 4096 vmalloc+0x... phys=...
0xffffc90000400000-0xffffc90000484000 540672 pcpu_alloc+0x... phys=...
phys= 后面是每页的物理地址(只显示前几页),能看出物理页有多散。如果一段 1 MiB 的 vmalloc 区有几百个不连续的 phys,说明物理内存碎片化严重;但这对 vmalloc 本身没影响,只影响依赖物理连续的下游(DMA、hugepage)。
vmap 与 ioremap:不分配页的映射
vmap() 把「已经存在的页」映射到虚拟连续区,不分配新页:
void *vmap(struct page **pages, unsigned int count, unsigned long flags, pgprot_t prot);
void vunmap(const void *addr);
典型用途:把散落的 page 拼成一段连续虚拟地址给 vm_map_ram/大页拆分;或把 alloc_pages() 拿到的复合页映射到 vmalloc 区。
ioremap() 是另一条路:它把设备物理地址映射到内核虚拟地址,页表项指向 MMIO 而不是 RAM:
void __iomem *ioremap(phys_addr_t phys_addr, size_t size);
void iounmap(volatile void __iomem *addr);
ioremap 的 vm_struct->flags 是 VM_IOREMAP,phys_addr 记录设备地址;它不能 vfree(),必须 iounmap()。devm_ioremap_resource() 是「request_mem_region + ioremap」的 devm 封装,驱动里优先用它。
vfree()一个kmalloc指针:vfree会按 vmalloc 区解析地址,算出错误的vmap_area,通常直接 panic;用kvfree()或is_vmalloc_addr()判断。kfree()一个vmalloc指针:同样错;kfree只认 slab,vmalloc指针必须vfree/kvfree。- 忘记 guard page 的存在:
vmalloc(4096)实际占 8192 虚拟地址;按「请求大小」算地址空间会低估一倍。 - 在原子上下文
vmalloc(GFP_ATOMIC):__vmalloc_area_node()要kmalloc_array、建页表,可能睡眠;GFP_ATOMIC也不保证不睡,应避免在中断里 vmalloc。 vmap()的 pages 生命周期比映射短:vunmap()之前调用者必须保证 pages 还有效;提前__free_pages()会让映射指向已释放页。ioremap()用vfree()释放:VM_IOREMAP区域必须iounmap();vfree会尝试释放不存在的pages[]。/proc/vmallocinfo里看到巨大vmap_area就以为泄漏:module_alloc、pcpu_alloc、vmemmap都会占大段;先看调用者,再看是否持续增长。
3.9.12 GFP 与回收语义:分配器什么时候可以等
这一节回答:GFP_KERNEL、GFP_ATOMIC、GFP_NOIO、GFP_NOWAIT 的差别不只是「能不能睡眠」,它们对回收器、文件系统、内存水位的影响各是什么?__GFP_NOFAIL 为什么被内核文档称为「几乎总是错的」?
gfp_t 是一个位掩码,分两类:
- zone modifier:从哪个 zone 拿(
__GFP_DMA、__GFP_DMA32、__GFP_HIGHMEM、__GFP_MOVABLE); - action modifier:分配行为(能否回收、能否 I/O、能否用紧急池、是否清零、是否计入 memcg)。
6.8 的常用组合:
| 标志 | 可睡眠 | 可直接回收 | 可发起 I/O | 可调 FS | 典型场景 |
|---|---|---|---|---|---|
GFP_ATOMIC | 否 | 否 | 否 | 否 | 硬中断、spinlock 临界区 |
GFP_NOWAIT | 否 | 否 | 否 | 否 | 不想等待的乐观分配 |
GFP_NOIO | 是 | 是 | 否 | 否 | 块层/SCSI 的回收路径 |
GFP_NOFS | 是 | 是 | 是 | 否 | 文件系统内部(避免递归) |
GFP_KERNEL | 是 | 是 | 是 | 是 | 大多数进程上下文 |
GFP_HIGHUSER_MOVABLE | 是 | 是 | 是 | 是 | 用户匿名页(可迁移) |
GFP_KERNEL_ACCOUNT | 是 | 是 | 是 | 是 | 计入 memcg 的内核对象 |
几个容易混淆的点:
GFP_NOFS不是「不睡眠」,它允许直接回收与 I/O,只是不允许调用文件系统,防止「文件系统持锁 → 分配内存 → 又进文件系统」递归死锁;GFP_NOIO更严格,连 I/O 都不允许,用于「回收路径本身要分配内存」的场景;GFP_ATOMIC不等于「一定成功」,它只是「不等待」,失败返回NULL;GFP_NOWAIT与GFP_ATOMIC的区别是后者可以用紧急保留页(__GFP_HIGH),前者不用。
current_gfp_context() 会在任务带有 PF_MEMALLOC_NOIO/PF_MEMALLOC_NOFS 时,把 GFP_KERNEL 降级成 GFP_NOIO/GFP_NOFS:
/* 进入可能递归的代码前,保存并设置上下文 */
unsigned int noio = memalloc_noio_save();
p = kmalloc(size, GFP_KERNEL); /* 实际按 GFP_NOIO 处理 */
memalloc_noio_restore(noio);
这比在每个调用点手写 GFP_NOIO 可靠,也不会漏掉深层调用。
回收标志
| 标志 | 含义 |
|---|---|
__GFP_DIRECT_RECLAIM | 允许分配者自己进入直接回收 |
__GFP_KSWAPD_RECLAIM | 允许唤醒 kswapd |
__GFP_RECLAIM | 上面两者之和 |
__GFP_NORETRY | 慢路径只试一次,失败就返回,不 OOM |
__GFP_RETRY_MAYFAIL | 可以重试,但失败不 OOM(大块分配常用) |
__GFP_NOFAIL | 无限重试直到成功;可能永久阻塞 |
__GFP_MEMALLOC | 允许使用紧急保留页(PF_MEMALLOC 路径) |
__GFP_NOMEMALLOC | 禁止使用紧急保留页 |
__GFP_ZERO | 返回前清零 |
__GFP_NOFAIL 的危险在于「成功」的代价:内存耗尽时它会一直重试,持有锁的任务可能永远不返回,其他任务跟着卡死。内核文档建议:只有在「失败会导致数据损坏/无法恢复,且调用者没有退路」时才用,并且不能用于 GFP_ATOMIC(原子上下文无法睡眠,重试没有意义)。
mempool:保底而不是加速
mempool 解决「回收路径必须能拿到内存」的问题。它维护一个预留对象池:
mempool_t *pool = mempool_create(4, mempool_alloc_slab, mempool_free_slab, cache);
void *obj = mempool_alloc(pool, GFP_NOIO);
mempool_free(obj, pool);
mempool_alloc() 的顺序:
- 池里有预留对象 → 直接取;
- 池空 → 用给定
gfp现分配; - 现分配失败 → 睡眠等别人
mempool_free()归还。
所以它保证「只要池里有对象,就不会失败」,但不保证「一定不睡眠」。mempool_destroy() 要在所有对象归还后调用,否则池里的对象会泄漏。
kvmalloc:先 slab 后 vmalloc
void *p = kvmalloc(size, GFP_KERNEL);
kvfree(p);
kvmalloc() 的逻辑:
size <= PAGE_SIZE:直接kmalloc;size较大:先kmalloc带__GFP_NORETRY | __GFP_NOWARN,失败再vmalloc;- 如果带
__GFP_ZERO,两条路径都会清零(kvmalloc内部处理,不要自己再 memset)。
kvfree() 用 is_vmalloc_addr() 判断走 vfree() 还是 kfree()。所以 kvmalloc 的返回值必须用 kvfree(),不能 kfree()/vfree() 二选一——你无法预知它走了哪条路。
- 在 spinlock 里用
GFP_KERNEL:可能睡眠,触发BUG: sleeping function called from invalid context;用GFP_ATOMIC。 - 在文件系统路径用
GFP_KERNEL递归分配:可能死锁;用GFP_NOFS或memalloc_nofs_save()。 - 把
__GFP_NOFAIL当「更可靠的 kmalloc」:它会把任务钉死在分配里,内存耗尽时无法 OOM;除非无退路,否则用__GFP_RETRY_MAYFAIL。 kvmalloc返回值用kfree:如果走了 vmalloc 分支,kfree会崩;必须kvfree。mempool_create()后忘记mempool_destroy():池里预留的对象不会自动还,cache 也无法销毁。GFP_ATOMIC分配大块:高阶原子分配几乎必失败(应急池很小),且失败后没有回收手段;大块分配应在进程上下文用GFP_KERNEL。
分配 API 总表:从页到对象
| API | 返回 | 大小 | 释放 | 上下文 |
|---|---|---|---|---|
alloc_pages(gfp, order) | struct page * | 2^order 页 | __free_pages | 可睡眠(GFP_KERNEL) |
__get_free_pages(gfp, order) | 虚拟地址 | 2^order 页 | free_pages | 直接映射区 |
kmalloc(size, gfp) | void * | ≤8 KiB 走 slab;更大走 page allocator(kmalloc_large),仍要求物理连续 | kfree | 可睡眠/原子 |
kzalloc | 清零指针 | 同上 | kfree | 同上 |
kvmalloc | 连续虚拟地址 | 大块 | kvfree | 可睡眠 |
vmalloc(size) | 虚拟连续 | 任意 | vfree | 可睡眠 |
kmem_cache_alloc(cache, gfp) | 对象 | 固定 | kmem_cache_free | 可睡眠/原子 |
dma_alloc_coherent | 物理连续 | 任意 | dma_free_coherent | 可睡眠 |
选型:小对象用 kmalloc/slab;大块但不必物理连续用 vmalloc;需要物理连续给设备用 CMA/dma_alloc_coherent;高端内存用 kmap_local_page。
3.9.13 实验:让 slab 增长、观察取整与泄漏
这一节回答:怎样用一个可卸载模块,把某个 cache 的 active_objs、num_objs、slabs 拉起来,再用 /proc/slabinfo、slabtop、bpftrace、kmemleak 四种证据确认「是缓存还是泄漏」?
模块:slab_grow.c
// slab_grow.c — 创建专用 cache 并分配 N 个对象;leak=1 时故意泄漏一个(Linux 6.8)
#include <linux/debugfs.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#define MAX_OBJS 4096
struct grow_obj {
u64 id;
u64 payload[4];
char tag[16];
};
static struct kmem_cache *grow_cache;
static void *objs[MAX_OBJS];
static unsigned int count = 256;
static bool leak;
static struct dentry *grow_dir;
module_param(count, uint, 0444);
module_param(leak, bool, 0444);
MODULE_PARM_DESC(count, "objects to allocate (max 4096)");
MODULE_PARM_DESC(leak, "leak one object on unload for kmemleak demo");
static ssize_t stats_read(struct file *file, char __user *ubuf,
size_t len, loff_t *ppos)
{
char buf[160];
int n;
n = scnprintf(buf, sizeof(buf),
"object_size=%zu cache_size=%u allocated=%u leak=%d\n",
sizeof(struct grow_obj), kmem_cache_size(grow_cache),
count, leak);
return simple_read_from_buffer(ubuf, len, ppos, buf, n);
}
static const struct file_operations stats_fops = {
.owner = THIS_MODULE,
.read = stats_read,
};
static int __init slab_grow_init(void)
{
unsigned int i;
int ret = 0;
if (count > MAX_OBJS)
count = MAX_OBJS;
grow_cache = kmem_cache_create("grow_obj", sizeof(struct grow_obj),
__alignof__(struct grow_obj),
SLAB_HWCACHE_ALIGN, NULL);
if (!grow_cache)
return -ENOMEM;
for (i = 0; i < count; i++) {
objs[i] = kmem_cache_alloc(grow_cache, GFP_KERNEL);
if (!objs[i]) {
ret = -ENOMEM;
count = i; /* 只释放已分配的部分 */
goto err_free;
}
*(u64 *)objs[i] = i;
}
grow_dir = debugfs_create_dir("slab_grow", NULL);
debugfs_create_file("stats", 0444, grow_dir, NULL, &stats_fops);
pr_info("slab_grow: cache_size=%u allocated=%u\n",
kmem_cache_size(grow_cache), count);
return 0;
err_free:
while (i--)
kmem_cache_free(grow_cache, objs[i]);
kmem_cache_destroy(grow_cache);
return ret;
}
static void __exit slab_grow_exit(void)
{
unsigned int i;
debugfs_remove_recursive(grow_dir);
for (i = 0; i < count; i++) {
if (leak && i == count - 1)
continue; /* 故意泄漏最后一个 */
kmem_cache_free(grow_cache, objs[i]);
}
kmem_cache_destroy(grow_cache);
pr_info("slab_grow: unloaded (leak=%d)\n", leak);
}
module_init(slab_grow_init);
module_exit(slab_grow_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("grow a dedicated slab cache and optionally leak one object");
加载并观察:
sudo insmod slab_grow.ko count=1024
cat /sys/kernel/debug/slab_grow/stats
# object_size=56 cache_size=64 allocated=1024 leak=0
grep grow_obj /proc/slabinfo
# grow_obj 1024 1024 64 64 1 : tunables ...
object_size=56 是 8 + 32 + 16;cache_size=64 是因为 SLAB_HWCACHE_ALIGN 把对象对齐到 cache line(64 字节)。1024 个对象 × 64 B = 64 KiB,objperslab=64,正好 16 个 slab。
用户态:slabstat.c
/* slabstat.c — 读 /proc/slabinfo,跟踪某个 cache 的 active/total 变化
* 编译: cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 -o slabstat slabstat.c
* 用法: sudo ./slabstat grow_obj
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static int read_one(const char *want, unsigned long *active,
unsigned long *total, unsigned long *size)
{
FILE *fp = fopen("/proc/slabinfo", "r");
char line[512];
int found = 0;
if (!fp) { perror("/proc/slabinfo"); return -1; }
while (fgets(line, sizeof line, fp)) {
char name[128];
unsigned long a, t, s, per, pages;
if (line[0] == '#')
continue;
if (sscanf(line, "%127s %lu %lu %lu %lu %lu",
name, &a, &t, &s, &per, &pages) != 6)
continue;
if (strcmp(name, want) == 0) {
*active = a; *total = t; *size = s;
found = 1;
break;
}
}
fclose(fp);
return found ? 0 : -1;
}
int main(int argc, char *argv[])
{
const char *name = argc > 1 ? argv[1] : "grow_obj";
unsigned long a1, t1, s1, a2, t2, s2;
if (read_one(name, &a1, &t1, &s1) < 0) {
fprintf(stderr, "cache %s not found (need root?)\n", name);
return 1;
}
printf("%-16s active=%lu total=%lu objsize=%lu\n", name, a1, t1, s1);
sleep(1);
if (read_one(name, &a2, &t2, &s2) < 0)
return 1;
printf("delta: active=%+ld total=%+ld\n",
(long)(a2 - a1), (long)(t2 - t1));
printf("estimated bytes: %lu\n", t2 * s2);
return 0;
}
运行:
sudo ./slabstat grow_obj
sudo slabtop -s c -o | head -15
slabstat 的 delta 在模块加载后应为 0;如果持续为正,说明有人不断分配不释放。slabtop -s c 按 cache 占用排序,grow_obj 会排在前面。
泄漏与 kmemleak
# 1. 故意泄漏一个对象
sudo rmmod slab_grow
sudo insmod slab_grow.ko count=64 leak=1
sudo rmmod slab_grow
# 2. 触发 kmemleak 扫描(需要 CONFIG_DEBUG_KMEMLEAK)
sudo sh -c 'echo scan > /sys/kernel/debug/kmemleak'
sudo cat /sys/kernel/debug/kmemleak
kmemleak 会打印泄漏对象的分配栈(kmem_cache_alloc → slab_grow_init)。注意:kmem_cache_destroy() 后 cache 本身被销毁,但泄漏的对象仍占着 slab 页;kmemleak 能抓到「没有指针指向的已分配块」,但需要 CONFIG_DEBUG_KMEMLEAK 且扫描时间足够。
bpftrace:按调用点统计分配
# 谁在分配 grow_obj(按调用栈)
sudo bpftrace -e '
tracepoint:kmem:kmem_cache_alloc /str(args->name) == "grow_obj"/
{ @[kstack(4)] = count(); }'
# kmalloc 的请求大小 vs 实际大小(看取整浪费)
sudo bpftrace -e '
tracepoint:kmem:kmalloc
{ @waste[args->bytes_req] = sum(args->bytes_alloc - args->bytes_req); }
interval:s:5 { exit(); }'
第一个脚本验证「分配确实来自 slab_grow_init」;第二个脚本统计「每个请求大小浪费了多少字节」,@waste[100] 很大说明大量 100 字节请求落在 128 桶。
vmalloc 区观测
# 按大小排序,看谁占了大段虚拟地址
sudo awk '{print $2, $4}' /proc/vmallocinfo | sort -k1 -nr | head -15
# 统计 vmalloc 区总大小
sudo awk '{sum += $2} END {printf "total=%d MiB\n", sum/1024/1024}' /proc/vmallocinfo
# 看某个模块的 vmalloc 段
sudo grep module_alloc /proc/vmallocinfo | head
如果 total 持续增长而模块没有增加,检查是否有驱动在热路径 vmalloc 后忘记 vfree;/proc/vmallocinfo 的调用者列能直接指到函数。
某服务运行一天后 slabtop 显示 kmalloc-4k 涨了 200 MB,但 ps 的 RSS 几乎不变。三个可能:
- 内核对象泄漏:驱动在错误路径漏了
kfree,slab 涨、进程 RSS 不涨;用slub_debug=U或 kmemleak 抓分配栈。 - page cache 的
struct page/radix tree 节点:文件缓存增长会带来kmalloc的 radix tree 节点,属于正常;看/proc/meminfo的Cached是否同步增长。 - memcg 统计口径:容器里
memory.current包含内核内存,ps的 RSS 不含;看memory.stat的slab字段。
先分清「slab 涨」与「进程内存涨」是两件事,再决定查驱动还是查缓存。
3.9.14 本章出口检查
| 检查项 | 达标线 | 没达标去哪 |
|---|---|---|
| 画出对象→slab→page→buddy | 能说出每层的分配单位 | 3.9.1、3.9.2 |
| 说清 SLUB 快路径 | per-CPU freelist + cmpxchg_double | 3.9.2 |
| 解释 freelist 存在哪 | 空闲对象内部的前 8 字节 | 3.9.2 |
| 按场景选接口 | 小对象/大块/物理连续各一例 | 3.9.1、3.9.3 |
说清 kfree 与 vfree 的边界 | vmalloc 指针不能 kfree | 3.9.4 |
读 slabinfo 六列 | 能指出哪列反映泄漏 | 3.9.7 |
| 用 kmemleak 或 bpftrace 定位 | 能给出命令与判读方法 | 3.9.7 |
| 说清 SLUB 慢路径三条分支 | cpu partial / node partial / new_slab | 3.9.9 |
| 算出 kmalloc 请求落在哪个桶 | 96/192 非 2 的幂、8 KiB 上限 | 3.9.10 |
| 解释 vmalloc 的 guard page | 为什么 4 KiB 请求占 8 KiB 地址 | 3.9.11 |
| 选对 GFP 与回收标志 | NOIO/NOFS/NORETRY 的边界 | 3.9.12 |
| 跑通 slab_grow 与 slabstat | 能读出 active/total 与泄漏 | 3.9.13 |
Q1 kmalloc(100, GFP_KERNEL) 实际从哪个 cache 分配?ksize() 返回多少?如果结构体从 100 字节涨到 130 字节,会怎样?
Q2 SLUB 的 cmpxchg_double 在防什么?没有它会出现什么错误?
Q3 为什么 vmalloc 不能用于 DMA?如果一定要用大块 DMA 缓冲,该用什么?
Q4 kvmalloc(64 * 1024, GFP_KERNEL) 通常走哪条路径?kvfree() 怎么知道该用 kfree 还是 vfree?
Q5 /proc/slabinfo 里某个 cache 的 active_objs 每小时涨 1000、num_objs 也跟着涨,但进程数稳定。最可能是什么问题?怎么确认?
答案
A1 落在 kmalloc-128(100 向上取整到 128),ksize() 返回 128。涨到 130 字节会跳到 kmalloc-192(内核有 96/192 这两个非 2 的幂桶),实际占用从 128 变 192,增加 50%。
A2 防「读 freelist 与写 freelist 之间被中断/抢占打断,另一个上下文分配了同一个对象」。没有它会导致两个调用者拿到同一块内存(double allocation),表现为神秘的数据覆盖。
A3 vmalloc 的物理页不连续,设备 DMA 引擎按物理地址访问,会读到错误的页。大块 DMA 缓冲要用 dma_alloc_coherent()(物理连续或 IOMMU 映射后的连续地址视图),或 alloc_pages 高阶 + dma_map_page。
A4 64 KiB 超过 kmalloc 上限(8 KiB),所以直接走 vmalloc。kvfree() 内部用 is_vmalloc_addr() 判断地址范围,决定调 vfree() 还是 kfree()——所以 kvmalloc 的返回值必须用 kvfree,不能混用。
A5 最可能是内核对象泄漏(某条错误路径忘了释放),也可能是缓存增长(如 dentry 缓存,属正常)。确认方法:① slabtop -s c 看是哪个 cache;② 开 slub_debug=U 或 kmemleak 拿分配调用栈;③ 用 bpftrace 统计该 cache 的 alloc/free 差值(kmem_cache_alloc 与 kmem_cache_free 按调用栈配对)。如果 alloc 持续大于 free,就是泄漏。
母本对应
| 母本 | 章节 | 本书落点 | 程度 |
|---|---|---|---|
| 奔跑吧 Linux 内核 | 第 2 章 2.5 slab 分配器 | 3.9.2–3.9.3 | 完整(SLUB 结构、快慢路径) |
| 奔跑吧 Linux 内核 | 第 2 章 2.6 vmalloc | 3.9.4 | 完整 |
| 奔跑吧 Linux 内核 | 第 2 章 2.19 总结内存管理数据结构和 API | 3.9.1、3.9.3、3.9.12.1 | 完整(分配 API 总表) |
| 奔跑吧 Linux 内核 | 第 6 章 6.4 内存检测 | 3.9.7 | 补充(kmemleak 与 slub_debug) |
| OSTEP | 第 17 章 空闲空间管理 | 3.9.2 | 背景(空闲链表思想) |
| 李治军 | 第 6 章 内存管理 | 3.9.1 | 背景(0.11 没有 slab,只有页) |
| 奔跑吧 Linux 内核 | 第 2 章 2.19 内存管理 API 总结 | 3.9.10、3.9.12 | 对照(4.x 的 kmalloc 与 GFP 组合) |
下一站:3.10 页回收、RMAP、KSM 与页面迁移 处理「内存不够」的另一半:LRU 怎么挑受害者、反向映射怎么找到所有映射、KSM 怎么合并相同页、Dirty COW 漏洞如何从这套机制里长出来。