Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 KiBGFP_KERNEL 可以,GFP_ATOMIC 不行
slabkmalloc/kmem_cache_alloc页内连续,对象不跨页是8 B – 8 KiB同上
vmallocvmalloc/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」的折中,适合「通常不大、偶尔很大」的缓冲区。

定义 3.9.1 · slab 与缓存

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;
}

三个设计点:

  1. per-CPU freelist 让分配无锁。每个 CPU 有自己的 kmem_cache_cpu,从本地链表摘对象不需要任何锁;只有本地 slab 用完、需要向 node partial 链表要新 slab 时才拿 node->list_lock。
  2. cmpxchg_double 防并发。中断可能在「读 freelist」与「写 freelist」之间打断,另一个上下文也来分配同一个对象。SLUB 用 128 位的原子比较交换同时更新 freelist 与 tid,失败就重试。tid 是事务号,防止 ABA。
  3. 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 快的原因之一。

SLUB 的调试设施

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 的三个好处:

  1. 缓存局部性:同类对象集中,减少 cache line 争用;
  2. 构造/析构:kmem_cache_create(..., ctor) 可以在分配/释放时初始化;
  3. 可观测:/proc/slabinfo 里单独一行,泄漏一眼可见。

代价是要自己管理生命周期(模块卸载前必须 kmem_cache_destroy(),且此时不能有未释放对象)。

内核分配的八个坑
  1. kfree() 一个 vmalloc() 的指针:kfree 只认 slab 地址,会算出错误的 slab 并崩溃。vmalloc 的指针必须 vfree()/kvfree()。
  2. ksize() 用在 vmalloc 指针上:未定义。ksize 只对 slab 对象有效。
  3. kmalloc(0) 返回 ZERO_SIZE_PTR((void *)16):不是 NULL,kfree 它是安全的,但解引用会崩。
  4. kmalloc_array(n, size) 的整数溢出:n * size 溢出会分配一小块却按大数组用。用 kmalloc_array/kcalloc/struct_size(),它们内部用 check_mul_overflow()。
  5. 在原子上下文用 GFP_KERNEL:与 3.7 相同,会睡眠。中断里用 GFP_ATOMIC,并接受可能失败。
  6. kmem_cache_destroy() 时还有对象未释放:内核会打印警告并泄漏整个 slab;模块卸载路径必须先释放全部对象。
  7. krealloc() 后继续用旧指针:它可能搬移,旧指针失效。写 p = krealloc(p, ...),且失败时 p 被覆盖成 NULL 会丢原指针——正确写法是先用临时变量。
  8. 忘了 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 之后的安全限制(避免泄露内核堆布局)。

例 3.9.1 · 一次 kmalloc 取整造成的 3 倍放大

某驱动为每个请求 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_slabstruct kmem_cache_cpu __percpu *每 CPU 的当前 slab 与 freelist每次分配/释放
flagsslab_flags_tSLAB_HWCACHE_ALIGN、SLAB_TYPESAFE_BY_RCU、SLAB_ACCOUNT 等创建后固定
min_partialunsigned longnode partial 链表最少保留的空 slab 数创建时算
sizeunsigned int含元数据的槽位大小(对齐后)创建后固定
object_sizeunsigned int用户请求的对象大小创建后固定
offsetunsigned intfreepointer 在对象内的偏移创建时定
cpu_partialunsigned int每 CPU partial 链表的目标对象数创建时算
oostruct kmem_cache_order_objectsorder 与每 slab 对象数打包创建时算
nodestruct kmem_cache_node *node[MAX_NUMNODES]每 NUMA 节点的 partial 链表与计数慢路径
randomunsigned int开启 CONFIG_SLAB_FREELIST_RANDOM 后的随机种子创建时
ctorvoid (*)(void *)构造器,每次分配新对象时调用创建后固定
nameconst char */proc/slabinfo 与 sysfs 里的名字创建后固定
refcountint引用计数,合并/销毁时用创建/销毁

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 位)
_refcountslab 页的引用计数

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():

  1. 如果对象属于当前 CPU 的 slab,直接压回 c->freelist(快路径);
  2. 否则检查是否是「本 CPU 的 frozen slab」,是就 cmpxchg_double 压回;
  3. 都不是,走 __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。
SLUB 慢路径的六个坑
  1. 以为 kmem_cache_alloc() 一定不睡眠:GFP_KERNEL 在慢路径要拿 node->list_lock、可能 alloc_pages() 睡眠;只有 GFP_ATOMIC 且快路径命中时才不会睡。
  2. 在中断里用 kmem_cache_alloc(GFP_KERNEL):慢路径睡眠会触发 BUG: sleeping function called from invalid context;中断里用 GFP_ATOMIC 并接受失败。
  3. kmem_cache_destroy() 前还有对象:SLUB 会警告 kmem_cache_destroy ... called after all objects freed 并拒绝销毁,cache 泄漏;卸载路径必须先释放全部对象。
  4. 把 min_partial 当成对象缓存数:它是空 slab 数,不是对象数;调大它会增加内存占用,不增加可分配对象。
  5. slabinfo 的 active_objs 等于「正在使用」:它包含已分配但被驱动缓存的对象;真正的泄漏要看「持续增长」而不是绝对值。
  6. 以为 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–8kmalloc-88512
9–16kmalloc-1616256
17–32kmalloc-3232128
33–64kmalloc-646464
65–96kmalloc-969642
97–128kmalloc-12812832
129–192kmalloc-19219221
193–256kmalloc-25625616
257–512kmalloc-5125128
513–1024kmalloc-1k10244
1025–2048kmalloc-2k20482
2049–4096kmalloc-4k40961
4097–8192kmalloc-8k81921(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()。

例 3.9.1 · 结构体跨过 192 字节的代价

某网络驱动每个包的状态结构是 184 字节,落在 kmalloc-192,每包 192 字节。加了一个 16 字节的统计字段后变成 200 字节,跳到 kmalloc-256,内存放大 33%。如果每秒 100 万包、每包生命周期 1 秒,常驻内存从 192 MB 涨到 256 MB。

修法不是「把字段删掉」,而是:

  1. 把统计字段移到 per-CPU 计数器,不放进 per-packet 结构;
  2. 用 pahole -C pkt_state vmlinux 看空洞,重排字段把 200 压回 192;
  3. 如果确实需要 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;
}

三个细节:

  1. guard page:默认多要一页,但不映射。越界写会命中未映射页,触发 page fault 而不是静默踩坏下一个 vmalloc 区。VM_NO_GUARD 用于「确定不会越界」的内核内部映射(如 per-cpu)。
  2. 对齐:align 可以是 1(页对齐)到 PAGE_SIZE 的幂;VM_ALLOC 的 shift 支持 huge vmap 的 PMD 对齐。
  3. 地址范围: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() 不是「立刻拆页表、立刻还地址」:

  1. remove_vm_area() 把 vmap_area 从占用树摘掉,挂到 vmap_area_root 的待释放链表;
  2. __free_vmap_area() 把地址区间还给 free_vmap_area_root;
  3. 页表拆除通过 free_unmap_vmap_area() 延迟到 vmap_area_lock 之外,由 purge_vmap_area_lazy() 批量做;
  4. 物理页 __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 封装,驱动里优先用它。

vmalloc 的七个坑
  1. vfree() 一个 kmalloc 指针:vfree 会按 vmalloc 区解析地址,算出错误的 vmap_area,通常直接 panic;用 kvfree() 或 is_vmalloc_addr() 判断。
  2. kfree() 一个 vmalloc 指针:同样错;kfree 只认 slab,vmalloc 指针必须 vfree/kvfree。
  3. 忘记 guard page 的存在:vmalloc(4096) 实际占 8192 虚拟地址;按「请求大小」算地址空间会低估一倍。
  4. 在原子上下文 vmalloc(GFP_ATOMIC):__vmalloc_area_node() 要 kmalloc_array、建页表,可能睡眠;GFP_ATOMIC 也不保证不睡,应避免在中断里 vmalloc。
  5. vmap() 的 pages 生命周期比映射短:vunmap() 之前调用者必须保证 pages 还有效;提前 __free_pages() 会让映射指向已释放页。
  6. ioremap() 用 vfree() 释放:VM_IOREMAP 区域必须 iounmap();vfree 会尝试释放不存在的 pages[]。
  7. /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() 的顺序:

  1. 池里有预留对象 → 直接取;
  2. 池空 → 用给定 gfp 现分配;
  3. 现分配失败 → 睡眠等别人 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() 二选一——你无法预知它走了哪条路。

GFP 的六个坑
  1. 在 spinlock 里用 GFP_KERNEL:可能睡眠,触发 BUG: sleeping function called from invalid context;用 GFP_ATOMIC。
  2. 在文件系统路径用 GFP_KERNEL 递归分配:可能死锁;用 GFP_NOFS 或 memalloc_nofs_save()。
  3. 把 __GFP_NOFAIL 当「更可靠的 kmalloc」:它会把任务钉死在分配里,内存耗尽时无法 OOM;除非无退路,否则用 __GFP_RETRY_MAYFAIL。
  4. kvmalloc 返回值用 kfree:如果走了 vmalloc 分支,kfree 会崩;必须 kvfree。
  5. mempool_create() 后忘记 mempool_destroy():池里预留的对象不会自动还,cache 也无法销毁。
  6. 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 的调用者列能直接指到函数。

例 3.9.2 · 「slab 涨了 200 MB,但进程 RSS 没涨」

某服务运行一天后 slabtop 显示 kmalloc-4k 涨了 200 MB,但 ps 的 RSS 几乎不变。三个可能:

  1. 内核对象泄漏:驱动在错误路径漏了 kfree,slab 涨、进程 RSS 不涨;用 slub_debug=U 或 kmemleak 抓分配栈。
  2. page cache 的 struct page/radix tree 节点:文件缓存增长会带来 kmalloc 的 radix tree 节点,属于正常;看 /proc/meminfo 的 Cached 是否同步增长。
  3. 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_double3.9.2
解释 freelist 存在哪空闲对象内部的前 8 字节3.9.2
按场景选接口小对象/大块/物理连续各一例3.9.1、3.9.3
说清 kfree 与 vfree 的边界vmalloc 指针不能 kfree3.9.4
读 slabinfo 六列能指出哪列反映泄漏3.9.7
用 kmemleak 或 bpftrace 定位能给出命令与判读方法3.9.7
说清 SLUB 慢路径三条分支cpu partial / node partial / new_slab3.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
即时自测 · 3.9

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 vmalloc3.9.4完整
奔跑吧 Linux 内核第 2 章 2.19 总结内存管理数据结构和 API3.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 漏洞如何从这套机制里长出来。