本文基于Linux kernel v6.18 源码,重点说明 Linux 如何配置 PCIe ACS、ACS 和 IOMMU Group,以及 P2P Direct 场景下 kernel 如何处理 ACS redirect。
1. 概述
1.1 问题背景
PCIe ACS 解决的是“设备间 P2P TLP 是否必须回到上游路径”的问题。IOMMU 解决的是“DMA 地址如何翻译和保护”的问题。两者配合时,ACS 决定设备之间是否存在绕过 IOMMU 的 P2P 路径,IOMMU 据此决定哪些设备能被拆成独立的 IOMMU Group。
PCI Core 在枚举 PCIe Topology 时发现并配置 ACS Control;IOMMU Core 不直接配置 ACS,而是读取 ACS path 的隔离结果来决定 IOMMU Group 边界;P2P Direct 路径也会读取 ACS redirect 状态,如果 redirect 打开,就不能按直接 bus address 路径处理。图中的 Policy input 表示内核默认策略和pci=参数会共同影响 ACS Control 的最终值。
在 Linux 中,ACS 不是由 IOMMU driver 直接写寄存器完成的,而是由 PCI Core 在 PCI 设备初始化时发现 ACS Extended Capability 并配置PCI_ACS_CTRL。IOMMU Core 后续通过pci_acs_path_enabled()查询路径隔离能力。
1.2 代码范围
主要代码路径如下:
| 模块 | 文件 | 作用 |
|---|---|---|
| PCI Core | drivers/pci/pci.c | ACS 能力发现、默认启用、内核参数配置、路径检查 |
| PCI Quirk | drivers/pci/quirks.c | 设备特定 ACS 能力判断和 workaround |
| IOMMU Core | drivers/iommu/iommu.c | PCI 设备 IOMMU Group 构建 |
| P2PDMA | drivers/pci/p2pdma.c | P2P 距离计算、ACS redirect 检查 |
| DMA-IOMMU | drivers/iommu/dma-iommu.c | P2P page 在 IOMMU DMA API 中的映射处理 |
1.3 关键路径
Linux ACS 的关键路径可以简化为:
- IOMMU 被检测到后调用
pci_request_acs(),要求 PCI Core 尽量启用 ACS。 - PCI 设备初始化时调用
pci_acs_init(),发现 ACS Capability 并配置 ACS Control。 - IOMMU 建组时调用
pci_device_group(),用pci_acs_path_enabled()判断 P2P 隔离边界。 - P2PDMA 选择 Direct 路径时调用
pci_p2pdma_distance_many(),检查路径上是否存在 ACS redirect。
2. ACS 初始化
Linux 中 ACS 的最终配置由三类输入共同决定:IOMMU 是否请求 ACS、设备或平台 quirk、管理员通过pci=指定的内核参数。
2.1 使能来源
ACS 使能的入口不是某个单独的 IOMMU driver 写 ACS 寄存器,而是 IOMMU 检测路径先调用pci_request_acs(),设置 PCI Core 的全局变量pci_acs_enable。之后每个 PCI 设备初始化时,pci_enable_acs()根据这个全局请求决定是否应用 kernel 默认 ACS 策略。需要注意的是,pci_acs_enable只是“请求启用 ACS”的软件开关;真正能不能启用,还取决于设备是否有标准 ACS Capability,或者是否存在设备特定 quirk。
2.2 默认策略
ACS 默认策略由pci_std_enable_acs()决定。Linux 在标准 ACS Capability 存在且系统请求启用 ACS 时,会在PCI_ACS_CTRL中尝试打开:
PCI_ACS_SV:Source ValidationPCI_ACS_RR:P2P Request RedirectPCI_ACS_CR:P2P Completion RedirectPCI_ACS_UF:Upstream ForwardingPCI_ACS_TB:Translation Blocking,仅在 ATS disabled、external facing、untrusted 设备上启用
这里的目标是优先保证 DMA 隔离:P2P Request/Completion 被 redirect 到上游路径,IOMMU 才有机会参与地址翻译和权限检查。
2.3 Quirk 处理
drivers/pci/quirks.c中有两类 ACS quirk:
pci_dev_specific_acs_enabled():设备没有标准 ACS Capability,或者标准能力不足时,用设备特定知识判断它是否具备等价隔离能力。pci_dev_specific_enable_acs():对部分平台写厂商寄存器,启用等价 ACS 行为。
典型例子是 Intel PCH Root Port。部分设备通过 LPC RCBA、UPDCR、MPC 等寄存器禁用 peer decode 或启用 requester ID 检查,从而提供SV/RR/CR/UF等价能力。
因此,quirk 有两个作用:一是让没有标准 ACS Capability 的硬件表达“等价隔离能力”;二是在初始化阶段通过厂商寄存器补上标准 ACS Control 无法覆盖的控制。
2.4 内核参数
Linux 主线支持两个直接影响 ACS 控制位的pci=参数:
| 参数 | 作用 | 风险 |
|---|---|---|
pci=disable_acs_redir=<pci_dev>[;...] | 强制关闭匹配设备上的RR/CR/EC,允许 P2P traffic 不被强制上游转发 | 会降低设备间隔离,可能让更多设备进入同一个 IOMMU Group |
pci=config_acs=<flags>@<pci_dev>[;...] | 按 bit 配置 ACS flags,0表示关闭,1表示开启,x表示保持不变 | 管理员需要理解拓扑和安全影响 |
config_acs的 bit 定义来自Documentation/admin-guide/kernel-parameters.txt:
| bit | ACS 控制位 |
|---|---|
| 0 | Source Validation |
| 1 | Translation Blocking |
| 2 | P2P Request Redirect |
| 3 | P2P Completion Redirect |
| 4 | Upstream Forwarding |
| 5 | P2P Egress Control |
| 6 | Direct Translated P2P |
这两个参数是在pci_setup()中解析的,但真正应用发生在pci_enable_acs()里。disable_acs_redir会先清除RR/CR/EC,然后config_acs再按管理员指定 bit 覆盖 ACS Control,所以最终结果以最后写入PCI_ACS_CTRL的值为准。
2.5 初始化流程
PCI Core 的 ACS 初始化入口是pci_acs_init()。pci_acs_init()会把 ACS Extended Capability 的 offset 保存在dev->acs_cap。即使设备没有标准 ACS Capability,Linux 仍会调用pci_enable_acs(),因为部分 Root Port 可能通过 quirk 实现等价 ACS 行为。更完整的初始化流程如下:
- IOMMU 检测路径调用
pci_request_acs(),设置全局pci_acs_enable,表示后续 PCI 设备初始化时应尽量启用 ACS 默认隔离策略。 - PCI 设备初始化进入
pci_acs_init(),先通过pci_find_ext_capability()查找标准 ACS Extended Capability,并把 offset 保存到dev->acs_cap。 pci_acs_init()继续调用pci_enable_acs(),这是 ACS Control 的主要配置入口。pci_enable_acs()先调用pci_dev_specific_enable_acs(),让设备特定 quirk 有机会通过厂商寄存器完成等价 ACS 配置。- 如果 quirk 没有接管,且设备存在标准 ACS Capability,PCI Core 读取
PCI_ACS_CAP和当前PCI_ACS_CTRL,并保存 firmware 原始配置。 - 默认策略由
pci_std_enable_acs()应用,通常会尝试设置SV/RR/CR/UF,并在条件满足时设置TB。 - 管理员参数随后生效:
pci=disable_acs_redir可清除RR/CR/EC,pci=config_acs可按 bit 覆盖指定 ACS 控制位。 - 最终配置通过
pci_write_config_word()写回PCI_ACS_CTRL,后续 IOMMU Group 和 P2P 判断会读取这个有效 ACS 状态。
3. ACS 与 IOMMU
3.1 隔离边界
IOMMU 只能保护经过 IOMMU 的 DMA。如果两个 PCIe 设备可以通过 Switch 直接 P2P,不经过 Root Complex/IOMMU,那么 IOMMU 页表无法阻止这种访问。ACS 的意义就是把这种 P2P 路径 redirect 到上游,或者报告 violation,从而让 IOMMU Group 的隔离假设成立。在 Linux 中,IOMMU Group 是隔离粒度。能互相绕过 IOMMU 的设备必须在同一个 group 中,不能单独分配给不同安全域。
3.2 IOMMU Group
PCI 设备的 group 构建逻辑在drivers/iommu/iommu.c::pci_device_group()。其中核心判断是:
REQ_ACS_FLAGS = PCI_ACS_SV | PCI_ACS_RR | PCI_ACS_CR | PCI_ACS_UFpci_device_group()从设备向上遍历 PCI 层级:
- 先处理 DMA alias。如果设备存在上游 DMA alias,需要使用 alias 所在的最小 IOMMU 粒度。
- 从该位置继续向上查找,直到遇到满足
REQ_ACS_FLAGS的 ACS 隔离路径。 - 如果路径上缺少 ACS 隔离能力,就继续向上合并到桥或上游设备所在的 group。
- 如果没有找到共享 group,才新建一个 IOMMU Group。
3.3 DMA 重映射
IOMMU Group 建好后,IOMMU Core 为 group 分配 default domain。普通 DMA API 最终会使用这个 domain 完成 IOVA 分配和 IOMMU 映射。
关键点是:ACS 不做地址翻译,IOMMU 也不做 PCIe 路由控制。ACS 负责保证不可信 P2P 不绕开上游路径,IOMMU 负责对经过自己的 DMA 请求做地址翻译和权限控制。
3.4 VFIO 直通
VFIO 依赖 IOMMU Group 作为用户态直通的安全边界。vfio获取设备 group 后,会通过 IOMMU API 管理 DMA ownership 和 domain attach。
如果 ACS 不足导致多个设备处于同一个 group,那么这些设备必须一起作为安全边界看待。用户态不能只安全地直通其中一个设备,因为同组设备可能通过 P2P 或 DMA alias 互相影响。
3.5 ACS 缺失
当某段 PCIe 路径缺少 Linux 需要的SV/RR/CR/UF时,pci_acs_path_enabled()返回 false。IOMMU Core 会把隔离边界继续向上扩大,表现为更多设备被放入同一个 IOMMU Group。
4. P2P Direct
4.1 P2PDMA 模型
Linux P2PDMA 把参与方分为:
| 角色 | 含义 |
|---|---|
| Provider | 提供 P2P memory,例如 PCI BAR 中的一段内存 |
| Client | 发起 DMA 到 P2P memory 的设备 |
| Orchestrator | 选择 provider/client 并组织数据路径的驱动 |
Provider 通过pci_p2pdma_add_resource()注册 P2P memory。Client 仍然使用普通dma_map_sg(),P2PDMA 和 DMA-IOMMU 层会根据 page 类型选择合适映射方式。
4.2 距离计算
P2P Direct 距离计算入口是pci_p2pdma_distance_many()。它对每个 client 调用calc_map_type_and_dist():
- 从 provider 和 client 各自向上找公共上游桥。
- 统计路径距离。
- 检查路径上的桥是否设置 ACS redirect。
- 根据结果返回
PCI_P2PDMA_MAP_BUS_ADDR、PCI_P2PDMA_MAP_THRU_HOST_BRIDGE或PCI_P2PDMA_MAP_NOT_SUPPORTED。
pci_p2pdma_map_type表示 P2PDMA page 最终应该按哪种 DMA 地址路径处理,定义在include/linux/pci-p2pdma.h:
enumpci_p2pdma_map_type{PCI_P2PDMA_MAP_UNKNOWN=0,PCI_P2PDMA_MAP_NONE,PCI_P2PDMA_MAP_NOT_SUPPORTED,PCI_P2PDMA_MAP_BUS_ADDR,PCI_P2PDMA_MAP_THRU_HOST_BRIDGE,};| 枚举值 | 含义 |
|---|---|
PCI_P2PDMA_MAP_UNKNOWN | 内部初始状态,表示还没有计算 map type,正常 API 不应返回给调用者 |
PCI_P2PDMA_MAP_NONE | 当前 page 不是 PCI P2PDMA page,后续按普通 DMA page 处理 |
PCI_P2PDMA_MAP_NOT_SUPPORTED | P2P 路径不可用,或者必须经过不在 allowlist 中的 host bridge,DMA 映射应返回错误 |
PCI_P2PDMA_MAP_BUS_ADDR | provider 和 client 可通过 PCI Switch 直接通信,不经过 host bridge;DMA engine 使用 PCI bus address,是真正的 P2P Direct |
PCI_P2PDMA_MAP_THRU_HOST_BRIDGE | provider 和 client 可以通信,但路径经过 allowlist 中的 host bridge;使用普通物理地址或 IOVA 映射,不是 Direct P2P |
在 DMA-IOMMU 路径中,PCI_P2PDMA_MAP_BUS_ADDR会把 scatterlist segment 标记成 bus address,iommu_map_sg()跳过该 segment;PCI_P2PDMA_MAP_THRU_HOST_BRIDGE和PCI_P2PDMA_MAP_NONE则继续走普通 IOVA 映射流程。
4.3 ACS 影响
calc_map_type_and_dist()中的关键判断是pci_bridge_has_acs_redir()。如果路径上任何桥打开了 ACS redirect,Linux 就认为 provider 和 client 之间的 traffic 会被强制走 host bridge,而不是 Direct P2P。
因此:
- 没有 ACS redirect:可以返回
PCI_P2PDMA_MAP_BUS_ADDR,DMA engine 使用 PCI bus address,形成 Direct P2P。 - 有 ACS redirect:返回
PCI_P2PDMA_MAP_THRU_HOST_BRIDGE或PCI_P2PDMA_MAP_NOT_SUPPORTED,取决于 host bridge 是否可用或在 allowlist 中。 - verbose 模式下,kernel 会打印提示,建议使用
pci=disable_acs_redir=<path>关闭该路径上的 redirect。
驱动不能直接调用一个通用的pci_disable_acs_redir()来关闭 ACS redirect。Linux 中没有导出的此类 PCI Driver API;disable_acs_redir是 PCI Core 解析的内核命令行参数,最终在pci_enable_acs()中通过__pci_config_acs()清除匹配设备的RR/CR/EC。设备特定的pci_dev_specific_disable_acs_redir()也属于 PCI Core 内部 quirk 路径,不是普通驱动可直接依赖的接口。
因此,驱动侧通常只通过pci_p2pdma_distance_many()、pci_p2pdma_map_type()等 P2PDMA 路径判断是否可 Direct P2P;如果 ACS redirect 阻断了 Direct P2P,只能回退到 host bridge 路径或失败,并由管理员通过pci=disable_acs_redir=<path>这类参数承担关闭隔离的决策。
P2PDMA 调用流程如下:
4.4 Direct 条件
P2P Direct 成立时,通常需要满足:
- provider 和 client 都能归约到 PCI 设备。
- 两者在同一可路由层级内,或 host bridge 支持对应路径。
- 路径上没有会强制 redirect 的 ACS 控制位。
- provider 的 P2P resource 已注册并可用于 DMA。
这也是为什么 ACS 和 P2P Direct 存在天然冲突:安全隔离希望打开RR/CR,Direct P2P 性能路径希望关闭 redirect。
4.5 回退路径
DMA-IOMMU 映射时会调用pci_p2pdma_state():
PCI_P2PDMA_MAP_BUS_ADDR:把 segment 标记为 bus address,iommu_map_sg()跳过该 segment,DMA 地址来自pci_p2pdma_bus_addr_map()。PCI_P2PDMA_MAP_THRU_HOST_BRIDGE:按普通 IOVA 流程映射。PCI_P2PDMA_MAP_NOT_SUPPORTED:返回错误。PCI_P2PDMA_MAP_NONE:不是 P2P page,走普通 DMA 映射。
所以,P2P Direct 不是绕过整个 DMA API,而是在 DMA API 内部根据 P2P map type 选择 bus address 或普通 IOVA。
5. 典型流程
5.1 设备枚举
PCI 枚举过程中,设备 capability 被解析,pci_acs_init()保存dev->acs_cap并尝试配置 ACS。这个阶段决定硬件 ACS Control 的初始状态。
5.2 ACS 启用
如果系统检测到 IOMMU,相关初始化路径会调用pci_request_acs()。之后 PCI Core 按默认策略启用 ACS 隔离位,并叠加设备 quirk 和管理员参数。
5.3 IOMMU 建组
IOMMU driver probe 设备时,通过ops->device_group()获取 group。对 PCI 设备,常见路径会走pci_device_group(),并用pci_acs_path_enabled()判断从设备到上游的隔离边界。
如果 ACS path 满足SV/RR/CR/UF,设备可以在更细粒度上分组;如果不满足,group 会向上合并。
5.4 VFIO 使用
VFIO 只信任 IOMMU Group,而不是单个 PCI Function。用户态直通时,group 内所有设备都属于同一个隔离单元。ACS 缺失或被关闭 redirect 可能导致 group 变大,这是安全边界变化,不只是显示上的变化。
5.5 P2P 判定
P2PDMA 不会为了 Direct 自动关闭 ACS redirect。它会检测路径,发现 redirect 后:
- 不能作为 Direct bus address 路径使用。
- 在 verbose 模式打印
pci=disable_acs_redir=...建议。 - 由管理员决定是否牺牲隔离来换取 Direct P2P。
6. 总结
6.1 核心关系
ACS 与 IOMMU 的关系可以概括为:
- ACS 保证 P2P traffic 不绕过上游路径。
- IOMMU 对经过自身的 DMA 做地址翻译和权限控制。
- IOMMU Group 使用 ACS 判断设备之间是否可被安全拆分。
6.2 配置原则
默认配置偏向隔离:Linux 在 IOMMU 存在时会请求启用 ACS,并打开SV/RR/CR/UF等关键位。管理员可以用config_acs或disable_acs_redir覆盖配置,但这会改变安全边界。
6.3 注意点
disable_acs_redir有助于 P2P Direct,但会降低隔离。config_acs可以精细控制 ACS bit,但错误配置可能导致 IOMMU Group 变化。- Linux 的 P2PDMA 只检测和提示,不自动为 Direct P2P 关闭 redirect。
7. 参考资料
- Linux kernel V6.18 Source Code
drivers/pci/pci.c:pci_request_acs()、pci_enable_acs()、pci_acs_enabled()、pci_acs_path_enabled()、pci_acs_init()drivers/pci/quirks.c:pci_dev_specific_acs_enabled()、pci_dev_specific_enable_acs()、Intel PCH ACS workarounddrivers/iommu/iommu.c:pci_device_group()、REQ_ACS_FLAGS、IOMMU default domain 和 DMA ownershipdrivers/pci/p2pdma.c:pci_p2pdma_distance_many()、calc_map_type_and_dist()、pci_bridge_has_acs_redir()drivers/iommu/dma-iommu.c:P2PDMA page 在dma_map_sg()路径中的处理Documentation/admin-guide/kernel-parameters.txt:pci=disable_acs_redir=、pci=config_acs=Documentation/driver-api/pci/p2pdma.rst:Linux P2PDMA provider/client/orchestrator 模型