4、GPU内存管理基础:IOMMU/SMMU原理、连续内存分配器(CMA)、DMA-BUF机制、物理地址与虚拟地址映射

各位同学,咱们今天聊点硬核的——GPU内存管理。说实话,这块内容我当年刚入行时也啃了很久。你想想看,CPU和GPU是两个完全不同的处理器,它们各自有各自的内存视图,怎么让它们高效地共享数据?这就是内存管理要解决的核心问题。

我习惯把GPU内存管理比作一个跨国物流系统。CPU这边是中文地址,GPU那边是英文地址,中间还得有个海关(IOMMU/SMMU)来做地址翻译。搞不好就会丢包、延迟,甚至系统崩溃。嗯,咱们一个一个来看。

4.1 IOMMU/SMMU原理:地址翻译的“海关”

先说说IOMMU(Input-Output Memory Management Unit),在ARM平台上我们通常叫它SMMU(System Memory Management Unit)。这东西说白了就是一个硬件单元,专门给外设(比如GPU、网卡、硬盘控制器)做地址翻译用的。

为什么需要IOMMU?

我刚开始做驱动时,觉得直接让GPU访问物理内存不就完了吗?干嘛多此一举?后来踩了坑才明白——没有IOMMU,你面临三个大麻烦:

  • 碎片化问题:GPU需要大块连续物理内存,但系统跑久了物理内存就碎成渣了
  • 安全性问题:没有地址隔离,GPU可以乱读写任何物理地址,恶意程序可以利用这点
  • 兼容性问题:32位外设无法访问64位物理地址空间

IOMMU怎么解决?它给每个外设提供了一个独立的虚拟地址空间。GPU看到的是一个连续的虚拟地址空间,背后映射到分散的物理页上。就像你给快递员看一个整齐的门牌号列表,实际上包裹分散在不同仓库里。

核心概念:IOMMU页表

IOMMU内部维护着多级页表,和CPU的MMU页表结构类似。当GPU发起DMA访问时,IOMMU硬件自动完成虚拟地址到物理地址的转换。这个过程对驱动开发者来说是透明的,但配置页表是我们驱动的活儿。

我在项目中遇到过一个问题:某个GPU芯片的SMMU实现有bug,当页表项数量超过一定阈值时,TLB(快表)刷新会漏掉某些条目。结果就是GPU读到了脏数据,画面出现随机花屏。排查了整整两周才定位到,最后只能通过软件限制页表大小来规避。

避坑指南:我曾经在调试SMMU时发现,不同厂商的SMMU实现细节差异很大。比如ARM的SMMUv2和SMMUv3在页表格式、中断处理上完全不同。建议你在写驱动前,先仔细阅读芯片手册中的SMMU章节,特别是关于TLB刷新的时序要求。

4.2 连续内存分配器(CMA):大块内存的“救星”

说到连续内存,就不得不提CMA(Contiguous Memory Allocator)。这是Linux内核中一个非常巧妙的设计。

CMA的工作原理

CMA的核心思想是:平时把大块物理内存借给系统用,当驱动需要连续内存时,再把这些内存收回来。说白了就是“平时当散钱花,用时再凑整”。

具体流程是这样的:

  1. 系统启动时,预留一块物理内存区域作为CMA池
  2. 平时,CMA区域的内存可以给普通进程使用(可移动页面)
  3. 当驱动调用dma_alloc_coherent()cma_alloc()时,内核会迁移CMA区域中的页面,腾出连续空间
  4. 驱动使用完毕后,调用dma_free_coherent()释放,内存回到CMA池

听起来很完美对吧?但实际用起来有不少坑。

注意:CMA的迁移操作是同步的,而且可能很慢。我曾经在一个4K分辨率显示项目中,每次申请CMA内存都要等几十毫秒,导致开机动画卡顿。后来我改成在系统初始化时就预分配好,才解决了这个问题。

配置CMA大小是个技术活。太小了不够用,太大了浪费内存。我一般这样估算:

应用场景 CMA大小建议 说明
1080P显示 32MB - 64MB 双缓冲+少量GPU纹理
4K显示+简单GPU 128MB - 256MB 考虑多缓冲和合成
高负载GPU渲染 256MB - 512MB 纹理、帧缓冲、中间数据

在设备树中配置CMA的示例:

reserved-memory {
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    gpu_cma: gpu_cma@0 {
        compatible = "shared-dma-pool";
        reusable;
        size = <0x0 0x10000000>;  /* 256MB */
        alignment = <0x0 0x1000>;
        linux,cma-default;
    };
};

4.3 DMA-BUF机制:跨设备共享的“快递单”

DMA-BUF是Linux内核中用于跨设备共享内存的框架。我把它理解成一张“快递单”——它不直接搬运数据,而是记录数据在哪、怎么访问。

DMA-BUF的核心设计

DMA-BUF本质上是一个文件描述符(fd),背后关联着一块内存。不同的驱动可以通过这个fd来共享同一块物理内存,而不用做数据拷贝。

举个例子,摄像头采集到一帧图像,要送给GPU做处理,再送给显示控制器输出。如果没有DMA-BUF,你得把数据从摄像头驱动拷贝到GPU驱动,再拷贝到显示驱动——三次拷贝,延迟和带宽都受不了。

有了DMA-BUF,流程就变成了:

  1. 摄像头驱动分配DMA-BUF,把采集的数据放进去
  2. 摄像头驱动把DMA-BUF的fd传给GPU驱动
  3. GPU驱动通过fd获取DMA-BUF,映射到GPU地址空间,直接处理
  4. 处理完后,把fd传给显示驱动,直接显示

整个过程零拷贝,数据始终在物理内存中不动,只是不同硬件通过IOMMU映射来访问它。

关键API

  • dma_buf_export():创建DMA-BUF,导出者(如摄像头驱动)调用
  • dma_buf_get():通过fd获取DMA-BUF对象,使用者(如GPU驱动)调用
  • dma_buf_map_attachment():将DMA-BUF映射到特定设备的IOMMU地址空间
  • dma_buf_unmap_attachment():解除映射

我记得有一次调试一个多摄像头方案,三个摄像头同时采集数据送给GPU做拼接。一开始没注意DMA-BUF的同步问题,结果GPU读到的数据总是“撕裂”的——上半帧是时间点A的数据,下半帧是时间点B的数据。后来加了fence(栅栏)机制才搞定,确保GPU在摄像头写完数据后才开始读。

个人经验:DMA-BUF的同步是个容易忽略的点。建议你使用dma_fence机制来做生产者-消费者同步。另外,注意DMA-BUF的引用计数管理,我曾经因为少了一个dma_buf_put()导致内存泄漏,系统跑了三天就OOM了。

4.4 物理地址与虚拟地址映射:打通“任督二脉”

最后聊聊地址映射。这是GPU驱动开发中最基础也最容易出错的地方。

三种地址空间

在嵌入式GPU系统中,我们面对三种地址:

  • CPU虚拟地址:用户态程序看到的地址,通过MMU转换
  • 物理地址:真正的硬件内存地址
  • IO虚拟地址(IOVA):GPU等外设看到的地址,通过IOMMU/SMMU转换

驱动开发中常见的映射操作:

/* 分配一致内存(CPU和GPU都能访问) */
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

/* 流式DMA映射(适合数据流场景) */
dma_addr_t dma_addr;
dma_addr = dma_map_single(dev, cpu_addr, size, DMA_TO_DEVICE);
/* 使用完后解除映射 */
dma_unmap_single(dev, dma_addr, size, DMA_TO_DEVICE);

/* 通过IOMMU建立映射 */
struct iommu_domain *domain;
unsigned long iova = iommu_domain_alloc(domain);
iommu_map(domain, iova, phys_addr, size, prot);

映射的注意事项

我踩过最深的一个坑是关于cache一致性的。CPU有cache,GPU没有(或者说有但和CPU不共享)。当你用CPU写了一段数据,然后让GPU去读,如果CPU的数据还在cache里没写回内存,GPU读到的就是旧数据。

解决办法有两种:

  1. 使用dma_alloc_coherent()分配的内存,它默认是non-cacheable的,CPU和GPU都能直接访问
  2. 使用流式DMA映射,通过dma_map_single()dma_unmap_single()来管理cache的刷写

重要提醒:千万不要在CPU访问DMA缓冲区时忘记做cache操作。我曾经在一个项目中,CPU写了一个命令缓冲区,然后通知GPU去执行。结果GPU执行的全是垃圾命令,直接导致GPU挂死。查了两天才发现是cache没刷写。

另外,IOVA的分配也要注意碎片化。我建议你使用iommu_dma_alloc()这类高层API,它们会自动管理IOVA的分配和释放。如果自己手动管理IOVA,很容易出现地址冲突或者泄漏。

嗯,关于GPU内存管理的基础知识,今天就先聊到这儿。这些内容看起来琐碎,但每一个细节都关系到系统的稳定性和性能。下次咱们接着聊更深入的内存优化技巧。