上一篇的 buddy 只会按 2 的幂给页,内核里绝大多数分配是"给我 33 字节"这种碎需求.填这道缝的是对象分配器:把页切成等尺寸的对象小格,反复回收利用.如今这个角色是 SLUB,这篇把 kmalloc 的真实单价(和它的取整黑幕),自建 cache 的姿势,还有 vmalloc 这条特殊通道,全部用实测数据摊开.
这篇回答几件事:
- kmalloc(33) 到底占了多少内存
- 超过多大 kmalloc 就不再是 slab,走的什么路
- kmem_cache_create 什么时候值得用
- vmalloc 和 kmalloc 的适用边界
先看单价.kmalloc 按固定尺寸的桶分配,桶就那几档:8,16,32,64,96,128,192,256,512,1K,2K,4K,8K.请求 33 字节,落在 64 的桶里 — 白送 31 字节.想知道自己真实花了多少,ksize 一问便知,实测一串:
kmalloc( 1) -> ksize= 8
kmalloc( 33) -> ksize= 64
kmalloc( 65) -> ksize= 96
kmalloc( 200) -> ksize= 256
kmalloc( 8192) -> ksize= 8192
kmalloc(70000) -> ksize=131072
最后一行是彩蛋:70000 字节已经超过最大的 kmalloc-8k 桶,直接回落到上一章的 buddy 页分配器,order-5 块是 128KB,所以你拿到了 131072 字节 — 请求 70KB 实占 128KB,接近翻倍.这也是为什么 kmalloc 的文档口径一直是"小对象专用":几 KB 以上就该考虑 vmalloc 或者干脆按页要.做内存账,一律按 ksize 计价,不然报表全是假的.
SLUB 的实现哲学是"少管理".每个 cache(一个尺寸桶,或一个自建 cache)在每个 CPU 上有一个"活跃 slab":一整页或几页,内部用空闲对象自己当链表(freelist),分配就是取链头,一个原子操作搞定,连锁都常免.活跃 slab 用完换一页,旧页里对象全释放后整页还 buddy.对比它的前辈 SLAB 精心维护 per-CPU 数组和着色,SLUB 用最少的元数据换最短的快路径,这十年内核的偏心把它送成了唯一主流(SLOB 已经删除,SLAB 也可不编).slabinfo 的列能验证这套几何,kmalloc-64 这行:
kmalloc-64 6400 6400 64 64 1 : slabdata 100 100 0
对象 64 字节,每 slab 64 个,占 1 页 — 64×64 恰好 4096,一页切得干干净净.系统此刻有 100 个这样的 slab,6400 个对象全部活跃(active 等于 total),内核自己就是 64 字节对象的大户,struct 一把抓.
自建 cache 是固定尺寸高频对象的正解.kmem_cache_create("kblog_cache", 128, ...) 建一个 128 字节专用 cache,实测两个对象地址相差恰好 0x80 — 步长就是对象尺寸.它比 kmalloc 强在哪:对象可以连带初始化(constructor),专用 cache 的对象布局稳定,debug(slub_debug)可以按 cache 单独开,更关键的是尺寸贴身 — kmalloc 的桶取整在百万级对象下浪费惊人,自建 cache 一分不多.判断标准很简单:同一结构体类型的分配会同时存在成千上万份(inode,task_struct,dentry 全是这么管的),就值得一个专属 cache;临时一次性的缓冲,老实 kmalloc.
vmalloc 是另一条通道:虚拟地址连续,物理页随便凑.实测 vmalloc(1MB) 返回 0xffff8000 开头 — 1.4 篇认过的 vmalloc 区,模块就住在那.它的页是从 buddy 零敲碎打来的,再用页表缝成连续虚像,/proc/vmallocinfo 里每一段都记着分配者:调用栈符号,页数,属性,泄漏排查直接按调用者聚合.代价两张:一是每次访问都过页表(TLB 压力,且不能直接当 DMA 地址给不认识 scatter 的设备),二是分配和映射本身比 kmalloc 贵.所以适用面清晰:要的是"一大块连续的地址空间"而不是"一大块连续的物理内存"时 — 大数组,大哈希表,别的进程的栈,都用它;DMA 缓冲反着来,上一章说过那套.
最后留几个容易忽略的细节:
- kfree 和 kmem_cache_free 不能混,ksize 能反查尺寸但反查不了主人;free 错 cache 是内存破坏级的 bug,kmalloc 的对象只能 kfree.
- kzalloc 是 kmalloc 加 GFP_ZERO,高频路径里它比先 malloc 后 memset 略省(GFP_ZERO 走构造路径批量清),更重要的是省一行代码和一次漏清零的风险.
- kmem_cache_alloc 的对象不保证清零,前一个主人留下的内容原样奉上 — 从 slab 里"复活"的数据泄漏是真实漏洞形态,不确定就显式 memset.
- SLUB 的 freepointer 藏在对象内部(空闲对象的第一个字),所以对象尺寸会因 CONFIG_SLAB_FREELIST_HARDENED 等配置微涨,同一段代码在不同配置内核的 ksize 可能不同档.
- /proc/slabinfo 的 tunables 列在现代内核已经全部失效(SLUB 不用那些旋钮),看到 0 0 0 别去调.
- vmalloc 最小分配单元是页,vmalloc(1) 实占一页加映射开销;小对象用它是暴殄天物.
- execmem,modules 这些区域在 vmallocinfo 里单列属性,内存布局排查时先看它再想别的.