RFC-0070:对 PCI 协议的更改以支持旧版中断 | |
---|---|
状态 | 已接受 |
领域 |
|
说明 | 针对虚假 PCI 旧版中断的缓解措施。 |
问题 | |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2020-01-17 |
审核日期(年-月-日) | 2020-02-25 |
总结
在用户空间中,PCI 总线驱动程序需要能够停用旧版级别触发的中断,直到设备中断得到处理为止,以防止同一 IRQ 不断唤醒总线驱动程序的 IRQ 线程。为了实现这一点,我们需要让设备驱动程序通过某种方式通知总线驱动程序它已准备好处理新的中断,并重新启用其旧版中断生成。
设计初衷
大多数新型 PCI 设备都通过有消息信号中断 (MSI) 运行,该 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 运行的设备进行更改,就必须进行此调用。这不会导致虚假中断,并且可满足上述首次使用的需求。
这与 Linux 处理用户空间 I/O 中断的处理方式类似。
对于第二种用法,我们将创建一个备用的旧版 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 工作器停用设备的旧版中断生成,以发出设备驱动程序的虚拟中断信号。
- 在
PciProtocol
banjo 以及 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/uioX
sysfs 节点上执行阻塞read()
,以等待中断。中断在触发后会处于停用状态,但驱动程序可以通过对 sysfs 节点进行write()
调用来重新启用中断。大多数 Windows PCI 驱动程序都是使用内核模式驱动程序框架 (KMDF) 构建的。它们的中断处理程序会在内核中断调度过程中被调用,而驱动程序会注册在 IRQ 环境中运行的处理程序。