
PCIe中ACS知识小记如果你搞过虚拟化、设备直通或者排查过IOMMU分组问题肯定绕不开PCIe里的ACS。ACS这三个字母在别的领域可能还代表什么自助借还系统但在PCIe语境下它专指Access Control Services访问控制服务。名字听着挺官方但你可以把它理解为PCIe交换网络里的门禁系统——每个端口配一个保安决定哪些TLP能直接抄近道哪些必须绕到前台过安检。这篇小记围绕ACS展开覆盖它为什么存在、寄存器层面怎么工作、枚举阶段如何发现它、在虚拟化和安全场景下怎么用以及我在实际环境里踩过哪些坑。PS顺带回应一下那些搜索热度很高的词比如PCIe枚举过程、PCIe Switch、根复合体、vfio直通这些其实都是理解ACS的必要上下文。1. ACS到底是什么为什么非要它不可1.1 没有ACS时PCIe世界里的“私相授受”先看一个最常见的场景。假设一台服务器上有一张GPU和一个NVMe SSD它们挂在同一个PCIe Switch下面。PCIe设计上是允许端点之间直接通信的这叫Peer-to-Peer传输。GPU要读NVMe里的数据理论上可以直接发一个Memory Read请求PCIe Switch发现目标地址不在上游就悄悄在内部把请求转发给NVMe整个过程根本不经过根复合体Root Complex。这个设计初衷是好的——减少绕路、降低延迟、节省CPU和内存带宽。但问题来了。如果整个转发过程都在Switch内部完成那么IOMMUIntel叫VT-dAMD叫IOMMU是看不到这次传输的。IOMMU是系统里负责DMA地址翻译和安全检查的关卡它管不到的事务等于在安全模型里开了一个后门。恶意设备或者被攻破的固件完全可以利用这个P2P通道绕过IOMMU去读写另一块设备的内存甚至通过反复读写把系统内存摸个遍。典型的DMA攻击就是这么干的。ACS的存在就是为了把这个“私相授受”的通道管起来。它是一组定义在PCIe配置空间里的能力和控制位由Switch端口或Root Port来实现。开启之后端口会对经过它的TLP做检查、拦截、重定向核心目的就一句话所有本该内部转发的P2P流量要么被合法放行要么被强制送到上游让IOMMU过一遍。1.2 ACS在PCIe拓扑中的位置ACS不是随随便便哪个设备都能实现的。它需要设备具备“转发”能力所以在拓扑中一般出现在两类位置Root Port根复合体下面的PCIe端口比如CPU直出的PCIe插槽。它控制着下游端点发上来的TLP能不能继续向上游或者向其他Root Port转发。PCIe Switch的Downstream Port下游端口设备挂到Switch上时连接的那个端口。ACS在这里主要管的是“从一个下游端口进来的TLP能不能直接转到另一个下游端口”。至于Endpoint设备本身一般不需要实现ACS因为端点只是收发TLP不做转发。不过规范并没有禁止端点上实现ACS只是实际产品里极少见。PCIe Switch的Upstream Port上游端口通常也不实现ACS因为出口只有一条路谈不上“访问控制”。记住这个拓扑关系之后再去看ACS的控制位就会顺很多——它本质上就是给“转发的决策点”加了一个可编程的安全策略。2. ACS的核心机制控制位逐个拆解2.1 ACS能力结构长什么样ACS通过PCIe的扩展能力Extended Capability机制暴露给软件。每个PCIe扩展能力都有一个固定的Capability IDACS的ID是0x000D。在PCIe配置空间的0x100开始会有一条扩展能力链表软件从0x100读出第一个Capability Header根据Header里的Next Capability Offset继续往后遍历直到找到ID为0x000D的那一项。ACS能力结构里最关键的两个寄存器是ACS Capability Register偏移0x0416位表示这个端口硬件支持哪些ACS功能。对应位是1才说明硬件能做这件事。ACS Control Register偏移0x0616位表示当前软件使能了哪些ACS功能。这一位能不能写1取决于上面Capability Register里对应位是否为1。后面还可能有Egress Control Vector之类的扩展寄存器那些是配合出口控制用的后面再细说。2.2 源验证与翻译阻断先看两个偏向“校验”的控制位。Source Validation源验证Bit 0。这个功能用来校验TLP的发起者是不是“合法居民”。在PCIe体系里每个设备都有唯一的Requester ID由总线号、设备号、功能号组成。当一个端口收到请求TLP时它会检查这个请求的Requester ID是否落在自己允许的设备范围内。如果发现一个设备声称自己来自其他总线号那就有伪造嫌疑端口可以拒绝该TLP继续转发。这是防止设备伪装身份发起DMA攻击的第一道闸。Translation Blocking翻译阻断Bit 1。这个功能是配合IOMMU使用的。在开启了DMA重映射的系统里设备访问的地址空间应该是经过IOMMU翻译后的地址而不是原始的物理地址。如果一个设备发出的请求没有经过地址翻译或者翻译关系不对端口可以直接把请求挡住。用大白话说就是你出门可以但必须带上贴了标签的行李没标签的行李一律不许出门。2.3 请求重定向与完成重定向接下来是ACS里最核心、也最影响性能的两个控制位。P2P Request RedirectP2P请求重定向Bit 2。这个位一旦置1端口就不会再允许P2P请求在内部直接转发。本来Switch收到请求后可以判断目标在下游另一个端口然后内部直接传过去但现在它会把这个请求强行送到上游交给根复合体处理。根复合体里的IOMMU就有机会检查地址、做翻译、决定下一步怎么走。P2P Completion RedirectP2P完成重定向Bit 3。请求被重定向之后对应的完成报文Completion也得跟着重定向否则请求方可能等不到回应。这一位就是确保完成报文走同样的“绕行路线”不会出现请求走上面、回复走下面这种路由混乱的局面。这两个位是ACS的精髓。只要这两个被置位P2P绕过IOMMU的通道基本就断了。代价也很直观——流量不再走Switch内部捷径而是绕经RC延迟变高、占用RC内部带宽。这就是后面要讲的性能和安全权衡的起点。2.4 上游转发与出口控制剩下的控制位里有两个实战中经常一起出现。Upstream Forwarding上游转发Bit 4。这个位置1后端口会对所有原本可能直接向下游转发的TLP做一次“强制转弯”一律送到上游端口。它和P2P Request Redirect的侧重点不完全一样重定向改变的是“目标方向”而Upstream Forwarding更像是“默认全走上面”。在一些需要彻底切断P2P路径的强隔离场景里这个位会配合重定向位一起开。Egress Control出口控制Bit 5。这个功能更细它通过Egress Control Vector按位控制出口方向。比如Switch有8个下游端口软件可以在向量里标记“哪些端口之间允许互相转发”。开启Egress Control后TLP只有在目标方向对应的位为1时才能被转发出去。这个适合做非常精细的访问策略不过在普通服务器直通场景里用得不算多更多出现在安全等级要求很高的平台设计里。把这些控制位串起来ACS的实际工作流程可以这么理解一个TLP到达某个端口端口先做源验证没问题再看是否需要翻译阻断然后判断目标方向如果开了重定向就送上游如果开了出口控制还要查一下出口向量。每一步都是一道门禁只有全部放行才真正转发。3. 枚举过程与检查、开启ACS的实战3.1 枚举时如何发现ACS能力PCIe的枚举过程就是系统软件BIOS/UEFI或者操作系统内核从根复合体开始一级一级给总线号、设备号分配资源、扫描设备、读取配置空间的过程。在扫描过程中软件会从每个PCIe设备的配置空间0x100偏移开始沿着扩展能力链表往下走把设备支持的所有扩展能力都读一遍。ACS能力就在这个链表里。对Linux内核来说这个过程发生在pci_scan_device和pci_init_capabilities系列函数里。内核读到ACS能力后会把相关信息缓存到pci_dev结构体中后续IOMMU分组、设备直通权限判断都会用到这些信息。所以你可以认为枚举阶段对ACS的识别直接决定了后面能不能安全地做设备隔离。有一个常见的误解系统默认会开启所有ACS能力。实际不是绝大多数平台在枚举时只是识别ACS能力并不会主动把ACS Control Register全部置1。因为ACS一旦全开会影响P2P性能而系统不知道你的使用场景到底是性能优先还是安全优先所以选择保守。这就是为什么很多机器上lspci看ACSCap全是加号但ACSCtl全是减号。3.2 用lspci快速确认ACS状态Linux下最直接的检查方式是lspci。加三个v再指定设备地址能看到详细的扩展能力信息。比如看00:1c.0这个Root Portlspci -vvv -s 00:1c.0输出里会出现类似这样的内容Capabilities: [100] Access Control Services ACSCap: SrcValid TransBlk ReqRedir CmpRedir UpstreamFwd EgressCtrl- DirectTransP2P- ACSCtl: SrcValid TransBlk ReqRedir CmpRedir UpstreamFwd EgressCtrl- DirectTransP2P-ACSCap是硬件支持的能力ACSCtl是当前使能的状态。如果ACSCtl一栏能看到SrcValid、ReqRedir、CmpRedir这些加号说明ACS已经生效。如果ACSCap里本身就没有某个能力那ACSCtl里对应位怎么都不会变成加号软件再折腾也没用。3.3 手动开启ACS的两种方式实际环境中最常见的问题是设备支持ACS但没开启或者平台没默认开。这时有两种办法强制开启。第一种Linux内核参数pcie_acs_override。这是最省事的方案。在GRUB内核命令行里加参数pcie_acs_overridedownstream,multifunction重启后内核会在枚举阶段对Root Port和Switch Downstream Port强制设置ACS控制位。downstream覆盖Root Port和Switch下游端口multifunction覆盖多功能设备。这个参数最初就是为了解决设备直通时IOMMU分组过大的问题在不少平台下实测有效。需要特别提醒pcie_acs_override只能“使能硬件已经支持的ACS位”。如果ACSCap里对应位本来就是0软件写控制寄存器也不会生效。它不是让硬件凭空多出一个功能只是把已有的门禁全部打开。第二种setpci直接写配置空间。这种方式适合做临时实验或者内核参数照顾不到的场景。先找到ACS能力在配置空间里的偏移。可以用lspci -xxx把原始配置空间dump出来在0x100之后找Capability ID为0x000D的位置。假设找到的能力基地址是0x100那么ACS Control Register就在0x106处写一个16位值开启控制位setpci -s 00:1c.0 0x106.w0x001f0x001f对应Source Validation、Translation Blocking、P2P Request Redirect、P2P Completion Redirect、Upstream Forwarding这五个控制位置1。这个操作是即时的但重启后失效而且不同设备能力位不一样直接写死0x001f不一定正确最好先读ACSCap确认支持到哪几位。改错了配置空间导致设备异常的情况我不是没见过下手前建议做好记录方便恢复。4. ACS在虚拟化直通和安全加固中的价值4.1 IOMMU Group与设备直通做PCIe设备直通PCI Passthrough的人对IOMMU Group这个词应该不陌生。在Linux的VFIO框架下IOMMU把设备划分到不同的group里同一个group内的设备必须一起直通给同一个虚拟机不能拆开。为什么因为如果两个设备之间没有ACS隔离其中一个设备可以P2P访问另一个设备那给它们分配不同的信任域就是自欺欺人——guest A里的设备可以通过P2P摸到guest B的设备内存。所以系统在计算IOMMU group的时候会沿着设备到根复合体的路径检查每一跳是否具备ACS隔离能力。如果某个Switch下游端口不支持ACS或者没开启那么它下面的所有设备会被划到同一个group。这时候你想把其中一张网卡直通给VM1、另一张直通给VM2系统直接拒绝因为风险不可控。我在实际环境里遇到过一台机器两张相同的网卡挂在同一个PCIe Switch下lspci 看ACSCap都支持ACS但ACSCtl全是减号导致两个设备永远在同一个IOMMU group。加上pcie_acs_overridedownstream重启之后ACSCtl全部置位group被正确拆开直通马上就能配了。这个坑非常典型。4.2 SR-IOV与Switch级隔离ACS和SR-IOV也经常一起出现。SR-IOV允许一个物理功能PF拆出多个虚拟功能VF每个VF可以直接直通给不同虚拟机。但是VF之间如果支持P2P通信那就又回到隔离问题上了。虽然SR-IOV规范里还有一个独立的地址翻译和隔离机制但ACS在Switch层面的隔离仍然是重要补充——它能保证不同端口下的VF之间没有不受控的P2P路径。另外在多GPU服务器或者AI训练节点里GPU与GPU之间的通信常常通过PCIe P2P完成。如果ACS把P2P请求全部重定向到上游那这种通信就会被迫绕经系统内存性能下降很明显。所以这类机器上管理员往往需要非常小心地决定哪些端口要开ACS哪些端口要保留P2P通路。有些平台甚至支持通过ACSCap里的Direct Translated P2P来做一个折中——允许经过地址翻译的P2P请求直接转发兼顾安全与性能。这个功能在支持IOMMU的平台上非常有用但并不是所有设备都实现到位。4.3 代价性能与安全的权衡ACS不是一个白拿的特性它在安全上补了洞但也确实动了性能的蛋糕。最典型的例子就是GPU和NVMe之间的P2P传输。原来数据从GPU直接到SSD或者从SSD直接到GPU走Switch内部通道延迟极低带宽跑满链路。开启了P2P Request Redirect之后数据必须先到RC经过IOMMU翻译检查再发回Switch等于绕了一大圈。虽然现在RC内部带宽也很大但对那种高并发、低延迟的加速场景开销依然肉眼可见。所以我不建议在任何场景下无脑全开ACS。正确的做法是先明确你的核心诉求。如果做云平台、多租户直通、安全加固ACS能开就开性能损失是买安全性和可管理性的成本如果你是在单机跑AI训练、追求极致P2P性能那就要评估ACS带来的延迟增加和带宽下降能不能接受。PCIe生态里很多问题都不是“越强越好”而是“适不适合”。5. 常见问题与排查经验5.1 这些问题我基本都踩过先说几个我实际遇到、也比较有代表性的问题。第一个是“lspci里连ACSCap都看不到”。这种情况多见于老平台或者低端消费级PCIe Switch。有的芯片为了省面积、省成本直接把ACS能力砍掉了。遇到这种硬件软件层面基本无解只能考虑换平台或者接受IOMMU group合并的现实。第二个是“ACSCap显示支持但ACSCtl怎么都写不进去”。用pcie_acs_override之后部分设备依然显示减号。这通常是因为设备固件或者驱动在初始化之后又把控制位改回去了也可能是平台BIOS对某些位做了锁定。排查时可以先用setpci手动写一次马上lspci确认如果立刻被还原说明硬件层面有锁定机制软件改不了。第三个是“开了ACS之后P2P性能掉了一半还多”。这不是故障是ACS重定向的正常副作用。如果业务不能接受那就只能把ACS关掉但前提是你接受对应的安全风险。生产环境里不能既要又要必须提前和业务方对齐。5.2 常见问题速查表现象可能原因排查建议lspci看不到ACS能力设备或端口硬件不支持ACS更换硬件或接受IOMMU group合并ACSCap有加号但ACSCtl全减号系统默认未开启ACS加pcie_acs_override或setpci手动开IOMMU group里设备过多路径上缺少ACS隔离开启ACS后重新扫描设备开启ACS后P2P性能骤降P2P请求被重定向绕经RC评估业务是否能接受必要时关闭pcie_acs_override不生效硬件能力位为0或BIOS锁定先看ACSCap再检查内核cmdline同一Switch下设备无法分别直通未开启ACS导致group未拆开用downstream参数后重启重新分组顺带提一句很多人在看IOMMU group划分时会忽略Root Port本身是否具备ACS能力。有些CPU的Root Port在ACSCap里是空的这样即使下面的Switch都开了ACS从CPU角度看多个Root Port之间的隔离依然有问题。排查时要沿着设备到RC的整条路径看不要只看最后一级Switch。5.3 关于ACS override的注意事项pcie_acs_override是排查利器但也有应用边界。它本身属于一种“软件兜底”方案不是规范里的标准配置流程。在部分虚拟化平台上开启ACS override之后确实能拆散IOMMU group但要知道这相当于你向系统承诺“这些设备是隔离的”如果硬件实际并没有完全实现相关能力这个承诺可能过重。我习惯在上线前做一次验证开启override后用lspci逐个确认ACSCtl里的关键位是否真的变为加号尤其是ReqRedir和CmpRedir。然后跑一轮实际直通测试确认虚拟机和物理机之间的DMA访问正常没有出现不可访问或者数据损坏的问题。不要只看group拆开了就认为万事大吉PCIe的坑往往藏在链路层下面的细节里。另外提醒一句内核里对ACS的修复和override逻辑不同版本之间有变动某些老内核可能不支持multifunction参数或者行为有细微差别。遇到不生效可以升级内核也可以在启动后手工setpci配合验证两种手段结合更容易定位问题。收个尾分享一点实际体会ACS这个话题说大不大说小不小。它不像PCIe链路训练或者弹性缓存那样天天被人挂在嘴边但一到虚拟化直通、DMA安全、多设备隔离这些场景它就成了绕不开的隐形关卡。我自己的体会是理解ACS的关键不在于背下那几位控制位的定义而在于时刻记住PCIe是一个允许P2P的网络——凡是网络就需要访问控制。想通了这一点再回头看那些Capability位每一位的设计意图都变得很自然。如果你现在正被IOMMU group拆不开、设备无法直通的问题卡着我建议先别急着在系统里乱改参数老老实实把拓扑捋一遍设备在哪、经过几个Switch、每级端口ACSCap和ACSCtl到底是什么状态然后再决定是开override还是换硬件。PCIe这种东西数据不会骗人lspci一打就全明白了。