RK平台Linux IOMMU开发:从原理到实战 电子说
在瑞芯微(RK)芯片的 Linux 开发中,IOMMU(输入输出内存管理单元)是个关键部件 —— 它能实现设备虚拟地址(IOVA)与物理地址的转换,还能控制读写权限、处理缺页 / 总线异常,广泛用于显示(VOP)、编解码(VPU/HEVC)等场景。今天就从原理、驱动、实战、问题排查、Linux 内存管理支撑五个维度,带大家快速上手 RK 平台 IOMMU 开发。
RK IOMMU 采用二级页表设计(类似 Linux 内核页表),配合 32 位地址划分,逻辑清晰且易扩展。
可以把二级页表理解为“图书馆查书系统”:
•一级页表(Directory Table, DT):相当于“图书目录”,每个条目(DTE)指向一本 “页码簿”(二级页表);
•二级页表(Page Table, PT):相当于“页码簿”,每个条目(PTE)指向实际的 “书页”(物理内存页)。
结构示意图如下:
MMU_DTE_ADDR(一级页表基地址) → 一级页表(DT)→ 二级页表(PT)→ 物理内存页
代码中定义了页表大小:
•一级页表(DT):1024 个条目(NUM_DT_ENTRIES = 1024),每个条目 4 字节,刚好占 1 个 4KB 页(SPAGE_SIZE = 4096);
•二级页表(PT):同样 1024 个条目(NUM_PT_ENTRIES = 1024),也占 1 个 4KB 页。
RK IOMMU 的 32 位虚拟地址(IOVA)被拆成 3 部分,对应二级页表的索引和页内偏移:
|
地址范围(bit)
|
作用
|
大小
|
说明
|
|
31~22
|
一级页表索引(DTE)
|
10 位
|
对应 DT 的 1024 个条目
|
|
21~12
|
二级页表索引(PTE)
|
10 位
|
对应 PT 的 1024 个条目
|
|
11~0
|
页内偏移
|
12 位
|
对应 4KB 页的每个字节位置
|
比如虚拟地址0x00001000:
•DTE 索引 = 0x00001000 >> 22 = 0;
•PTE 索引 = (0x00001000 & 0x3FF000) >> 12 = 1;
•页内偏移 = 0x00001000 & 0xFFF = 0。
每个页表条目(DTE/PTE)都有固定格式,核心是 “存在位” 和 “权限位”,相当于给地址转换加了 “安全锁”。
|
字段
|
位范围
|
作用
|
|
PT 地址
|
31~12
|
二级页表的物理基地址
|
|
保留位
|
11~1
|
未使用,设为 0
|
|
存在位(Valid)
|
bit0
|
1 = 二级页表存在;0 = 不存在
|
代码中通过rk_mk_dte()生成 DTE,rk_dte_is_pt_valid()判断二级页表是否存在。
|
字段
|
位范围
|
作用
|
|
物理页地址
|
31~12
|
实际物理内存页的基地址
|
|
保留位
|
11~9
|
未使用,设为 0
|
|
缓存 / 属性位
|
8~3
|
控制缓存策略(如读缓存、写缓冲)
|
|
写权限位
|
bit2
|
1 = 允许写;0 = 只读
|
|
读权限位
|
bit1
|
1 = 允许读;0 = 禁止读
|
|
存在位(Valid)
|
bit0
|
1 = 物理页存在;0 = 不存在
|
代码中通过rk_mk_pte()生成 PTE,rk_pte_is_page_valid()判断物理页是否存在。
RK IOMMU 驱动基于 Linux 内核 IOMMU 框架实现,核心是驱动文件和DTS 节点配置,两者配合才能让硬件生效。
RK IOMMU 驱动源码路径:
drivers/iommu/rockchip-iommu.c

该文件实现了 Linux IOMMU 框架的核心回调(struct iommu_ops rk_iommu_ops),比如:
•domain_alloc:申请 IOMMU 域(管理页表的容器);
•map/unmap:建立 / 解除虚拟地址与物理地址的映射;
•attach_dev/detach_dev:绑定 / 解绑设备与域;
•flush_iotlb_all:清空 IOMMU TLB 缓存(避免旧映射干扰)。
DTS(设备树)是内核识别 IOMMU 硬件的关键,需要配置中断、时钟、电源域等信息。参考文档:Documentation/devicetree/bindings/iommu/rockchip,iommu.txt。
vopl_iommu: iommu@ff8f3f00 {compatible = "rockchip,iommu"; // 硬件版本,v2用"rockchip,iommu-v2"reg = <0x0 0xff8f3f00 0x0 0x1000>; // IOMMU寄存器基地址+大小interrupts =119 IRQ_TYPE_LEVEL_HIGH 0>; // 异常中断(缺页/总线错)clocks = <&cru ACLK_VOP1>, <&cru HCLK_VOP1>; // IOMMU依赖的时钟clock-names = "aclk", "hclk"; // 时钟名称,与clocks对应power-domains = <&power RK3399_PD_VOPL>; // 电源域,控制IOMMU供电iommu-cells = <0>; // 固定为0,IOMMU框架要求rockchip,disable-mmu-reset; // 可选,禁用MMU复位(规避部分芯片bug)};
•compatible:区分 IOMMU 硬件版本(v1 支持 32 位地址,v2 支持 40 位地址);
•interrupts:缺页 / 总线异常时触发中断,内核通过rk_pagefault_done()处理;
•clocks/power-domains:控制 IOMMU 的时钟和供电(必须先开时钟 / 电源才能访问寄存器)。
掌握基础后,实战分为内核配置、基础流程、调试技巧三部分,新手可按步骤操作。
首先需要在 Linux 内核中启用 RK IOMMU 选项,步骤如下:
1.进入内核源码目录,执行make menuconfig;
2.按路径找到选项:
Device Drivers → IOMMU Hardware Support → Rockchip IOMMU Support;
3.勾选该选项(设为y),依赖项(IOMMU_SUPPORT、ARM/ARM64)会自动启用;
4.保存配置并编译内核(make -j8),烧录镜像。
IOMMU 的核心是 “域(domain)管理映射,设备绑定域后使用映射”,最简流程如下(代码示例):
域是管理页表的容器,一个域可绑定多个设备(共享页表):
// 申请域(platform_bus_type对应平台设备,如VOP、VPU)struct iommu_domain *domain = iommu_domain_alloc(&platform_bus_type);if (!domain) {pr_err("IOMMU domain alloc failed!n");return -ENOMEM;}
通过iommu_map()创建映射,参数需注意地址对齐(必须是 4KB 的整数倍):
dma_addr_t iova = 0x10000000; // 要映射的IOMMU虚拟地址(IOVA)phys_addr_t paddr = 0x80000000; // 对应的物理地址(需通过Linux内存分配接口获取)size_t size = 0x1000; // 映射大小(4KB,必须是4KB的倍数)int prot = IOMMU_READ | IOMMU_WRITE; // 权限:可读可写// 建立映射int ret = iommu_map(domain, iova, paddr, size, prot);if (ret) {pr_err("IOMMU map failed! ret=%dn", ret);goto err_free_domain;}
将设备(如 VOP)绑定到域,设备才能使用该域的映射:
struct device *dev = &vopl_dev; // 要绑定的设备(如VOPL设备)// 绑定设备ret = iommu_attach_device(domain, dev);if (ret) {pr_err("IOMMU attach device failed! ret=%dn", ret);goto err_unmap;}
绑定完成后,设备(如 VOP)访问0x10000000(IOVA)时,会自动转换为0x80000000(物理地址)。
开发中若遇到“地址映射错误”,可通过Dump 页表排查问题(以 RK3399 VOPL IOMMU 为例):
假设要查虚拟地址0x10000000的映射:
1.查一级页表基地址:读 IOMMU 寄存器MMU_DTE_ADDR(地址0xff8f3f00),命令:
io -4 0xff8f3f00 → 得到 DT 基地址(如0x90000000);
2.算 DTE 索引与地址:
DTE 索引 = 0x10000000 >> 22 = 4 → DTE 地址 = 0x90000000 + 4*4 = 0x90000010;
3.查二级页表基地址:读 DTE 地址,命令:
io -4 0x90000010 → 得到 PT 基地址(如0x90001000);
4.算 PTE 索引与地址:
PTE 索引 = (0x10000000 & 0x3FF000) >> 12 = 0 → PTE 地址 = 0x90001000 + 0*4 = 0x90001000;
5.查物理页地址:读 PTE 地址,命令:
io -4 0x90001000 → 得到物理页基地址(如0x80000000);
6.算最终物理地址:
物理地址 = 物理页基地址 + 页内偏移 = 0x80000000 + (0x10000000 & 0xFFF) = 0x80000000。
通过以上步骤,可验证映射是否正确。
RK IOMMU 开发中,这些问题很容易遇到,提前掌握解决方案能少走弯路:
|
问题现象
|
可能原因
|
解决方案
|
|
报“pagefault 中断”
|
1. 访问未映射的 IOVA;2. 越界访问;3. 未映射就访问
|
1. 检查iommu_map的 IOVA 范围;2. 确认访问地址未超出映射大小;3. 确保map在attach前执行
|
|
enable stall 异常
|
缺页中断未处理,设备继续访问 IOMMU
|
先通过rk_pagefault_done()处理缺页,再重新使能 stall
|
|
IOMMU 寄存器无法访问
|
未开启 IOMMU 的电源域(PD)或时钟
|
调用pm_runtime_get_sync(dev)开启电源,clk_bulk_enable()开启时钟
|
|
持续报中断
|
DTS 中中断号配置错误
|
核对芯片手册,修正interrupts字段的中断号
|
|
开机闪屏(VOP 场景)
|
使能 IOMMU 时 VOP 正在取帧
|
等待 VOP 帧显示完成后,再使能 IOMMU
|
|
刷 TLB 导致性能下降
|
离散 buffer 多次刷 TLB
|
添加标志位,批量映射后只刷一次 TLB(参考代码shootdown_entire)
|
IOMMU 的核心是 “设备虚拟地址→物理地址” 转换,而物理地址的分配、管理依赖 Linux 内存管理机制。以下从核心概念、内存分配、地址转换、TLB 协同四个方面,讲解与 IOMMU 开发强相关的内存管理逻辑。
Linux 采用 “虚拟地址统一管理” 机制,所有 CPU 访问(内核 / 用户)、设备访问(需 IOMMU)都基于虚拟地址,最终通过页表转换到物理地址。
Linux 将 32/64 位地址空间分为 “用户空间” 和 “内核空间”(以 ARM64 为例):
|
地址空间
|
范围(ARM64)
|
用途
|
与 IOMMU 关联
|
|
用户空间
|
0x00000000_00000000 ~ 0x0000007F_FFFFFFFF
|
应用程序代码 / 数据
|
设备一般不直接访问,需通过内核中转
|
|
内核空间
|
0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF
|
内核代码 / 驱动 / 共享内存
|
IOMMU 映射的物理内存多来自此空间
|
Linux 内核(如 ARM64)采用四级页表(比 RK IOMMU 的二级页表更精细),用于 CPU 的虚拟地址→物理地址转换:
•PGD(页全局目录):最高级页表,对应地址高位(如 ARM64 的 63~48 位);
•PUD(页上级目录):二级页表,对应地址中位(47~39 位);
•PMD(页中间目录):三级页表,对应地址中低位(38~30 位);
•PTE(页表项):最低级页表,对应物理页基地址(29~12 位)+ 权限位。
与 RK IOMMU 页表的区别:
|
对比维度
|
Linux 内核页表
|
RK IOMMU 页表
|
|
服务对象
|
CPU(内核 / 用户空间访问)
|
外设(如 VOP、VPU)
|
|
页表层级
|
四级(PGD→PUD→PMD→PTE)
|
二级(DT→PT)
|
|
虚拟地址类型
|
CPU 虚拟地址(内核 / 用户 VA)
|
设备虚拟地址(IOVA)
|
|
管理主体
|
内核内存管理模块(MM)
|
RK IOMMU 驱动
|
IOMMU 映射的paddr(物理地址),需通过 Linux 内核提供的内存分配接口获取,核心接口分三类:
伙伴系统是 Linux 内核最底层的内存分配机制,管理 “物理页帧”(4KB/2MB/1GB 等),适合分配连续物理内存(IOMMU 常需连续内存,避免设备访问离散地址出错)。
核心接口:
// 分配order个连续页(大小=2^order * 4KB),返回页结构体指针struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);// 简化接口:分配1个页(4KB),返回内核虚拟地址void *__get_free_page(gfp_t gfp_mask);// 示例:分配4个连续页(16KB),用于IOMMU映射struct page *pages = alloc_pages(GFP_KERNEL | __GFP_DMA, 2); // order=2 → 2^2=4页if (!pages) {pr_err("Alloc contiguous pages failed!n");return -ENOMEM;}// 转换为物理地址(供iommu_map使用)phys_addr_t paddr = page_to_phys(pages);
•gfp_mask:分配标志,GFP_KERNEL表示内核可睡眠等待内存,__GFP_DMA表示从 DMA 可用区域分配(适合设备访问);
•order:分配页数的指数,order=0→1 页(4KB),order=1→2 页(8KB),最大order=11→2048 页(8MB)。
Slab 分配器基于伙伴系统,将连续页拆分为 “小对象”(如结构体、缓冲区),适合分配小于 4KB 的内存,不适合 IOMMU 映射(IOMMU 需物理页对齐的地址)。
核心接口:
// 分配size字节内存,返回内核虚拟地址(地址页对齐)void *kmalloc(size_t size, gfp_t gfp_mask);// 示例:分配256字节内存(用于驱动私有数据,不用于IOMMU映射)struct iommu_priv *priv = kmalloc(sizeof(*priv), GFP_KERNEL);
DMA 分配接口是 IOMMU 开发的核心接口,它直接返回“物理地址” 和 “内核虚拟地址”,且确保内存可被设备直接访问(如避开不可缓存区域、锁定页面不被回收)。
核心接口:
// 分配size字节连续物理内存,返回:// - 内核虚拟地址(virt_addr):供内核访问;// - DMA地址(dma_addr):即物理地址,供IOMMU映射使用;void *dma_alloc_coherent(struct device *dev, size_t size,dma_addr_t *dma_addr, gfp_t gfp_mask);// 示例:为VOP设备分配4KB内存,用于IOMMU映射dma_addr_t paddr; // 输出物理地址(供iommu_map)void *virt_addr = dma_alloc_coherent(&vopl_dev, 0x1000, &paddr, GFP_KERNEL);if (!virt_addr) {pr_err("DMA alloc failed!n");return -ENOMEM;}// 后续调用iommu_map(domain, iova, paddr, 0x1000, prot)
为什么优先用dma_alloc_coherent?
•自动确保内存“设备可访问”(如在 RK 芯片的 DMA 区域);
•直接返回物理地址(dma_addr),无需手动转换;
•自动锁定页面(避免被内核回收或 swap 到磁盘,导致设备访问失效)。
IOMMU 开发中,常需在 “内核虚拟地址(VA)” 与 “物理地址(PA)” 之间转换,核心转换接口:
|
转换方向
|
接口函数
|
适用场景
|
|
内核 VA → 物理 PA
|
phys_addr_t virt_to_phys(const void *virt)
|
已知内核虚拟地址,获取物理地址供 IOMMU 映射
|
|
物理 PA → 内核 VA
|
void *phys_to_virt(phys_addr_t phys)
|
已知物理地址,获取内核虚拟地址供内核访问
|
|
内核 VA → DMA 地址(PA)
|
dma_addr_t virt_to_dma(struct device *dev, const void *virt)
|
设备相关的物理地址转换
|
|
页结构体→ 物理 PA
|
phys_addr_t page_to_phys(const struct page *page)
|
从alloc_pages返回的page获取 PA
|
示例:结合 IOMMU 映射的转换流程
// 1. 用alloc_pages分配页struct page *page = alloc_pages(GFP_KERNEL, 0);// 2. 转换为物理地址(供iommu_map)phys_addr_t paddr = page_to_phys(page);// 3. 转换为内核虚拟地址(供内核写数据)void *virt_addr = page_address(page);// 4. 内核写数据到虚拟地址memcpy(virt_addr, "IOMMU test data", 16);// 5. IOMMU映射(IOVA→PA)iommu_map(domain, 0x10000000, paddr, 0x1000, IOMMU_READ | IOMMU_WRITE);
TLB(Translation Lookaside Buffer)是 “页表缓存”,用于加速地址转换(CPU 有 CPU TLB,IOMMU 有 IOMMU TLB)。当页表修改后(如iommu_map/iommu_unmap),需刷新 TLB,否则旧映射会干扰新映射。
内核修改页表后(如mmap/kmap),需调用以下接口刷新 CPU TLB:
// 刷新指定虚拟地址范围的TLB(用户空间)void flush_tlb_range(struct vm_area_struct *vma, unsigned long start, unsigned long end);// 刷新整个内核空间的TLBvoid flush_tlb_kernel_range(unsigned long start, unsigned long end);
RK IOMMU 驱动提供专用接口,刷新 IOMMU TLB:
// 刷新整个IOMMU TLB(最常用,如unmap后)void iommu_flush_iotlb_all(struct iommu_domain *domain);// 刷新指定IOVA范围的TLB(精细刷新,减少性能损耗)void iommu_flush_iotlb_range(struct iommu_domain *domain, dma_addr_t iova, size_t size);
实战注意点:
•修改 IOMMU 页表后(如iommu_unmap),必须调用iommu_flush_iotlb_all,否则设备仍会使用旧映射;
•批量修改映射时,建议先完成所有iommu_map,再统一刷新 TLB(避免多次刷新导致性能下降)。
Linux 内核会对 “不常用内存” 进行回收(如 swap 到磁盘),但 IOMMU 映射的物理内存若被回收,会导致设备访问 “无效地址”(报 pagefault)。因此需通过内存锁定接口,禁止内核回收相关内存。
核心接口:
// 锁定指定页,禁止被回收或移动int get_page(struct page *page);// 解锁页,允许回收(需与get_page成对调用)void put_page(struct page *page);// 示例:锁定IOMMU映射的页struct page *page = alloc_pages(GFP_KERNEL, 0);get_page(page); // 锁定页// ... 执行IOMMU映射、设备访问 ...put_page(page); // 设备停止访问后,解锁页
DMA 分配的内存无需手动锁定:dma_alloc_coherent会自动锁定内存,直到调用dma_free_coherent释放,无需额外调用get_page。
RK 平台 IOMMU 开发的核心是 “Linux 内存管理为底层支撑 + IOMMU 实现设备地址转换”,关键逻辑可总结为 3 步:
1.内存分配:通过dma_alloc_coherent/alloc_pages获取物理地址(paddr),确保内存可被设备访问;
2.地址映射:调用iommu_map建立 IOVA→paddr 的映射,刷新 IOMMU TLB;
3.设备访问:设备绑定 IOMMU 域后,通过 IOVA 访问物理内存,内核通过页表确保 CPU 与设备访问的一致性。
开发中需重点关注:
•物理地址需从 Linux 内存接口获取,不可硬编码(不同芯片物理地址范围不同);
•页表修改后必须刷新 TLB(CPU TLB/IOMMU TLB 分别处理);
•设备访问的内存需锁定,避免被内核回收。
若需进一步深入,可参考 Linux 内核文档:Documentation/mm/(内存管理原理)、Documentation/devicetree/bindings/iommu/(IOMMU 设备树规范),或 RK 官方芯片手册的 “内存控制器” 章节。
有疑问的小伙伴欢迎在评论区留言,一起交流 IOMMU 与内存管理的协同开发技巧~
全部0条评论
快来发表一下你的评论吧 !