内存管理-13-CMA内存-2-ChatGPT
基于 Linux-6.1.114
一、分配大块(比如8MB)的CMA内存使用什么接口?
分配大块 CMA 内存的接口
1. 核心接口:cma_alloc()
#include <linux/cma.h> struct page *cma_alloc(struct cma *cma, unsigned long count, unsigned int align, bool no_warn);
参数:
cma: CMA 区域指针(如 dma_contiguous_default_area 或设备私有 CMA)
count: 请求的页数(8MB = 2048 页)
align: 对齐要求(以 PAGE_SIZE 为单位的 order,0 表示不额外对齐)
no_warn: 失败时是否抑制告警日志
返回值:
成功返回起始 struct page *,失败返回 NULL
2. 分配 8MB 示例
#include <linux/cma.h> #include <linux/dma-contiguous.h> struct page *pages; unsigned long count = (8 * SZ_1M) >> PAGE_SHIFT; // 2048 pages pages = cma_alloc(dev_get_cma_area(dev), count, 0, false); if (!pages) { pr_err("CMA alloc 8MB failed\n"); return -ENOMEM; } /* 使用 ... */ void *vaddr = page_address(pages); // 或 vmap phys_addr_t phys = page_to_phys(pages); /* 释放 */ cma_release(dev_get_cma_area(dev), pages, count);
3. 内部工作流程
cma_alloc() --> bitmap_find_next_zero_area_off() // 在 CMA bitmap 中找连续空闲区域 --> bitmap_set() // 标记为已分配 --> alloc_contig_range(pfn, pfn+count, MIGRATE_CMA, gfp_mask) --> __alloc_contig_migrate_range() // 迁移该范围内的已用页到其他地方 --> isolate_freepages_range() // 隔离空闲页 --> 返回连续物理页
关键点:alloc_contig_range() 会把该范围内已经被借出去使用的 MOVABLE 页面迁移走,腾出连续空间。这就是 CMA 设计的核心——平时借出去,设备需要时迁移回来。####
4. 上层封装接口
4.1 DMA 子系统接口(最常用)
#include <linux/dma-mapping.h> // 分配 coherent DMA 内存(自动使用 CMA) void *vaddr = dma_alloc_coherent(dev, 8 * SZ_1M, &dma_handle, GFP_KERNEL); // 释放 dma_free_coherent(dev, 8 * SZ_1M, vaddr, dma_handle);
内部调用链:dma_alloc_coherent --> dma_alloc_attrs --> dma_alloc_contiguous --> cma_alloc
4.2 直接使用 dma_alloc_contiguous()
#include <linux/dma-contiguous.h> struct page *page = dma_alloc_contiguous(dev, 8 * SZ_1M, GFP_KERNEL); // 释放 dma_free_contiguous(dev, page, 8 * SZ_1M);
这个接口会按优先级尝试:设备私有 CMA --> per-NUMA CMA --> 全局默认 CMA。####
4.3 ION/DMA-BUF heap(Android/MTK 常用)
在 Android/MTK 平台上,大块 CMA 通常通过 DMA-BUF CMA heap 分配:
/* 用户态通过 ioctl 分配 */ int heap_fd = open("/dev/dma_heap/linux,cma", O_RDWR); struct dma_heap_allocation_data data = { .len = 8 * 1024 * 1024, .fd_flags = O_RDWR | O_CLOEXEC, }; ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data); // data.fd 就是 DMA-BUF fd
5. 接口选择建议
------------------------------------------------------------------------- 场景 推荐接口 ------------------------------------------------------------------------- 驱动中为设备分配 DMA buffer dma_alloc_coherent() 需要物理页但不需要 DMA 映射 cma_alloc() 直接调用 需要在多个设备间共享 buffer DMA-BUF CMA heap 内核模块中指定特定 CMA 区域 cma_alloc(specific_cma, ...) -------------------------------------------------------------------------
6. 注意事项
(1) 必须可睡眠:cma_alloc 内部会迁移页面(可能耗时数十毫秒),不能在原子上下文调用。
(2) 对齐最小值:CMA 分配对齐至少是 pageblock_nr_pages(通常 1024 页 = 4MB),小于此值的 align 参数会被内部提升
(3) 可能失败:如果 CMA 区域中的页面被 pin 住(如 mlock、get_user_pages 长期持有),迁移无法完成,分配会失败
(4) 重试机制:cma_alloc 内部最多重试 5 次,每次间隔 100ms(等待 pin 释放)
二、CMA内存的分配会调用伙伴系统相关的接口吗
CMA 分配不走常规的 buddy 分配路径(不走 __alloc_pages / rmqueue)。它走的是一条独立的路径:
1. CMA 分配的实际调用链
cma_alloc() --> alloc_contig_range(pfn_start, pfn_end, MIGRATE_CMA, gfp_mask) --> start_isolate_page_range() // 将目标 pageblock 标记为 MIGRATE_ISOLATE --> __alloc_contig_migrate_range() // 迁移已被占用的页 │ --> migrate_pages() // 逐页迁移(迁移目标页通过 buddy 分配) │ --> alloc_migration_target() │ --> __alloc_pages() // <-- 这里会用 buddy 分配迁移目标页#### │ --> isolate_freepages_range() // 从 buddy 空闲链表中"摘下"目标范围内的空闲页 │ --> __isolate_free_page() // 直接从 free_list 中 del,不走 rmqueue │ --> undo_isolate_page_range() // 恢复 pageblock 迁移类型
2. 关键区别
--------------------------------------------------------------------------------------------------- 操作 是否调用 buddy 分配接口 --------------------------------------------------------------------------------------------------- CMA 分配目标页本身 否 — 通过 isolate_freepages_range() 直接从 free_list 摘取 迁移已用页到其他位置 是 — 迁移目标需要通过 __alloc_pages() 从 buddy 分配一个新页 CMA 释放 是 — cma_release() --> __free_pages() 归还到 buddy ---------------------------------------------------------------------------------------------------
3. 详细说明
CMA 拿到连续物理页的方式:不是"从 buddy 系统分配",而是"把 buddy 系统中属于目标范围的空闲页直接隔离出来,同时把该范围内已分配的页迁移走"。本质上是:
(1) 划一个物理地址范围(比如 pfn 0x1000 ~ 0x1800)
(2) 这个范围里的空闲页 --> 直接从 buddy free_list 上摘走(del_page_from_free_list)
(3) 这个范围里的已用页 --> 在其他地方分配一个新页(这里走 buddy),把内容复制过去,取消旧页的映射。
4. CMA 释放
cma_release() --> __free_pages() //归还给 buddy 系统的 MIGRATE_CMA 空闲链表
释放确实调用了伙伴系统的接口,页面回到 buddy 的 MIGRATE_CMA 空闲链表上,之后可以再被"借"给 MOVABLE 分配使用。
5. 总结
┌─────────────────────────────────────────────────────────────────┐ │ cma_alloc 全过程 │ ├─────────────────────────────────────────────────────────────────┤ │ 1. 隔离目标范围 (不走 buddy 分配) │ │ 2. 迁移已用页 --> 迁移目标通过 buddy 分配 <-- 间接调用 │ │ 3. 摘取空闲页 (直接操作 free_list, 不走 rmqueue) │ │ 4. 返回连续页 │ ├─────────────────────────────────────────────────────────────────┤ │ cma_release │ │ --> __free_pages() --> buddy 系统 <-- 直接调用 │ └─────────────────────────────────────────────────────────────────┘
所以严格说:CMA 分配过程中间接调用了 buddy 接口(为迁移分配目标页),但获取最终连续内存本身不是通过 buddy 分配的,而是通过隔离和摘取。
三、一般哪类型的页更容易 pin 住
匿名页更容易被 pin 住。
1. 原因分析
**文件页(page cache)**几乎不会被长期 pin:
(1) 文件页的 backing store 是磁盘文件,迁移时只需要在新位置重新建立映射,原页面内容随时可以从磁盘重读;
(2) page_mapcount 通常不高(只有映射了该文件的进程);
(3) 即使被 mmap 映射,page migration 也能顺利处理(unmap → copy → remap);
(4) 文件页不太会被 get_user_pages 长期持有;
2. 匿名页容易被 pin 的常见场景:
------------------------------------------------------------------------------------------------------ Pin 来源 说明 典型持续时间 ------------------------------------------------------------------------------------------------------ get_user_pages()/pin_user_pages() DMA 传输中固定用户态 buffer 几毫秒到永久 VFIO (GPU/设备直通) 将整个进程地址空间 pin 给设备 整个设备使用周期 RDMA (InfiniBand/RoCE) 网络零拷贝注册的内存区域 连接生命期 mlock/mlockall 用户显式锁定页面,不允许换出 进程生命期 io_uring 固定 buffer IORING_REGISTER_BUFFERS 注册到注销之间 V4L2 USERPTR/DMA-BUF 多媒体框架把用户态 buffer pin 给硬件 帧处理周期 perf_event 性能计数器 ring buffer event 生命期 ------------------------------------------------------------------------------------------------------
3. 匿名页容易被 pin 而文件页不容易的原因
根本原因:直接 I/O 和设备 DMA 操作的目标几乎都是用户态匿名页(堆、栈、mmap 匿名映射)。
用户进程:
char *buf = malloc(8MB); //匿名页 ioctl(gpu_fd, SUBMIT_JOB, buf); //驱动 pin_user_pages → 匿名页被 pin int fd = open("file", O_RDONLY); char *map = mmap(..., fd, ...); //文件页,很少有驱动对文件映射做 get_user_pages
设备驱动通过 get_user_pages / pin_user_pages 锁定的内存,几乎 100% 是用户态通过 malloc/mmap(MAP_ANONYMOUS) 分配的匿名页。
4. 对 CMA 的影响
由于 CMA 区域的空闲页会被借给 MOVABLE 分配(而匿名页是 MOVABLE 类型),这些被借出的匿名页如果被 pin 住了:
CMA 区域: [匿名页A(pinned)] [匿名页B(free)] [匿名页C(pinned)] [文件页D] ↑ 无法迁移 ↑ 无法迁移 ↑ 可迁移 cma_alloc → alloc_contig_range → migrate_pages → 文件页D: 成功迁移 ✓ → 匿名页B: 无人引用,直接回收 ✓ → 匿名页A: pin count > 0,迁移失败 ✗ → cma_alloc 失败
5. 实际 MTK/Android 中最常见的 pin 源
(1) GPU 驱动(Mali/IMG):渲染时 pin 用户态的 buffer 给 GPU 做 DMA.
(2) Camera ISP:preview/capture 时 pin 用户态 buffer
(3) Video codec:编解码时 pin 用户态 frame buffer
(4) ION/DMA-BUF:跨进程共享 buffer 被 map 到多个设备
这些在 Android 多媒体管线中极为常见,是 CMA 分配失败的主要原因。
四、什么场景下需要将页面pin住
1. 需要 Pin 住页面的场景
核心原则:当有外部实体(硬件设备或其他非 MMU 管理的上下文)正在通过物理地址直接访问该页时,必须 pin 住,否则页面被迁移或回收后,物理地址失效,设备访问会导致数据损坏或系统崩溃。
1.1 硬件 DMA 传输
这是最主要的 pin 场景。DMA 引擎通过物理地址访问内存,不经过 CPU 的 MMU:
CPU 视角: 虚拟地址 → MMU → 物理地址 → 内存
DMA 视角: 物理地址 ─────────────────→ 内存 (绕过 MMU)
如果页面在 DMA 传输期间被迁移到新物理地址,DMA 仍然访问旧地址 → 读到垃圾数据或写坏其他人的内存。
典型场景:
---------------------------------------------------------------------------- 设备 操作 pin 时长 ---------------------------------------------------------------------------- 网卡 零拷贝发送/接收用户态 buffer 单次传输(微秒级) NVMe/SCSI Direct I/O 读写用户 buffer 单次 I/O(毫秒级) GPU 渲染命令引用的纹理/顶点 buffer 渲染帧周期 Camera ISP 写入 preview/capture frame 帧处理周期 Video encoder 读取待编码的 YUV frame 编码周期 ----------------------------------------------------------------------------
1.2 RDMA/网络零拷贝
InfiniBand / RoCE 网络允许远端机器直接读写本机内存(Remote DMA)。注册的内存区域在整个 RDMA 连接生命周期内必须保持物理地址不变:
ibv_reg_mr(pd, buf, size, access_flags); //注册 → pin // ... 连接持续数小时 ... ibv_dereg_mr(mr); //注销 → unpin
pin 时长:连接生命期(可能数小时到数天)。 这是对 CMA 影响最大的场景之一。
1.3 设备直通 (VFIO/GPU passthrough)
虚拟化场景中,把物理设备直接给虚拟机使用。设备的 IOMMU 页表映射指向 host 物理地址,整个 VM 使用的内存都必须 pin:
// VFIO pin 整个虚拟机内存 vfio_pin_pages(device, user_pfn, npage, ...);
pin 时长:虚拟机整个运行周期。
1.4 用户态显式锁定
//mlock 防止页面被换出到 swap mlock(addr, len); mlockall(MCL_CURRENT | MCL_FUTURE);
严格说 mlock 不等于 pin(它阻止 swap-out 但不阻止 migration),但在某些旧内核或特定配置下会阻止迁移。
1.5 io_uring 固定 buffer
struct iovec iovs[N] = { ... }; io_uring_register(ring_fd, IORING_REGISTER_BUFFERS, iovs, N); //内部调用 pin_user_pages,页面被 pin 直到 unregister
pin 时长:注册到注销期间(可能整个进程生命期)。
1.6 内核内部临时 pin
pages = get_user_pages(addr, nr_pages, flags, ...); //内核访问用户态内存时临时 pin // ... 内核读写这些页 ... put_page(pages[i]); // 立即 unpin
用于 read()/write() 系统调用中内核需要直接访问用户态 buffer 的场景,通常很短暂。
2. Pin 与不 Pin 的判断标准
--------------------------------------------------------------------------------------------- 条件 是否需要 pin --------------------------------------------------------------------------------------------- 只有 CPU 通过 MMU 虚拟地址访问 不需要 — 迁移后更新页表即可 设备通过物理地址/IOVA 访问 需要 — 设备不走 MMU,地址变了它不知道 有 IOMMU 且可以更新 IOMMU 映射 理论上不需要,但实践中很少这么做 内核代码持有 struct page * 指针操作页面内容 需要 — page_to_virt 的结果不能变 ---------------------------------------------------------------------------------------------
3. 对 CMA 的影响程度
(1) 影响最大(长期 pin,常驻):
VFIO/GPU passthrough > RDMA > io_uring 固定 buffer > mlock
(2) 影响中等(中等时长):
GPU 渲染 buffer > Camera/Video codec frame buffer
(3) 影响最小(极短暂):
Direct I/O > splice/sendfile > 普通 read/write 的 get_user_pages
在 Android/MTK 平台上,Camera 和 GPU 是 CMA pin 的主要来源。
五、上面说的 pin/unpin 是如何实现的
1. Page Pin/Unpin 的实现机制
核心原理:通过 refcount 编码 pin 信息####。Pin 不是一个独立的标志位,而是编码在页面的引用计数(_refcount)中。内核通过给 refcount 加一个特殊的偏移量来区分"普通引用"和"pin 引用":
#define GUP_PIN_COUNTING_BIAS (1U << 10) // = 1024
1.1 Pin 操作的实现(pin_user_pages --> try_grab_folio)
(1) 小页(order-0 普通页)
/* * mm/gup.c — try_grab_folio() 中 FOLL_PIN 分支 * 普通引用 (get_user_pages): refcount += 1 * Pin 引用 (pin_user_pages): refcount += GUP_PIN_COUNTING_BIAS (1024) * 加上 try_get_folio 已经 +1,总共增加 1024 */ folio_ref_add(folio, refs * (GUP_PIN_COUNTING_BIAS - 1));
每次 pin 一个小页,_refcount 增加 1024。
(2) 大页(compound page / THP)
/* 大页使用独立的 compound_pincount 字段, 同时 refcount 也 +1(保证页面不被释放) */ atomic_add(refs, folio_pincount_ptr(folio));
大页有专门的 compound_pincount 原子计数器,pin 计数精确。
1.2 Unpin 操作的实现(unpin_user_page)
/* unpin_user_page --> */ static void gup_put_folio(struct folio *folio, int refs, unsigned int flags) { if (flags & FOLL_PIN) { node_stat_mod_folio(folio, NR_FOLL_PIN_RELEASED, refs); if (folio_test_large(folio)) atomic_sub(refs, folio_pincount_ptr(folio)); //大页:减 pincount else refs *= GUP_PIN_COUNTING_BIAS; //小页:实际减 1024 } folio_put_refs(folio, refs); //减 ref }
2. 迁移时如何检测 Pin
迁移(migrate_pages --> folio_migrate_mapping)通过检查 refcount 是否恰好等于预期值来判断页面是否被额外 pin 住:
//mm/migrate.c — folio_migrate_mapping() int expected_count = folio_expected_refs(mapping, folio) + extra_count; //匿名页预期 refcount = 1(仅 isolation 持有) //文件页预期 refcount = 1 + nr_pages(page cache xarray 引用)+ private if (folio_ref_count(folio) != expected_count) return -EAGAIN; // refcount 不符合预期 --> 有人额外持有 --> 无法迁移
如果页面被 pin 了,refcount 会多出 1024(或更多),远超预期值,迁移直接返回 -EAGAIN(重试)或最终失败。
3. 判断页面是否被 pin 的快速检查
static inline bool folio_maybe_dma_pinned(struct folio *folio) //include/linux/mm.h { if (folio_test_large(folio)) return atomic_read(folio_pincount_ptr(folio)) > 0; /* 小页:refcount >= 1024 则"可能被 pin" */ return ((unsigned int)folio_ref_count(folio)) >= GUP_PIN_COUNTING_BIAS; }
4. 完整流程图
驱动调用 pin_user_pages(user_addr, nr_pages, ...) │ ├─ 遍历用户页表,找到物理页 ├─ 对每个页调用 try_grab_folio(page, 1, FOLL_PIN) │ ├─ 小页: refcount += 1024 │ └─ 大页: refcount += 1, compound_pincount += 1 │ └─ 返回 struct page *pages[] 给驱动 驱动设置 DMA: dma_map_page(dev, pages[i], ...) --> 设备通过物理地址访问页面(期间页面不能移动) 如果此时有人想迁移该页 (CMA alloc / compaction / NUMA balance): migrate_pages() --> folio_migrate_mapping() --> folio_ref_count(folio) = 1025 (1 正常引用 + 1024 pin) --> expected_count = 1 --> 1025 != 1 --> return -EAGAIN ← 迁移失败,页面保持原位 DMA 完成后,驱动释放: dma_unmap_page(dev, ...); unpin_user_page(pages[i]); --> 小页: refcount -= 1024 --> 页面此时可以正常迁移了
5. get_user_pages vs pin_user_pages 的区别
---------------------------------------------------------------------------------------------- get_user_pages (FOLL_GET) pin_user_pages (FOLL_PIN) ---------------------------------------------------------------------------------------------- refcount 增量 +1 +1024 是否阻止迁移 是(refcount 不等于预期) 是(更明确) 能否被系统检测为"被 pin" 不能精确区分 能(folio_maybe_dma_pinned) 释放 put_page() unpin_user_page() 用途 临时内核访问 DMA / 长期硬件访问 ----------------------------------------------------------------------------------------------
pin_user_pages 是较新的 API(5.6+ 引入),用 1024 的偏移量使得系统能区分"被设备 pin 住"和"只是普通引用多了几个"。旧的 get_user_pages 只加 1,和正常的 page cache 引用、进程映射引用混在一起,系统无法判断是否有 DMA pin。
6. 总结
---------------------------------------------------------------------------------------------- 层面 实现 ---------------------------------------------------------------------------------------------- 数据结构 page->_refcount + 大页的 compound_pincount Pin 编码 小页每 pin 一次 refcount += 1024;大页 pincount += 1 检测方法 refcount >= 1024 或 pincount > 0 阻止迁移原理 迁移时检查 refcount == 预期值,pin 导致不等 --> -EAGAIN 释放 unpin 减去相同的偏移量 ----------------------------------------------------------------------------------------------
六、CMA内存的增加除了 __free_pages() 释放进PCP和buddy的外,还有其他动态增加的接口吗?
没有。CMA 区域的总大小在启动时就固定了,运行时不能动态扩大。
1. 不存在的操作:
--------------------------------------------------------------------- 操作 是否存在 --------------------------------------------------------------------- 运行时动态增大 CMA 区域 ❌ 不存在 将非 CMA 内存转换为 CMA 内存 ❌ 不存在 在线添加新的 CMA 区域 ❌ 不存在 memory hotplug 增加 CMA ❌ hotplug 加的内存不会成为 CMA ---------------------------------------------------------------------
2. 总结
CMA 空闲页的增减完全是 CMA 区域内部的状态变化:
增加(页面回到 CMA free_list): (1) __free_pages / free_unref_page --> pageblock 是 CMA 的页面释放回 buddy (2) cma_release() --> 设备驱动归还独占内存 (3) 内存回收(reclaim) --> 回收 CMA 区域上的 page cache/匿名页 (4) 迁移(migrate) --> CMA 区域上的 MOVABLE 页被迁移走后空出 减少(页面从 CMA free_list 取走): (1) alloc_pages + ALLOC_CMA --> 从 CMA free_list 分配给 MOVABLE 请求 (2) cma_alloc() --> 设备驱动独占分配
所有这些都不改变 CMA 区域的总大小,只改变其中"空闲 vs 已用"的比例。
posted on 2026-08-13 21:32 Hello-World3 阅读(3) 评论(0) 收藏 举报
浙公网安备 33010602011771号