TaterLi 个人博客

[内核学习]SLUB与kmalloc

上一篇的 buddy 只会按 2 的幂给页,内核里绝大多数分配是"给我 33 字节"这种碎需求.填这道缝的是对象分配器:把页切成等尺寸的对象小格,反复回收利用.如今这个角色是 SLUB,这篇把 kmalloc 的真实单价(和它的取整黑幕),自建 cache 的姿势,还有 vmalloc 这条特殊通道,全部用实测数据摊开.

这篇回答几件事:

先看单价.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 缓冲反着来,上一章说过那套.

最后留几个容易忽略的细节:

退出移动版