1. Linux内核DMA与IOMMU

在Linux系统中,CPU是数据运算核心,而外设是数据交互核心,若所有数据搬运都由CPU参与,会产生大量内存拷贝、总线等待开销,严重占用CPU算力,降低系统吞吐与实时性。

DMA(Direct Memory Access,直接内存访问)子系统的核心作用是解放CPU,允许外设直接与内存进行数据传输,无需CPU逐字节拷贝,仅需CPU发起传输指令、等待传输完成中断即可,大幅提升大数据传输场景的效率。

而随着64位平台、大内存、多设备并发场景普及,普通DMA存在地址不隔离、32位寻址受限、物理内存碎片化访问不安全等问题。 IOMMU(Input/Output Memory Management Unit,输入输出内存管理单元)应运而生,为DMA设备提供硬件地址翻译、内存隔离、权限管控、大内存寻址能力,是现代ARM64、X86平台必不可少的I/O虚拟化与内存管理单元。

1.1. DMA核心概念

1.1.1. DMA基础定义

DMA是Linux系统中独立于CPU的专用硬件数据搬运控制器,集成于SoC内部或外接总线控制器。其核心作用是卸载CPU的数据拷贝工作,实现内存与外设、内存与内存之间的全自动数据传输,全程无需CPU逐字节干预数据读写,仅需CPU在传输前配置参数、传输后处理中断即可。

1.1.2. DMA传输类型

Linux内核DMA子系统支持三类标准传输方式,适配不同应用场景:

  • 外设到内存(DEV_TO_MEM):摄像头、麦克风、SD卡等外设采集数据,通过DMA自动写入系统内存;

  • 内存到外设(MEM_TO_DEV):系统内存中的图像、音频、数据,通过DMA自动推送至显示屏、喇叭、网络网口;

  • 内存到内存(MEM_TO_MEM):无需CPU参与,DMA直接完成两块内存区域的数据拷贝,适用于大数据内存迁移。

1.1.3. DMA内存工作类型

Linux内核DMA子系统根据内存映射方式、缓存一致性、内存形态、传输机制,将DMA分为三大核心类型:一致性DMA(Coherent DMA)、流式DMA(Streaming DMA)、散射-聚集DMA(Scatter-Gather DMA)。

1.1.3.1. 一致性DMA

一致性DMA内存是内核为DMA设备专属分配的硬件自动缓存一致的物理连续内存。该内存由硬件保证CPU Cache与设备内存数据实时同步,软件无需手动刷新、失效缓存,CPU与DMA设备可随时读写数据且数据完全一致。

核心特性:

  • 物理内存连续、IOVA虚拟地址连续,完全适配DMA硬件寻址要求;

  • 硬件自动维护Cache一致性,无缓存脏数据问题;

  • 分配阶段固定映射,生命周期内地址不变;

  • 内存开销大,内核会预留对齐、冗余内存,内存利用率低。

适用场景:

仅适用于小数据、频繁读写、控制交互类场景:设备状态寄存器缓冲区、指令交互缓冲区、中断状态缓存、小型协议头数据,不适合大数据批量传输。

优缺点:

  • 优点:开发简单、无需缓存操作、数据稳定性高、无同步异常;

  • 缺点:内存浪费严重、不支持超大内存分配、系统长期运行易内存碎片化。

1.1.3.2. 流式DMA

流式DMA是基于系统普通动态内存的临时映射DMA传输机制,无固定内存预留,可直接复用内核栈内存、kmalloc内存、用户态内存,由开发者软件手动管理缓存一致性,是大数据流式传输的首选方案。

核心特性:

  • 支持动态映射、临时使用、用完即释,内存利用率极高;

  • 无硬件自动缓存同步,必须手动根据传输方向操作缓存;

  • 单次映射对应单次传输,不支持长期持有DMA地址;

  • 内存可以是物理连续普通内存,适配常规DMA通道。

适用场景:

适用于大数据、单次批量、流式收发场景:网络数据包收发、视频帧传输、音频数据流、SD卡高速读写、USB批量传输。

优缺点:

  • 优点:内存利用率高、无冗余开销、支持超大批量数据传输、性能最优;

  • 缺点:开发复杂度高,缓存同步错误会导致数据错乱、传输失败,需严格匹配数据传输方向。

1.1.3.3. 散射-聚集DMA

散射聚集DMA是Linux高级DMA传输机制,专门解决物理内存碎片化问题。无需连续物理内存,可将多块离散的物理内存页,通过内核SG链表整理后,一次性提交给DMA控制器完成连续传输,是高性能大数据传输的核心方案。

核心特性:

  • 支持物理不连续、虚拟连续内存传输,彻底规避内存碎片化问题;

  • 一次配置、多块内存批量传输,减少DMA配置次数,降低CPU开销;

  • 需要硬件DMA控制器支持SG模式,普通简易DMA控制器不兼容;

  • 搭配IOMMU可实现极致的内存利用率与传输稳定性。

适用场景:

适用于超大块数据、多段缓存拼接、内存高度碎片化场景:4K视频编解码、千兆/万兆网络数据包、大型文件读写、GPU图像渲染数据传输。

优缺点:

  • 优点:无需连续大块物理内存、内存利用率100%、传输效率最高、适配系统长期运行碎片化场景;

  • 缺点:依赖硬件SG能力、驱动开发复杂度最高、需要维护SG链表结构。

1.1.4. DMA工作流程

Linux DMA的工作逻辑可概括为:CPU配置、硬件搬运、中断收尾、资源释放4个步骤。

  1. 初始化配置

该阶段由CPU执行,仅执行一次。驱动预先完成基础配置:配置设备DMA寻址掩码、分配对应类型的DMA内存、申请空闲DMA通道、配置传输方向与传输长度,初始化DMA硬件寄存器参数。

  1. 硬件自动传输

该阶段由无CPU参与。开启DMA通道后,DMA控制器自动抢占总线,根据预设参数独立完成内存与外设之间的数据批量搬运,全程无需CPU干预,自主完成数据传输。

  1. 中断收尾处理

该阶段由CPU轻量介入。数据传输完成或出现异常时,DMA触发硬件中断,CPU响应中断,完成数据校验、业务处理、清空中断标志、复位通道等收尾操作,等待下一次传输。

  1. 资源释放

业务结束或驱动卸载时,关闭DMA通道、释放DMA内存、归还硬件通道资源、注销中断,避免内存泄漏和硬件资源占用。

1.2. IOMMU核心概念

1.2.1. IOMMU基础定义

IOMMU是专门为DMA设备设计的硬件内存管理单元,对标CPU侧的MMU(内存管理单元)。CPU的MMU负责CPU虚拟地址转物理地址,服务于进程内存管理; 而IOMMU负责设备IO虚拟地址(IOVA)转系统物理地址(PA),专门服务于所有DMA外设的内存访问。

现在的64位嵌入式设备、服务器全部标配IOMMU,主要用来解决普通DMA的各种短板,提升系统稳定性、安全性,同时支持虚拟机设备直通功能。

1.2.2. 传统裸DMA的致命缺陷

没有IOMMU的系统,DMA设备直接裸奔访问物理内存,漏洞和缺陷非常明显:

  • 大内存用不了:很多老旧DMA设备最多只能访问4GB内存,现在设备8G/16G大内存的高位空间完全无法使用;

  • 碎片内存用不了:普通DMA必须用整块连续内存,系统运行久了内存碎片化,没有大块内存就无法传输大数据;

  • 毫无安全性:所有设备共用同一内存空间,一个设备出问题、或者恶意程序可以随意篡改系统内核和其他设备的内存,直接导致系统崩溃;

  • 无权限管控:所有设备默认拥有全部内存读写权限,没有任何限制,安全隐患极大

1.2.3. IOMMU核心功能

  • 地址翻译:给DMA设备用虚拟地址,IOMMU自动翻译成真实物理地址。让只能访问4GB的老旧设备,也能使用全部64位大内存,同时把零散内存拼接成整块可用内存;

  • 设备隔离:给每个设备单独划分独立内存空间,就像给每个设备单独分配“独立房间”,互不打扰,一个设备故障不会影响其他设备和系统;

  • 权限管控:可以单独设置某块内存只能读、不能写,禁止外设篡改系统核心内存数据,从硬件层面防崩溃、防篡改;

  • 碎片整合:把系统零散的内存碎片,通过地址映射拼接成设备可用的连续内存,大幅提升内存利用率;

  • 虚拟机直通:支持把物理硬件直接分配给虚拟机使用,提升虚拟机的硬件性能。

1.3. DMA与IOMMU关联与工作流程

总结二者关系:DMA是数据搬运的执行者,IOMMU是DMA访问内存的管控者与翻译者。DMA负责“搬数据”,IOMMU负责“管地址、管权限、管隔离”,二者协同完成安全、高效的I/O数据传输。

有无IOMMU系统传输流程对比:

  1. 无IOMMU系统:CPU分配物理连续内存 -> 直接将物理地址告知DMA设备 -> DMA直接访问物理内存完成数据传输,无地址翻译、无权限校验。

  2. 有IOMMU系统:CPU分配物理内存 -> IOMMU创建页表,建立“IOVA虚拟地址->物理地址”映射 -> CPU将IOVA虚拟地址下发给DMA设备 -> DMA通过IOVA发起内存访问 -> IOMMU硬件实时翻译为物理地址、校验访问权限 -> 完成数据读写传输。

1.4. DMA核心结构体

注解

本章节主要是为了后续使用DMA子系统的复杂驱动(如DRM驱动)作前置铺垫的,以下涉及一些结构体和函数可能出现得很突兀,是为了方便后续章节出现的结构体和函数可以从本章节找到。

1.4.1. DMA控制器设备结构体

DMA控制器设备结构体(struct dma_device)用于描述一个DMA控制器硬件,包含控制器属性、通道、操作函数集,是实现多DMA控制器共存、资源统一调度、标准化DMA传输开发的基础核心结构体。

dma_device结构体(内核源码/include/linux/dmaengine.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct dma_device {
    struct kref ref;                    // 设备引用计数
    unsigned int chancnt;               // 可用DMA通道总数
    struct list_head channels;          // 所属DMA通道链表
    struct list_head global_node;       // 全局DMA设备链表节点

    dma_cap_mask_t  cap_mask;           // 控制器能力掩码,标识支持的传输类型
    enum dma_residue_granularity residue_granularity; // 传输剩余数据精度

    struct device *dev;                 // 绑定的内核设备结构体
    struct module *owner;               // 模块所有者

    /* 通道资源申请与释放 */
    int (*device_alloc_chan_resources)(struct dma_chan *chan);
    void (*device_free_chan_resources)(struct dma_chan *chan);

    /* 核心传输预处理接口 */
    // 内存拷贝DMA
    struct dma_async_tx_descriptor *(*device_prep_dma_memcpy)(
        struct dma_chan *chan, dma_addr_t dst, dma_addr_t src,
        size_t len, unsigned long flags);

    // 外设散射聚集DMA
    struct dma_async_tx_descriptor *(*device_prep_slave_sg)(
        struct dma_chan *chan, struct scatterlist *sgl,
        unsigned int sg_len, enum dma_transfer_direction direction,
        unsigned long flags, void *context);

    // 循环DMA传输,音频/视频周期性传输使用
    struct dma_async_tx_descriptor *(*device_prep_dma_cyclic)(
        struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len,
        size_t period_len, enum dma_transfer_direction direction,
        unsigned long flags);

    /* 设备控制接口 */
    int (*device_config)(struct dma_chan *chan,
                        struct dma_slave_config *config); // 通道参数配置
    int (*device_pause)(struct dma_chan *chan);           // 暂停传输
    int (*device_resume)(struct dma_chan *chan);          // 恢复传输
    int (*device_terminate_all)(struct dma_chan *chan);   // 终止所有传输

    /* 状态查询与任务下发 */
    enum dma_status (*device_tx_status)(struct dma_chan *chan,
                                        dma_cookie_t cookie,
                                        struct dma_tx_state *txstate);
    void (*device_issue_pending)(struct dma_chan *chan); // 下发pending任务
    /* 其他成员省略 */
};

1.4.2. DMA通道结构体

DMA通道结构体(struct dma_chan)用于抽象、描述单条DMA物理通道的全部属性、运行状态、挂载归属与私有配置,是驱动操作、管理、调度单条DMA传输通路的核心载体。

dma_chan结构体(内核源码/include/linux/dmaengine.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct dma_chan {
    struct dma_device *device;          // 所属DMA控制器设备
    struct device *slave;               // 绑定的从设备
    dma_cookie_t cookie;                // 当前传输Cookie标记
    dma_cookie_t completed_cookie;      // 已完成传输Cookie标记

    /* sysfs文件系统节点属性 */
    int chan_id;                        // 通道唯一ID
    struct dma_chan_dev *dev;           // 通道设备节点
    const char *name;                   // 通道名称

    struct list_head device_node;       // 挂载到dma_device的链表节点
    struct dma_chan_percpu __percpu *local; // 多核私有数据
    int client_count;                   // 绑定客户端数量
    int table_count;                    // 传输描述符表数量

    /* DMA路由配置 */
    struct dma_router *router;          // DMA路由控制器
    void *route_data;                   // 路由私有数据

    void *private;                      // 厂商自定义私有数据
    /* 其他成员省略 */
};

1.4.3. DMA通道设备节点结构体

DMA通道设备节点结构体(struct dma_chan_dev)是DMA通道对应的设备节点管理结构体,用于将抽象的DMA通道实例化为内核标准设备, 在sysfs文件系统中生成独立的通道设备节点,支撑DMA通道的用户态查询、调试与状态管理,是DMA通道设备化、可视化管理的基础结构体。

dma_chan_dev结构体(内核源码/include/linux/dmaengine.h)
1
2
3
4
5
6
struct dma_chan_dev {
    struct dma_chan *chan;        // 绑定对应的DMA通道结构体
    struct device device;         // 内核标准设备结构体
    int dev_id;                   // 通道设备唯一ID
    bool chan_dma_dev;            // 标记是否为DMA通道专属设备
};

1.4.4. DMA共享缓冲区结构体

DMA共享缓冲区结构体(struct dma_buf)是Linux内核DMA共享缓冲区(DMA-BUF)的核心管理结构体,用于实现跨设备、跨驱动、跨进程的DMA内存共享。 统一管理一块DMA可用物理缓冲区的生命周期、设备挂载、同步与操作方法,是多媒体、GPU、摄像头、多设备协同DMA传输的核心基础结构体,支撑内存零拷贝共享传输。

dma_buf结构体(内核源码/include/linux/dma-buf.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct dma_buf {
    size_t size;                        // DMA缓冲区总大小
    struct file *file;                  // 文件指针,用于引用计数与跨进程共享
    struct list_head attachments;       // 挂载的设备附件链表,记录绑定的DMA设备
    const struct dma_buf_ops *ops;      // 缓冲区专属操作方法集
    struct mutex lock;                  // 互斥锁,保护链表、映射等操作
    unsigned vmapping_counter;          // 虚拟映射引用计数
    struct iosys_map vmap_ptr;          // 内核虚拟映射地址
    const char *exp_name;               // 缓冲区导出模块名称
    struct module *owner;               // 所属内核模块,用于生命周期管理
    struct list_head list_node;         // 全局DMA-BUF链表节点
    void *priv;                         // 厂商自定义私有数据
    struct dma_resv *resv;              // 缓冲区同步屏障对象,用于DMA异步同步
    /* 其他成员省略 */
};

1.4.5. DMA缓冲区设备挂载结构体

DMA缓冲区设备挂载结构体(struct dma_buf_attachment)是DMA-BUF子系统的设备挂载核心结构体,用于建立DMA共享缓冲区与外设设备的绑定关系。 每一个设备挂载一块DMA共享缓冲区时,内核都会生成一个attachment实例,用于记录设备信息、缓冲区映射表、传输方向、DMA映射属性与私有数据,是实现多设备、跨驱动零拷贝共享DMA内存的关键载体。

dma_buf_attachment结构体(内核源码/include/linux/dma-buf.h)
1
2
3
4
5
6
7
8
9
struct dma_buf_attachment {
    struct dma_buf *dmabuf;             // 绑定的DMA共享缓冲区
    struct device *dev;                 // 挂载的目标外设设备
    struct list_head node;              // 挂载到dmabuf的链表节点
    struct sg_table *sgt;               // 缓冲区散射聚集映射表
    enum dma_data_direction dir;        // DMA数据传输方向
    void *priv;                         // 私有扩展数据
    unsigned long dma_map_attrs;        // DMA映射属性标识
};

1.4.6. DMA缓冲区导出信息结构体

DMA缓冲区导出信息结构体(struct dma_buf_export_info)是DMA-BUF子系统的缓冲区导出配置结构体,用于驱动创建自定义DMA共享缓冲区时,统一传入缓冲区基础配置参数。

dma_buf_export_info结构体(内核源码/include/linux/dma-buf.h)
1
2
3
4
5
6
7
8
9
struct dma_buf_export_info {
    const char *exp_name;               // 缓冲区导出名称
    struct module *owner;               // 所属内核模块,用于生命周期管理
    const struct dma_buf_ops *ops;      // 自定义DMA缓冲区操作方法集
    size_t size;                        // 待创建缓冲区总大小
    int flags;                          // 缓冲区创建属性标志
    struct dma_resv *resv;              // 缓冲区异步同步屏障对象
    void *priv;                         // 驱动自定义私有数据
};

1.4.7. DMA-IOMMU映射管理结构体

DMA-IOMMU映射管理结构体(struct dma_iommu_mapping)是DMA与IOMMU联动的底层映射管理核心结构体,专门用于维护DMA设备的IOMMU虚拟地址空间映射关系。负责管理IOVA地址位图分配、绑定IOMMU地址域、维护映射资源引用计数与线程锁,实现DMA设备IOVA地址的批量分配、回收与安全管控。

dma_iommu_mapping结构体(内核源码/arch/arm/include/asm/dma-iommu.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct dma_iommu_mapping {
    struct iommu_domain *domain;    // 绑定的IOMMU地址域
    unsigned long   **bitmaps;      // IOVA地址分配位图数组
    unsigned int    nr_bitmaps;     // 位图数组数量
    size_t  bitmap_size;            // 单张位图大小
    dma_addr_t  base;               // IOMMU映射IOVA基地址

    spinlock_t  lock;               // 位图操作自旋锁
    struct kref kref;               // 映射资源引用计数
};

1.5. DMA子系统核心函数

1.5.1. DMA掩码与一致性掩码配置

1.5.1.1. dma_set_mask_and_coherent函数

dma_set_mask_and_coherent函数用于一次性设置设备DMA寻址掩码和一致性内存掩码,声明设备支持的DMA地址范围,是DMA内存分配的前置函数。

函数原型:

1
int dma_set_mask_and_coherent(struct device *dev, u64 mask);

参数说明:

  • dev:设备结构体指针,当前需要配置DMA能力的设备;

  • mask:DMA地址掩码,由DMA_BIT_MASK(n)生成,代表设备支持的地址位宽。

返回值:

  • 0:配置成功;

  • -EINVAL:掩码不合法,设备不支持对应位宽;

  • -ENXIO:设备无DMA能力。

1.5.2. 分配DMA一致性内存

1.5.2.1. dma_alloc_coherent函数

dma_alloc_coherent函数用于分配硬件Cache一致性的DMA内存,同时返回CPU虚拟地址与DMA设备IOVA地址,无需手动刷新缓存。

函数原型:

1
2
void *dma_alloc_coherent(struct device *dev, size_t size,
                        dma_addr_t *dma_handle, gfp_t gfp);

参数说明:

  • dev:设备结构体指针;

  • size:需要分配的内存大小;

  • dma_handle:出参,用于接收DMA设备使用的IOVA地址;

  • gfp:内存分配标志,常用GFP_KERNEL(内核常规分配)。

返回值:

  • 非NULL:分配成功,返回CPU虚拟地址;

  • NULL:内存分配失败。

1.5.3. 释放DMA一致性内存

1.5.3.1. dma_free_coherent函数

dma_free_coherent函数用于释放由dma_alloc_coherent分配的一致性DMA内存,与分配函数成对使用,防止内存泄漏。

函数原型:

1
2
void dma_free_coherent(struct device *dev, size_t size,
                    void *cpu_addr, dma_addr_t dma_handle);

参数说明:

  • dev:设备结构体指针;

  • size:释放的内存大小,必须与分配时大小一致;

  • cpu_addr:分配得到的CPU虚拟地址;

  • dma_handle:分配得到的DMA IOVA地址。

1.5.4. 获取带属性的SG散射聚集表

1.5.4.1. dma_get_sgtable_attrs函数

dma_get_sgtable_attrs函数用于根据DMA内存地址、CPU虚拟地址及内存属性,生成标准化的SG散射聚集表,用于DMA批量传输。支持自定义DMA内存属性,适配无内核虚拟映射、写合并等特殊DMA内存场景。

函数原型:

1
2
3
int dma_get_sgtable_attrs(struct device *dev, struct sg_table *sgt,
                        void *cpu_addr, dma_addr_t dma_addr,
                        size_t size, unsigned long attrs);

参数说明:

  • dev:绑定的设备结构体指针;

  • sgt:出参,初始化完成的SG表结构体;

  • cpu_addr:DMA内存对应的CPU虚拟地址;

  • dma_addr:DMA设备访问的IOVA/物理地址;

  • size:内存总大小;

  • attrs:DMA内存属性(如DMA_ATTR_NO_KERNEL_MAPPING、DMA_ATTR_WRITE_COMBINE)。

返回值:

  • 0:SG表生成成功;

  • 负数:生成失败,返回内核标准错误码。

1.6. IOMMU核心结构体

1.6.1. IOMMU硬件操作函数集结构体

IOMMU硬件操作函数集结构体(struct iommu_ops)是硬件适配层核心操作集结构体,是实现IOMMU通用核心层与各厂商硬件差异化驱动解耦的关键载体。 该结构体统一标准化定义了IOMMU硬件必备的底层操作回调接口,屏蔽不同芯片平台IOMMU硬件的寄存器差异、映射逻辑差异、总线适配差异。

iommu_ops结构体(内核源码/include/linux/iommu.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct iommu_ops {
    /* 硬件能力校验 */
    bool (*capable)(struct device *dev, enum iommu_cap);

    /* IOMMU地址域分配 */
    struct iommu_domain *(*domain_alloc)(unsigned iommu_domain_type);

    /* 设备IOMMU探测、释放与收尾 */
    struct iommu_device *(*probe_device)(struct device *dev);
    void (*release_device)(struct device *dev);
    void (*probe_finalize)(struct device *dev);

    /* 获取设备IOMMU分组 */
    struct iommu_group *(*device_group)(struct device *dev);

    /* 解析设备预留内存区域 */
    void (*get_resv_regions)(struct device *dev, struct list_head *list);

    /* 设备树属性解析、延迟挂载判断 */
    int (*of_xlate)(struct device *dev, struct of_phandle_args *args);
    bool (*is_attach_deferred)(struct device *dev);

    /* IOMMU设备特性开关 */
    int (*dev_enable_feat)(struct device *dev, enum iommu_dev_features f);
    int (*dev_disable_feat)(struct device *dev, enum iommu_dev_features f);

    /* 获取设备默认域类型 */
    int (*def_domain_type)(struct device *dev);

    /* 默认地址域操作集、硬件页掩码、模块归属 */
    const struct iommu_domain_ops *default_domain_ops;
    unsigned long pgsize_bitmap;
    struct module *owner;

    /* 其他成员省略 */
};

1.6.2. IOMMU地址域结构体

IOMMU地址域结构体(struct iommu_domain)是实现设备地址隔离与内存映射管理的核心单元,是IOMMU硬件完成地址翻译、设备隔离的基础载体。 该结构体用于抽象一套独立的设备IOVA虚拟地址空间,每一个iommu_domain实例对应一块完全独立的I/O地址域,严格管控当前地址域内的内存映射关系与访问权限。

iommu_domain结构体(内核源码/include/linux/iommu.h)
1
2
3
4
5
6
7
8
9
struct iommu_domain {
    unsigned type;                          // IOMMU域类型标识
    const struct iommu_domain_ops *ops;     // 地址域专属操作方法集
    unsigned long pgsize_bitmap;            // 硬件支持的页尺寸位图
    iommu_fault_handler_t handler;          // IOMMU异常故障处理回调
    void *handler_token;                    // 异常处理回调私有参数
    struct iommu_domain_geometry geometry;  // 地址域空间孔径、范围属性
    struct iommu_dma_cookie *iova_cookie;   // IOVA地址空间管理标记
};

1.6.3. IOMMU地址域孔径几何属性结构体

IOMMU地址域孔径几何属性结构体(struct iommu_domain_geometry)用于描述单个IOMMU地址域的有效映射地址孔径范围,限定当前地址域允许设备访问的IOVA虚拟地址区间,是IOMMU地址空间权限管控的基础配置结构体。

iommu_domain_geometry结构体(内核源码/include/linux/iommu.h)
1
2
3
4
5
struct iommu_domain_geometry {
    dma_addr_t aperture_start; /* 地址域可映射的首个IOVA地址 */
    dma_addr_t aperture_end;   /* 地址域可映射的末尾IOVA地址 */
    bool force_aperture;       /* 是否强制DMA仅允许在合法孔径范围内访问 */
};

1.7. IOMMU驱动核心函数

1.7.1. 分配IOMMU地址域

1.7.1.1. iommu_domain_alloc函数

iommu_domain_alloc函数用于为指定总线设备分配独立的IOMMU地址映射域。

函数原型:

1
struct iommu_domain *iommu_domain_alloc(struct bus_type *bus);

参数说明:

  • bus:设备所属总线类型。

返回值:成功返回iommu_domain结构体指针,失败返回NULL。

1.7.2. 释放IOMMU地址域

1.7.2.1. iommu_domain_free函数

iommu_domain_free函数用于销毁并释放IOMMU地址域资源,与 iommu_domain_alloc 成对使用。

函数原型:

1
void iommu_domain_free(struct iommu_domain *domain);

参数说明:

  • domain:需要销毁释放的IOMMU地址域结构体指针。

1.7.3. IOMMU地址映射

1.7.3.1. iommu_map函数

iommu_map函数用于建立IOVA虚拟地址到物理地址的映射关系。

函数原型:

1
2
int iommu_map(struct iommu_domain *domain, dma_addr_t iova,
          phys_addr_t paddr, size_t size, int prot);

参数说明:

  • domain:IOMMU地址域;

  • iova:设备虚拟地址;

  • paddr:物理内存地址;

  • size:映射内存大小;

  • prot:访问权限。

返回值:0成功,负数失败。

1.7.4. IOMMU地址解映射

1.7.4.1. iommu_unmap函数

iommu_unmap函数用于销毁指定IOVA地址的映射关系,释放IOMMU页表资源。

函数原型:

1
size_t iommu_unmap(struct iommu_domain *domain, dma_addr_t iova, size_t size);

参数说明:

  • domain:待解映射的IOMMU独立地址域;

  • iova:需要解除映射的设备IOVA虚拟起始地址;

  • size:待解除映射的内存区域大小,需与映射时大小一致。

返回值:成功返回解映射的字节大小,失败返回负数。

1.7.5. 设备绑定IOMMU地址域

1.7.5.1. iommu_attach_device函数

iommu_attach_device函数用于将指定外设设备挂载绑定到目标IOMMU地址域,让设备隶属于该独立地址空间,生效当前domain的地址映射、权限管控与隔离规则,是设备启用IOMMU地址翻译的核心前置函数。

函数原型:

1
int iommu_attach_device(struct iommu_domain *domain, struct device *dev);

参数说明:

  • domain:待绑定的IOMMU独立地址域;

  • dev:需要挂载到地址域的外设设备结构体。

返回值:0绑定成功;负数绑定失败。

1.7.6. SG表批量IOMMU映射

1.7.6.1. iommu_map_sgtable函数

iommu_map_sgtable函数用于基于散射聚集SG表,批量完成多段离散物理内存的IOMMU地址映射,将碎片化物理内存统一映射为连续的IOVA虚拟地址,适配SG-DMA大数据传输场景。

函数原型:

1
2
size_t iommu_map_sgtable(struct iommu_domain *domain, dma_addr_t iova,
                        struct sg_table *sgt, int prot);

参数说明:

  • domain:目标IOMMU地址域;

  • iova:起始IOVA虚拟地址;

  • sgt:散射聚集内存表,存储多段离散物理内存信息;

  • prot:内存访问权限。

返回值:成功返回实际映射的总字节大小,失败返回0。

1.7.7. 刷新全部IOTLB缓存

1.7.7.1. iommu_flush_iotlb_all函数

iommu_flush_iotlb_all函数用于强制刷新指定IOMMU地址域的全部IOTLB地址翻译缓存,清空老旧映射缓存。 在页表映射更新、解映射、权限修改后调用,避免硬件缓存残留旧地址映射导致的数据访问异常、传输失败问题。

函数原型:

1
void iommu_flush_iotlb_all(struct iommu_domain *domain);

参数说明

  • domain:需要刷新缓存的IOMMU地址域。

1.8. DMA一致性内存实验

本实验进行DMA一致性内存测试,结合平台设备框架、IOMMU地址映射、DMA内存管理、字符设备等实现用户空间与内核DMA物理内存的数据交互,验证DMA一致性内存与IOMMU映射机制。

本章的示例代码目录为: linux_driver/35_dma_iommu

1.8.1. 设备树插件详解

本实验设备树插件(lubancat-dma-iommu-overlay.dts)主要用来匹配平台驱动,完整代码如下:

lubancat-dma-iommu-overlay.dts(位于linux_driver/35_dma_iommu/lubancat-dma-iommu-overlay.dts)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target-path = "/";

        __overlay__ {
            dma_iommu_test: dma-iommu-test {
                compatible = "fire,dma-iommu";
                status = "okay";
            };
        };
    };
};

关键说明:

  • 在根节点下增加dma_iommu_test节点,compatible属性标识需与驱动中of_match_table的属性完全一致,否则驱动无法匹配设备。

1.8.2. 驱动代码详解

核心定义与数据结构

核心定义与数据结构(位于linux_driver/35_dma_iommu/dma_iommu.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* 设备名称 */
#define DEVICE_NAME "dma_iommu"
/* DMA缓冲区大小设置为4KB */
#define DMA_BUFFER_SIZE (4 * 1024)

/* DMA IOMMU私有数据结构体 */
struct dma_iommu_priv {
    dma_addr_t dma_addr;             /* DMA地址 */
    void *virt_addr;                 /* 虚拟地址 */
    size_t size;                     /* 缓冲区大小 */
    size_t actual_len;               /* 记录有效数据长度 */
    struct mutex lock;               /* 互斥体 */
    struct cdev cdev;                /* 字符设备 */
    struct device *dev;              /* 设备指针 */
    dev_t devt;                      /* 设备号 */
    struct class *class;             /* 设备类指针 */
};

关键说明:

  • 第8行:dma_addr_t为DMA专用地址类型,存储IOMMU映射后的IOVA地址,非物理地址。

  • 第9行:CPU专属访问地址,属于内核态线性虚拟地址,用户空间不可直接访问,通过该地址完成内核侧DMA缓冲区数据读写。

  • 第12行:互斥锁绑定设备,确保同一时间只有一个进程可读写DMA缓冲区。

probe函数

probe函数(位于linux_driver/35_dma_iommu/dma_iommu.c)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
/* 设备探测函数 */
static int dma_iommu_probe(struct platform_device *pdev)
{
    /* 私有数据指针 */
    struct dma_iommu_priv *priv;
    /* 设备指针 */
    struct device *dev = &pdev->dev;
    /* 返回值 */
    int ret;

    /* 分配私有数据内存 */
    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 设置缓冲区大小 */
    priv->size = DMA_BUFFER_SIZE;
    /* 初始化有效数据长度为0 */
    priv->actual_len = 0;
    /* 保存设备指针到私有数据 */
    priv->dev = dev;

    /* 初始化互斥锁 */
    mutex_init(&priv->lock);

    /* 设置DMA掩码为64位 */
    ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
    if (ret)
    {
        printk(KERN_WARNING "Cannot set DMA mask to 64 bit, trying 32 bit\n");
        ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));          /* 尝试设置32位DMA掩码 */
        if (ret)
        {
            printk(KERN_ERR "No suitable DMA available\n");
            return ret;
        }
    }

    /* 分配DMA一致性内存 */
    priv->virt_addr = dma_alloc_coherent(dev, priv->size, &priv->dma_addr, GFP_KERNEL);
    if (!priv->virt_addr)
    {
        printk(KERN_ERR "Failed to allocate DMA memory\n");
        return -ENOMEM;
    }

    printk(KERN_INFO "DMA memory allocated:\n");
    printk(KERN_INFO "  CPU Virtual address: 0x%pK\n", priv->virt_addr);  /* 打印内核虚拟地址 */
    printk(KERN_INFO "  Device IOVA address: %pad\n", &priv->dma_addr);   /* 打印DMA地址 */

    /* 清空DMA缓冲区 */
    memset(priv->virt_addr, 0, priv->size);

    /* 创建设备类 */
    priv->class = class_create(THIS_MODULE, DEVICE_NAME);
    if (IS_ERR(priv->class))
    {
        printk(KERN_ERR "Failed to create class\n");
        ret = PTR_ERR(priv->class);
        goto err_free_dma;
    }

    /* 动态分配字符设备号 */
    ret = alloc_chrdev_region(&priv->devt, 0, 1, DEVICE_NAME);
    if (ret)
    {
        printk(KERN_ERR "Failed to allocate char device region\n");
        goto err_destroy_class;
    }

    /* 初始化字符设备 */
    cdev_init(&priv->cdev, &dma_iommu_fops);
    /* 设置设备所有者 */
    priv->cdev.owner = THIS_MODULE;

    /* 添加字符设备 */
    ret = cdev_add(&priv->cdev, priv->devt, 1);
    if (ret)
    {
        printk(KERN_ERR "Failed to add cdev\n");
        goto err_unregister_region;
    }
    /* 打印设备号信息 */
    printk(KERN_INFO "char device major=%d, minor=%d\n", MAJOR(priv->devt), MINOR(priv->devt));

    /* 创建设备节点 */
    if (device_create(priv->class, NULL, priv->devt, NULL, DEVICE_NAME) == NULL)
    {
        printk(KERN_ERR "Failed to create device\n");
        goto err_del_cdev;
    }
    /* 保存私有数据到平台设备 */
    platform_set_drvdata(pdev, priv);

    return 0;

err_del_cdev:
    /* 删除字符设备 */
    cdev_del(&priv->cdev);
err_unregister_region:
    /* 释放设备号 */
    unregister_chrdev_region(priv->devt, 1);
err_destroy_class:
    /* 销毁设备类 */
    class_destroy(priv->class);
err_free_dma:
    /* 释放DMA内存 */
    dma_free_coherent(dev, priv->size, priv->virt_addr, priv->dma_addr);
    return ret;
}

关键说明:

  • 第17行:设置DMA缓冲区大小为DMA_BUFFER_SIZE,即4k大小。

  • 第19行:初始化有效数据长度为0,默认没有数据写入DMA缓冲区。

  • 第27-37行:优先配置64位DMA掩码,适配现代平台,失败自动降级32位。

  • 第40行:dma_alloc_coherent核心函数,一次性返回内核虚拟地址与IOMMU设备地址。

  • 第52行:清空DMA缓冲区,避免有脏数据残留。

写入数据函数

写入数据函数(位于linux_driver/35_dma_iommu/dma_iommu.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* 写入数据函数 */
static ssize_t dma_iommu_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    /* 获取私有数据 */
    struct dma_iommu_priv *priv = file->private_data;
    /* 写入长度 */
    size_t len;

    /* 获取互斥锁 */
    mutex_lock(&priv->lock);

    /* 限制最大写入,留1字节给字符串结束符 */
    len = min(count, priv->size - 1);

    /* 只清空本次要用的区域 */
    memset(priv->virt_addr, 0, len + 1);

    /* 将数据从用户空间复制到内核空间 */
    if (copy_from_user(priv->virt_addr, buf, len))
    {
        mutex_unlock(&priv->lock);     /* 释放互斥锁 */
        return -EFAULT;                /* 返回错误 */
    }

    /* 添加字符串结束符 */
    ((char *)priv->virt_addr)[len] = '\0';

    /* 记录当前有效数据长度*/
    priv->actual_len = len;

    /* 打印写入信息 */
    dev_info(priv->dev, "Written to DMA memory: \"%s\"\n", (char *)priv->virt_addr);

    /* 重置文件偏移量 */
    *ppos = 0;

    /* 释放互斥锁 */
    mutex_unlock(&priv->lock);

    /* 返回写入的字节数 */
    return len;
}

关键说明:

  • 第13行:预留1字节空间,防止字符串无结束符导致内核内存乱码、越界读取。

  • 第16行:每次写入前清空要用的缓冲区,避免新旧数据叠加污染和清空过大区域耗时过长。

  • 第19行:将数据从用户空间复制到内核空间,写入到DMA缓冲区。

  • 第26行:手动添加字符串结束符,保证内核打印和读取数据正常。

  • 第29行:记录当前有效数据长度,方便读取数据函数确认可读数据长度。

  • 第35行:重置偏移量,适配常规文件读写逻辑,下次读取从头开始。

读取数据函数

读取数据函数(位于linux_driver/35_dma_iommu/dma_iommu.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* 读取数据函数 */
static ssize_t dma_iommu_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    /* 获取私有数据 */
    struct dma_iommu_priv *priv = file->private_data;
    /* 剩余可读数据长度 */
    size_t remaining;
    /* 本次读取长度 */
    size_t len;

    /* 获取互斥锁 */
    mutex_lock(&priv->lock);

    /* 没有有效数据或偏移已读到末尾直接返回 */
    if (priv->actual_len == 0 || *ppos >= priv->actual_len)
    {
        mutex_unlock(&priv->lock);
        return 0;
    }

    /* 获取剩余可读字节 */
    remaining = priv->actual_len - *ppos;
    /* 限制本次读取数据长度不超过剩余、也不超过用户请求 */
    len = min(count, remaining);

    /* 将数据从内核空间复制到用户空间 */
    if (copy_to_user(buf, (char *)priv->virt_addr + *ppos, len))
    {
        mutex_unlock(&priv->lock);     /* 释放互斥锁 */
        return -EFAULT;                /* 返回错误 */
    }

    /* 更新文件偏移量 */
    *ppos += len;

    /* 释放互斥锁 */
    mutex_unlock(&priv->lock);

    /* 返回读取的字节数 */
    return len;
}

关键说明:

  • 第15行:DMA缓冲区没有有效数据或偏移已读到末尾直接返回,不进行读取。

  • 第24行:通过min函数限制读取长度,防止越界访问DMA缓冲区。

  • 第27行:copy_to_user内核态向用户态拷贝数据,禁止直接指针访问。

  • 第34行:重置偏移量,避免数据重复读取。

设备打开/关闭函数与操作集

设备打开/关闭函数与操作集(位于linux_driver/35_dma_iommu/dma_iommu.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 打开设备函数 */
static int dma_iommu_open(struct inode *inode, struct file *file)
{
    /* 获取私有数据 */
    struct dma_iommu_priv *priv = container_of(inode->i_cdev, struct dma_iommu_priv, cdev);
    /* 保存私有数据到文件指针 */
    file->private_data = priv;

    return 0;
}

/* 关闭设备函数 */
static int dma_iommu_release(struct inode *inode, struct file *file)
{
    return 0;
}

/* 字符设备的文件操作结构体 */
static const struct file_operations dma_iommu_fops = {
    .owner = THIS_MODULE,
    .open = dma_iommu_open,
    .read = dma_iommu_read,
    .write = dma_iommu_write,
    .release = dma_iommu_release,
};

设备树匹配与驱动注册

设备树匹配与驱动注册(位于linux_driver/35_dma_iommu/dma_iommu.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* 设备树匹配表 */
static const struct of_device_id dma_iommu_of_match[] = {
    { .compatible = "fire,dma-iommu" },
    { }
};
/* 声明设备树匹配表,供内核自动匹配设备 */
MODULE_DEVICE_TABLE(of, dma_iommu_of_match);

/* 平台驱动结构体 */
static struct platform_driver dma_iommu_driver = {
    .probe = dma_iommu_probe,
    .remove = dma_iommu_remove,
    .driver = {
        .name = "dma-iommu-test",
        .of_match_table = dma_iommu_of_match,
    },
};

module_platform_driver(dma_iommu_driver);

关键说明:

  • 第3行:compatible字符串必须与设备树插件完全一致,大小写敏感,否则驱动无法匹配。

  • 第19行:module_platform_driver一键注册/注销平台驱动,简化驱动入口出口代码。

1.8.3. Makefile说明

本节实验使用的Makefile如下所示,编写该Makefile时,只需要根据实际情况修改变量KERNEL_DIR、obj-m和test_app即可。

Makefile(位于linux_driver/35_dma_iommu/Makefile)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#指定内核路径,可以是相对路径或绝对路径
KERNEL_DIR=../../kernel/
# KERNEL_DIR=/home/guest/LubanCat_Linux_Generic_Full_SDK/kernel-6.1
# KERNEL_DIR=/home/guest/LubanCat_Linux_rk3588_SDK/kernel

#指定目标架构为arm64
ARCH=arm64


#指定交叉编译工具链的前缀
CROSS_COMPILE=aarch64-linux-gnu-
# CROSS_COMPILE=/home/guest/LubanCat_Linux_Generic_Full_SDK/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-

#导出为环境变量
export  ARCH  CROSS_COMPILE

#指定要编译的内核模块目标文件
obj-m := dma_iommu.o


#all :默认目标,执行时会编译驱动模块
#$(MAKE) :调用make工具
#-C $(KERNEL_DIR) :指定的内核源码目录
#M=$(CURDIR) :模块的源码位于当前目录
#modules :编译模块
all:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

.PHONE:clean

#清理编译生成的文件
clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

1.8.4. 编译设备树和驱动

1.8.4.1. 编译设备树

修改内核目录/arch/arm64/boot/dts/rockchip/overlays下的Makefile文件,添加我们编辑好的设备树插件,并把设备树插件文件放在和Makefile文件同级目录下,以进行设备树插件的编译。

../_images/subsystem_dma_iommu_0.jpg

然后在内核源码顶层目录执行以下命令编译设备树插件:

1
2
3
#这里以rk356x系列4.19.232内核配置文件为例
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

提示

其余系列板卡参考 使用内核的构建脚本编译设备树插件 章节进行编译。

1.8.4.2. 加载设备树

编译出来的设备树插件位于 内核源码/arch/arm64/boot/dts/rockchip/overlay/lubancat-dma-iommu-overlay.dtbo, 将设备树插件先传到板卡,再拷贝到板卡的 /boot/dtb/overlay/ 目录下。

1
2
3
4
#先传输到板卡

#再拷贝到板卡的/boot/dtb/overlay/目录下
sudo cp -f lubancat-dma-iommu-overlay.dtbo /boot/dtb/overlay/

然后在 /boot/uEnv/uEnv.txt 按照格式添加我们的设备树插件,需要在#overlay_start和#overlay_end之间添加,然后重启开发板,那么系统就会加载我们编译的设备树插件。

../_images/subsystem_dma_iommu_1.jpg

1.8.4.3. 编译驱动

在实验目录下输入 make 即可编译驱动,编译得到内核模块dma_iommu.ko。

1.8.5. 程序运行结果

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

1.8.5.1. 实验操作

使用以下命令加载驱动并测试内存读写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#加载驱动
sudo insmod dma_iommu.ko

#信息输出如下,以下是DDR容量为16G的板卡
[   57.792710] DMA memory allocated:
[   57.792741]   CPU Virtual address: 0x0000000006f36ce1
[   57.792748]   Device IOVA address: 0x000000010c66e000
[   57.792837] char device major=234, minor=0

#写入数据到DMA缓冲区
echo -n "hello world!" > /dev/dma_iommu

#信息打印如下
[  128.850827] dma-iommu-test dma-iommu-test: Written to DMA memory: "hello world!"

#读取写入的数据
cat /dev/dma_iommu

#信息打印如下
hello world!

可以从驱动加载信息看到,IOMMU设备IOVA地址位置为0x000000010c66e000,对应4294 MB(通过16进制地址先转10进制再除2次1024得到)内存位置,说明IOMMU地址转译功能正常,访问大于4G内存地址正常。

echo命令默认会添加换行符,增加-n参数改为不添加换行符,可以看到写入数据和读取的数据一致,说明DMA一致性内存功能正常。

提示

如果使用DDR容量小于4G的ARM64板卡,驱动也是设置的dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)),该设置是告诉内核这个设备可以访问的地址范围是64位空间。内核会根据这个掩码,给设备分配在这个范围内的内存,低4GB地址空间也是64位地址空间的一部分,因此,驱动不需要做任何修改功能也会是正常的。

1.8.6. 实验注意事项

  • 内存成对释放:dma_alloc_coherent分配的内存必须通过dma_free_coherent释放,模块卸载不释放会造成内核内存泄漏。

  • 地址不可混用:CPU虚拟地址、IOVA地址、物理地址三者数值不同,不可混用,外设只能使用IOVA地址,CPU只能操作虚拟地址。

  • 避免并发访问:所有读写缓冲区操作必须加互斥锁,避免多进程同时读写导致数据错乱。