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在粗粒度与细粒度管理之间权衡。
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 分配流程(位图视角)
- 计算所需位数: 根据请求的页数
count和order_per_bit,计算需要连续的n个空闲位。 - 位图扫描: 调用
bitmap_find_next_zero_area()查找连续n个零位。 - 位图标记: 找到后,调用
bitmap_set()将这些位全部置1。 - 页面隔离: 将对应的物理页从伙伴系统迁移到CMA隔离列表。
3.3.3 释放流程(位图视角)
- 位图清零: 调用
bitmap_clear()将对应位清零。 - 页面归还: 将物理页从隔离列表移回伙伴系统。
- 迁移类型恢复: 将pageblock的迁移类型恢复为
MIGRATE_CMA。
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 灵活调整。
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 |