3. CMA核心数据结构:struct cma、struct cma_alloc、bitmap管理、pageblock类型

CMA(Contiguous Memory Allocator)在内核中通过一套精炼的数据结构来管理预留的连续内存区域。理解这些结构是掌握CMA分配与释放流程的基础。本章将深入剖析四个核心要素:struct cma(区域描述符)、struct cma_alloc(分配上下文)、bitmap管理(位图分配算法)以及pageblock类型(迁移类型标记)。

核心要点: CMA的内存管理以“位图”为核心,以“pageblock”为最小隔离单元,通过struct cma描述区域属性,struct cma_alloc记录单次分配细节。

3.1 struct cma:CMA区域描述符

struct cma 是每个CMA区域在内核中的“身份证”。系统中可以存在多个CMA区域(例如:Camera专用、GPU专用、通用DMA),每个区域对应一个 struct cma 实例。

成员 类型 描述
base_pfn unsigned long CMA区域起始页框号(Page Frame Number)
count unsigned long CMA区域包含的物理页总数
bitmap unsigned long * 指向位图内存的指针,每位代表一个pageblock
order_per_bit unsigned int 位图中每一位所代表的分配阶(order),通常为0
name const char * 区域名称(如"camera"、"gpu")
reserve_pages_on_error bool 分配失败时是否保留已隔离页面
设计意图: order_per_bit 通常设为0,意味着位图中的每一位对应一个pageblock(通常为2^order页)。如果设为1,则每位对应2个pageblock,以此类推。这种设计允许CMA在粗粒度与细粒度管理之间权衡。
struct cma 内存布局示意图 struct cma base_pfn: 0x1A0000 count: 8192 页 order_per_bit: 0 name: "camera" bitmap: 0xFFFF... reserve_pages_on_error: 1 指向 bitmap (位图) 位[0]: 1 (已分配) 位[1]: 0 (空闲) 位[2]: 0 (空闲) 位[3]: 1 (已分配) 位[4]: 1 (已分配) 位[5]: 0 (空闲) 映射 物理内存页 页[0] (已隔离) 页[1] (可迁移) 页[2] (可迁移) 页[3] (已隔离) 页[4] (已隔离) 页[5] (可迁移)

3.2 struct cma_alloc:分配上下文

当驱动发起一次CMA分配请求时,内核会创建一个 struct cma_alloc 实例来跟踪这次分配的生命周期。该结构体在较新内核版本中引入,用于替代分散的参数传递。

// 内核源码: include/linux/cma.h
struct cma_alloc {
    struct cma *cma;        // 所属CMA区域
    unsigned long start;    // 分配起始页框号
    unsigned long count;    // 分配页数
    unsigned int order;     // 分配阶 (2^order 页)
    gfp_t gfp_mask;        // 分配标志
    const char *owner;      // 分配者名称(调试用)
};
成员 作用 典型值
cma 绑定到目标CMA区域 cma_areas[0]
start 分配成功后记录起始PFN 0x1A0100
count 请求的连续页数 256
order 分配粒度,影响位图扫描 8 (256页)
gfp_mask GFP标志,控制行为 GFP_KERNEL
owner 调试标识 "v4l2_m2m"
注意: struct cma_alloc 并非所有内核版本都存在。在较老版本(如Linux 4.x)中,分配参数直接通过函数参数传递。新版本引入此结构体是为了支持更复杂的分配策略和调试追踪。

3.3 bitmap管理:分配与释放的核心算法

CMA使用位图(bitmap)来跟踪每个pageblock的分配状态。位图是CMA实现O(1)分配查找的关键数据结构。

3.3.1 位图与pageblock的映射关系

  • 位图大小: cma->count >> cma->order_per_bit 位。
  • 每位含义: 0表示空闲,1表示已分配(已隔离)。
  • 分配阶: 当 order_per_bit = 0 时,每位对应一个pageblock(通常为2^0=1页,但实际pageblock大小由架构决定,如ARM64为4MB)。

3.3.2 分配流程(位图视角)

  1. 计算所需位数: 根据请求的页数 count 和 order_per_bit,计算需要连续的 n 个空闲位。
  2. 位图扫描: 调用 bitmap_find_next_zero_area() 查找连续 n 个零位。
  3. 位图标记: 找到后,调用 bitmap_set() 将这些位全部置1。
  4. 页面隔离: 将对应的物理页从伙伴系统迁移到CMA隔离列表。

3.3.3 释放流程(位图视角)

  1. 位图清零: 调用 bitmap_clear() 将对应位清零。
  2. 页面归还: 将物理页从隔离列表移回伙伴系统。
  3. 迁移类型恢复: 将pageblock的迁移类型恢复为 MIGRATE_CMA。
CMA位图分配与释放流程 分配路径 1. bitmap_find_next_zero_area() 2. bitmap_set() 置1 3. 页面隔离 (migrate) 释放路径 1. bitmap_clear() 清零 2. 页面归还伙伴系统 3. 恢复 MIGRATE_CMA 类型 关键性能指标 位图扫描时间复杂度 O(n),n为连续空闲位长度;实际分配延迟通常在微秒级

3.4 pageblock类型:MIGRATE_CMA与迁移隔离

CMA依赖伙伴系统的pageblock迁移类型机制来实现内存的“动态隔离”。每个pageblock(通常为2^pageblock_order页,ARM64上为4MB)都有一个迁移类型标签。

3.4.1 关键迁移类型

迁移类型 枚举值 用途
MIGRATE_UNMOVABLE 0 不可移动页(内核分配、VMA锁定)
MIGRATE_MOVABLE 1 可移动页(用户空间匿名页、页面缓存)
MIGRATE_RECLAIMABLE 2 可回收页(文件系统元数据)
MIGRATE_CMA 3 CMA预留区域,可迁移但不可用于非CMA分配
MIGRATE_ISOLATE 4 临时隔离,正在迁移或CMA分配中

3.4.2 CMA如何利用pageblock类型

  • 初始化时: CMA区域内的所有pageblock被标记为 MIGRATE_CMA。
  • 分配时: 当驱动请求CMA内存,内核将这些pageblock临时标记为 MIGRATE_ISOLATE,并将其中已分配的不可移动页迁移出去。
  • 释放时: 将pageblock恢复为 MIGRATE_CMA,页面归还伙伴系统。
  • 防止误用: 普通伙伴系统分配不会从 MIGRATE_CMA 类型的pageblock中分配内存,除非显式指定 GFP_CMA 标志。
架构差异: x86上pageblock_order通常为2^9=512页(2MB),ARM64上为2^10=1024页(4MB)。CMA的位图粒度与pageblock大小解耦,通过 order_per_bit 灵活调整。
pageblock迁移类型状态转换(CMA视角) MIGRATE_CMA 初始状态 / 空闲状态 MIGRATE_ISOLATE 分配中 / 迁移中 MIGRATE_MOVABLE 迁移目标状态 CMA分配 释放完成 页面迁移 迁移完成,恢复CMA类型 注意:MIGRATE_ISOLATE 是临时状态,分配或迁移完成后必须立即转换 如果长时间停留在 ISOLATE 状态,会导致伙伴系统碎片化

3.5 数据结构协同工作示例

以下是一个典型的CMA分配调用链,展示各数据结构如何协同工作:

// 驱动调用示例
struct cma *cma = cma_areas[0];          // 获取CMA区域
struct cma_alloc alloc = {
    .cma = cma,
    .count = 256,                         // 请求256页(1MB)
    .order = 8,                           // 2^8 = 256页
    .gfp_mask = GFP_KERNEL,
    .owner = "my_driver",
};

// 1. cma_alloc() 内部调用 bitmap_find_next_zero_area()
//    在位图中查找连续256个零位
// 2. 找到后 bitmap_set() 标记为已分配
// 3. 将对应pageblock从 MIGRATE_CMA 切换为 MIGRATE_ISOLATE
// 4. 迁移pageblock中已有的不可移动页
// 5. 返回起始物理地址

unsigned long pfn = cma_alloc(&alloc);
if (pfn) {
    // 分配成功,pfn 指向连续256页的起始
    phys_addr_t pa = pfn << PAGE_SHIFT;
    // 驱动进行DMA映射...
}
调试技巧: 通过 /sys/kernel/debug/cma/ 下的接口可以查看每个CMA区域的位图使用率、分配次数和失败次数。使用 cat /sys/kernel/debug/cma/camera/bitmap 可以导出原始位图数据。

3.6 本章小结

数据结构 核心作用 关键字段
struct cma 描述一个CMA区域 base_pfn, count, bitmap, order_per_bit
struct cma_alloc 封装单次分配请求 cma, start, count, order, owner
bitmap 跟踪pageblock分配状态 每位0/1,支持快速查找
pageblock类型 控制内存迁移与隔离 MIGRATE_CMA / ISOLATE / MOVABLE