| RFC-0070:PCI 协议更改,以支持旧版中断 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 针对虚假 PCI 旧版中断的缓解措施。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2020-01-17 |
| 审核日期(年-月-日) | 2020-02-25 |
摘要
在用户空间中,PCI 总线驱动程序需要能够停用旧版电平触发的中断,直到设备中断得到处理为止,以防止同一 IRQ 不断错误地唤醒总线驱动程序的 IRQ 线程。为此,我们需要一种方法,让设备驱动程序通知总线驱动程序它已准备好处理新的中断,并重新启用其旧版中断生成。
设计初衷
大多数现代 PCI 设备通过消息信号中断 (MSI) 运行,这些中断通过 PCI 配置空间中的可选 MSI 或 MSI-X 功能进行控制。这些中断特定于给定的设备,并通过内核句柄与 MsiInterruptDispatcher 之间的直接映射进行管理。每个 MSI 仅提供给单个设备,从驱动程序的角度来看,可以将其视为标准系统中断。
不过,PCI 旧版中断通过一组在所有 PCI 设备之间共享的中断线路运行,并在系统固件表(例如 ACPI,如 PCI 固件规范中所定义)中详细说明。根据规范,这些中断是电平触发的,并且低电平有效。当触发中断时,系统软件负责确定哪个设备导致了中断,以便对中断进行处理并释放线路。在内核 PCI 总线驱动程序 (kPCI) 中,这是通过以下方式处理的:所有旧版中断都具有一个与内核中的所有 PciInterruptDispatcher 注册的共享中断处理程序。然后,此处理程序会确定哪个设备生成了中断,并向相应的中断对象发出信号。然后,设备的生成中断能力会被停用。下次该驱动程序等待调度程序时,Unmask 钩子将取消屏蔽并重新启用设备的旧版中断生成功能。
借助用户空间 PCI 总线驱动程序 (uPCI),所有这些机制都已移至用户空间。uPCI 本身现在运行一个低开销 IRQ 工作线程,该工作线程可确定负责的设备并发出设备驱动程序与之交互的虚拟中断信号。不过,由于中断是基于电平的,因此如果我们缺少设备的驱动程序,或者给定的驱动程序无法正确处理中断,中断将持续触发。但是,如果 uPCI 停用了设备的中断,以便驱动程序可以处理它,那么我们没有现有方法供 PCI 设备驱动程序重新启用中断。目前,无法通知 uPCI 总线驱动程序设备驱动程序已在提供的虚拟中断上调用 zx_interrupt_wait 或 zx_port_wait,因此总线驱动程序不知道何时应重新启用设备的中断。
设计
我们需要针对 PCI 驱动程序中的两种不同中断用法进行设计。
- 知道自己是 PCI 驱动程序并直接调用
PciProtocol方法的驱动程序。 - 以阻止使用
PciProtocol方法的方式使用中断的驱动程序。
当使用 PCI_IRQ_MODE_LEGACY 配置的旧版中断针对共享线路触发时,总线驱动程序负责通知正确的设备。与 kPCI 驱动程序类似,总线在向驱动程序发出中断可供处理的信号时,会停用设备的旧版中断,从而有效地屏蔽该中断。我们将添加新的 PCI 协议方法,以允许设备驱动程序请求重新启用/取消屏蔽其中断。对于在某些配置中可能与使用旧版中断的设备交互的驱动程序,此调用是必需的,但对于仅使用 MSI 运行的设备,则无需进行任何更改。这样就不会产生虚假中断,并且能够满足上述第一种使用情况的需求。
对于第二种用法,我们将创建替代的旧版 IRQ 模式 PCI_IRQ_MODE_LEGACY_ACKLESS。此模式不会由 ConfigureIrqMode() 选择,需要具有此特殊要求的司机专门选择。系统将监控以这种方式配置中断的设备,以查看每秒的中断次数是否超过配置的次数。如果发生这种情况,设备生成中断的能力将被停用。这与 Linux 对启动中断的处理方式类似。
实现
您可以按顺序进行更改,而无需担心迁移或 CQ 问题。
- 修改
pci_configure_irq_mode以添加一个用于存储为不了解中断模式的驱动程序选择的 IRQ 模式的 out 参数,并更新现有调用方。 - 添加了一个新的协议方法
pci_legacy_interrupt_ack(或简称为pci_interrupt_ack),用于重新启用设备的旧版中断,并返回ZX_OK;如果设备未配置为使用旧版中断,则返回ZX_ERR_BAD_STATE。 - 更新
pci_configure_irq_mode的现有调用方和PCI_IRQ_MODE_LEGACY的用户,以便在中断处理中使用新的协议方法。 - 更新以抽象方式处理中断的驱动程序,并确保它们使用
PCI_IRQ_MODE_LEGACY_NOACK而不是PCI_IRQ_MODE_LEGACY。 - 让 uPCI IRQ 工作线程在所有驱动程序都迁移完毕后,向设备驱动程序的虚拟中断发送信号时,停用设备的旧版中断生成。
- 在
PciProtocolbanjo 以及 Fuchsia.dev 中详细记录了 PCI 中断的使用情况。
性能
遇到的大多数 PCI 设备都将使用 MSI 运行。仍使用旧版中断的设备类型通常仅限于旧版硬件、性能要求较低的集成 SOC 设备、不支持 MSI 的模拟环境,以及很少使用中断的设备。
由于新的 PCI 协议方法需要从驱动程序 devhost 代理写入到 uPCI,因此希望处理旧版中断的驱动程序将向其中断处理例程添加额外的通道写入。可以通过对调用本身进行基准比较来分析此问题,也可以通过检查 Zircon 汇总基准中的渠道写入的汇总费用来分析此问题。
安全注意事项
无。
隐私注意事项
无。
测试
CQ/CI 中的现有集成测试和端到端测试将验证中断在更改后是否仍能正常工作,而新的单元测试将验证对 pci_configure_irq_mode 协议方法的更改操作。
文档
需要扩充 PCI 文档,以说明有关中断模式的操作理论。此外,请注意 zx_interrupt_wait 和 zx_port_wait 文档中需要 pci_legacy_interrupt_ack。
缺点、替代方案和未知因素
缺点
大多数驱动程序会倾向于仅使用 MSI / MSI-X 中断模式,而根本不需要考虑此 API,因此,系统中的更改范围更小,仅要求可能会遇到使用旧版中断的设备的驱动程序来处理这种情况,而不是所有驱动程序。不过,这会带来一个风险,即为特定设备设置编写的驱动程序可能会遇到以下问题:如果未能确认,则只会收到第一个中断。对于支持多种设备的驱动程序,可能会出现此问题。
具有多个后端的情况也会增加一些复杂性。例如,我们的 xHCI 驱动程序具有类似以下内容。如果其 PCI 支持涉及旧版中断,则可能需要如下所示:
// Initialize the proper setup and obtain an interrupt
if (pci_.is_valid()) {
pci_init();
} else {
mmio_init();
}
do {
// Wait loop on the interrupt
// Handle the interrupt
if (mode_ == XHCI_MODE_PCI && irq_mode_ == PCI_IRQ_MODE_LEGACY)
status = pci_.LegacyInterruptAck();
}
}
如果省略了确认代码,此驱动程序在 MSI 模式下可以正常工作,但在旧版模式下,在第一次中断后将不再接收任何中断。改进 PCI 驱动程序周围的测试框架将有助于在开发过程中更好地发现这些错误。
考虑的替代方案
将过多的未处理中断标记为伪中断并停用该中断
与 Linux 类似,我们可以简单地为连续的虚假中断设置一个阈值,如果达到该阈值,我们可以停用或忽略该中断线路,直到重新启动。此方法的一个主要问题是,当 Linux 处理共享中断时,它会在所有处理程序完成之前,通过内核中的硬 IRQ 处理程序调用处理程序链,然后再确认中断。这样可确保在确认之前运行所有驱动程序中断处理程序,因此仅当没有处理程序正确处理中断时才会发生虚假中断。在 Zircon 中使用 uPCI 驱动程序且设备驱动程序处于进程外时,我们可以向它们发送信号以唤醒其 IRQ 处理线程,但我们无法知道它们是否已运行完成。在常见情况下,这会导致伪中断,具体取决于驱动程序的 IRQ 线程的调度速度以及处理给定中断条件的速度。不过,在常见情况下,此方法仍会导致比确认提案更多的伪中断。
添加了用于等待中断的 PCI 协议方法
考虑过的一种方案是添加一种方法来处理等待任何类型的中断,从而有效地 pci_interrupt_wait,避免需要在中断中添加额外的条件。
遗憾的是,这会导致 PCI 中断需要以不同于其他中断的方式进行处理。就我们所知,我们认为使任何中断对象与任何其他中断对象具有相同的接口具有很大的价值,并且驱动程序作者能够继续使用 zx_interrupt_wait、zx_port_bind、zx_port_wait 和 zx_object_wait_async 非常重要。我们系统中的大多数驱动程序都具有 IRQ 端口与多个中断的某种组合,或者需要处理多个后端(UART、PCI、USB),因此请务必不要违反围绕中断对象的接口。
在派生的 InterruptDispatcher 中处理此问题
我们可以保留 kPCI 的 PciInterruptDispatcher 概念,并将此工作委托给它,从而保持 PCI 设备驱动程序和内核之间的中断处理。遗憾的是,用户空间 PCI 驱动程序与 Zircon 内核之间存在大量耦合。
- 每个专用
InterruptDispatcher都需要能够写入 PCI 设备的控制寄存器以停用旧版中断。这需要一个 VMO(与我们对 MSI 的处理方式类似),或者需要向创建此对象的任何系统调用提供地址。这是无法避免的,因为调度程序必须知道它对应于哪个设备。 - 中断禁用是控制寄存器中的一个单一位,用户空间总线驱动程序在总线和设备初始化期间会频繁修改该位,如果存在任何待处理的中断,则会带来严重的竞态条件风险。
- 如果我们使用设备本身中的派生
InterruptDispatcher,仍然需要处理确定共享线路上的中断是由谁触发的问题。由于设备不再通过总线来处理中断,这意味着我们需要在内核中保留类似于 kPCI 的SharedIrqHandler的逻辑。 - 现在,我们还需要再次通过内核从 ACPI / 主板文件获取对 PCI 旧版 IRQ 路由表的了解。现在,这在用户空间和 ACPI 中处理。
除非我们愿意承认 PCI 是一种需要一些自定义内核代码以及大约 2 个额外系统调用的特殊驱动程序,否则目前我无法看到采用此方法的可行途径。
在先技术和参考资料
在我的研究中,完全在用户空间中处理此问题是 Zircon 独有的问题。
OSX 的 DriverKit 使用
IOInterruptDispatchSource,该IOInterruptDispatchSource与内核中的中断配置和处理协同工作。此外,PCIDriverKit 仅支持 MSI 和 MSI-X 中断模式。大多数 Linux PCI 中断都在内核本身中处理。共享中断具有由驱动程序注册到其中的处理程序链。当触发中断时,内核会按顺序调用每个处理程序,直到其中一个处理程序处理了中断。如果共享中断线路有足够多的未被任何句柄处理的伪中断,内核会停用该中断。
Linux 还通过其用户空间 I/O (UIO) 接口支持简单的用户空间驱动程序。这样,驱动程序便可对提供的
/dev/uioXsysfs 节点执行阻塞read()以等待中断。中断在触发时会被停用,但驱动程序可以通过对 sysfs 节点进行write()调用来重新启用中断。大多数 Windows PCI 驱动程序都是使用内核模式驱动程序框架 (KMDF) 构建的。它们的中断处理程序作为内核中断调度的一部分被调用,驱动程序注册在 IRQ 上下文中运行的处理程序。