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

电子说

1.4w人已加入

描述

 

 

在瑞芯微(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 字节,刚好占 个 4KB 页(SPAGE_SIZE = 4096);

 

 

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

 

 

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

 

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

 

 

地址范围(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

 

 

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

 

每个页表条目(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

内存管理

该文件实现了 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 = <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 的时钟和供电(必须先开时钟 电源才能访问寄存器)。

 

 

三、实战开发:从 0 到 使用 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. 基础使用流程:步实现地址映射

 

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

 

 

步骤 1:申请 IOMMU 域(domain

 

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

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
#include // 申请域(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;}

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

 

通过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;}

步骤 3:绑定设备与域

 

将设备(如 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;}

步骤 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_map IOVA 范围;2. 确认访问地址未超出映射大小;3. 确保mapattach前执行

 

 

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

 

 

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

 

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

 

 

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

 

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

 

 

1)地址空间划分

 

Linux 将 32/64 位地址空间分为 用户空间” 和 内核空间(以 ARM64 为例):

 

 

地址空间

 

 

范围(ARM64

 

 

用途

 

 

 IOMMU 关联

 

 

用户空间

 

 

0x00000000_00000000 ~ 0x0000007F_FFFFFFFF

 

 

应用程序代码 / 数据

 

 

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

 

 

内核空间

 

 

0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF

 

 

内核代码 / 驱动 共享内存

 

 

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

 

 

2Linux 内核页表层级(与 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(内核 用户空间访问)

 

 

外设(如 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),返回页结构体指针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)。

 

 

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

 

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);

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

 

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 到磁盘,导致设备访问失效)。

 

 

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分配页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);

4. TLB 协同:内核 TLB 与 IOMMU TLB 的刷新

 

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

 

 

1Linux 内核 TLB 刷新(CPU 用)

 

内核修改页表后(如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);

2IOMMU TLB 刷新(设备用)

 

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(避免多次刷新导致性能下降)。

 

 

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

 

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 实现设备地址转换,关键逻辑可总结为 步:

 

 

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 与内存管理的协同开发技巧~

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分