3. 物理内存管理:伙伴系统、slab分配器、连续内存分配器(CMA)

物理内存是系统最核心的硬件资源之一。Linux 内核通过三层递进的机制管理物理内存:伙伴系统(Buddy System)负责大块连续页面的分配与回收;slab 分配器在伙伴系统之上提供细粒度的小对象缓存;连续内存分配器(CMA)则为需要大量连续物理内存的设备(如 DMA、GPU)预留可动态迁移的区域。理解这三层结构,是掌握 mmap 与 DMA 缓冲区分配的基础。

3.1 伙伴系统(Buddy System)

伙伴系统是内核物理内存分配的核心算法,以 2order 个连续物理页(order 从 0 到 MAX_ORDER-1)为基本单位进行管理。其核心思想是将空闲页块按大小分组,分配时从最小满足需求的块中拆分,释放时与相邻的“伙伴”合并,从而减少外部碎片。

关键数据结构:每个内存域(zone)维护一个 free_area[MAX_ORDER] 数组,每个数组项对应一个特定 order 的空闲页块链表。

3.1.1 分配与释放流程

  1. 分配请求:调用 alloc_pages(gfp_mask, order),从目标 zone 的 free_area[order] 链表查找空闲块。
  2. 拆分(Buddy Splitting):若当前 order 链表为空,则向更高 order 链表请求一个块,将其一分为二,低半部分放入当前 order 链表,高半部分返回给调用者。
  3. 释放与合并(Buddy Coalescing):释放时检查伙伴页块是否空闲,若空闲则合并为更高 order 的块,并递归向上合并。
性能要点:伙伴系统分配/释放的时间复杂度为 O(1),但拆分和合并操作会引入少量开销。对于频繁分配/释放小对象的场景,应使用 slab 分配器。

3.1.2 伙伴系统结构图

伙伴系统结构图 free_area[] order=0 (4KB) order=1 (8KB) order=2 (16KB) ... order=9 (2MB) order=10 (4MB) ... MAX_ORDER-1 页块 A 页块 B 页块 C 页块 D 页块 E 分配策略: 从所需 order 开始查找,若为空则向高 order 请求并拆分;释放时检查伙伴块,合并后向上递归。

3.2 slab 分配器

slab 分配器用于管理内核中频繁分配/释放的小对象(如 task_struct、inode、dentry 等)。它基于伙伴系统分配的连续页面,通过 slab 缓存 机制实现对象复用,避免频繁的伙伴系统操作。

3.2.1 核心概念

术语 说明
cache 管理特定类型对象的缓存,每个 cache 包含多个 slab。
slab 由一个或多个连续物理页组成,内部划分为多个等大小的对象槽。
object slab 中分配的最小单元,即内核数据结构实例。
着色(coloring) 通过偏移调整对象在 slab 中的起始位置,优化 CPU 缓存利用率。

3.2.2 分配器层次结构

slab 分配器层次结构 伙伴系统(Buddy System) kmem_cache 链表(每个 cache 管理一种对象类型) slab(task_struct) slab(inode) slab(dentry) 每个 slab 内部划分为多个对象槽,支持对象复用和着色优化。 分配时优先从 partial 链表获取,避免全空 slab 的浪费。

3.2.3 常用 API

/* 创建缓存 */
struct kmem_cache *kmem_cache_create(const char *name,
                                     unsigned int size,
                                     unsigned int align,
                                     slab_flags_t flags,
                                     void (*ctor)(void *));

/* 从缓存分配对象 */
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);

/* 释放对象回缓存 */
void kmem_cache_free(struct kmem_cache *cachep, void *objp);

/* 销毁缓存 */
void kmem_cache_destroy(struct kmem_cache *cachep);
注意事项:slab 分配器适用于大小固定、频繁分配/释放的对象。对于大小可变或极少分配的对象,应直接使用伙伴系统或 kmalloc(基于 slab 实现)。

3.3 连续内存分配器(CMA)

CMA 是为解决嵌入式系统中大块连续物理内存分配难题而设计的机制。它预留一块物理内存区域,平时可供可移动页面使用,当 DMA 等设备需要连续内存时,通过 页面迁移 将预留区域中的页面腾空,分配给设备。

3.3.1 工作原理

  1. 预留阶段:系统启动时通过设备树或内核参数预留一块物理内存区域(如 cma=64M)。
  2. 日常使用:预留区域中的页面被加入伙伴系统的 MIGRATE_CMA 类型链表,可被可移动页面(如匿名页、文件页)占用。
  3. 分配触发:设备驱动调用 dma_alloc_coherent 或 cma_alloc 时,内核开始迁移预留区域中的页面。
  4. 页面迁移:通过 migrate_pages 将占用页面迁移到其他区域,腾出连续物理空间。
  5. 分配完成:将腾空的连续物理页分配给设备,并返回物理地址和虚拟地址。

3.3.2 CMA 与伙伴系统、slab 的关系

特性 伙伴系统 slab 分配器 CMA
分配粒度 2order 页(4KB ~ 4MB+) 字节级(对象大小) 连续页块(通常 1MB ~ 数百 MB)
主要用途 通用物理页分配 内核小对象缓存 DMA/GPU/多媒体缓冲区
碎片处理 合并伙伴减少外部碎片 对象复用减少内部碎片 页面迁移消除外部碎片
分配延迟 低(O(1)) 极低(缓存命中) 高(涉及页面迁移)
底层依赖 硬件页面表 伙伴系统 伙伴系统 + 页面迁移

3.3.3 CMA 分配流程

CMA 分配与页面迁移流程 1. 设备请求 CMA cma_alloc() 2. 扫描 CMA 区域 查找空闲连续块 3. 页面迁移 migrate_pages() 4. 分配 返回页块 页面迁移细节 CMA 区域(被占用) 迁移到其他 zone CMA 区域空闲 迁移完成后,CMA 区域变为连续空闲块,可直接分配给设备。 CMA 配置示例(设备树) reserved-memory { #address-cells = <2>; linux,cma { compatible = "shared-dma-pool"; size = <0x0 0x4000000>; /* 64MB */ reusable; }; };

3.3.4 驱动中使用 CMA

#include <linux/dma-mapping.h>

/* 分配 DMA 缓冲区(自动使用 CMA) */
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

/* 使用缓冲区... */

/* 释放 DMA 缓冲区 */
dma_free_coherent(dev, size, cpu_addr, dma_handle);
最佳实践:在嵌入式系统中,建议为多媒体、GPU、网卡等需要大块连续内存的设备预留 CMA 区域。CMA 区域大小通常为总内存的 5%~20%,具体取决于设备需求。

3.4 三层协同工作

伙伴系统、slab 分配器和 CMA 并非独立运行,而是形成完整的物理内存管理栈:

  • 伙伴系统 是基石,管理所有物理页的分配与回收。
  • slab 分配器 构建在伙伴系统之上,通过缓存机制高效管理小对象。
  • CMA 利用伙伴系统的 MIGRATE_CMA 类型链表和页面迁移机制,实现大块连续内存的按需分配。