RK平台Linux IOMMU开发:从原理到实战

瑞芯微RK)芯片的Linux开发中,IOMMU(输入输出内存管理单元)是个关键部件——它能实现设备虚拟地址(IOVA)与物理地址的转换,还能控制读写权限、处理缺页/总线异常,广泛用于显示(VOP)、编解码(VPU/HEVC)等场景。今天就从原理、驱动、实战、问题排查、Linux内存管理支撑五个维度,带大家快速上手RK平台IOMMU开发。

一、先搞懂:RK IOMMU的核心结构

RK IOMMU采用二级页表设计(类似Linux内核页表),配合32位地址划分,逻辑清晰且易扩展。

1.二级页表:地址转换的目录-页码体系

可以把二级页表理解为图书馆查书系统

一级页表(Directory Table, DT:相当于图书目录,每个条目(DTE)指向一本页码簿(二级页表);

二级页表(Page Table, PT:相当于页码簿,每个条目(PTE)指向实际的书页(物理内存页)。

结构示意图如下:

MMU_DTE_ADDR(一级页表基地址) → 一级页表(DT)→ 二级页表(PT)→ 物理内存页

代码中定义了页表大小:

一级页表(DT):1024个条目(NUM_DT_ENTRIES = 1024),每个条目4字节,刚好占14KB页(SPAGE_SIZE = 4096);

二级页表(PT):同样1024个条目(NUM_PT_ENTRIES = 1024),也占14KB页。

2. 32位地址划分:3段式拆分

RK IOMMU32位虚拟地址(IOVA)被拆成3部分,对应二级页表的索引和页内偏移:

地址范围(bit

作用

大小

说明

31~22

一级页表索引(DTE

10

对应DT1024个条目

21~12

二级页表索引(PTE

10

对应PT1024个条目

11~0

页内偏移

12

对应4KB页的每个字节位置

比如虚拟地址0x00001000

DTE索引=0x00001000 >> 22 = 0

PTE索引=(0x00001000 & 0x3FF000) >> 12 = 1

页内偏移=0x00001000 & 0xFFF = 0

3. DTEPTE:页表条目的开关与权限

每个页表条目(DTE/PTE)都有固定格式,核心是存在位权限位,相当于给地址转换加了安全锁

1DTE(一级页表条目):指向二级页表

字段

位范围

作用

PT地址

31~12

二级页表的物理基地址

保留位

11~1

未使用,设为0

存在位(Valid

bit0

1 =二级页表存在;0 =不存在

代码中通过rk_mk_dte()生成DTErk_dte_is_pt_valid()判断二级页表是否存在。

2PTE(二级页表条目):指向物理内存页

字段

位范围

作用

物理页地址

31~12

实际物理内存页的基地址

保留位

11~9

未使用,设为0

缓存/属性位

8~3

控制缓存策略(如读缓存、写缓冲)

写权限位

bit2

1 =允许写;0 =只读

读权限位

bit1

1 =允许读;0 =禁止读

存在位(Valid

bit0

1 =物理页存在;0 =不存在

代码中通过rk_mk_pte()生成PTErk_pte_is_page_valid()判断物理页是否存在。

二、驱动与配置:让IOMMU “跑起来

RK IOMMU驱动基于Linux内核IOMMU框架实现,核心是驱动文件DTS节点配置,两者配合才能让硬件生效。

1.驱动文件:核心代码在哪?

RK IOMMU驱动源码路径:

drivers/iommu/rockchip-iommu.c

wKgZO2kamRKAK2o1AAD26xw2UU0245.png

该文件实现了Linux IOMMU框架的核心回调(struct iommu_ops rk_iommu_ops),比如:

domain_alloc:申请IOMMU域(管理页表的容器);

map/unmap:建立/解除虚拟地址与物理地址的映射;

attach_dev/detach_dev:绑定/解绑设备与域;

flush_iotlb_all:清空IOMMU TLB缓存(避免旧映射干扰)。

2. DTS节点配置:告诉内核硬件信息

DTS(设备树)是内核识别IOMMU硬件的关键,需要配置中断、时钟电源等信息。参考文档:Documentation/devicetree/bindings/iommu/rockchip,iommu.txt

关键配置项说明(以RK3399 VOPL IOMMU为例):

vopl_iommu: iommu@ff8f3f00 { compatible ="rockchip,iommu"; //硬件版本,v2用"rockchip,iommu-v2" reg = <0x00xff8f3f000x00x1000>; //IOMMU寄存器基地址+大小 interrupts = 
    
     119
    IRQ_TYPE_LEVEL_HIGH0>; //异常中断(缺页/总线错) 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的时钟和供电(必须先开时钟/电源才能访问寄存器)。

三、实战开发:从01使用IOMMU

掌握基础后,实战分为内核配置、基础流程、调试技巧三部分,新手可按步骤操作。

1.第一步:开启内核IOMMU支持

首先需要在Linux内核中启用RK IOMMU选项,步骤如下:

1.进入内核源码目录,执行make menuconfig

2.按路径找到选项:

Device Drivers → IOMMU Hardware Support → Rockchip IOMMU Support

3.勾选该选项(设为y),依赖项(IOMMU_SUPPORTARM/ARM64)会自动启用;

4.保存配置并编译内核(make -j8),烧录镜像。

2.基础使用流程:4步实现地址映射

IOMMU的核心是域(domain)管理映射,设备绑定域后使用映射,最简流程如下(代码示例):

步骤1:申请IOMMU域(domain

域是管理页表的容器,一个域可绑定多个设备(共享页表):

#include
      // 申请域(platform_bus_type对应平台设备,如VOP、VPU)structiommu_domain*domain =iommu_domain_alloc(&platform_bus_type);if(!domain) { pr_err("IOMMU domain alloc failed!n"); return-ENOMEM;}

步骤2:建立虚拟地址-物理地址映射

通过iommu_map()创建映射,参数需注意地址对齐(必须是4KB的整数倍):

dma_addr_tiova =0x10000000; // 要映射的IOMMU虚拟地址(IOVA)phys_addr_tpaddr =0x80000000; // 对应的物理地址(需通过Linux内存分配接口获取)size_tsize =0x1000; // 映射大小(4KB,必须是4KB的倍数)intprot = IOMMU_READ | IOMMU_WRITE; // 权限:可读可写// 建立映射intret =iommu_map(domain, iova, paddr, size, prot);if(ret) { pr_err("IOMMU map failed! ret=%dn", ret); gotoerr_free_domain;}

步骤3:绑定设备与域

将设备(如VOP)绑定到域,设备才能使用该域的映射:

structdevice *dev = &vopl_dev; // 要绑定的设备(如VOPL设备)// 绑定设备ret = iommu_attach_device(domain, dev);if(ret) { pr_err("IOMMU attach device failed! ret=%dn", ret); gotoerr_unmap;}

步骤4:启动设备访问IOMMU

绑定完成后,设备(如VOP)访问0x10000000IOVA)时,会自动转换为0x80000000(物理地址)。

3.进阶:调试时如何Dump页表?

开发中若遇到地址映射错误,可通过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.访问未映射的IOVA2.越界访问;3.未映射就访问

1.检查iommu_mapIOVA范围;2.确认访问地址未超出映射大小;3.确保mapattach前执行

enable stall异常

缺页中断未处理,设备继续访问IOMMU

先通过rk_pagefault_done()处理缺页,再重新使能stall

IOMMU寄存器无法访问

未开启IOMMU的电源域(PD)或时钟

调用pm_runtime_get_sync(dev)开启电源,clk_bulk_enable()开启时钟

持续报中断

DTS中中断号配置错误

核对芯片手册,修正interrupts字段的中断号

开机闪屏(VOP场景)

使能IOMMUVOP正在取帧

等待VOP帧显示完成后,再使能IOMMU

TLB导致性能下降

离散buffer多次刷TLB

添加标志位,批量映射后只刷一次TLB(参考代码shootdown_entire

五、Linux系统内存管理:IOMMU开发的底层支撑

IOMMU的核心是设备虚拟地址物理地址转换,而物理地址的分配、管理依赖Linux内存管理机制。以下从核心概念、内存分配、地址转换、TLB协同四个方面,讲解与IOMMU开发强相关的内存管理逻辑。

1.核心概念:Linux地址空间与页表

Linux采用虚拟地址统一管理机制,所有CPU访问(内核/用户)、设备访问(需IOMMU)都基于虚拟地址,最终通过页表转换到物理地址。

1)地址空间划分

Linux32/64位地址空间分为用户空间内核空间(以ARM64为例):

地址空间

范围(ARM64

用途

IOMMU关联

用户空间

0x00000000_00000000 ~ 0x0000007F_FFFFFFFF

应用程序代码/数据

设备一般不直接访问,需通过内核中转

内核空间

0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF

内核代码/驱动/共享内存

IOMMU映射的物理内存多来自此空间

2Linux内核页表层级(与IOMMU页表对比)

Linux内核(如ARM64)采用四级页表(比RK IOMMU的二级页表更精细),用于CPU的虚拟地址物理地址转换:

PGD(页全局目录):最高级页表,对应地址高位(如ARM6463~48位);

PUD(页上级目录):二级页表,对应地址中位(47~39位);

PMD(页中间目录):三级页表,对应地址中低位(38~30位);

PTE(页表项):最低级页表,对应物理页基地址(29~12位)+权限位。

RK IOMMU页表的区别

对比维度

Linux内核页表

RK IOMMU页表

服务对象

CPU(内核/用户空间访问)

外设(如VOPVPU

页表层级

四级(PGD→PUD→PMD→PTE

二级(DT→PT

虚拟地址类型

CPU虚拟地址(内核/用户VA

设备虚拟地址(IOVA

管理主体

内核内存管理模块(MM

RK IOMMU驱动

2.内存分配:IOMMU所需物理地址从哪来?

IOMMU映射的paddr(物理地址),需通过Linux内核提供的内存分配接口获取,核心接口分三类:

1)伙伴系统:分配连续物理页(适合大块内存)

伙伴系统是Linux内核最底层的内存分配机制,管理物理页帧4KB/2MB/1GB等),适合分配连续物理内存(IOMMU常需连续内存,避免设备访问离散地址出错)。

核心接口:

// 分配order个连续页(大小=2^order * 4KB),返回页结构体指针structpage*alloc_pages(gfp_tgfp_mask,unsignedintorder);// 简化接口:分配1个页(4KB),返回内核虚拟地址void*__get_free_page(gfp_tgfp_mask);// 示例:分配4个连续页(16KB),用于IOMMU映射structpage*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_tpaddr =page_to_phys(pages);

gfp_mask:分配标志,GFP_KERNEL表示内核可睡眠等待内存,__GFP_DMA表示从DMA可用区域分配(适合设备访问);

order:分配页数的指数,order=0→1页(4KB),order=1→2页(8KB),最大order=11→2048页(8MB)。

2Slab分配器:分配小内存(适合小块对象)

Slab分配器基于伙伴系统,将连续页拆分为小对象(如结构体、缓冲区),适合分配小于4KB的内存,不适合IOMMU映射(IOMMU需物理页对齐的地址)。

核心接口:

// 分配size字节内存,返回内核虚拟地址(地址页对齐)void*kmalloc(size_tsize,gfp_tgfp_mask);// 示例:分配256字节内存(用于驱动私有数据,不用于IOMMU映射)structiommu_priv*priv =kmalloc(sizeof(*priv), GFP_KERNEL);

3DMA分配接口:直接分配设备可访问内存

DMA分配接口是IOMMU开发的核心接口,它直接返回物理地址内核虚拟地址,且确保内存可被设备直接访问(如避开不可缓存区域、锁定页面不被回收)。

核心接口:

// 分配size字节连续物理内存,返回:// - 内核虚拟地址(virt_addr):供内核访问;// - DMA地址(dma_addr):即物理地址,供IOMMU映射使用;void*dma_alloc_coherent(structdevice *dev,size_tsize, dma_addr_t*dma_addr,gfp_tgfp_mask);// 示例:为VOP设备分配4KB内存,用于IOMMU映射dma_addr_tpaddr; // 输出物理地址(供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到磁盘,导致设备访问失效)。

3.地址转换:虚拟地址与物理地址的转换

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分配页structpage*page =alloc_pages(GFP_KERNEL,0);// 2. 转换为物理地址(供iommu_map)phys_addr_tpaddr =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);

4. TLB协同:内核TLBIOMMU TLB的刷新

TLBTranslation Lookaside Buffer)是页表缓存,用于加速地址转换(CPUCPU TLBIOMMUIOMMU TLB)。当页表修改后(如iommu_map/iommu_unmap),需刷新TLB,否则旧映射会干扰新映射。

1Linux内核TLB刷新(CPU用)

内核修改页表后(如mmap/kmap),需调用以下接口刷新CPU TLB

// 刷新指定虚拟地址范围的TLB(用户空间)voidflush_tlb_range(structvm_area_struct *vma,unsignedlongstart,unsignedlongend);// 刷新整个内核空间的TLBvoidflush_tlb_kernel_range(unsignedlongstart,unsignedlongend);

2IOMMU TLB刷新(设备用)

RK IOMMU驱动提供专用接口,刷新IOMMU TLB

// 刷新整个IOMMU TLB(最常用,如unmap后)voidiommu_flush_iotlb_all(structiommu_domain *domain);// 刷新指定IOVA范围的TLB(精细刷新,减少性能损耗)voidiommu_flush_iotlb_range(structiommu_domain *domain,dma_addr_tiova,size_tsize);

实战注意点

修改IOMMU页表后(如iommu_unmap),必须调用iommu_flush_iotlb_all,否则设备仍会使用旧映射;

批量修改映射时,建议先完成所有iommu_map,再统一刷新TLB(避免多次刷新导致性能下降)。

5.内存锁定:避免IOMMU映射的内存被回收

Linux内核会对不常用内存进行回收(如swap到磁盘),但IOMMU映射的物理内存若被回收,会导致设备访问无效地址(报pagefault)。因此需通过内存锁定接口,禁止内核回收相关内存。

核心接口:

// 锁定指定页,禁止被回收或移动intget_page(structpage *page);// 解锁页,允许回收(需与get_page成对调用)voidput_page(structpage *page);// 示例:锁定IOMMU映射的页structpage *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内存接口获取,不可硬编码(不同芯片物理地址范围不同);

页表修改后必须刷新TLBCPU TLB/IOMMU TLB分别处理);

设备访问的内存需锁定,避免被内核回收。

若需进一步深入,可参考Linux内核文档:Documentation/mm/(内存管理原理)、Documentation/devicetree/bindings/iommu/IOMMU设备树规范),或RK官方芯片手册的内存控制器章节。

有疑问的小伙伴欢迎在评论区留言,一起交流IOMMU与内存管理的协同开发技巧~

  • 随机文章
  • 热门文章