RFC-0070:PCI 协议变更以支持旧版中断

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) 中,所有具有已向内核中的所有 PciInterruptDispatcher 注册的共享中断处理程序的旧版中断都会处理此问题。然后,此处理程序确定哪个设备生成了中断,并向相应的中断对象发送信号。然后,设备生成中断的能力会被停用。下次该驱动程序等待调度程序时,Unmask 钩子将同时取消屏蔽并重新启用设备的旧版中断生成功能。

借助用户空间 PCI 总线驱动程序 (uPCI),所有这些机制都已移至用户空间。uPCI 本身现在运行一个开销较低的 IRQ 工作器,该工作器用于确定负责的设备,并发出设备驱动程序与之交互的虚拟中断信号。不过,由于中断是基于级别的,因此如果我们缺少设备的驱动程序,或者给定驱动程序未能正确处理中断,中断将会持续触发。但是,如果 uPCI 停用设备的某个中断以便驱动程序可以处理该中断,那么我们目前没有任何方法可以让 PCI 设备驱动程序重新启用该中断。目前无法通知 uPCI 总线驱动程序设备驱动程序已对所提供的虚拟中断调用 zx_interrupt_waitzx_port_wait,因此总线驱动程序不知道何时应重新启用设备的中断。

设计

我们需要针对 PCI 驱动程序中的两种不同中断用法进行设计。

  1. 知道自己是 PCI 驱动程序并直接调用 PciProtocol 方法的驱动程序。
  2. 以阻止使用 PciProtocol 方法的方式使用中断的驱动程序。

当使用 PCI_IRQ_MODE_LEGACY 配置的旧版中断针对共享线路触发时,总线驱动程序负责通知正确的设备。与 kPCI 驱动程序类似,当向驱动程序发出可处理中断的信号时,总线会停用设备的旧版中断,从而有效屏蔽中断。我们将添加一个新的 PCI 协议方法,以允许设备驱动程序请求重新启用/取消屏蔽其中断。对于可能与在某些配置中使用旧版中断的设备交互的驱动程序,此调用是必需的,但对于仅使用 MSI 运行的设备,则无需进行任何更改。这样就不会产生虚假中断,并且符合前面所述的首次使用情形的需求。

这与 Linux 处理用户空间 I/O 中断类似。

对于第二种用法,我们将创建一个备用旧版 IRQ 模式 PCI_IRQ_MODE_LEGACY_ACKLESSConfigureIrqMode() 不会选择此模式,需要具有此独特要求的驱动程序专门选择此模式。系统会监控以这种方式配置中断的设备,以查看每秒中断次数是否超出配置的次数。如果发生这种情况,设备将无法再生成中断。这与 Linux 处理启动中断的方式类似。

实现

您可以按顺序进行更改,而无需进行任何迁移或 CQ 问题排查。

  1. 修改 pci_configure_irq_mode 以添加 out 参数,以存储为不受中断模式影响的驱动程序选择的 IRQ 模式,并更新现有调用方。
  2. 添加了新的协议方法 pci_legacy_interrupt_ack(或简称 pci_interrupt_ack),用于重新为设备启用旧版中断,并在设备未配置为使用旧版中断时返回 ZX_OKZX_ERR_BAD_STATE
  3. 更新 pci_configure_irq_mode 的现有调用方和 PCI_IRQ_MODE_LEGACY 的用户,以便在其中断处理中使用新协议方法。
  4. 更新以抽象方式处理中断的驱动程序,并确保它们使用 PCI_IRQ_MODE_LEGACY_NOACK,而不是 PCI_IRQ_MODE_LEGACY
  5. 在所有驱动程序迁移完毕后,让 uPCI IRQ 工作器在向设备驱动程序的虚拟中断发送信号时停用设备的旧版中断生成。
  6. PciProtocol banjo 和 Fuchsia.dev 中详细记录 PCI 中断的使用方式。

性能

遇到的大多数 PCI 设备都将使用 MSI 进行操作。仍使用旧版中断的设备类型通常仅限于旧版硬件、性能要求较低的集成 SOC 设备、不支持 MSI 的模拟环境,以及仅很少使用中断的设备。

由于这种新的 PCI 协议方法需要从驱动程序 devhost 代理写入 uPCI,因此希望处理旧版中断的驱动程序将向其中断处理例程添加额外的通道写入。您可以通过对调用本身进行基准测试或在 Zircon 汇总基准测试中检查通道写入的汇总开销来对其进行性能分析。

安全注意事项

无。

隐私注意事项

无。

测试

CQ/CI 中的现有集成和端到端测试将验证更改后中断是否仍能正常运行,而新的单元测试将验证对 pci_configure_irq_mode 协议方法所做的更改的运行情况。

文档

需要扩展 PCI 文档,以说明有关中断模式的操作理论。此外,在 zx_interrupt_waitzx_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();
  }
}

如果省略了 ack 代码,则此驱动程序将能够正常使用 MSI,但在旧版模式下,在收到第一个中断后不会再收到任何中断。改进 PCI 驱动程序周围的测试框架,有助于在开发过程中更好地捕获这些错误。

考虑的替代方案

将过多的未处理中断标记为虚假中断并停用中断

与 Linux 类似,我们只需为顺序虚假中断设置阈值,如果达到该阈值,我们就可以停用或忽略该中断线,直到重启为止。这种方法的一个主要问题是,当 Linux 处理共享中断时,它会先通过内核中的硬 IRQ 处理程序调用处理程序链,然后在所有处理程序完成后确认中断。这样可以确保所有驱动程序中断处理程序都在 ACK 之前运行,因此只有在没有任何处理程序正确处理中断时才会发生虚假中断。在 Zircon 中,由于 uPCI 驱动程序和设备驱动程序不在进程中,我们可以向它们发送信号来唤醒其 irq 处理线程,但无法知道它们是否已运行完毕。在常见情况下,这会导致虚假中断,具体取决于驱动程序的 IRQ 线程的调度和处理给定中断条件的速度。不过,在常见情况下,与 ack 方案相比,这种方法仍然会导致更多虚假中断。

添加了用于等待中断的 PCI 协议方法

我们考虑过的一个方案是添加一个方法来处理等待任何类型的 interrupt,以便有效地 pci_interrupt_wait,从而避免在 interrupt 中需要额外的条件。

遗憾的是,这导致 PCI 中断需要以与其他中断不同的方式进行处理。我认为,尽可能让任何中断对象与任何其他中断对象具有相同的接口非常有价值,并且重要的是,驱动程序作者必须能够继续使用 zx_interrupt_waitzx_port_bindzx_port_waitzx_object_wait_async。系统中的大多数驱动程序都需要处理 IRQ 端口与多个中断的组合,或者多个后端(UART、PCI、USB),因此请务必遵守中断对象周围的接口。

在派生 InterruptDispatcher 中处理此问题

我们可以保留 kPCI 的 PciInterruptDispatcher 概念,并将此工作委托给它,以便在 PCI 设备驱动程序和内核之间进行中断处理。遗憾的是,这会导致用户空间 PCI 驱动程序与 Zircon 内核之间存在大量耦合。

  1. 每个专用 InterruptDispatcher 都需要能够写入 PCI 设备的控制寄存器,以停用旧版中断。理想情况下,这需要使用 VMO(与我们对 MSI 的方法类似),或者向创建此对象的任何系统调用提供地址。无法规避此问题,因为调度程序必须知道它对应的是哪个设备。
  2. 中断停用是控制寄存器中的单个位,用户空间总线驱动程序会在总线和设备初始化期间频繁修改该位,如果有任何待处理的中断,则存在严重的竞态条件风险。
  3. 如果我们在设备本身中使用派生的 InterruptDispatcher,则仍需要处理确定共享线路上是哪个中断触发的问题。由于设备不再通过总线处理中断,因此我们需要在内核中保留与 kPCI 的 SharedIrqHandler 类似的逻辑。
  4. 现在,我们还需要再次通过内核了解 ACPI / 板文件中的 PCI 旧版 IRQ 路由表。现在,此操作在用户空间和 ACPI 中处理。

目前,我看不到采用这种方法的合理途径,除非我们愿意承认 PCI 是一种特殊的驱动程序,需要一些自定义内核代码以及大约 2 个额外的系统调用才能正常运行。

在先技术和参考文档

在我的研究中,完全在用户空间中处理此问题是 Zircon 特有的问题。

  1. OSX 的 DriverKit 使用 IOInterruptDispatchSource,该 IOInterruptDispatchSource 与内核中的中断配置和处理协同工作。此外,PCIDriverKit 仅支持 MSI 和 MSI-X 中断模式。

    PCIDriverKit > IOPCIDevice

  2. 大多数 Linux PCI 中断都在内核本身中处理。共享中断由驱动程序向其注册了一组处理程序。当中断触发时,内核会依次调用每个处理程序,直到某个处理程序处理中断。如果共享中断线有足够的虚假中断,而任何句柄都未处理这些中断,则内核会停用中断。

    Linux 启动中断

    Linux 还通过其用户空间 I/O (UIO) 接口支持简单的用户空间驱动程序。这样,驱动程序就可以对提供的 /dev/uioX sysfs 节点执行阻塞 read() 以等待中断。中断在触发时会被停用,但驱动程序可以通过对 sysfs 节点进行 write() 调用来重新启用中断。

    用户空间 I/O HOWTO

  3. 大多数 Windows PCI 驱动程序都是使用内核模式驱动程序框架 (KMDF) 构建的。内核中会调用中断处理程序,驱动程序会注册在 IRQ 上下文中运行的处理程序。

    中断服务例程简介