想象你经营一家快递驿站。每天都有成千上万个巴掌大的小包裹——文件描述符、网络连接、进程描述符——被取走、又送回。如果每次存取都跑去城市大仓库(伙伴系统)翻找,仓库会被逼疯:大仓库最小只肯租 4KB 的整页纸箱,寄一支笔也要占一个大箱,大量空间白白空着(内部碎片)。更别提每次取件还要在大仓库里搜索合并,慢得让人抓狂。于是你决定按包裹尺寸定制一排排专用储物柜,柜门预先编号、串成空闲清单,小件存取只需伸手一拿一放。这就是 Slab 分配器的思路。后来你发现厚厚的柜子管理本太繁琐,便升级成 Slub 精简流程;在街角小店,又摆起了 SLOB 式流动摊。本文就用这座驿站作比,帮你彻底看懂 Slab 的对象布局、着色、每 CPU 缓存,以及 Slub、SLOB 与对象池模式的取舍。

岗位关联

掌握 Slab/Slub 与对象缓存思想,你才能读懂 /proc/slabinfo,在高并发服务内存优化、Linux 内核调优、对象池设计等场景中做出靠谱的工程决策,成为团队里真正懂内存的人。

目录
  1. 可视化演示
  2. 1. 分配困境:小包裹的大难题
  3. 2. Slab架构:驿站的储物柜系统
  4. 3. 对象布局:柜内空间怎么排
  5. 4. 着色对齐:错峰散场防拥堵
  6. 5. per-CPU缓存:收件员的手边货架
  7. 6. Slub优化:精简版驿站
  8. 7. SLOB精简:地摊式空间利用
  9. 8. 对象池决策:什么时候租柜子
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

1. 分配困境:小包裹的大难题

驿站刚开业时,你以为找大仓库帮忙就行——所有包裹都从城市仓库里取。可仓库最小只肯按“整页纸箱”(4KB 物理页)出货,而内核中最常取的包裹往往只有几十到几百字节:一个文件描述符、一个网络缓冲区、一个进程控制块。为寄一支笔租一个整箱,纸箱里大部分空间空着,这就是内部碎片;而且每次都要在仓库的库存表里来回合并、拆分,取件路径长、锁竞争激烈。

从驿站回到内核:伙伴系统(buddy system)以页为最小单位管理物理内存,对于小于一页的对象,直接分配会浪费大量空间。更糟的是,高频创建销毁的小对象会让伙伴系统频繁分裂与合并,分配延迟被放大。于是核心思路浮现:与其每次都向仓库申请一整页,不如预先申请一些页,把它们切成一模一样的小格子,需要时直接递给客户。

所以,驿站的第一步不是改造仓库,而是设计储物柜:在伙伴系统之上,按包裹尺寸预切空间。这个“预切”动作,正是 slab 缓存思想的起点。

碎片率=Lslab mod LobjLslab\displaystyle \text{碎片率} = \frac{L_{slab} \bmod L_{obj}}{L_{slab}}
简单说:一整块 slab 空间按对象大小切完后,剩下的“边角料”占比越小越好。
  1. 对象大小$L_{obj}$ 表示每个缓存对象占用的字节数,比如一个文件描述符结构体就是包裹的尺寸。
    LobjL_{obj}
  2. 余数计算$L_{slab} \bmod L_{obj}$ 是总空间整除对象大小后剩下来的余数,就像切豆腐剩下的碎渣。
    Lslab mod LobjL_{slab} \bmod L_{obj}
  3. 碎片率余数除以总空间得到内部碎片比例;slab 分配器通过选择合适的 slab 总大小来让这个比例尽量低。
    Lslab mod LobjLslab\frac{L_{slab} \bmod L_{obj}}{L_{slab}}

2. Slab架构:驿站的储物柜系统

有了预切想法后,你开始分门别类地设立柜区:文件包裹专门放第 1 区,网络包裹放第 2 区,进程描述符放第 3 区。每个区都有统一的格子规格,并且装订一本台账,记录哪些柜全满、哪些半空、哪些完全清空。内核里,这类“台账”就是 kmem_cache。

内核为每一种对象类型建立一个 kmem_cache,它知道对象大小、对齐方式、构造函数等元信息,并维护若干 slab。每个 slab 是一块连续的物理内存(通常由伙伴系统分配的一页或几页),被切成等大小的小格。slab 自身有三种状态:全满(full)、半满(partial)、全空(empty)。分配时优先从半满列表取;释放时若 slab 变空,可以归还给伙伴系统或留在空列表备用。

为什么要维护三态链表?因为满的 slab 没有空闲对象,不值得反复扫描;空 slab 可以释放内存;半满 slab 是“中间缓冲区”,分配释放的命中率高。举个直观公式:一个 slab 能容纳多少个对象,取决于去掉管理头部后,剩余空间除以“对象大小 + 对齐填充”。

驿站台账越来越厚,但好处是取件逻辑极简单:先看半满列表,拿一个空闲格,更新台账。这就是 Slab 架构的核心骨架。

Nobj=⌊Lslab−LheadLobj+Aalign⌋\displaystyle N_{obj} = \left\lfloor \frac{L_{slab} - L_{head}}{L_{obj} + A_{align}} \right\rfloor
通俗讲:柜区总长度减去前台占位,除以每个柜格的实际占用(含隔板厚度),向下取整就是能放几个柜子。
  1. 可用空间$L_{slab} - L_{head}$ 是 slab 总长度减去管理头部后,留给对象柜格的净长度。
    Lslab−LheadL_{slab} - L_{head}
  2. 单个柜格占用$L_{obj} + A_{align}$ 表示每个对象本体加上对齐填充后的完整宽度,就像柜格之间要留隔板。
    Lobj+AalignL_{obj} + A_{align}
  3. 向下取整$\left\lfloor \cdot \right\rfloor$ 表示取整数部分,因为不可能放半个对象;余下的空间就成了内部碎片或着色区。
    ⌊Lslab−LheadLobj+Aalign⌋\left\lfloor \frac{L_{slab} - L_{head}}{L_{obj} + A_{align}} \right\rfloor

3. 对象布局:柜内空间怎么排

走进驿站内部,你会发现每个柜门背后都挂着一块小登记牌:如果柜子是空的,登记牌上就写着“下一个空柜在哪”,所有空柜通过这条线索串成一条链。你从链头取一个空柜,再把链头指向下一个;顾客还回包裹时,空柜重新挂到链头。这条“空柜链”就是 freelist。

在 slab 内部,布局依次是:管理头部(记录 slab 状态、freelist 头等)、对象区、对齐填充区。对象区被等间距切开,对象与对象之间没有额外盒子,只靠固定步长定位。空闲对象用 freelist 串联,分配时从表头弹出,释放时压回表头——都是 O(1) 操作。

一个 slab 的内存总占用可以表示为头部、对象区和填充区的和。填充区既是为了对齐,也可以被“着色”使用(见下一节)。这个公式帮助理解 slab 的物理构成:

当柜子全部排布好后,另一个问题浮现:所有柜区如果起点对齐,会不会出现“不同柜区的相同柜号在 CPU 缓存里打架”?这就是着色要解决的冲突。

Lslab=Lhead+Nobj×Lobj+Lfill\displaystyle L_{slab} = L_{head} + N_{obj} \times L_{obj} + L_{fill}
意思是:柜区总面积 = 前台占位 + 所有柜格面积 + 走道/隔板面积。
  1. 头部$L_{head}$ 是 slab 管理头的大小,相当于驿站前台登记桌。
    LheadL_{head}
  2. 对象区$N_{obj} \times L_{obj}$ 是所有对象占用的总空间,柜格越多、格子越大,占用越高。
    Nobj×LobjN_{obj} \times L_{obj}
  3. 填充区$L_{fill}$ 是对齐填充或着色预留区,保证每个对象地址对齐到合适的硬件边界。
    LfillL_{fill}

4. 着色对齐:错峰散场防拥堵

想象两栋宿舍楼的房间号完全对齐:1 层 101、2 层 201……如果每次散场所有人都从同一个楼梯口挤出去,必然拥堵。CPU 硬件缓存也有类似的“楼梯口”:L1 缓存被分成若干组(set),不同物理地址若映射到同一组,就会互相驱逐,导致缓存命中率下降。Slab 分配器用“着色”来错峰。

着色(cache coloring)的做法是:给不同 slab 的起始地址加上不同的偏移量(color),让每个 slab 里对象的实际地址在缓存中散开。虽然对象在 slab 内部仍然等距排列,但 slab 与 slab 之间,同序号的格子不再对齐到同一个缓存组,从而减少冲突。

颜色数不是无限的,它受 slab 中可用于偏移的填充空间大小限制。可以理解为楼梯口的错峰档位不能超过宿舍楼能腾出的缓冲区。对象 i 的地址计算如下:

着色看似只挪动了一点点位置,却能显著降低缓存冲突。不过,传统 SLAB 的着色也带来了额外管理复杂度,后来的 SLUB 干脆把它砍掉了。缓存优化真是不断做减法的艺术。

addri=base+color+i×Sobj\displaystyle \text{addr}_i = \text{base} + \text{color} + i \times S_{obj}
即第 i 个柜门的实际位置 = 区块起点 + 错峰偏移 + 柜子相对步长。
  1. 基址base 是 slab 分配到的物理内存起始地址,相当于一栋宿舍楼的入口。
    base\text{base}
  2. 着色偏移color 是本次 slab 选定的偏移量,取值随着 slab 序号在 0 到最大颜色数之间轮转。
    color\text{color}
  3. 对象步长$i \times S_{obj}$ 是第 $i$ 个对象相对基址的常规偏移,步长就是对象大小。
    i×Sobji \times S_{obj}

5. per-CPU缓存:收件员的手边货架

储物柜再多,如果所有收件员都跑去同一个柜区取件,还是要排队。于是你给每位收件员配了一个手边小货架:常用柜格先抓一把放在手边,客户来了直接递,不用每次跑大柜区。手边货架空了,再跑去柜区批量补货。这个“手边货架”就是 per-CPU 缓存。

在内核里,每个 CPU 都维护自己的 kmem_cache_cpu 结构,内部有 freelist 和当前 slab 指针。分配对象时,优先从本 CPU 的 freelist 取,全程无锁,因为不会有其他 CPU 同时碰这个列表。只有当本地 freelist 为空时,才需要从 slab 的 partial 链表批量转移一批对象过来,这个批大小就是 batchcount。

per-CPU 缓存还涉及“冷热页”概念:刚分配的页是热的,因为它在 CPU 缓存里还热乎着;放回本地 freelist 时保持热度,等下次复用。CPU 本地缓存命中率决定了性能上限。命中率公式非常直观:

每个收件员各忙各的,互不干扰,分配路径既快又免锁。这就是为什么内核在大型服务器上特别依赖 per-CPU 缓存来支撑高并发。

H=NhitNhit+Nmiss\displaystyle H = \frac{N_{hit}}{N_{hit} + N_{miss}}
所有取件请求中,从手边货架直接递出去的占比越高,流程越顺。
  1. 命中数$N_{hit}$ 表示从 per-CPU freelist 成功分配的次数,等于手边货架直接取货次数。
    NhitN_{hit}
  2. 未命中数$N_{miss}$ 表示本地空仓、需要回主柜区补货的次数。
    NmissN_{miss}
  3. 命中率命中次数占所有请求次数的比例,越接近 1,说明本地缓存越有效。
    NhitNhit+Nmiss\frac{N_{hit}}{N_{hit} + N_{miss}}

6. Slub优化:精简版驿站

传统 SLAB 是个功能齐全的驿站:有厚厚的台账、复杂的柜门登记牌、不同颜色的着色标记。但后来你发现,大多数时候用到的只是“从手边货架拿一个、放一个”,台账和着色反而拖慢了效率。于是你升级成 Slub:把能砍的都砍掉。

SLUB 首先去掉了 per-slab 的独立管理对象,不再为每个 slab 额外分配头部元数据。空闲链表的指针直接存放在空闲对象内部——反正对象空闲时里面也没数据,正好用来记“下一个空柜在哪”。这样既省内存,又避免额外的 cache line 访问。其次,SLUB 去掉了着色机制,因为现代 CPU 的随机映射和大型 cache 让着色收益变小。同时,SLUB 保留了 per-CPU freelist,并引入 cpu_partial 链表:本地缓存可以持有多个部分空闲的 slab,避免频繁回写全局链表。

理论上,SLUB 中 slab 的额外内存开销相比传统 SLAB 大幅下降:

SLUB 凭借简洁的代码路径和较低的内存开销,已成为 Linux 内核默认的 slab 系分配器。驿站的升级故事告诉我们:好的系统设计,往往是把不必要的东西减干净。

Slab overhead={Lhead+Lcolortraditional SLAB0SLUB (freelist inside object)\displaystyle \text{Slab overhead} =\begin{cases}L_{head} + L_{color} & \text{traditional SLAB} \\0 & \text{SLUB (freelist inside object)}\end{cases}
传统 SLAB 需要额外头部 + 着色区,而 SLUB 把 freelist 塞进空对象,实现了零额外管理内存。
  1. 传统 SLAB 开销$L_{head} + L_{color}$ 是头部和着色区占用的额外空间,柜子还没装货,先花了装修费。
    Lhead+LcolorL_{head} + L_{color}
  2. SLUB 开销0 表示没有独立管理头和着色区,因为空对象自身就是空的柜子,正好存放指针。
    00
  3. 条件选择cases 结构按分配器类型二选一,体现 SLUB 精简后为 0 的额外开销。
    {Lhead+Lcolortraditional SLAB0SLUB\begin{cases} L_{head} + L_{color} & \text{traditional SLAB} \\ 0 & \text{SLUB} \end{cases}

7. SLOB精简:地摊式空间利用

如果你开的是街角迷你店,根本放不下一排排大柜子。这时你会选择“地摊模式”:不预先定制柜格,包裹来了,看哪块空地能塞下就放哪;顾客走了,空出来的地方再给下一位用。这就是 SLOB(Simple List Of Blocks)的哲学。

SLOB 不维护 kmem_cache 和 per-CPU 缓存,而是维护一个已分配/空闲块组成的链表。分配时在链表上寻找足够大的空闲块,找到后切出一块,剩余部分放回链表。它用最少的元数据换来了极高的空间利用率,非常适合内存极度受限的嵌入式系统、启动早期等场景。代价是分配路径需要遍历链表,速度明显慢,且长时间使用后可能产生外部碎片。

空间利用率可以这样衡量:

SLOB 不是性能之王,而是“空间第一”的极端特长生。它告诉你:当条件苛刻时,可抛弃缓存和预分配,用最朴素的链表换生存空间。

U=SusedStotal\displaystyle U = \frac{S_{used}}{S_{total}}
地摊的总面积里,真正被商品占用的比例越高,说明空间利用越“抠”。
  1. 已用空间$S_{used}$ 是当前被对象占用的字节数,相当于摊位上被货物占用的面积。
    SusedS_{used}
  2. 总空间$S_{total}$ 是 SLOB 管理的全部内存,包括空闲块和链表指针的占地。
    StotalS_{total}
  3. 利用率两者相除得到空间利用率,SLOB 的目标是让这个值尽可能高,哪怕牺牲速度。
    U=SusedStotalU = \frac{S_{used}}{S_{total}}

8. 对象池决策:什么时候租柜子

看到 Slab 的好处后,你可能会把所有东西都塞进柜子。但驿站经营久了,你发现并不是所有包裹都值得租柜:如果一个月才来一个特大件,订制一整套柜子反而浪费场地。对象池模式的决策,本质上就是评估“是否值得预分配”。

对象池复用对象,避免重复构造和析构。内核 slab 是对象池模式的经典实现;用户态的网络服务器、游戏引擎、AI 推理框架也大量使用对象池。到底要不要用,看三个条件:对象请求频率是否高、对象是否小于一页、构造/析构是否昂贵。如果三个都满足,对象池收益显著;如果都是低频大块,直接走伙伴系统或 malloc 反而更好。

工程上还要调优:slab 的 order(一次分配几页)、align(对齐字节)、batchcount(每批补货数量)、cpu_partial 阈值。监控方面看 /proc/slabinfo、/sys/kernel/slab/<name> 中的对象数量、slab 数量、批次命中情况。收益大小可以抽象成以下加速比:

Slab、SLUB、SLOB 其实是一套决策树:内存充足且追求速度,选 SLUB;内存极紧,选 SLOB;需要特殊构造行为,再回到 SLAB。驿站柜子不是越多越好,而是在正确的位置,用正确的柜型,服务正确频率的客人。

Speedup=Tconstruct+TdestroyTacquire+Trelease\displaystyle \text{Speedup} = \frac{T_{construct} + T_{destroy}}{T_{acquire} + T_{release}}
如果“新建+销毁”的时间远大于“从池里拿+还”的时间,池化就越划算。
  1. 传统开销$T_{construct} + T_{destroy}$ 是每次全新创建和销毁对象的总时间,相当于重新包装、拆包装。
    Tconstruct+TdestroyT_{construct} + T_{destroy}
  2. 池化开销$T_{acquire} + T_{release}$ 是从缓存池取出和归还的时间,相当于从手边货架一拿一放。
    Tacquire+TreleaseT_{acquire} + T_{release}
  3. 加速比两者相除,结果越大,说明对象池越值得;若接近 1,则不值得付出额外内存。
    Speedup=Tconstruct+TdestroyTacquire+Trelease\text{Speedup} = \frac{T_{construct} + T_{destroy}}{T_{acquire} + T_{release}}

AI 应用场景

场景1:大模型推理服务的 KV Cache 对象池

阶段:推理服务部署与内存管理

解决的问题:大模型推理时,每请求都会为 Key/Value 缓存申请核心内存块,高频创建释放带来内存碎片和延迟抖动。传统方式直接分配块,在大并发下锁竞争严重。

具体案例:在使用 vLLM、TensorRT-LLM 等推理框架时,引擎会预先分配一批固定大小的 KV block,维护空闲 block 链表;请求到来直接从池中取,结束后归还。这和 slab 针对小对象预构造对象的思路如出一辙:快递驿站提前备好规格统一的储物柜,客人来了即取即走。

场景2:深度学习训练 Pipeline 中的张量池

阶段:数据加载与预处理

解决的问题:DataLoader 每个 batch 都要创建/销毁大量小张量、numpy 数组,Python 层分配开销拖慢训练吞吐。用对象池复用固定大小的 buffer,可显著降低分配次数。

具体案例:在 CPU 数据预处理中,预分配一个 batch buffer 池,数据增强结果写入空闲 buffer,训练迭代完成后归还。好比在训练流水线里为每个员工配了手边货架,不用每次都去大仓库申请。

场景3:Linux 内核 Slab 参数调优保障 AI 集群稳定性

阶段:基础设施运维与稳定性保障

解决的问题:AI 集群中高并发网络收发(socket、sk_buff 等小对象)会让内核 slab 内存快速增长,分配锁竞争加剧。通过调整内核参数可以提升缓存命中率。

具体案例:运维在压测时观察 /proc/slabinfo,动态调整 slub_min_objects、cpu_partial 等参数,将每 CPU 缓存命中率从 80% 提升到 95% 以上,网络吞吐显著改善。这正是 Slab/ Slub 的 per-CPU 批量转移思想在基础设施层的落地。

Python 演示

python50 行
import random  class Slab:    def __init__(self, capacity):        self.capacity = capacity        self.free = list(range(capacity))  class SlubCache:    def __init__(self, cpu_limit=8):        self.cpu_limit = cpu_limit        self.slabs = [Slab(cpu_limit * 4)]        self.cpu_free = []        self.hits = 0        self.misses = 0     def _refill(self):        slab = self.slabs[-1]        if not slab.free:            slab = Slab(self.cpu_limit * 4)            self.slabs.append(slab)        for _ in range(self.cpu_limit):            if not slab.free:                break            self.cpu_free.append(slab.free.pop())     def allocate(self):        if not self.cpu_free:            self.misses += 1            self._refill()        else:            self.hits += 1        return self.cpu_free.pop()     def free(self, ptr):        if len(self.cpu_free) >= self.cpu_limit:            self.slabs[-1].free.append(ptr)        else:            self.cpu_free.append(ptr)  cache = SlubCache(cpu_limit=8)for _ in range(20000):    p = cache.allocate()    cache.free(p) print("命中次数:", cache.hits)print("慢路径次数:", cache.misses)print("命中率: %.1f%%" % (cache.hits / (cache.hits + cache.misses) * 100))
目的:用 50 行左右模拟 SLUB 的 per-CPU 缓存 + slab 批量补货逻辑,直观观察“手边货架”带来的高命中率。
  1. self.slabs = [Slab(cpu_limit * 4)]初始化一个 slab,相当于租下第一批储物柜,可容纳 cpu_limit * 4 个对象。
  2. self.cpu_free = []这是每个 CPU 的免锁手边货架,先空着。
  3. def _refill(self):本地缓存空了,从 slab 批量搬运 cpu_limit 个空闲对象到货架上。
  4. if not slab.free:当前 slab 的空闲列表空了,就新建一个 slab,模拟向伙伴系统申请新页。
  5. def allocate(self):分配时优先操作 cpu_free,没货才走慢路径。
  6. self.hits += 1` / `self.misses += 1分别记录手边货架命中和补货次数。
  7. def free(self, ptr):归还时若手边货架已满,则把对象放回 slab 空闲列表,保持货架不堆爆。
  8. cache.free(cache.allocate())模拟“取一个还一个”的高频分配释放模式,最终统计命中率。

常见误区

误区以为内核只要有了 Slab,所有内存分配都走 Slab,伙伴系统已经没用了。

正解Slab 是建立在伙伴系统之上的“二次加工”层。伙伴系统是大仓库,按页管理物理内存;Slab 是驿站,从仓库整页领货后切成小格。大块分配(如 DMA、页表)仍然直接由伙伴系统处理,Slab 并不能取代它。

为何常见:学习时只盯着 slab 内部图,忽略了 slab 的内存来源,加上现代内核默认 SLUB,让人误以为 Slab 是唯一分配器。

误区SLUB 既然是“简化版 Slab”,那 per-CPU 缓存应该也被简化没了,所有对象都直接在主链表上分配。

正解SLUB 恰恰保留了最核心的 per-CPU freelist,还引入了 cpu_partial 链表,让每个 CPU 可以持有多个部分空闲的 slab。它砍掉的是传统 SLAB 的管理头、着色等“装修工程”,而不是“手边货架”这个性能命脉。

为何常见:“简化”被误读为“删减所有缓存”,但实际简化的是冗余结构,不是关键优化路径。

误区看到“cache coloring”这个词,以为是给对象涂颜色、让内存好看。

正解着色是对 slab 起始地址做偏移,让不同 slab 中同序号的格子错开映射到 CPU 缓存的不同组,减少缓存冲突。它不改变总内存大小,只调整对象的相对位置,如同错峰散场疏散。

为何常见:“color”在中文语境里太容易联想到视觉装饰,而忘记它是硬件 cache set 冲突优化术语。

误区SLOB 只维护简单链表,少了很多缓存和管理头,直觉上应该非常快。

正解SLOB 为了省内存牺牲了速度:分配时需要遍历空闲块链表找合适位置,且没有 per-CPU 缓存,多核高并发下还要保护全局链表。它只在内存容量极小时使用,性能通常远不如 SLUB。

为何常见:简单 = 快的直觉在这里失效,因为速度瓶颈不是结构复杂度,而是查找路径和锁竞争。

误区学了对象池模式后,把所有对象都放进池里,认为一定能提升性能。

正解对象池省去了构造/析构和分配器元数据开销,但会长期占住内存。如果对象请求频率极低、对象很大、构造开销很小,池化的内存浪费和复杂度反而会成为负担。正确做法是像驿站评估“要不要租柜”一样,看频率、尺寸、初始化代价。

为何常见:只看到对象池的成功案例,忽略它适用的边界条件,盲目套用。

面试题

Q1. Slab 分配器主要解决什么场景?

单选题 · easy

  • A.大块连续内存分配
  • B.小对象频繁分配释放← 正确答案
  • C.虚拟地址空间管理
  • D.文件系统日志恢复

答案:B。Slab 的核心目标是处理小对象高频创建/销毁,像驿站为小包裹预置统一柜格。大块内存交给伙伴系统,虚拟地址管理是 VMA 的事,日志恢复与 Slab 无关。

Q2. Linux 默认使用的 slab 系分配器是?

单选题 · easy

  • A.SLOB 分配器
  • B.SLAB 分配器
  • C.SLUB 分配器← 正确答案
  • D.Buddy 分配器

答案:C。SLUB 是传统 Slab 的精简优化版,目前是 Linux 官方默认分配器。SLOB 仅用于极端嵌入式场景,Buddy 是底层页分配器而非 slab 系。

Q3. kmem_cache 最主要的作用是什么?

单选题 · easy

  • A.记录已分配的物理页
  • B.管理一类对象的缓存池← 正确答案
  • C.负责用户态内存回收
  • D.加速磁盘 I/O 调度

答案:B。kmem_cache 相当于驿站中某一类储物柜的台账,记录对象大小、slab 列表和构造信息,专门管理一种对象类型的缓存池。

Q4. 在 Slab 语境下,“内部碎片”通常指什么?

单选题 · mid

  • A.已经释放但未归还的内存
  • B.slab 切分后剩下的零头← 正确答案
  • C.硬件缓存中的冗余行
  • D.进程间无法共享的内存

答案:B。内部碎片来自分配单位大于需求时浪费的空间。slab 总大小按对象整除后余下的边角料就是典型内部碎片,伙伴系统按页分配小对象时更严重。

Q5. slab 着色(coloring)的主要目标是?

单选题 · mid

  • A.让 CPU 缓存命中率下降
  • B.避免不同 slab 发生缓存冲突← 正确答案
  • C.使对象地址对所有进程可见
  • D.保证 slab 页在物理上连续

答案:B。给不同 slab 增加错峰偏移,让同序号对象尽量落进不同 cache set,减少相互驱逐。它不是为了“好看”,而是为了提升缓存命中率。

Q6. per-CPU 缓存带来的最大好处是什么?

单选题 · mid

  • A.提高内存总容量上限
  • B.免锁的高频分配路径← 正确答案
  • C.自动压缩内存碎片
  • D.支持 NUMA 节点迁移

答案:B。每个 CPU 操作自己的 freelist 不需要加锁,分配和释放路径极短,这是高并发下性能的关键。它不增加总容量,也不负责碎片压缩。

Q7. 下列哪些属于对象池模式的优势?

多选题 · mid

  • A.减少构造析构开销← 正确答案
  • B.缩短分配路径延迟← 正确答案
  • C.支持大页内存分配
  • D.降低内部碎片总量← 正确答案

答案:ABD。对象池复用已有对象,避免反复构造/析构;常配合 per-CPU 缓存降低锁开销;预切固定大小减少零头碎片。C 与大页分配无必然关系,对象池并不天然支持大页。

Q8. SLUB 的 cpu_partial 链表上存放的是什么?

单选题 · hard

  • A.完全空闲的 slab
  • B.部分空闲的 slab← 正确答案
  • C.全部已满的 slab
  • D.正在使用的单个对象

答案:B。cpu_partial 让每个 CPU 持有一点“半货柜”,本地缓存快空时可快速从中取货,不必立刻访问全局 partial 链表。满的 slab 没有空闲对象,不会出现在 partial 链上。

Q9. SLUB 在缓存对象尺寸小于指针大小时,如何维护 freelist?

单选题 · extreme

  • A.放弃空闲链表的支持
  • B.将相邻对象合并处理← 正确答案
  • C.额外分配管理内存
  • D.使用哈希表记录空闲

答案:B。对象太小,内部放不下一个指针,SLUB 会把相邻的几个空闲对象合并成更大的虚拟对象,再把指针存进去。就像几个小格子临时拼成一个大格子来写“下一空柜”编号。

Q10. 关于 SLOB 分配器的特点,下列说法正确的是?

多选题 · hard

  • A.面向嵌入式设备← 正确答案
  • B.不使用固定缓存← 正确答案
  • C.内存占用率低← 正确答案
  • D.分配速度最快

答案:ABC。SLOB 为省内存不建缓存,直接在空闲块链表上查找切分,空间利用率高但速度慢。它在地摊式管理下没有固定储物柜,因此分配路径要遍历链表,D 恰好说反。