Linux 7.2 slab 延迟构建 freelist 优化:内存分配性能提升达 70% 的原理与实践

Linux 7.2 slab 延迟构建 freelist 优化:内存分配性能提升达 70% 的原理与实践 最近在排查一个线上服务的内存问题时,又和slab打上了交道。看着/proc/meminfo里居高不下的SUnreclaim,以及slabtop里那些密密麻麻的kmalloc-*缓存,一个老问题又浮上心头:内核里这些为了“高效”而生的内存缓存,真的足够高效吗?或者说,它们的“高效”是不是也付出了我们看不见的代价?这个疑问,在 Linux 内核 7.2 版本的一个底层重构中,似乎找到了一个有趣的答案。这次改动直指slab分配器的核心机制之一——freelist的构建时机。官方数据显示,在某些场景下,这项优化能让单次内存分配的速度提升最高达 70%。这听起来有点反直觉:slab本身就是为了快速分配而设计的缓存,怎么还能有这么大的提升空间?这“延迟构建freelist”的背后,到底动了哪块蛋糕,又解决了什么样真实存在的性能瓶颈?1. 先别急着看优化:理解slab和freelist的“传统艺能”要理解这次优化的价值,我们得先回到问题的起点:在没有优化之前,slab分配器是怎么工作的,以及freelist在其中扮演了什么角色。1.1slab的本质:内核的“对象池”你可以把slab分配器想象成内核内部的一个高级“对象池”管理器。它的核心目标,是解决内核频繁申请和释放小块内存(比如struct file,struct dentry,struct inode等)时带来的两个经典问题:内存碎片:频繁的、不同大小的内存分配释放,容易在物理内存中产生大量无法被利用的小空隙。初始化开销:每次分配内存后,内核数据结构都需要初始化(清零、设置链表头等),这个开销在频繁操作下不容忽视。slab的解决方案很聪明:它预先向伙伴系统(buddy system)申请一整页或几页连续的内存(称为一个slab),然后在这块内存上,切割出大量大小完全相同的“槽位”(object)。所有同类型的对象(比如所有的struct file)都从对应的slab缓存(比如filp_cachep)中分配。这样,内存不仅大小固定、地址连续,减少了外部碎片,还可以在对象释放时,通过一些技巧(如SLAB_POISON)来快速检测内存错误,而不是立即归还给系统。1.2freelist:slab的“空闲车位指示牌”在一个slab内部,如何快速知道哪个object是空闲的、可以分配出去的呢?这就是freelist的职责。在传统的实现中(我们称之为“立即构建freelist”),当一个slab被新创建出来时,内核会做这样几件事:从伙伴系统拿到干净的物理页。立刻遍历这个slab里的每一个object。为每个object计算下一个空闲object的地址,并将这个地址写入当前object的起始位置(或某个特定偏移处),从而串起一条单向链表。链表的头指针保存在slab的管理结构里。这个过程,相当于在停车场刚建好时,管理员就立刻给每一个车位挂上一个牌子,牌子上写着“下一个空闲车位是X号”。当需要分配时,管理员(分配函数)只需要看一眼链表头,就知道该把车停到哪个车位,然后更新链表头为牌子上的下一个车位地址,速度极快。听起来很完美,不是吗?