ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SMMU与DMA地址转换:ARM系统内存管理单元架构解析

SMMU与DMA地址转换:ARM系统内存管理单元架构解析 1. 从DMA乱序到系统内存管理单元做ARM体系结构底层的朋友对SMMU这个词应该不陌生。但很多刚接触这块的人第一次看到SMMUSystem Memory Management Unit系统内存管理单元时会下意识地把它当成一个“稍微复杂一点的MMU”。这个理解方向没有错但远远不够。SMMU解决的问题本质上和CPU侧的MMU是同一个地址转换和访问控制。但场景完全不同——MMU管的是CPU发起的访问而SMMU管的是设备发起的DMA访问。在没有SMMU的年代设备要访问内存直接在物理地址上操作想写哪里就写哪里。操作系统为了安全只能依赖设备自身的寄存器做限制可靠性全凭设备厂商的自觉。这在嵌入式裸机时代问题不大但一旦上了Linux、跑起虚拟化情况就完全失控了。我举个实际场景一台服务器上跑着多个虚拟机宿主机把物理内存分配给VM1和VM2。如果设备DMA可以随意访问物理内存VM1里的设备驱动一旦出错甚至被攻击就可能把数据写到VM2的内存区域整个隔离体系瞬间崩塌。这就是SMMU必须存在的核心原因——它要解决的不仅是地址映射更是系统级的隔离、安全和虚拟化支持。ARM从ARMv8开始把SMMU架构标准化到了ARMv9时代SMMU已经演进到v3.x版本功能覆盖了SVAShared Virtual Addressing共享虚拟地址、PASIDProcess Address Space ID、嵌套转换、MPAMMemory System Resource Partitioning and Monitoring等一整套现代系统级方案。这篇文章我基于ARM IHI 0070SMMUv3架构规范以及ARMv8/v9体系结构的相关特性把SMMU的整体架构和核心功能拆开讲一遍。适合Linux内核开发者、BSP工程师、虚拟化平台开发者以及所有想搞清楚DMA路径上数据到底怎么走的人阅读。2. 为什么设备DMA也需要“虚拟内存”——SMMU的核心价值2.1 没有SMMU的三个痛点先把问题摆清楚如果系统里不装SMMU或者SMMU处于bypass模式设备DMA直接访问物理地址你会立刻撞上三堵墙第一堵墙物理内存连续性问题。设备DMA通常需要连续的物理内存块尤其是那些不支持的SGScatter/Gather的简单设备。系统跑久了内存碎片化严重想分配一块大的连续物理内存非常困难哪怕你有CMAContiguous Memory Allocator上限和灵活性都受限制。有了SMMU之后设备看到的是连续的I/O虚拟地址IOVA底层物理页可以散落在任何位置由SMMU页表把它们串成连续的视图。这对驱动开发者来说简直是解脱——不用再为DMA buffer的物理连续性耗尽心思。第二堵墙安全隔离问题。前面提到的虚拟机场景只是其中一个例子。就算没有虚拟化在单操作系统环境下一个存在漏洞的设备驱动如果能让设备发起任意地址的DMA写操作攻击者可以直接改写内核代码段或页表拿下整个系统。这就是著名的DMA攻击路径。SMMU通过页表权限位读写权限、执行权限、特权域标记为每一次DMA访问建立一道关卡。第三堵墙设备地址空间大小限制。32位时代的设备DMA地址范围往往只有32位甚至更少但服务器的物理内存可能已经远远超过这个范围。如果设备无法访问高地址内存就得靠bounce buffer反弹缓冲区在低地址区域做中转性能开销很大。SMMU可以把高地址物理内存映射到设备可见的IOVA地址空间内绕过设备自身的地址宽度限制。这一点在NVMe SSD、GPU、高速网卡这类大吞吐设备上收益尤其明显。2.2 SMMU在系统软件栈中的位置从系统架构视图来看SMMU位于设备端口和互连如CCI互联或NoC总线矩阵之间拦截设备发起的所有访问。SMMU本身也连接在互连上以便访问内存中的页表描述符。CPU侧通过MMU访问内存是一套流程设备侧通过SMMU访问内存是另一套平行流程两者互不干扰但又通过共享页表实现某种协同——这就是后面要讲的SVA的基础。在软件栈中SMMU驱动Linux内核里的drivers/iommu/arm-smmu-v3.c向上对接IOMMU框架为设备驱动提供iommu_dma_ops为VFIO提供设备直通能力为KVM虚拟化提供嵌套转换支持。可以说现代ARM服务器平台上的虚拟化、容器、DPDK这类高性能数据面应用底层全都依赖SMMU这套机制。我个人的理解SMMU是ARM体系从“嵌入式为主”走向“数据中心级服务器”的一块关键拼图。没有SMMU的成熟ARM服务器上的虚拟化性能和隔离能力根本没法看。3. SMMU核心组件拆解——STE、CD、页表与TBU/TCU架构3.1 STE和CD设备与进程的“身份档案”SMMUv3里有两个极其重要的数据结构一个是STEStream Table Entry流表项一个是CDContext Descriptor上下文描述符。很多初学者在这两个概念上容易搞混我拆开讲。STE是与设备端挂钩的。系统中的每个设备更准确地说是每个stream即设备发出的请求流通过StreamID在Stream Table中索引到自己的STE。STE里面记录了这个设备是走bypass还是translate如果走translate启用stage1还是stage2页表基地址在哪设备的特权域设置以及是否启用PASID等。你可以把STE理解成设备DMA的“第一层档案”。CD则更细化到进程级别。当设备支持PASID并且SMMU配置为stage1转换时每个CD对应一个进程地址空间CD中存放着该进程的页表基地址TTB0/TTB1、ASIDAddress Space ID地址空间标识、T0SZ等参数。设备发起DMA时带上PASIDSMMU根据StreamID找到STE再根据PASID在CD Table中找到对应的CD从而拿到该进程的页表完成地址翻译。这就是SVA的核心路径。有个细节值得注意的是STE和CD是常驻内存的数据结构由软件通常是内核驱动创建和维护SMMU硬件只是读取它们。硬件会通过缓存来加速访问所以引入了STE缓存和CD缓存的失效问题。软件修改了STE或CD之后必须执行CMDQ命令如CFGI_STE、CFGI_CD来让硬件缓存失效否则硬件可能还在用旧的配置。3.2 Stage1与Stage2像极了CPU侧的MMUSMMU的地址翻译分为两个阶段术语叫Stage1和Stage2。如果你熟悉ARM CPU的虚拟化扩展会发现这里的思路几乎是一一对应的Stage1负责VA虚拟地址更准确地说是IPAIOVA到IPAIntermediate Physical Address中间物理地址的转换Stage2负责IPA到PAPhysical Address物理地址的转换。在虚拟化场景下Stage1通常由Guest OS的驱动和SMMU驱动共同管理转换Guest虚拟地址到Guest物理地址Stage2由Hypervisor管理转换Guest物理地址到真实物理地址。两级转换合成后得到最终的物理地址然后发起对内存的访问。Stage1和Stage2的页表格式不完全一样。Stage1支持4KB、64KB等粒度的页支持连续的PTE页表项块也支持52位地址扩展。Stage2为虚拟化场景优化支持2MB到4GB范围内更大的映射粒度减少页表的层数和内存占用。两者都支持访问权限位AP位、脏位DBMDirty Bit Modifier、共享属性位SH以及缓存属性位MemAttr。这些属性最终会和设备请求自带的属性做合并仲裁决定这次访问在内存总线上的行为。3.3 TBU与TCU分布式还是集中式这是个架构问题如果你拆过真实的SoC会发现SMMU并不是一个黑盒子。ARM把SMMU功能拆成了两个逻辑组件TCUTranslation Control Unit转换控制单元和TBUTranslation Buffer Unit转换缓冲单元。TCU负责管理页表、缓存、命令队列等全局逻辑TBU则靠近设备端口负责处理具体请求的地址转换和缓存。这种拆分带来了极大的拓扑灵活性。小系统里一个TCU带一个TBU看起来就是个完整的SMMU。大系统里多个TBU分布在不同的设备端口旁边通过内部互连共享一个TCU或者多个TCU组成更大的逻辑SMMU。后面这种拓扑通常被称为Distributed SMMU在服务器SoC中非常常见因为不同设备的物理位置差异大集中式TBU会导致线延迟过高。这里给做内核开发的朋友提个醒SMMU驱动的调试中很多问题出在TBU/TCU拓扑上。比如某个设备明明配置好了STE但DMA就是失败检查一下是不是该设备端口连接的TBU没有正确使能或者该TBU被错误地配置成了bypass。这类问题在内核日志里往往只显示一个通用的“translation fault”不结合SoC手册去查设备到TBU的拓扑关系很容易兜圈子。4. SMMU功能全景梳理——从基本转换到高级特性4.1 地址转换与bypass模式SMMU最基本的功能就是地址转换但它的灵活性在于转换不是全局统一开启的而是按设备按stream粒度配置的。每个设备通过STE就可以独立配置成三种模式之一bypass直接直通、translate走页表转换、abort所有访问触发错误。bypass模式在系统启动早期特别有用。固件阶段SMMU默认可能处于bypass状态避免干扰固件或早期驱动对设备的初始化。等到内核IOMMU框架接管后再按策略逐个设备启用转换。这里的切换要小心如果设备正处于活跃DMA状态先bypass后立即切换到translate可能会导致DMA的中间状态踩到新页表上造成数据错乱。正确的流程是先停止设备DMA切换SMMU配置mapping好IOVA恢复设备DMA。4.2 PASID与SVA让设备直接“看到”进程地址空间SVAShared Virtual Addressing是SMUU近几代版本里我最喜欢的功能也是最被低估的一个。它让设备可以直接使用进程的虚拟地址做DMA无需驱动维护IOVA映射关系也无需ioctl接口切换DMA地址空间。核心实现依赖两个机制PASID标记请求流以及CD定位进程页表。PASID Process Address Space ID16位ID绑在设备请求的头上。当设备发起一次DMA如果STE配置为支持PASID且请求带有有效的PASID值SMMU就通过CD Table找到这个PASID对应的CD进而找到进程的页表。整个翻译过程对设备本身是透明的——设备看到的地址空间和CPU进程看到的完全一致只是它的“访存”要通过SMMU来完成翻译。SVA带来的性能收益是实实在在的。以NVMe SSD为例传统模式下每次I/O都要经过IOMMU层的dma_map/unmap操作有大量的TLB失效开销启用SVA后设备可以长期持有进程页表映射只有进程页表本身发生unmap时才需要同步SMMU页表开销大幅下降。不过SVA对设备和驱动都有要求——设备必须发得出PASID驱动必须用内核提供的SVA API如ioasid_set、iommu_sva_bind_device来管理生命周期。这是一套全新的编程模型不是所有驱动都适配。4.3 中断与错误处理机制SMMU功能再强出问题的时候也得有路可走。ARM SMMUv3设计了完整的中断和错误报告机制。事件Event记录在Event Queue里包括各种翻译错误translation fault、访问权限错误permission fault、页表遍历错误page request fault等等。软件通过Event Queue消费这些记录并通过中断如EVTQ中断得到通知。这里有个实操中的要点Event Queue里每条记录都包含设备StreamID和具体的错误原因但如果你没有把StreamID关联回SysFS里的设备节点排错就是大海捞针。所以我强烈建议在实际的调试阶段第一件事就是解开SMMU驱动的debugfs或者利用内核的iommu报错机制把StreamID和PCI BDFBus/Device/Function号对应起来最好再动态生成一张设备-StreamID映射表。有了这张表任何SMMU事件都能秒级定位是哪个设备闯的祸。另外SMMU还会产生Priority Queue优先级队列用于记录更轻量级的提示信息以及命令队列Command Queue本身出错时的Doorbell错误。这些机制共同构成了SMMU的“神经系统”读懂它们的输出信息是排查DMA问题的必备技能。4.4 PMU、QoS与MPAM不止于地址转换很多人对SMMU的理解止步于地址转换其实SMMU还承担着监控和资源管理功能。SMMU PMUPerformance Monitor Unit可以统计转换命中率、TLB未命中次数、遍历次数、事务数量等性能指标。对于性能调优来说这些数据非常宝贵——比如你怀疑某个设备的DMA路径存在多余的地址转换开销可以通过PMU确认到底命中率是多少而不是凭感觉猜测。SMMU还支持对DMA请求进行QoS标记和MPAM分区支持。前者允许系统根据设备优先级对DMA请求打标后者则配合ARM的MPAM可能单独设计机制为DMA流量做内存带宽和缓存可能包含LLC的分区控制。在多租户场景下这些功能让你能限制某个虚拟机或某个容器内的设备对缓存/带宽的占用避免“邻居噪声”干扰关键业务。5. 从配置到运行——SMMU一支命令队列的完整之旅5.1 配置数据结构的前前后后先讲清楚SMMU软件配置的整体流程。系统启动时固件UEFI/ATF负责做基础的SMMU初始化把SMMU置于一个可用的状态分配MMIO寄存器基地址、建立中断映射、配置Stream Table的基地址和大小。Linux内核里的arm-smmu-v3驱动随后接管做二次初始化检查硬件能力通过IDR寄存器读出版本、特性配置全局寄存器SMMU_CR0、SMMU_CR1等初始化命令队列和事件队列建立默认的Stream Table。这里最关键的一个操作是命令队列Command Queue的初始化。SMMU的命令队列是一个环形缓冲区软件往队列尾部写命令硬件从队列头部消费。软件写完后通过写Doorbell寄存器SMMU_CMDQ_DOORBELL通知硬件“有货了”。整个机制和网卡的DMA环形队列几乎一模一样如果你熟悉网卡驱动理解这个毫无压力。命令队列里的命令类型很多常用的有CFGI_STE使STE缓存失效、CFGI_CD使CD缓存失效、TLBI_NSNHTLB失效、CMD_SYNC同步屏障等。其中CMD_SYNC是排障时绕不开的一个——它用来确保之前所有的命令都被硬件处理完。很多时序问题都是因为软件在发出TLBI命令后没有等待CMD_SYNC完成就释放了内存页导致硬件可能还在用TLB中的旧映射访问已被回收的页。这个坑我踩过很多次每次都是“偶发性数据损坏”排查到怀疑人生。5.2 页表遍历路径一眼看懂配置完成后一次设备DMA请求被SMMU处理的路径大致如下设备发起DMA请求携带StreamID某些场景还带PASID和IOVA。SMMU根据StreamID在Stream Table中找到STE检查STE中的配置属性。如果配置为bypass直接放行如果为abort直接触发事件并阻塞如果为translate则进入页表遍历。根据STE指定的一级/二级转换配置逐级查找页表以4KB粒度为例Level 0到Level 3四级查找期间会用TLB缓存加速。最终得到物理地址和访问属性权限、缓存策略、共享属性发起对内存的实际访问。如果在遍历中发生缺页、权限不符、格式错误等问题SMMU不直接终止系统——它把错误记录进Event Queue并生成中断通知软件。熟悉CPU MMU的朋友看到这个流程会有强烈的既视感。确实SMMU的设计哲学就是“把CPU侧的MMU经验搬到DMA侧”只是它要面对的请求流更复杂多个设备、多个进程、多级虚拟化、多种地址格式所以数据结构和配置逻辑比CPU MMU复杂了一个量级。5.3 Linux内核里的SMMU驱动配置实践在实际项目中配置SMMU并不是每天都要做的事但一旦你的平台上有PCIe设备、有虚拟化、有高性能存储就必须理解它的配置入口。在Linux中主要配置节点有Device Tree或ACPI IORT表描述SMMU硬件节点的寄存器地址、中断、与设备端的拓扑关系。内核命令行参数iommu.passthrough1让所有设备默认bypass一般用于性能优先场景、arm-smmu-v3.disable_bypass0允许bypass等。sysfs/debugfs接口/sys/kernel/iommu_group/查看设备的IOMMU分组/sys/kernel/debug/arm-smmu-v3/打开内核配置项后可查看STE内容、命令队列状态等。配置实战里最容易踩的坑有三个。一是在设备驱动里忘记调用iommu_dma_set_mask_and_coherent()导致驱动使用过大的DMA maskIOMMU层无法给出匹配的IOVA范围。二是内核打开CONFIG_IOMMU_DEFAULT_PASSTHROUGH时所有设备默认不经SMMU转换你在虚拟化场景直通设备时会发现DMA直通物理内存安全隔离形同虚设。三是当SMMU和PCIe ATSAddress Translation Service配合时如果ATS启用了PCIe设备会主动发起地址翻译缓存请求ATC缓存驱动侧需要考虑缓存失效和PASID一致性否则会出现设备读写错地址的诡异问题。6. 调试SMMU我踩过的坑和排查套路6.1 “翻译错误”到底是谁的锅SMMU调试里最经典的场景是设备初始化正常一旦开始DMA数据搬运系统里就上报“SMMU translation fault”DMA直接失败。对于新手来说第一反应往往是“页表配错了吧”但根据我的经验实际原因分布要复杂得多StreamID配置错误。设备侧的StreamID是由SoC硬件连线决定的不是软件随便写的。驱动在创建STE时必须从设备树或ACPI表里解析出正确的StreamID。如果你发现设备上报的fault事件里的StreamID和预期不符多半是这里错了。STE的配置被硬件缓存了旧版本。修改STE后没发CFGI_STE命令或者发了命令但没等待同步完成。这种问题表现为“偶发性”。翻译后的物理地址超出了设备DMA的能力范围。有些设备的DMA地址线物理上没有接完高地址位被Truncate了。SMMU翻译成功但设备看到的地址是错的。设备侧和SMMU侧的访问属性不一致。设备发起的请求里带有的安全/非安全状态、缓存属性如果和STE配置的不匹配也会触发权限类错误。排错时我自己的习惯是这样的先看Event Queue里记录的StreamID和错误码把设备定位出来然后确认STE配置是否正确——用debugfs把STE dump出来逐字段检查再确认页表本身有没有问题——AArch64 Linux下可以通过内核的IOMMU调试节点查看设备的IOVA映射是否覆盖了DMA的内存区域。这套组合拳下来95%的问题都能定位出来。6.2 SMMU TLB相关的性能排查和同步问题除了功能性问题SMMU还经常因为TLB失效不及时导致性能“莫名其妙的差”。最典型的一个场景设备频繁做短时间的DMA映射和解除映射每次解除后软件都发TLBI命令同步但因为命令太多命令队列满了驱动被阻塞。性能数据看起来就是“DMA吞吐忽高忽低偶尔出现毫秒级卡顿”。针对这个问题思路不是去压命令队列的长度而是减少TLBI的频率。有两种常见做法一是合并TLBI把多个不连续的地址段合并成一条范围失效命令二是延迟TLBI先把它加到一个待失效列表里等累计到一定数量或超过时间阈值后再一次性失效——只要保证内存页在真正释放之前TLBI已执行就没有安全问题。还有一种情况要特别注意TLB失效命令的同步不能只靠“写命令队列 写Doorbell”必须配合CMD_SYNC完成后的通知机制。SMMUv3里CMD_SYNC可以设置为等待所有之前的命令都消费完毕并在指定的内存地址写入完成标志SEV事件也可以用来通知。驱动程序在等待时可以选择阻塞轮询或者睡眠等待中断。轮询在负载高时可能消耗大量CPU睡眠等待中断则延迟偏高。实践中很多驱动会在批量命令后用一次轮询等待单条命令用中断等待或者在每毫秒量级做一次批量同步。6.3 虚拟化嵌套场景难题主要出在两套页表的同步最后讲一个进阶话题虚拟化场景下的嵌套转换。前面已经提过Stage1由Guest管Stage2由Host管。当Guest里的设备驱动修改页表时SMMU里的缓存不会自动感知——必须有软件介入来同步。在KVM VFIO直通场景下这一般由主机的IOMMU驱动负责Guest的Stage1页表变更时主机侧捕获到相关操作通过CMDQ发出对应TLBI命令。嵌套场景特有的一个坑是PASID在虚拟化下的传递。Guest里的进程用PASID发起SVA访问Hypervisor需要把Guest的PASID空间映射到Host的PASID空间同时在嵌套的STE/CD配置里建立起对应的转换关系。如果中间任何一层配置错误表现出来的现象往往是部分进程的DMA正常部分进程的DMA直接失败或写错内存。这种问题的定位思路是先在Host层关掉嵌套转换用非嵌套模式跑一遍确认Host侧SMMU基本路径没问题再逐个设备打开嵌套配合QEMU/KVM的调试输出逐步确认每一级页表确实被访问到了。这个方法虽然笨但是极其有效。7. SMMU选型、版本演进和实际工程建议7.1 SMMUv3相比v2到底改了什么ARM SMMU从v2到v3是一次全面重构。如果你还在维护旧平台接触的是SMMUv2——基于MMIO访问的配置接口、独立的上下文Context结构、页表基地址配置方式等——那么面对v3时需要注意几个重大变化。最核心的变化是v3把大部分控制面功能搬进了内存队列命令队列、事件队列MMIO寄存器数量和功能大幅精简。这意味着软件可以通过“写内存 写门铃”的方式批量下发命令性能和可扩展性都大幅提升。v3还引入了更细粒度的缓存管理命令、更灵活的STE/CD组织方式支持线性表和两级表、以及对SVA/PASID的更原生支持。到了v3.1/3.2等后续版本又陆续加入了对MPAM、ATS/PRI、更复杂的拓扑支持等特性。如果你在为新平台选型基本直接奔着SMMUv3去就对了如果维护老平台理解v2和v3的差异能让你少走很多弯路。7.2 设计SMMU驱动的工程建议最后给那些需要在真实项目中开发和维护SMMU相关软件栈的朋友几条工程建议第一尽早建立设备StreamID映射表和SMMU事件日志的自动化关联。这是我前面反复强调的一点但值得再说一次——SMMU和调试相关的痛苦90%来源于“不知道是哪个设备出了问题”。把这张映射表做成启动阶段就自动构建配合日志分析工具后面任何SMMU事件都能秒级定位。第二CMDQ的合理设计是性能的生命线。如果命令队列太短批量配置设备时会出现生产者阻塞太长又浪费内存。合理的做法是根据平台上需要管理的设备数量做动态估算并实现批量读写和门铃合并。有的实现里会把多个STE更新命令攒在一起再写Doorbell性能差距非常明显。第三对SVA/PASID的引入要谨慎评估。功能再好驱动适配不到位就是灾难。建议先在非生产环境用SVA跑通典型负载确认性能确有收益并在设备断连、进程退出这类路径上做一遍故障注入测试再决定是否默认启用。第四不要忽略调试和可观测性建设。SMMU的PMU、debugfs、tracepoint如iommu trace事件能提供极好的运行态视角。我在项目里习惯把SMMU的PMU事件周期性地采集下来和设备的DMA吞吐曲线做时间对齐这样任何一次DMA性能抖动都能立刻判断是不是SMMU侧出了瓶颈。8. SMMU的未来演进与生态关联ARMv9时代SMMU作为系统内存管理核心的作用只会更重而不是更轻。CXLCompute Express Link设备、PCIe 5.0/6.0带来的高带宽场景、跨chiplet的分布式一致性问题都对SMMU提出了更高的要求。另一个值得关注的趋势是SMMU与内存压缩、加密以及可信计算技术的结合——比如对DMA访问进行加密解密处理或者与TrustZone结合实现更细粒度的安全隔离。对于系统架构设计师角色的人来说SMMU的选型和配置会直接影响整个平台的性能基线和安全边界。我在看一个新SoC的设计文档时一定会着重关注三件事SMMU节点的数量和位置决定设备分组和故障域、每个TBU连接了哪些设备端口决定DMA路径的延迟、以及SMMU对PASID/嵌套转换的支持程度决定虚拟化和SVA能做到什么级别。如果只是做上层应用SMMU似乎离得很远。但一旦你开始写设备驱动、调虚拟化性能、排查内存损坏就会发现SMMU几乎无处不在。理解了它很多“灵异”的DMA问题都会变得有迹可循。我建议每个做ARM底层开发的朋友至少完整读一遍ARM IHI 0070规范里的数据结构和命令队列章节再结合Linux内核源码走一遍初始化流程——这个过程虽然烧脑但绝对值得。
返回列表