什么是 Slab 系统?

Slab 系统是 Linux 内核中用于管理小块内存分配的一种机制,最初由 Jeff Bonwick 在 Solaris 操作系统中提出,后来被 Linux 采纳并优化。它构建在伙伴系统之上,专门解决伙伴系统在分配小块内存(小于一页,如 4KB)时产生的内部碎片问题。

  • 核心目标

    • 高效分配和回收小对象(比如内核中的结构体,如 task_structinode 等)。
    • 减少内部碎片。
    • 提高分配性能,避免频繁调用伙伴系统。
  • 基本概念

    • Slab 系统将内存划分为多个 slab,每个 slab 是一块连续的内存(通常由伙伴系统分配的页面组成),专门用于存储某一类固定大小的对象。
    • 它类似于一个“对象缓存”,预先分配并切割好内存块,应用程序或内核直接从中取用。

Slab 系统的工作原理

Slab 系统通过以下几个关键组件和步骤实现内存管理:

1. 基本结构

  • Cache(缓存)
    • 每个 slab 缓存对应一种对象类型(如 kmalloc-64 用于 64 字节对象,task_struct 用于进程描述符)。
    • 缓存包含多个 slab,管理对象的分配和回收。
  • Slab
    • 一个 slab 通常是一个或多个连续页面(比如 4KB),由伙伴系统分配。
    • Slab 被切割成多个固定大小的对象(比如 64 字节、128 字节等)。
  • Object(对象)
    • Slab 中的最小分配单位,就是实际分配给调用者的内存块。

2. 三种 Slab 状态

每个 slab 根据其使用情况分为三种状态,存放在不同的链表中:

  • Full(满):所有对象都被分配出去。
  • Partial(部分):部分对象已分配,部分空闲。
  • Empty(空):所有对象都空闲,未被使用。

3. 分配过程

当内核需要分配一个小对象时(比如通过 kmalloc()):

  1. 检查对应的缓存(比如 kmalloc-64)。
  2. 从缓存的 Partial 链表 中找一个有空闲对象的 slab。
  3. 如果没有 Partial slab,则从 Empty 链表 取一个空的 slab,或者从伙伴系统分配一个新页面并初始化为 slab。
  4. 在 slab 中标记一个空闲对象为已用,并返回其地址。

4. 释放过程

当对象被释放时(比如通过 kfree()):

  1. 找到对象所属的 slab(通过元数据或地址计算)。
  2. 将对象标记为空闲。
  3. 根据 slab 的状态更新链表:
    • 如果原来是 Full,变为 Partial。
    • 如果原来是 Partial,所有对象都空闲后变为 Empty。
  4. 如果 Empty slab 过多,可能会归还给伙伴系统,释放页面。

5. 与伙伴系统的关系

  • 伙伴系统提供大块内存(页面级别,如 4KB)。
  • Slab 系统接管这些页面,将其切分为小对象,负责细粒度管理。
  • 当 slab 需要更多内存时,从伙伴系统申请;当内存空闲过多时,归还给伙伴系统。

Slab 系统的设计细节

1. 对象大小

  • Slab 系统为不同大小的对象维护不同的缓存。例如,Linux 中的 kmalloc 缓存包括:
    • kmalloc-32(32 字节)
    • kmalloc-64(64 字节)
    • kmalloc-128(128 字节)
    • … 依次递增到 kmalloc-8192(8KB)。
  • 对象大小通常是对齐的(比如按 8 字节或 16 字节对齐),以满足硬件和性能要求。

2. 元数据管理

  • 每个 slab 包含元数据,用于跟踪对象状态(已用/空闲)。
  • 元数据可以存储在 slab 内部(占用部分空间)或外部(额外分配的控制块),具体取决于实现。
  • Linux 的 SLUB 分配器(现代默认实现)优化了元数据管理,将其嵌入 slab 中,减少开销。

3. 缓存预热(Cache Warming)

  • Slab 系统预先初始化一些 slab,确保分配请求能快速响应,避免初始化延迟。
  • 例如,常用的缓存(如 kmalloc-64)会保持一定数量的 Partial 或 Empty slab。

Slab 系统如何缓解内部碎片?

  • 伙伴系统的局限
    • 伙伴系统最小单位是一个页面(通常 4KB)。请求 64 字节时,分配 4KB,浪费 4032 字节(内部碎片)。
  • Slab 的解决方案
    • Slab 从伙伴系统拿 4KB 页面,然后切分为多个 64 字节对象(假设 64 字节对齐,可能容纳 60+ 个对象,具体数量因元数据而异)。
    • 请求 64 字节时,返回一个 64 字节对象,剩余空间留给其他请求,几乎无浪费。
  • 效果
    • 内部碎片被限制在 slab 的最后一个对象或元数据开销中,通常远小于伙伴系统的碎片。

Slab 系统的演进

Linux 中的 Slab 系统经历了多次优化,形成了三种主要实现:

  1. 原始 Slab(Slab Allocator)
    • 最早的实现,功能完备但管理复杂,开销较高。
  2. SLUB(Unqueued Slab Allocator)
    • 默认实现(现代 Linux 使用),简化了设计,去掉了队列,元数据嵌入 slab,性能更高。
    • 通过减少锁和优化缓存利用率,提升了效率。
  3. SLOB(Simple List of Blocks)
    • 适用于内存极小的嵌入式系统,极简设计,但牺牲了性能和功能。

SLUB 是当前主流,因为它在性能、内存利用率和复杂性之间取得了平衡。


优点

  1. 高效性
    • 预分配和缓存机制使得小对象分配非常快,避免了频繁调用伙伴系统。
  2. 减少内部碎片
    • 将页面切分为精确的对象大小,充分利用内存。
  3. 对象重用
    • 释放的对象保留其初始化状态,重用时无需重新构造,提升性能(如内核结构体的重用)。

缺点

  1. 外部碎片风险
    • 如果某些 slab 长期处于 Partial 状态,页面无法归还给伙伴系统,可能导致内存零散。
  2. 固定大小限制
    • Slab 只适合固定大小的对象,动态大小的请求仍需其他机制(如 kmalloc 向上取整)。
  3. 管理开销
    • 需要维护缓存和 slab 的元数据,占用少量额外内存。

示例:kmalloc 的 Slab 使用

假设调用 kmalloc(100, GFP_KERNEL)

  1. kmalloc 找到最近的缓存,比如 kmalloc-128(因为 100 < 128)。
  2. 检查 kmalloc-128 缓存的 Partial slab。
  3. 返回一个 128 字节的对象,实际使用 100 字节,28 字节是内部碎片(但远小于 4KB)。
  4. 如果缓存中没有空闲对象,从伙伴系统拿一个 4KB 页面,初始化为新的 slab。

总结

Slab 系统是 Linux 内存管理的重要补充,它通过缓存和对象化的方式,将伙伴系统的大块内存(页面)细化为小块,高效满足小内存请求。它显著减少了内部碎片,同时提升了分配性能,是内核内存管理的核心组件之一。