1. 内存管理概述

1.1 Linux内核内存管理层次结构

Linux内核的内存管理是一个分层架构,从物理页帧管理到虚拟地址空间映射,每一层都解决特定的抽象问题。理解这个层次结构是掌握slab/slub分配器的基础。

核心观点: Linux内存管理自底向上分为四个主要层次——物理页管理(伙伴系统)、通用对象缓存(slab/slub)、连续内存分配器(CMA/VMALLOC)和虚拟地址空间(VMA/页表)。slab/slub位于第二层,是连接物理页与内核对象的关键桥梁。

下图展示了Linux内核内存管理的完整层次结构及各层之间的调用关系:

Linux内核内存管理层次结构 用户空间 (User Space) malloc / free / mmap / brk 虚拟地址空间 (VMA / 页表) 缺页异常 / 页表缓存 / TLB刷新 连续内存分配器 (CMA / vmalloc / ioremap) 大块连续物理内存 / 非连续虚拟映射 Slab / Slub 对象缓存分配器 kmem_cache_alloc / kmalloc / 对象复用 伙伴系统 (Buddy System) — 物理页帧管理 alloc_pages / free_pages / 页面回收
层次关系说明: 伙伴系统管理物理页帧(最小单位4KB),slab/slub在伙伴系统之上将物理页切分为更小的对象缓存,供内核频繁创建和销毁的小对象(如inode、task_struct)使用。vmalloc和CMA则处理大块或非连续映射的需求。

1.2 伙伴系统与slab/slub的关系

伙伴系统(Buddy System)是Linux物理内存管理的基石,而slab/slub分配器是建立在伙伴系统之上的对象缓存层。两者之间是服务者与消费者的关系。

对比维度 伙伴系统 (Buddy System) Slab/Slub 分配器
管理粒度 物理页帧(4KB ~ 4MB 连续块) 内核对象(几十字节 ~ 几KB)
分配单位 2^order 个连续物理页 单个内核对象(如 inode、dentry)
主要接口 alloc_pages() / __get_free_pages() kmem_cache_alloc() / kmalloc()
碎片问题 外部碎片(连续页块分裂) 内部碎片(对象对齐填充)
缓存特性 无对象缓存,每次分配清零 对象复用,构造函数/析构函数
性能特征 分配/释放较慢(页级操作) 分配/释放极快(对象级操作)

下图展示了伙伴系统与slab/slub之间的协作流程:

伙伴系统与Slab/Slub协作流程 伙伴系统 物理页帧管理 alloc_pages() free_pages() Slab / Slub 分配器 对象缓存管理 kmem_cache_alloc() kmem_cache_free() 内核对象 task_struct inode / dentry buffer_head 分配物理页 返回对象 关键流程 当slab/slub缓存中对象耗尽 → 向伙伴系统申请2^order个物理页 → 切分为对象 → 放入空闲链表 性能指标对比 伙伴系统分配延迟:~1μs(页级) Slab/Slub分配延迟:~0.1μs(对象级) 缓存命中时slab分配比伙伴系统快 10~50 倍
重要理解: slab/slub并不替代伙伴系统,而是寄生在伙伴系统之上。当slab/slub需要更多内存时,通过调用 alloc_pages() 从伙伴系统获取整页;释放时则将整页归还给伙伴系统。这种分层设计使得内核既能高效管理大块物理内存,又能快速分配小对象。

1.3 Slab分配器的历史背景

Slab分配器的演进是Linux内核内存管理不断优化的缩影。从最初的Slab到Slub,再到近年来的Slab Kmalloc重构,每一次变革都针对特定的性能瓶颈和可扩展性问题。

时间 内核版本 事件 核心改进
1994年 Linux 1.0 SunOS 4.1 引入Slab分配器 对象缓存、构造函数/析构函数
1996年 Linux 2.0 Linux内核首次移植Slab 由Jeff Bonwick实现,替代旧式kmalloc
2001年 Linux 2.4 Slab成为默认分配器 支持NUMA、CPU本地缓存
2007年 Linux 2.6.22 Slub分配器合入主线 简化slab结构、减少元数据开销、提高可扩展性
2008年 Linux 2.6.28 Slub成为默认分配器 嵌入式和大规模SMP场景全面优化
2015年 Linux 4.0 Slab Kmalloc重构 统一kmalloc缓存、减少内部碎片
2020年至今 Linux 5.x / 6.x Slub持续优化 批量分配、CPU本地缓存深度优化、KASAN支持

下图展示了Slab分配器从诞生到成为Linux内核标配的演进路线:

Slab分配器演进时间线 1994 SunOS Slab 对象缓存概念 1996 Linux 2.0 首次移植Slab 2001 Linux 2.4 NUMA支持 2007 Linux 2.6.22 Slub合入主线 2008 Linux 2.6.28 Slub成为默认 为什么从Slab演进到Slub? 1. Slab元数据开销大:每个slab需要管理多个链表和着色信息 2. Slub简化设计:去掉着色、合并链表、减少CPU缓存行竞争 3. 大规模SMP可扩展性:Slub的CPU本地缓存更轻量
关键洞察: Slab分配器的设计哲学是“对象复用”——内核中大量对象(如inode、dentry、task_struct)被频繁创建和销毁,如果每次都通过伙伴系统分配和释放物理页,性能将极其低下。Slab通过维护一个已初始化对象的缓存池,实现了接近O(1)的分配和释放速度。Slub在此基础上进一步简化了数据结构,减少了元数据开销,使得在数百个CPU核心的系统上仍能保持高性能。

1.4 本章小结

  • 内存管理层次: 伙伴系统(物理页)→ Slab/Slub(对象缓存)→ vmalloc/CMA(大块/非连续)→ VMA(虚拟地址空间)
  • 伙伴系统与slab的关系: slab是伙伴系统的消费者,通过 alloc_pages() 获取物理页,切分为对象后缓存复用
  • 历史演进: SunOS Slab (1994) → Linux Slab (1996) → Slub (2007) → 默认Slub (2008) → 持续优化至今
  • 核心优势: 对象复用减少分配延迟、CPU本地缓存避免锁竞争、构造函数/析构函数保持对象状态