Linux 5.4.233 内核中 iommu_map
函数实现深度分析
iommu_map
是 Linux 内核中用于建立 IOVA(I/O 虚拟地址)到物理地址映射 的核心函数,其实现涉及 IOMMU 硬件操作、页表管理和内存保护机制。以下基于 5.4.233 内核源码进行详细解析:
1. 函数定义与参数
函数位于 drivers/iommu/iommu.c
:
int iommu_map(struct iommu_domain *domain, unsigned long iova,
phys_addr_t paddr, size_t size, int prot)
- domain:IOMMU 域,包含硬件特定的页表信息。
- iova:设备使用的虚拟地址起始位置。
- paddr:物理地址起始位置。
- size:映射的内存区域大小。
- prot:保护标志(如
IOMMU_READ
、IOMMU_WRITE
)。
2. 实现流程分析
(1) 参数合法性校验
if (!domain || !domain->ops->map)
return -EINVAL;
if (size & ~PAGE_MASK)
return -EINVAL; // 检查 size 是否页对齐
- 确保
domain
有效且支持映射操作。 - 映射大小必须是页对齐的(4KB/2MB/1GB)。
(2) 计算页表粒度和映射次数
size_t mapped = 0;
while (mapped < size) {
// 尝试以最大支持的页大小(如 1GB)进行映射
size_t pgsize = iommu_pgsize(domain, paddr, iova, size - mapped);
// 调用具体 IOMMU 驱动的 map 方法
ret = domain->ops->map(domain, iova + mapped, paddr + mapped,
pgsize, prot);
if (ret)
break;
mapped += pgsize;
}
- 页粒度选择:通过
iommu_pgsize()
选择当前硬件支持的最大页大小(优化页表项数量)。 - 循环映射:若映射失败(如部分区域已被占用),则尝试更小的页粒度。
(3) 底层驱动映射操作
以 Intel IOMMU (VT-d) 为例(drivers/iommu/intel-iommu.c
):
static int intel_iommu_map(...) {
// 获取页表地址
struct dmar_domain *dmar_domain = to_dmar_domain(domain);
// 操作页表项
return __domain_mapping(dmar_domain, iova, paddr, size, prot);
}
- 页表遍历:通过多级页表(如 4 级页表)查找或创建对应的页表项。
- 更新页表项:将
iova
映射到paddr
,并设置权限位(读/写/缓存等)。
(4) 错误处理与回滚
if (ret && mapped) {
// 部分映射失败时,回滚已建立的映射
iommu_unmap(domain, iova, mapped);
}
return ret ? ret : mapped == size ? 0 : -EINVAL;
- 原子性保证:若部分映射失败,需解除已完成的映射,避免内存泄漏。
3. 关键机制详解
(1) 页粒度优化
iommu_pgsize
逻辑:
选择同时满足以下条件的最大页大小:- 硬件支持(通过
domain->pgsize_bitmap
)。 iova
和paddr
对齐到该页大小。- 剩余映射大小不小于该页大小。
- 硬件支持(通过
(2) 硬件页表更新
- Intel VT-d 示例:
// 更新页表项并刷新 IOTLB domain_flush_cache(dmar_domain, pgd, sizeof(*pgd)); iommu_flush_iotlb_psi(domain, iova, pgsize, 0);
- 缓存一致性:通过
clflush
或内存屏障保证页表写入对 IOMMU 可见。 - IOTLB 刷新:使设备 DMA 请求使用新的映射。
- 缓存一致性:通过
(3) 锁机制
static int __iommu_map(...) {
mutex_lock(&domain->lock);
// 映射操作
mutex_unlock(&domain->lock);
}
- 锁类型:通常使用互斥锁 (
mutex
) 保护域的操作,防止并发修改页表。
4. 性能与安全设计
(1) 大页映射优化
- 优先使用 2MB 或 1GB 大页,减少页表层级,降低 TLB 未命中率。
(2) DMA 保护
if (!(prot & IOMMU_WRITE))
// 设置页表项为只读,阻止设备非法写入
- 通过页表权限位限制设备访问,防止恶意 DMA 覆盖内存。
5. 典型调用链示例
iommu_map()
└── intel_iommu_map()
└── __domain_mapping()
├── pgd_lookup() // 查找页全局目录
├── alloc_pte() // 分配页表项
└── domain_flush_cache() // 刷新缓存
6. 相关内核代码片段
- 页表项操作(Intel):
// 填充页表项物理地址和权限 *pte = paddr | DMA_PTE_READ | DMA_PTE_WRITE;
- IOTLB 刷新:
iommu_flush_iotlb(domain, iova, size);
7. Linux 5.4.233 内核中 AMD IOMMU 映射操作实现分析
AMD IOMMU(通常称为 AMD-Vi)的底层映射操作与 Intel VT-d 在架构设计上存在差异,但其核心目标一致:将设备 IOVA(I/O 虚拟地址)映射到物理内存,并管理页表权限。以下基于 5.4.233 内核源码,以 AMD IOMMU 为例解析底层驱动映射操作。
7.1. 关键数据结构与驱动入口
(1) AMD IOMMU 驱动入口
- 驱动代码位于
drivers/iommu/amd_iommu.c
。 - 映射操作入口函数为
amd_iommu_map
:static int amd_iommu_map(struct iommu_domain *domain, unsigned long iova, phys_addr_t paddr, size_t size, int prot)
(2) 核心数据结构
struct protection_domain
AMD 对iommu_domain
的扩展,包含页表根地址、设备列表、页表锁等:struct protection_domain { struct iommu_domain domain; // 继承 iommu_domain spinlock_t lock; // 自旋锁保护并发操作 unsigned mode; // 页表模式(如 4 级页表) u64 *pt_root; // 页表根指针(PML4) // ... 其他字段(设备链表、Domain ID 等) };
7.2. 映射操作流程(以 amd_iommu_map
为例)
(1) 参数转换与校验
struct protection_domain *pdom = to_pdomain(domain);
// 检查映射大小是否页对齐
if (!IS_ALIGNED(iova | paddr | size, PAGE_SIZE))
return -EINVAL;
- 将通用
iommu_domain
转换为 AMD 特有的protection_domain
。 - 验证
iova
、paddr
和size
的页对齐(AMD IOMMU 通常要求 4KB 对齐)。
7.3. 典型代码调用链
iommu_map()
└── amd_iommu_map()
├── iommu_map_page() // 逐级填充页表
│ ├── PML4 索引计算
│ ├── PDPE/PDE 分配
│ └── PTE 写入
└── domain_flush_np_cache() // 刷新 IOTLB
7.4. 安全与性能优化
(1) 地址空间隔离
- 每个
protection_domain
对应一组设备,确保设备只能访问所属域的 IOVA 范围。
(2) 硬件加速
- 利用 AMD IOMMU 的 ATS(Address Translation Services) 支持,允许设备缓存地址转换,减少页表查询开销。
(3) 中断映射
- AMD IOMMU 同时管理设备中断的虚拟化(通过 IRTE 表),但此部分与 DMA 映射解耦。
7.5. iommu_map_page关键代码解析:
7.5.1. 参数校验阶段:
BUG_ON(!IS_ALIGNED(bus_addr, page_size)); // 确保总线地址页对齐
BUG_ON(!IS_ALIGNED(phys_addr, page_size)); // 确保物理地址页对齐
使用内核调试宏确保地址对齐,避免非法映射
7.5.2. 页表项操作:
count = PAGE_SIZE_PTE_COUNT(page_size); // 计算所需PTE数量
pte = alloc_pte(dom, bus_addr, page_size, NULL, gfp, &updated); // 分配页表项
page_size
可能为4K/2M/1G等不同层级alloc_pte
可能涉及多级页表遍历
7.5.3. 旧映射清除:
for (i = 0; i < count; ++i)
freelist = free_clear_pte(&pte[i], pte[i], freelist);
通过链表freelist
回收旧页表页,采用延迟释放策略
7.5.4. 新PTE构造逻辑:
if (count > 1) { // 处理大页映射
__pte = PAGE_SIZE_PTE(__sme_set(phys_addr), page_size);
__pte |= PM_LEVEL_ENC(7) | IOMMU_PTE_PR | IOMMU_PTE_FC;
} else { // 普通页映射
__pte = __sme_set(phys_addr) | IOMMU_PTE_PR | IOMMU_PTE_FC;
}
PM_LEVEL_ENC(7)
设置页表层级__sme_set()
处理安全内存加密(如AMD SME)IOMMU_PTE_FC
表示完全缓存一致性
7.5.5. 权限设置:
if (prot & IOMMU_PROT_IR) __pte |= IOMMU_PTE_IR; // 读权限
if (prot & IOMMU_PROT_IW) __pte |= IOMMU_PTE_IW; // 写权限
权限位对应设备访问能力控制
7.5.6. 缓存刷新:
spin_lock_irqsave(&dom->lock, flags);
update_domain(dom); // 更新IOMMU域
spin_unlock_irqrestore(&dom->lock, flags);
通过自旋锁保护关键操作,update_domain()
会触发IOMMU TLB刷新
典型应用场景:
当设备需要通过DMA访问内存时,驱动调用此函数建立地址映射。例如:
- 网卡接收数据包缓冲区映射
- GPU显存与系统内存共享
- PCIe设备直接内存访问
性能优化点:
- 使用大页(count >1)减少TLB Miss
- 延迟释放页表页(free_page_list最后统一处理)
- 批量化PTE更新(循环写入多个PTE)
该函数体现了Linux内核中IOMMU子系统的核心映射机制,通过精细的页表管理实现设备安全访问内存的控制。
8. update_domain分析
函数功能
该函数用于更新保护域(protection_domain) 的状态,通常在域配置(如页表、设备映射等)发生变化后调用。其核心操作分为四步,确保硬件与内存管理单元(如IOMMU)的状态同步。
代码逐层解析
1. update_device_table(domain)
- 作用:更新与保护域关联的设备表(如IOMMU设备上下文)。
- 细节:
当域的页表或设备映射发生变更时,需将新配置写入硬件设备表。例如,在IOMMU中,设备表存储了地址转换规则,此函数可能更新这些条目。
2. domain_flush_devices(domain)
- 作用:刷新关联设备的缓存。
- 细节:
通知所有绑定到该域的硬件设备(如DMA设备)刷新其本地TLB或缓存。确保设备使用最新的地址映射,避免因缓存陈旧数据导致非法内存访问。
3. domain_flush_tlb_pde(domain)
- 作用:刷新保护域的TLB(转换后备缓冲区)中与页目录条目(PDE)相关的条目。
- 细节:
IOMMU或CPU的TLB可能缓存了旧的地址转换结果。此函数显式无效化这些缓存条目,强制后续访问重新加载最新页表数据。
4. domain_flush_complete(domain)
- 作用:确保所有刷新操作完成。
- 细节:
由于某些刷新操作可能是异步的(如硬件队列处理),此函数通过等待完成信号或检查状态寄存器,保证后续代码在刷新完全结束后执行。
典型应用场景
- 修改页表:调整内存映射后,需同步硬件状态。
- 动态绑定/解绑设备:当设备加入或离开保护域时,更新设备表并刷新缓存。
- 安全隔离:在虚拟化或安全敏感场景中,确保设备无法访问无效内存区域。
潜在关联机制
- IOMMU/SMMU:该函数可能直接操作IOMMU硬件寄存器。
- 缓存一致性:通过内存屏障或特定指令保证操作的顺序性。
- 并发控制:可能在调用前已持有保护域的锁,防止竞态条件。
小结
此函数通过更新配置→刷新缓存→同步完成的流程,确保保护域的变更在硬件层面生效,是内存虚拟化与设备隔离的关键底层操作。
9.struct protection_domain 结构解析
struct protection_domain
是 AMD IOMMU 驱动中用于描述 内存保护域(Memory Protection Domain)的核心数据结构。每个保护域代表一组设备共享的地址转换规则和内存访问权限。以下是其成员的详细说明:
1. 核心成员说明
成员名称 | 类型 | 作用描述 |
---|---|---|
list | struct list_head | 用于将域链接到全局保护域链表中(如系统所有域的列表)。 |
dev_list | struct list_head | 记录属于该域的所有设备的链表头(设备通过 struct device 关联)。 |
domain | struct iommu_domain | 通用 IOMMU 域结构,提供与 Linux 内核 IOMMU 框架交互的接口(见下文详细展开)。 |
lock | spinlock_t | 自旋锁,保护域内页表操作(如修改页表、刷新 TLB)的并发访问。 |
api_lock | struct mutex | 互斥锁,保护用户态 API 路径(如 iommu_map /iommu_unmap )的页表操作。 |
id | u16 | 域的唯一标识符,写入设备表(Device Table)以关联设备与域。 |
mode | int | 分页模式级别(0-6),表示页表的层级(如 4 级页表对应 mode=4 )。 |
pt_root | u64* | 页表根指针,指向该域的页表根目录(物理地址)。 |
glx | int | Guest CR3 表的层级数(用于虚拟化场景)。 |
gcr3_tbl | u64* | Guest CR3 表指针,用于虚拟机监控程序(如嵌套分页)。 |
flags | unsigned long | 标志位,标识域的类型(如是否为直通域、是否启用虚拟化功能等)。 |
dev_cnt | unsigned | 绑定到该域的设备数量。 |
dev_iommu | unsigned[MAX_IOMMUS] | 记录每个 IOMMU 硬件单元上该域的引用计数(用于多 IOMMU 系统)。 |
9.2. struct iommu_domain
详解
struct iommu_domain
是 Linux 内核 IOMMU 框架定义的 通用域描述符,用于抽象不同厂商(如 Intel、AMD)的 IOMMU 实现。其核心字段如下:
9.2.1 结构定义(简化版)
struct iommu_domain {
enum iommu_domain_type type; // 域类型(如 DMA 域、直通域)
const struct iommu_ops *ops; // 操作函数集(厂商实现)
unsigned long pgsize_bitmap; // 支持的页大小位图(如 4K、2M、1G)
struct iommu_domain_geometry geometry; // 地址空间几何信息(起始/结束地址)
void *iova_cookie; // 私有数据(如 IOMMU DMA 映射器句柄)
// ... 其他内核内部字段(如设备链表、TLB 刷新状态等)
};
9.2.2 关键成员说明
成员名称 | 作用 |
---|---|
type | 定义域的类型: - IOMMU_DOMAIN_DMA :标准 DMA 映射域(使用 IOMMU 页表)- IOMMU_DOMAIN_IDENTITY :直通模式(物理地址直接映射)- IOMMU_DOMAIN_UNMANAGED :非托管域(用户手动管理页表) |
ops | 函数指针表,指向厂商实现的 IOMMU 操作函数(如 map 、unmap 、attach_dev 等)。示例: - map() :将虚拟地址映射到物理地址- unmap() :取消映射- flush_iotlb_all() :刷新整个 TLB |
pgsize_bitmap | 表示该域支持的页大小(如 `4K |
geometry | 定义域的地址空间范围(如限制设备可访问的 DMA 地址范围)。 |
iova_cookie | 厂商私有数据指针,通常用于关联 DMA 映射器(如管理 IOVA 分配器)。 |
9.3. protection_domain
与 iommu_domain
的关系
-
继承与扩展:
struct protection_domain
内嵌了struct iommu_domain
,表明它是 AMD 对通用 IOMMU 域的具体实现。通过iommu_domain
的ops
字段,AMD 驱动注册了自定义的映射、刷新等函数。 -
分工协作:
- 通用逻辑:由内核 IOMMU 框架通过
iommu_domain
统一处理(如设备绑定、地址分配)。 - 硬件相关操作:由
protection_domain
的私有字段(如pt_root
、gcr3_tbl
)和ops
中的 AMD 实现函数完成。
- 通用逻辑:由内核 IOMMU 框架通过
9.4. 典型应用场景
-
设备绑定
当设备通过iommu_attach_device()
绑定到域时,内核会调用protection_domain
的ops->attach_dev()
函数,将设备添加到dev_list
并更新设备表。 -
地址映射
用户调用iommu_map()
时,最终通过ops->map()
操作 AMD 页表(修改pt_root
指向的页表结构)。 -
TLB 刷新
映射更新后,调用ops->flush_iotlb()
触发domain_flush_pages()
,利用pt_root
和lock
完成缓存刷新。
附:AMD IOMMU 页表模式示例
- Mode=0:禁用分页(直通模式)
- Mode=4:4 级页表(支持 48 位地址)
- Mode=5:5 级页表(支持 57 位地址)
- Mode=6:6 级页表(保留)