1. 内存管理概述:物理内存与虚拟内存、伙伴系统的局限性、引入slab分配器的动机
1.1 物理内存与虚拟内存:内核视角的地址空间
在Linux系统中,内存管理单元(MMU)将CPU发出的虚拟地址转换为物理地址。内核必须同时管理这两种地址空间:物理内存是有限的硬件资源,而虚拟内存为每个进程提供独立的、连续的地址空间。
核心概念: 物理内存由页框(Page Frame)组成,通常大小为4KB。虚拟内存通过页表映射到物理页框,实现了隔离、按需分页和交换功能。
内核直接管理物理内存,其虚拟地址空间与物理内存之间存在线性映射(如直接映射区,即低端内存)。对于高端内存(超过896MB的物理内存),内核需要通过临时映射来访问。
1.2 伙伴系统(Buddy System):物理页框分配器
Linux内核使用伙伴系统作为物理页框分配的核心算法。它将空闲页框按2的幂次方(order)组织成链表,用于分配和释放连续的物理内存块。
| 阶数 (Order) | 连续页框数 | 大小 (4KB页) | 典型用途 |
|---|---|---|---|
| 0 | 1 | 4 KB | 单个页框分配 |
| 1 | 2 | 8 KB | 小DMA缓冲区 |
| 2 | 4 | 16 KB | 文件系统元数据 |
| 3 | 8 | 32 KB | 网络协议栈 |
| 4 | 16 | 64 KB | 中等大小内核对象 |
| 10 | 1024 | 4 MB | 大块连续内存 |
工作原理: 当请求order为n的块时,若对应链表非空则直接分配;否则从更高阶链表分裂,直到满足需求。释放时检查伙伴块是否空闲,若空闲则合并回更高阶。
1.3 伙伴系统的局限性
尽管伙伴系统在管理物理页框方面非常高效,但它并不适合内核中频繁发生的小对象分配与释放场景。其局限性主要体现在以下几个方面:
- 内部碎片严重: 分配的最小单位是单个页框(4KB)。如果内核需要分配一个仅几十字节的对象(如
task_struct、inode),伙伴系统会浪费大量内存。 - 分配速度慢: 对于小对象,每次分配都需要经过伙伴系统的分裂/合并操作,时间开销大,无法满足高频分配场景。
- 缺乏对象缓存: 伙伴系统不感知对象类型,每次分配/释放都需要重新初始化对象,无法复用已构造的对象状态。
- CPU缓存不友好: 频繁分配和释放相同大小的对象时,对象可能分散在不同的页框中,导致CPU缓存命中率下降。
| 问题 | 具体表现 | 影响 |
|---|---|---|
| 内部碎片 | 分配32字节对象占用4KB页框 | 内存利用率极低(<1%) |
| 分配延迟 | 分裂/合并操作耗时 | 影响实时性和吞吐量 |
| 无对象缓存 | 每次分配后需重新初始化 | 增加CPU开销 |
| 缓存局部性差 | 对象分散在不同页框 | 降低L1/L2缓存命中率 |
性能瓶颈: 在极端情况下,内核频繁分配和释放小对象(如网络连接、文件描述符)时,伙伴系统的开销可能占CPU时间的10%以上,成为系统性能的瓶颈。
1.4 引入slab分配器的动机
为了解决伙伴系统的上述局限性,Sun Microsystems在Solaris 2.4中首次引入了slab分配器,随后被Linux内核采用。引入slab分配器的核心动机包括:
- 消除内部碎片: slab分配器将页框划分为多个相同大小的对象槽,精确匹配内核对象的大小,将碎片降至最低。
- 提高分配/释放速度: 通过维护每个CPU的本地缓存(per-CPU cache)和部分空闲链表,slab分配器可以在常数时间内完成分配和释放,避免伙伴系统的分裂合并开销。
- 对象缓存与复用: slab分配器支持对象构造函数和析构函数,分配时直接返回已构造好的对象,释放时仅标记为空闲,大幅减少初始化开销。
- 硬件缓存友好: slab分配器尽量将同一类型的对象集中存放在连续的页框中,提高CPU缓存的局部性。
- 统计与调试支持: slab分配器提供丰富的统计信息(如对象使用率、缓存命中率),便于内核开发者进行性能分析和内存泄漏检测。
关键设计思想: slab分配器并不替代伙伴系统,而是作为伙伴系统之上的对象缓存层。它从伙伴系统获取整页框,然后将其划分为多个相同大小的对象槽,专门服务于内核小对象的频繁分配与释放需求。