内存管理:Linux内核驱动slab分配器与内存池优化 - 第1节
1.1 Linux 内核内存管理架构总览
Linux 内核的内存管理子系统是驱动开发中最核心、最复杂的模块之一。它向上为进程、文件系统、网络协议栈等提供虚拟地址空间,向下管理物理内存的分配与回收。理解其整体架构,是掌握 slab 分配器与内存池优化的前提。
内核内存管理采用分层设计,从最底层的物理页管理到最上层的进程虚拟地址空间,每一层都解决特定的问题:
- 物理页管理(伙伴系统):以 4KB 页为基本单位,管理物理内存的分配与释放,解决外部碎片问题。
- slab 分配器:在伙伴系统之上,为内核对象(如 task_struct、inode)提供高速、低碎片的分配与缓存。
- 内存池(mempool):在 slab 分配器之上,为原子上下文或紧急场景提供预留内存保障。
- 虚拟内存管理:管理进程的页表、VMA、缺页异常等。
1.1.1 内存管理分层架构图
下图展示了 Linux 内核内存管理的核心分层关系,以及数据流在层间的传递路径:
1.2 伙伴系统与 slab 分配器的关系
伙伴系统(Buddy System)是 Linux 内核最底层的物理页分配器,它以 2 的幂次阶(order) 为单位管理连续物理页。slab 分配器则建立在伙伴系统之上,专门用于分配小尺寸、频繁创建/销毁的内核对象。
两者的核心关系可以概括为:
- 伙伴系统提供“原材料”:slab 分配器通过
__get_free_pages()从伙伴系统申请整页(通常为 1 页),然后将其切分为多个等大小的对象缓存。 - slab 分配器解决“碎片与效率”:伙伴系统只能分配 2^n 页的连续内存,对于几十字节的对象(如 task_struct 约 2KB)会造成严重内部碎片;slab 分配器通过对象复用和着色(coloring)技术,大幅降低碎片并提升分配速度。
- 内存池提供“安全网”:在 slab 分配器之上,内存池(mempool)为原子上下文(如中断处理)预留少量对象,确保在内存紧张时仍能分配成功。
1.2.1 伙伴系统 vs slab 分配器对比
| 对比维度 | 伙伴系统 | slab 分配器 |
|---|---|---|
| 分配粒度 | 物理页(4KB ~ 4MB) | 任意大小(8B ~ 4MB) |
| 主要问题 | 外部碎片(页级) | 内部碎片(对象级) |
| 分配速度 | 较慢(可能触发页回收) | 极快(常从空闲链表直接返回) |
| 适用场景 | 大块连续内存、DMA 缓冲区 | 内核对象、小数据结构 |
| 核心 API | alloc_pages() / __get_free_pages() |
kmem_cache_alloc() / kmalloc() |
| 碎片控制 | 伙伴合并算法 | 对象复用 + 着色 |
1.2.2 从伙伴系统到 slab 的分配流程
下图展示了一个典型的 slab 缓存(如 kmalloc-64)如何从伙伴系统获取物理页,并切分为多个 64 字节对象的过程:
1.3 为什么需要 slab 分配器
在 Linux 内核早期版本(2.0 以前),内核直接使用伙伴系统分配所有内存。随着内核功能增长,这种方式的缺陷越来越明显:
- 内部碎片严重:一个 300 字节的 task_struct 需要分配一个 4KB 页,浪费 92% 的空间。
- 分配速度慢:每次分配都需要遍历伙伴系统的空闲链表,可能触发页合并或回收。
- 缺乏对象缓存:频繁创建/销毁的对象(如 socket、inode)每次都要重新初始化,开销大。
slab 分配器的引入解决了上述问题,其核心优势包括:
- 对象复用:释放的对象不会真正返回伙伴系统,而是留在 slab 缓存中,下次分配时直接复用,避免重复初始化。
- 着色(Coloring):通过调整对象在页内的偏移,使不同 slab 页中的对象落在不同的 cache line 上,减少 CPU 缓存行颠簸。
- NUMA 感知:slab 分配器支持为每个内存节点维护独立的空闲链表,提升多核系统的内存访问局部性。
- 统计与调试:通过
/proc/slabinfo可以查看每个 slab 缓存的使用情况,便于驱动开发者定位内存泄漏。
1.3.1 使用 slab 分配器的典型场景
| 内核对象 | 对象大小 | 使用频率 | slab 缓存名称 |
|---|---|---|---|
| task_struct | ~2KB | 极高(进程创建/销毁) | task_struct |
| inode | ~600B | 高(文件操作) | inode_cache |
| dentry | ~200B | 极高(路径查找) | dentry_cache |
| socket | ~1KB | 高(网络通信) | sock_inode_cache |
| 驱动私有结构体 | 自定义 | 取决于驱动 | 驱动自定义 |
kmem_cache_create() 创建专用 slab 缓存,而不是使用通用的 kmalloc()。这能显著提升性能并减少碎片。
1.3.2 slab 分配器 vs 通用 kmalloc
通用 kmalloc() 底层也是基于 slab 分配器实现的,但它使用预定义的通用缓存(如 kmalloc-8、kmalloc-16、...、kmalloc-4M)。专用 slab 缓存相比通用 kmalloc 有以下优势:
- 更小的内部碎片:专用缓存的对象大小精确匹配结构体大小,而通用缓存只能向上取整到 2 的幂次。
- 更好的缓存局部性:同一类型的对象集中在同一组页中,CPU 缓存命中率更高。
- 更易调试:通过
/proc/slabinfo可以单独监控该类型对象的分配/释放情况。
以下代码展示了驱动如何创建和使用专用 slab 缓存:
/* 定义驱动私有结构体 */
struct my_device {
unsigned int id;
char name[32];
void __iomem *regs;
/* ... 其他字段 */
};
/* 创建 slab 缓存(通常在模块初始化时) */
static struct kmem_cache *my_device_cache;
static int __init my_driver_init(void)
{
my_device_cache = kmem_cache_create(
"my_device", /* 缓存名称,显示在 /proc/slabinfo */
sizeof(struct my_device), /* 对象大小 */
0, /* 对齐方式,0 表示默认 */
SLAB_HWCACHE_ALIGN, /* 标志:按硬件缓存行对齐 */
NULL /* 构造函数,通常为 NULL */
);
if (!my_device_cache)
return -ENOMEM;
return 0;
}
/* 分配对象 */
struct my_device *dev = kmem_cache_alloc(my_device_cache, GFP_KERNEL);
/* 释放对象 */
kmem_cache_free(my_device_cache, dev);
/* 销毁缓存(通常在模块退出时) */
static void __exit my_driver_exit(void)
{
kmem_cache_destroy(my_device_cache);
}
kmem_cache_create() 创建缓存时,如果指定了 SLAB_HWCACHE_ALIGN 标志,slab 分配器会尝试将对象对齐到 CPU 缓存行大小(通常为 64 或 128 字节),这能有效避免多核环境下的伪共享(False Sharing)问题。
1.3.3 slab 分配器的三种变体
Linux 内核目前支持三种 slab 分配器实现,驱动开发者需要了解它们的差异:
| 分配器 | 引入版本 | 核心特点 | 适用场景 |
|---|---|---|---|
| SLAB | 2.2 | 经典实现,每个 CPU 维护空闲对象链表,支持着色 | 通用场景,调试友好 |
| SLUB | 2.6.22 | 简化设计,移除 per-CPU 队列,使用页级空闲链表,减少锁开销 | 默认分配器(大多数发行版),高并发场景 |
| SLOB | 2.6.16 | 极度简化,适合嵌入式系统,内存占用最小 | 内存受限的嵌入式设备 |
当前主流内核(5.x / 6.x)默认使用 SLUB 分配器。SLUB 在保持 SLAB 大部分优点的同时,大幅简化了代码复杂度,并提升了多核扩展性。驱动开发者无需关心底层使用哪种分配器,因为 kmem_cache_alloc() / kmem_cache_free() 接口在所有变体上保持一致。
1.3.4 内存池(mempool)的引入
即使有了 slab 分配器,在极端内存压力下(如 GFP_ATOMIC 分配失败),驱动仍然可能面临分配失败的风险。内存池(mempool)在 slab 分配器之上提供了一层紧急预留机制:
- 在初始化时预先分配一定数量的对象,保存在池中。
- 正常分配时,优先从 slab 分配器获取;如果失败,则从预留池中取出。
- 释放时,如果预留池未满,则放回池中;否则正常释放给 slab 分配器。
内存池适用于以下场景:
- 中断处理程序或自旋锁上下文(只能使用 GFP_ATOMIC)。
- 块设备层的 I/O 请求,必须保证在内存紧张时仍能分配 bio 结构体。
- 网络设备驱动,确保在接收数据包时能分配 sk_buff。
1.3.5 本章小结
通过本章的学习,你应该掌握了以下核心概念:
- Linux 内核内存管理采用分层架构,伙伴系统管理物理页,slab 分配器管理内核对象,内存池提供紧急保障。
- 伙伴系统以 2^n 页为单位分配,slab 分配器在此基础上切分为等大小对象,解决内部碎片和分配效率问题。
- 驱动开发者应优先使用
kmem_cache_create()创建专用 slab 缓存,而不是通用kmalloc(),以获得更好的性能和可调试性。 - 在原子上下文或关键路径上,考虑使用内存池(mempool)作为最后的保障。