| RFC-0070:PCI 协议更改以支持旧版中断 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 针对虚假 PCI 旧版中断的缓解措施。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2020-01-17 |
| 审核日期(年-月-日) | 2020-02-25 |
摘要
在用户空间中,PCI 总线驱动程序需要能够停用旧版级别触发的中断,直到设备中断得到处理,以防止同一 IRQ 不断虚假地唤醒总线驱动程序的 IRQ 线程。 为此,我们需要一种方法,让设备驱动程序通知总线驱动程序它已准备好处理新的中断并重新启用其旧版中断生成。
设计初衷
大多数现代 PCI 设备都通过消息信号中断 (MSI) 运行,这些中断通过 PCI 配置空间中的可选 MSI 或 MSI-X 功能进行控制。这些中断特定于给定设备,并由内核句柄与 MsiInterruptDispatcher 之间的直接映射进行管理。每个 MSI 仅提供给单个设备,并且从驱动程序的角度来看,可以将其视为标准系统中断。
但是,PCI 旧版中断通过一组在所有 PCI 设备之间共享的中断线路运行,并在系统固件表(例如 PCI 固件规范定义的 ACPI)中详细说明。根据规范,这些中断是级别触发的,并且是低电平有效。当中断被触发时,系统软件有责任确定哪个设备负责中断,以便处理中断并释放线路。在内核 PCI 总线驱动程序 (kPCI) 中,这是通过所有旧版中断都具有在内核中的所有 PciInterruptDispatchers 中注册的共享中断处理程序来处理的。然后,此处理程序确定哪个设备生成了中断并向相应的中断对象发出信号。然后,设备的生成中断的能力将被停用。下次该驱动程序等待调度程序时,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 模式,并更新现有调用方。 - 添加一个新的协议方法
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),因此,请务必不要违反围绕中断对象的接口。
在派生的 InterruptDispatchers 中处理此问题
我们可以保留 kPCI 的 PciInterruptDispatcher 概念并将此工作委托给它,从而保持 PCI 设备驱动程序与内核之间的中断处理。遗憾的是,这在用户空间 PCI 驱动程序和 Zircon 内核之间存在大量耦合。
- 每个专用
InterruptDispatcher都需要能够写入 PCI 设备的控制寄存器以停用旧版中断。这理想情况下需要 VMO,与我们处理 MSI 的方法类似,或者需要向创建此对象的任何系统调用提供地址。这是无法避免的,因为调度程序必须知道它对应的设备。 - 中断停用是控制寄存器中的单个位,用户空间总线驱动程序在总线和设备初始化期间会频繁修改该位,如果存在任何待处理的中断,则会带来严重的竞争条件风险。
- 如果我们自己在设备中使用派生的
InterruptDispatcher,我们仍然需要处理确定共享线路上的中断是由谁触发的问题。由于设备不再与总线一起处理中断,这意味着我们需要在内核中保留类似于 kPCI 的SharedIrqHandler的逻辑。 - 现在,我们还需要再次通过内核来了解 ACPI / 板文件中的 PCI 旧版 IRQ 路由表。现在,这在用户空间和 ACPI 中进行处理。
目前,除非我们愿意承认 PCI 是一个需要一些自定义内核代码以及大约 2 个额外系统调用才能完成工作的特殊驱动程序,否则我看不出此方法有合理的出路。
在先技术和参考文档
在我的研究中,完全在用户空间中处理此问题是 Zircon 特有的问题。
OSX 的 DriverKit 使用
IOInterruptDispatchSource,该源与内核中的中断配置和处理协同工作。此外,PCIDriverKit 仅支持 MSI 和 MSI-X 中断模式。大多数 Linux PCI 中断都在内核本身中处理。共享中断具有由驱动程序注册的处理程序链。当中断被触发时,内核会按顺序调用每个处理程序,直到其中一个处理程序处理了中断。如果共享中断线路有足够多的虚假中断,而这些中断未被任何处理程序处理,则内核会停用中断。
Linux 还通过其用户空间 I/O (UIO) 接口支持简单的用户空间驱动程序。这允许驱动程序对提供的
/dev/uioXsysfs 节点执行阻塞read()以等待中断。中断在触发时会被停用,但驱动程序可以通过对 sysfs 节点进行write()调用来重新启用它们。大多数 Windows PCI 驱动程序都是使用内核模式驱动程序框架 (KMDF) 构建的。它们的中断处理程序作为内核中断调度的一部分被调用,并且驱动程序注册在 IRQ 上下文中运行的处理程序。