引言:GPU 要一块"物理连续"的缓冲区,buddy 却凑不出来
想象一块 GPU:它的 DMA 引擎要一块 4MB 的物理连续 内存当帧缓冲(framebuffer)。设备 DMA 不像 CPU------它没有 MMU 页表,只能对着连续的物理地址搬数据。
问题来了:buddy 分配器(#2)按 2 的幂给页,系统跑久了,4KB 空闲页东一块西一块,alloc_pages(order=10)(4MB)大概率失败------不是没内存,是凑不出连续块(外部碎片,正是 #9 讲的问题)。
compaction(#9)能"事后规整"凑连续块,但它是被动 + 有延迟 的------等设备要分配了才去规整,可能来不及。CMA(Contiguous Memory Allocator,连续内存分配器) 换个思路:启动时就预留一大块连续物理内存区,平时让 movable 页"借住",设备真要用时把这些 movable 页迁走、瞬间腾出连续区。
这篇讲的就是这条链路:预留(MIGRATE_CMA)→ 借用(buddy 把 movable 页分进去)→ 腾出(alloc_contig_range 迁走 movable 页)→ 分配(dma_alloc_contiguous)。
全景图
#mermaid-svg-WVtqGC5goZZaoEZO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WVtqGC5goZZaoEZO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WVtqGC5goZZaoEZO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WVtqGC5goZZaoEZO .error-icon{fill:#552222;}#mermaid-svg-WVtqGC5goZZaoEZO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WVtqGC5goZZaoEZO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WVtqGC5goZZaoEZO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WVtqGC5goZZaoEZO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WVtqGC5goZZaoEZO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WVtqGC5goZZaoEZO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WVtqGC5goZZaoEZO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WVtqGC5goZZaoEZO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WVtqGC5goZZaoEZO .marker.cross{stroke:#333333;}#mermaid-svg-WVtqGC5goZZaoEZO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WVtqGC5goZZaoEZO p{margin:0;}#mermaid-svg-WVtqGC5goZZaoEZO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WVtqGC5goZZaoEZO .cluster-label text{fill:#333;}#mermaid-svg-WVtqGC5goZZaoEZO .cluster-label span{color:#333;}#mermaid-svg-WVtqGC5goZZaoEZO .cluster-label span p{background-color:transparent;}#mermaid-svg-WVtqGC5goZZaoEZO .label text,#mermaid-svg-WVtqGC5goZZaoEZO span{fill:#333;color:#333;}#mermaid-svg-WVtqGC5goZZaoEZO .node rect,#mermaid-svg-WVtqGC5goZZaoEZO .node circle,#mermaid-svg-WVtqGC5goZZaoEZO .node ellipse,#mermaid-svg-WVtqGC5goZZaoEZO .node polygon,#mermaid-svg-WVtqGC5goZZaoEZO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WVtqGC5goZZaoEZO .rough-node .label text,#mermaid-svg-WVtqGC5goZZaoEZO .node .label text,#mermaid-svg-WVtqGC5goZZaoEZO .image-shape .label,#mermaid-svg-WVtqGC5goZZaoEZO .icon-shape .label{text-anchor:middle;}#mermaid-svg-WVtqGC5goZZaoEZO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WVtqGC5goZZaoEZO .rough-node .label,#mermaid-svg-WVtqGC5goZZaoEZO .node .label,#mermaid-svg-WVtqGC5goZZaoEZO .image-shape .label,#mermaid-svg-WVtqGC5goZZaoEZO .icon-shape .label{text-align:center;}#mermaid-svg-WVtqGC5goZZaoEZO .node.clickable{cursor:pointer;}#mermaid-svg-WVtqGC5goZZaoEZO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WVtqGC5goZZaoEZO .arrowheadPath{fill:#333333;}#mermaid-svg-WVtqGC5goZZaoEZO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WVtqGC5goZZaoEZO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WVtqGC5goZZaoEZO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WVtqGC5goZZaoEZO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WVtqGC5goZZaoEZO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WVtqGC5goZZaoEZO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WVtqGC5goZZaoEZO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WVtqGC5goZZaoEZO .cluster text{fill:#333;}#mermaid-svg-WVtqGC5goZZaoEZO .cluster span{color:#333;}#mermaid-svg-WVtqGC5goZZaoEZO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WVtqGC5goZZaoEZO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WVtqGC5goZZaoEZO rect.text{fill:none;stroke-width:0;}#mermaid-svg-WVtqGC5goZZaoEZO .icon-shape,#mermaid-svg-WVtqGC5goZZaoEZO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WVtqGC5goZZaoEZO .icon-shape p,#mermaid-svg-WVtqGC5goZZaoEZO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WVtqGC5goZZaoEZO .icon-shape .label rect,#mermaid-svg-WVtqGC5goZZaoEZO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WVtqGC5goZZaoEZO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WVtqGC5goZZaoEZO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WVtqGC5goZZaoEZO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 启动预留
cma_declare_contiguous_nid :239
标记 MIGRATE_CMA 区
mmzone.h:62
平时: movable 页借用
buddy 把 movable 页分进 CMA 区
设备要连续内存
dma_alloc_contiguous :357
cma_alloc cma.c:429
bitmap 找空闲位
alloc_contig_range :6109
腾出连续区
start_isolate_page_range :6149
隔离 pageblock
__alloc_contig_migrate_range :6030
迁走 movable 页(复用 #9)
isolate_freepages_range :6217
摘出空闲连续页
设备拿到连续物理内存
DMA 直接搬数据
一句话:启动预留 MIGRATE_CMA 区(movable 页平时借用)→ 设备 dma_alloc_contiguous 时 cma_alloc 用 bitmap 找空闲位 → alloc_contig_range 隔离 pageblock、迁走 movable 页、摘出连续空闲页 → 设备拿到物理连续内存做 DMA。
一、CMA 是什么:预留 + 借用 + 腾出
1.1 为什么 buddy 给不了连续块
buddy(#2)按 2 的幂分配,空闲页会随着分配/释放被切碎成小 order 的块。系统长期运行后,虽然空闲总量够,但凑不出高阶连续块(外部碎片,#9 详述)。而设备 DMA 恰恰要物理连续------这是"按需分配"的 buddy 与"要连续"的设备之间的根本矛盾。
1.2 CMA 的三步设计
CMA 的核心思想是把"连续"这个属性提前锁定,而不是等用时再凑:
| 阶段 | 干什么 | 关键点 |
|---|---|---|
| 预留 | 启动时划出一大块连续物理内存,标记 MIGRATE_CMA |
连续性是"生下来就有"的 |
| 借用 | 平时这块内存闲着浪费,让 movable 页(可迁移的用户页)借住 | 不独占,内存不浪费 |
| 腾出 | 设备要用时,把借住的 movable 页迁走,瞬间腾出连续区 | 复用 #9 compaction 的迁移 |
关键在"借用":CMA 区不是 从 buddy 里完全隔离的死区,而是允许 movable 页住进来 ------这样设备不用时,CMA 区照样能被普通分配利用,内存不浪费。设备要用时,alloc_contig_range 把这些 movable 页迁到别处(它们本来就能搬),腾出连续块。
Why 只借给 movable、不借给 unmovable :CMA 腾出连续区靠的是"迁移"(把页搬走)。而
MIGRATE_UNMOVABLE(内核直接映射、DMA pin 的页)物理地址是约定死的,搬不动 (#9 讲过)。所以 CMA 区只允许 movable 页借住------设备要用了,随时能把它们迁走;若让 unmovable 页住进来,设备来了就卡死。这也是为什么MIGRATE_CMA和MIGRATE_MOVABLE关系紧密(is_migrate_movable把 movable 和 CMA 一起当作"可迁移",mmzone.h:81)。
二、预留:cma_declare_contiguous 标记 MIGRATE_CMA 区
2.1 预留接口
启动时(或设备驱动 probe 时)调 cma_declare_contiguous_nid(cma.c:239)预留一块 CMA 区:
c
// cma_declare_contiguous_nid(mm/cma.c:239)
int __init cma_declare_contiguous_nid(phys_addr_t base,
phys_addr_t size, phys_addr_t limit,
phys_addr_t alignment, unsigned int order_per_bit,
bool fixed, const char *name, struct cma **res_cma,
int nid)
参数里关键的是 size(预留多大)和 order_per_bit(bitmap 一个 bit 代表多少页,通常 pageblock_order)。它把这块内存从 memblock 里"圈"出来(固定 base 或从 limit 以内挑),建立 struct cma 描述符。
2.2 struct cma:一块 CMA 区的描述符
c
// struct cma(mm/cma.h:13)
struct cma {
unsigned long base_pfn; // CMA 区起始页帧
unsigned long count; // 总页数
unsigned long *bitmap; // 位图:哪段被分配了
unsigned int order_per_bit; // 一个 bit 代表多少页(通常 pageblock)
...
};
bitmap 是 CMA 分配的核心:一个 bit 代表 order_per_bit 个页的一段 。分配时找一段连续的空闲 bit(bitmap_find_next_zero_area_off),置位表示"这段被占了"。这样"找连续空间"就变成"找位图里连续的 0",O(n) 高效。
2.3 MIGRATE_CMA:一种特殊的 migratetype
预留的 CMA 区的 pageblock 被标记为 MIGRATE_CMA(mmzone.h:62,enum migratetype 里的一种)。源码注释(:53-61)点破了它的设计意图:
MIGRATE_CMA migration type is designed to mimic the way ZONE_MOVABLE works... movable pages are migrated away from MIGRATE_CMA pageblocks and page allocator never implicitly change migration type of MIGRATE_CMA pageblock.
两个要点:
- buddy 可以把 movable 页分进 CMA 区 ------这就是"借用"。注意机制:CMA 不在
fallbacks[]数组(page_alloc.c:1597,只有 UNMOVABLE/RECLAIMABLE/MOVABLE 三者互 fallback)里,而是走独立的__rmqueue_cma_fallback(:1604)路径 ------分配类型是MIGRATE_MOVABLE时,gfp_to_alloc_flags_cma(:3033)置ALLOC_CMA标志,__rmqueue(:2086)才从 CMA 区拿页,还带一条平衡策略:CMA 空闲页超过 zone 空闲一半就优先从 CMA 拿(:2092-2096,避免 regular 区被挤爆)。 - CMA 区不会被 buddy 隐式改成别的类型 ------它永远是
MIGRATE_CMA,随时能被识别并腾出。
三、分配:dma_alloc_contiguous → cma_alloc
设备驱动要连续内存,走 dma_alloc_contiguous(kernel/dma/contiguous.c:357)。它按优先级挑 CMA 区------设备专属区 dev->cma_area → NUMA per-node 区 → 默认全局区,中间还挡一道"单页本来就连续,别浪费 CMA":
c
// dma_alloc_contiguous(kernel/dma/contiguous.c:357)------ 关键片段
struct page *dma_alloc_contiguous(struct device *dev, size_t size, gfp_t gfp)
{
if (!gfpflags_allow_blocking(gfp)) // ① CMA 要迁页=要能睡,不能睡直接放弃
return NULL;
if (dev->cma_area) // ② 设备专属 CMA 区
return cma_alloc_aligned(dev->cma_area, size, gfp);
if (size <= PAGE_SIZE) // ③ 单页物理必然连续,别浪费 CMA
return NULL;
... // ④ NUMA per-node 区(CONFIG_DMA_NUMA_CMA)
return cma_alloc_aligned(dma_contiguous_default_area, size, gfp); // ⑤ 默认全局区
}
// cma_alloc_aligned(contiguous.c:335)------ 把 size 换算成页数 + align,再进 cma_alloc
align = min(get_order(size), CONFIG_CMA_ALIGNMENT);
return cma_alloc(cma, size >> PAGE_SHIFT, align, gfp & __GFP_NOWARN);
cma_alloc_aligned 只是薄薄一层换算,真正的分配在 cma_alloc(cma.c:429):
c
// cma_alloc(mm/cma.c:429)------ 关键片段
for (;;) {
spin_lock_irq(&cma->lock);
bitmap_no = bitmap_find_next_zero_area_off(cma->bitmap,
bitmap_maxno, start, bitmap_count, mask, offset); // ① 找连续空闲位
if (bitmap_no >= bitmap_maxno) { ... break; }
bitmap_set(cma->bitmap, bitmap_no, bitmap_count); // ② 标记这段被占
spin_unlock_irq(&cma->lock);
pfn = cma->base_pfn + (bitmap_no << cma->order_per_bit);
mutex_lock(&cma_mutex);
ret = alloc_contig_range(pfn, pfn + count, MIGRATE_CMA, // ③ 真正腾出连续区
GFP_KERNEL | (no_warn ? __GFP_NOWARN : 0));
mutex_unlock(&cma_mutex);
if (ret == 0) {
page = pfn_to_page(pfn);
break;
}
cma_clear_bitmap(cma, pfn, count); // 失败回滚 bitmap
if (ret != -EBUSY)
break;
/* try again with a bit different memory target */
start = bitmap_no + mask + 1; // ④ -EBUSY 换位置重试
}
三步:bitmap 找连续空闲位 → 标记 → alloc_contig_range 真正腾出连续物理页 。alloc_contig_range 返回 -EBUSY 说明这块的 movable 页迁不走(比如有页正被 pin),就回滚 bitmap、换个位置重试。
四、腾出连续区:alloc_contig_range
alloc_contig_range(page_alloc.c:6109)是 CMA 的心脏------它把"被 movable 页借住"的连续区腾空。四步:
c
// alloc_contig_range(page_alloc.c:6109)------ 关键片段
struct compact_control cc = {
.order = -1,
.mode = MIGRATE_SYNC,
.alloc_contig = true, // 标记: 这是"腾连续区"而非"消碎片"
...
};
ret = start_isolate_page_range(start, end, migratetype, 0, gfp_mask); // ① 隔离
...
drain_all_pages(cc.zone); // ② 清 per-cpu 缓存
ret = __alloc_contig_migrate_range(&cc, start, end); // ③ 迁走 movable 页
...
outer_end = isolate_freepages_range(&cc, outer_start, end); // ④ 摘出连续空闲页
四步拆开:
start_isolate_page_range(:6149) :把范围内的 pageblock 标记MIGRATE_ISOLATE------临时"冻结"这一整段,阻止 buddy 再往里面分配,也防止别人并发动它。drain_all_pages(:6153):把各 CPU 的 per-cpu 页缓存清空,让散落的空闲页回到 buddy,方便后续统一处理。__alloc_contig_migrate_range(:6165) :把隔离区里还占着的 movable 页迁走------这是复用 #9 compaction 的核心。isolate_freepages_range(:6217):把腾出来的连续空闲页从 buddy 摘出来,交给调用者(设备)。
4.1 迁移复用 compaction 引擎
第 ③ 步 __alloc_contig_migrate_range(page_alloc.c:6030)的第一行注释说得直白:
c
/* This function is based on compact_zone() from compaction.c. */
int __alloc_contig_migrate_range(struct compact_control *cc,
unsigned long start, unsigned long end)
{
...
while (pfn < end || !list_empty(&cc->migratepages)) {
if (list_empty(&cc->migratepages)) {
ret = isolate_migratepages_range(cc, pfn, end); // ① 隔离 movable 页
...
}
...
ret = migrate_pages(&cc->migratepages, alloc_migration_target,
NULL, (unsigned long)&mtc, cc->mode, MR_CONTIG_RANGE, NULL); // ② 迁走
...
}
}
它逐段 把 movable 页隔离(isolate_migratepages_range)→ 迁走(migrate_pages,migrate.c:1885)。MR_CONTIG_RANGE 标记"这次迁移是为腾连续区"。
Why CMA 复用 compaction 而不是重写一套 :#9 的 compaction 已经解决"把 movable 页从 A 搬到 B、腾出连续空间"这件事,
compact_control+isolate_migratepages+migrate_pages这套引擎是现成的。CMA 只是换了目标 ------compaction 是"整个 zone 扫描、把碎片挤到一起",CMA 是"就这一块 [start, end) 范围、把里面的 movable 页全搬出去"。所以alloc_contig_range构造一个compact_control(alloc_contig = true、mode = MIGRATE_SYNC)复用同一套迁移代码。这正是内核"同一引擎、不同调用方"的复用典范------compaction、NUMA 迁移(#14)、CMA 三者的migrate_pages调用方。
五、释放:cma_release
设备用完连续内存,dma_free_contiguous(contiguous.c:407)→ cma_release(cma.c:560):
c
// cma_release(mm/cma.c:560)------ 关键片段
bool cma_release(struct cma *cma, const struct page *pages,
unsigned long count)
{
if (!cma_pages_valid(cma, pages, count))
return false;
...
free_contig_range(pfn, count); // ① 把连续页还给 buddy
cma_clear_bitmap(cma, pfn, count); // ② 清 bitmap 对应位
...
}
释放就是把连续页还给 buddy(free_contig_range)、清 bitmap 位。还回去的页又被标回 MIGRATE_CMA,于是又能被 movable 页借用------CMA 区回到"闲着等下一次设备分配"的循环。
六、与前面各篇的关系
| 篇 | 关系 |
|---|---|
| #2 buddy | CMA 区是 buddy 里一块特殊 MIGRATE_CMA 区,buddy 平时把 movable 页分进去 |
| #9 compaction | alloc_contig_range 复用 compact_control + isolate_migratepages + migrate_pages,只是范围限定在 [start, end) |
| #6 GUP | GUP pin 的页不可迁移,会挡住 CMA 腾出(-EBUSY)------pin 页和 CMA 连续内存天然冲突 |
| #14 NUMA | cma_declare_contiguous_nid 可指定 node,CMA 区有 NUMA 亲和 |
七、与 GPU / AI Infra 的关系 + 记住三点
7.1 CMA 是设备 DMA 的地基
- GPU 帧缓冲 / CUDA :GPU 的显存管理(
cudaMalloc走 UVA,背后是 GUP pin + 大页),而集成 GPU、摄像头、编解码器这类"要物理连续缓冲区"的设备,靠 CMA 兜底。 - DPDK / 网络:DPDK 要 hugepage 连续内存,某些场景也走 CMA。
- 与 IOMMU 的分工 :IOMMU(IOMMU 篇)能把不连续的物理页映射成连续的 IOVA,让设备"看起来"连续------有了 IOMMU,很多设备就不需要 CMA 了。但没有 IOMMU(或为了低延迟直通)的场景,CMA 的物理连续仍是刚需。两者是"软件重映射"和"硬件真连续"两条路。
7.2 记住三点
-
CMA = 预留 + 借用 + 腾出 :启动预留
MIGRATE_CMA区(cma_declare_contiguous_nidcma.c:239),平时让 movable 页借用(不浪费内存),设备要用时迁走 movable 页腾出连续区。只借给 movable,因为 unmovable 搬不动(#9)。 -
分配 = bitmap 找位 + alloc_contig_range 腾区 :
dma_alloc_contiguous(contiguous.c:357)→cma_alloc(cma.c:429)用 bitmap 找连续空闲位 →alloc_contig_range(page_alloc.c:6109)四步腾区:start_isolate_page_range(:6149)隔离 →drain_all_pages清缓存 →__alloc_contig_migrate_range(:6030)迁 movable 页 →isolate_freepages_range(:6217)摘连续页。 -
复用 compaction 引擎 :
__alloc_contig_migrate_range注释明说"based on compact_zone()",用isolate_migratepages_range+migrate_pages(MR_CONTIG_RANGE)迁页------compaction(#9)、NUMA 迁移(#14)、CMA 三者共用migrate_pages。释放cma_release(:560)把页还 buddy、清 bitmap,回到可借用状态。
附:本篇关键源码索引
| 符号 | 位置 |
|---|---|
enum migratetype(MIGRATE_UNMOVABLE/MOVABLE/CMA) |
include/linux/mmzone.h:45(:46 / :47 / :62) |
struct cma(base_pfn + count + bitmap + order_per_bit) |
mm/cma.h:13 |
cma_declare_contiguous_nid / cma_declare_contiguous |
mm/cma.c:239 / include/linux/cma.h:39 |
cma_alloc(bitmap 找位 + 腾区) |
mm/cma.c:429 |
cma_release(还 buddy + 清 bitmap) |
mm/cma.c:560 |
dma_alloc_contiguous(设备分配入口) |
kernel/dma/contiguous.c:357 |
cma_alloc_aligned(size→页数/align 换算) |
kernel/dma/contiguous.c:335 |
alloc_contig_range(四步腾区) |
mm/page_alloc.c:6109 |
__alloc_contig_migrate_range(复用 compact_zone) |
mm/page_alloc.c:6030 |
start_isolate_page_range(隔离 pageblock 为 MIGRATE_ISOLATE) |
mm/page_isolation.c:528 |
isolate_freepages_range(摘出连续空闲页) |
mm/compaction.c:722 |
migrate_pages(迁移引擎,接 #9) |
mm/migrate.c:1885 |