内存管理-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)    收藏  举报

导航