什么是 Slab 系统?
Slab 系统是 Linux 内核中用于管理小块内存分配的一种机制,最初由 Jeff Bonwick 在 Solaris 操作系统中提出,后来被 Linux 采纳并优化。它构建在伙伴系统之上,专门解决伙伴系统在分配小块内存(小于一页,如 4KB)时产生的内部碎片问题。
核心目标:
- 高效分配和回收小对象(比如内核中的结构体,如
task_struct
、inode
等)。 - 减少内部碎片。
- 提高分配性能,避免频繁调用伙伴系统。
- 高效分配和回收小对象(比如内核中的结构体,如
基本概念:
- Slab 系统将内存划分为多个 slab,每个 slab 是一块连续的内存(通常由伙伴系统分配的页面组成),专门用于存储某一类固定大小的对象。
- 它类似于一个“对象缓存”,预先分配并切割好内存块,应用程序或内核直接从中取用。
Slab 系统的工作原理
Slab 系统通过以下几个关键组件和步骤实现内存管理:
1. 基本结构
- Cache(缓存):
- 每个 slab 缓存对应一种对象类型(如
kmalloc-64
用于 64 字节对象,task_struct
用于进程描述符)。 - 缓存包含多个 slab,管理对象的分配和回收。
- 每个 slab 缓存对应一种对象类型(如
- Slab:
- 一个 slab 通常是一个或多个连续页面(比如 4KB),由伙伴系统分配。
- Slab 被切割成多个固定大小的对象(比如 64 字节、128 字节等)。
- Object(对象):
- Slab 中的最小分配单位,就是实际分配给调用者的内存块。
2. 三种 Slab 状态
每个 slab 根据其使用情况分为三种状态,存放在不同的链表中:
- Full(满):所有对象都被分配出去。
- Partial(部分):部分对象已分配,部分空闲。
- Empty(空):所有对象都空闲,未被使用。
3. 分配过程
当内核需要分配一个小对象时(比如通过 kmalloc()
):
- 检查对应的缓存(比如
kmalloc-64
)。 - 从缓存的 Partial 链表 中找一个有空闲对象的 slab。
- 如果没有 Partial slab,则从 Empty 链表 取一个空的 slab,或者从伙伴系统分配一个新页面并初始化为 slab。
- 在 slab 中标记一个空闲对象为已用,并返回其地址。
4. 释放过程
当对象被释放时(比如通过 kfree()
):
- 找到对象所属的 slab(通过元数据或地址计算)。
- 将对象标记为空闲。
- 根据 slab 的状态更新链表:
- 如果原来是 Full,变为 Partial。
- 如果原来是 Partial,所有对象都空闲后变为 Empty。
- 如果 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 系统经历了多次优化,形成了三种主要实现:
- 原始 Slab(Slab Allocator):
- 最早的实现,功能完备但管理复杂,开销较高。
- SLUB(Unqueued Slab Allocator):
- 默认实现(现代 Linux 使用),简化了设计,去掉了队列,元数据嵌入 slab,性能更高。
- 通过减少锁和优化缓存利用率,提升了效率。
- SLOB(Simple List of Blocks):
- 适用于内存极小的嵌入式系统,极简设计,但牺牲了性能和功能。
SLUB 是当前主流,因为它在性能、内存利用率和复杂性之间取得了平衡。
优点
- 高效性:
- 预分配和缓存机制使得小对象分配非常快,避免了频繁调用伙伴系统。
- 减少内部碎片:
- 将页面切分为精确的对象大小,充分利用内存。
- 对象重用:
- 释放的对象保留其初始化状态,重用时无需重新构造,提升性能(如内核结构体的重用)。
缺点
- 外部碎片风险:
- 如果某些 slab 长期处于 Partial 状态,页面无法归还给伙伴系统,可能导致内存零散。
- 固定大小限制:
- Slab 只适合固定大小的对象,动态大小的请求仍需其他机制(如
kmalloc
向上取整)。
- Slab 只适合固定大小的对象,动态大小的请求仍需其他机制(如
- 管理开销:
- 需要维护缓存和 slab 的元数据,占用少量额外内存。
示例:kmalloc 的 Slab 使用
假设调用 kmalloc(100, GFP_KERNEL)
:
kmalloc
找到最近的缓存,比如kmalloc-128
(因为 100 < 128)。- 检查
kmalloc-128
缓存的 Partial slab。 - 返回一个 128 字节的对象,实际使用 100 字节,28 字节是内部碎片(但远小于 4KB)。
- 如果缓存中没有空闲对象,从伙伴系统拿一个 4KB 页面,初始化为新的 slab。
总结
Slab 系统是 Linux 内存管理的重要补充,它通过缓存和对象化的方式,将伙伴系统的大块内存(页面)细化为小块,高效满足小内存请求。它显著减少了内部碎片,同时提升了分配性能,是内核内存管理的核心组件之一。