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 的过程。

伙伴系统分裂与合并流程 初始空闲块 (Order=2, 4页, 16KB) 请求 1 页 (Order=0) 伙伴 A (Order=1, 2页) 伙伴 B (Order=1, 2页) 已分配 (Order=0) 空闲 (Order=0) 释放已分配页 合并为 Order=1 (2页) 伙伴B也空闲,继续合并 最终合并回 Order=2 (4页)

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 分配器架构

Slab 分配器架构 Cache (例如: kmalloc-128) Slab (完全空闲) 对象槽 (空闲) Slab (部分使用) 对象槽 (已分配/空闲) 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 在内核虚拟地址空间中的典型位置。

内核虚拟地址空间布局 直接映射区 (线性映射, 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)是第一步。

3.4 本章小结