3. Linux 内核内存管理基础(二):伙伴系统、Slab 分配器、vmalloc 与 kmalloc 的区别
在上一节中,我们了解了内核如何通过页表将物理内存映射到虚拟地址空间。本节将深入探讨内核如何管理物理内存页帧,以及如何为不同粒度的内存请求提供服务。核心机制包括用于大块连续物理内存分配的伙伴系统(Buddy System)、用于小对象高效分配的Slab 分配器,以及用于非连续物理内存映射的 vmalloc 机制。理解这些是分析内存访问越界 Crash 的基石。
3.1 伙伴系统(Buddy System)
伙伴系统是内核管理物理页帧(Physical Page Frame)的核心算法,负责分配和回收连续的物理内存页。其设计目标是减少外部碎片,并实现高效的合并与分裂。
3.1.1 核心思想
- 阶(Order):内存块的大小以 2 的幂次方页为单位。阶为 0 表示 1 页(4KB),阶为 1 表示 2 页(8KB),阶为 n 表示 2n 页。
- 伙伴(Buddy):两个大小相同、物理地址连续且由同一大块分裂而来的内存块互为伙伴。
- 分配:请求分配 2n 页。系统在对应阶的空闲链表中查找。若找到,直接分配;若没有,则向高阶链表申请一个更大的块,将其一分为二(分裂),一块分配,另一块放入低阶链表。
- 回收:释放内存块时,检查其伙伴是否也为空闲。若是,则将两者合并为一个更大的块,并递归向上合并。
Crash 分析要点: 伙伴系统管理的是 物理页帧。当发生内存访问越界时,如果越界访问破坏了相邻物理页的页描述符(
struct page)或关键元数据,后续的伙伴系统操作(如合并、分裂)可能导致系统崩溃。常见的现象是 __free_one_page 函数中的 BUG_ON 触发。
3.1.2 伙伴系统分配与合并流程
下图展示了从 order=2 的链表中分配一个 order=0 页块的过程,以及释放后合并回 order=2 的过程。
3.2 Slab 分配器
伙伴系统以页为单位分配内存,但对于内核中频繁创建和销毁的小对象(如 task_struct、inode、dentry),直接使用伙伴系统会导致巨大的内部碎片和性能开销。Slab 分配器正是为解决此问题而生。
3.2.1 核心概念
- Cache(缓存):每个 Cache 负责管理一种特定类型的对象(如
kmalloc-64、task_struct)。 - Slab( slabs):一个 Slab 由一个或多个连续的物理页组成,被划分为多个相同大小的对象槽。
- 对象(Object):Cache 中分配的最小单元,即内核数据结构实例。
- 着色(Coloring):通过调整 Slab 内第一个对象的偏移量,使不同 Slab 中的对象在 CPU Cache 中均匀分布,提高缓存命中率。
3.2.2 Slab 分配器架构
Crash 分析技巧: 在 Crash 工具中,使用
kmem -s 或 slabinfo 命令可以查看所有 Slab Cache 的状态。如果某个 Cache 的 object_size 与 obj_offset 出现异常,或者 red_zone(红区)被破坏,通常意味着发生了 Slab 越界(Slab Out-of-Bounds)。这是分析 slab 相关 Crash 的关键线索。
3.3 vmalloc 与 kmalloc 的区别
这是内核开发中最容易混淆的两个内存分配接口。它们的核心区别在于物理内存的连续性和虚拟地址的映射方式。
3.3.1 核心区别对比
| 特性 | kmalloc | vmalloc |
|---|---|---|
| 物理内存连续性 | 保证物理地址连续 | 不保证物理地址连续(使用分散的物理页) |
| 虚拟地址连续性 | 保证虚拟地址连续 | 保证虚拟地址连续 |
| 分配速度 | 快(通常从 Slab 或伙伴系统直接分配) | 慢(需要建立页表映射,TLB 刷新开销大) |
| 适用场景 | 小内存分配(< 1MB),DMA 缓冲区,对物理连续性有要求的场景 | 大内存分配(> 1MB),模块加载,需要大块虚拟连续空间但物理不连续的场景 |
| 底层实现 | 基于 Slab 分配器(小对象)或伙伴系统(大对象) | 基于伙伴系统分配物理页,并在 vmalloc 区域建立页表映射 |
| 内存开销 | 小(直接使用物理地址) | 大(需要额外的页表项和 vmap_area 结构体) |
| 是否可睡眠 | GFP_ATOMIC 标志下不可睡眠;GFP_KERNEL 下可睡眠 | 总是可睡眠(可能触发页错误和 I/O) |
3.3.2 虚拟地址空间布局
下图展示了 kmalloc 和 vmalloc 在内核虚拟地址空间中的典型位置。
3.3.3 使用建议与 Crash 关联
- 优先使用 kmalloc:对于大多数内核数据结构,kmalloc 是首选,因为它速度快、开销小。
- 大内存用 vmalloc:当需要分配超过 1MB 的连续虚拟内存时,使用 vmalloc 可以避免伙伴系统的外部碎片问题。
- DMA 必须用 kmalloc:DMA 设备通常要求物理地址连续,因此必须使用
kmalloc或__get_free_pages。
Crash 分析陷阱: 一个常见的 Crash 原因是驱动在中断上下文(不可睡眠)中调用了
vmalloc。由于 vmalloc 可能触发页错误并导致进程睡眠,这会导致内核崩溃。另一个常见问题是 kmalloc 分配的内存被越界访问,破坏了相邻的 Slab 对象或页描述符,最终在 kfree 或 vmalloc 操作时触发 Oops。在分析 Crash 时,检查调用栈中的分配函数(__kmalloc vs __vmalloc)是第一步。