设备驱动程序生命周期

当确定需要设备驱动程序时,系统会将其加载到驱动程序主机进程中。决定它们是否加载的是绑定程序,该程序描述了驱动程序可以绑定到哪些设备。绑定程序使用小型领域专用语言进行定义,该语言会编译为与驱动程序一起分发的字节码。

Intel 以太网驱动程序中的绑定程序示例:

fuchsia.device.protocol == fuchsia.pci.protocol.PCI_DEVICE;
fuchsia.pci.vendor == fuchsia.pci.vendor.INTEL;
accept fuchsia.pci.device {
    0x100E, // Qemu
    0x15A3, // Broadwell
    0x1570, // Skylake
    0x1533, // I210 standalone
    0x15b7, // Skull Canyon NUC
    0x15b8, // I219
    0x15d8, // Kaby Lake NUC
}

绑定编译器会接受绑定程序,并输出定义宏 ZIRCON_DRIVER 的 C 头文件。ZIRCON_DRIVER 宏包含将绑定程序放入 ELF NOTE 部分所需的编译器指令,这样设备协调器就可以检查该程序,而无需将驱动程序完全加载到其进程中。

ZIRCON_DRIVER 的第二个参数是 zx_driver_ops_t 结构体指针(由 lib/ddk/driver.h 定义,该结构体定义了 init、bind、create 和 release 方法)。

当驱动程序加载到驱动程序主机进程中时,系统会调用 init(),并允许进行任何全局初始化。通常不需要执行任何操作。如果实现了 init() 方法并失败,则驱动程序加载将失败。

系统会调用 bind() 以向驱动程序提供要绑定的设备。该设备与驱动程序发布的绑定规则相匹配。如果 bind() 方法成功,驱动程序必须创建一个新设备,并将其添加为传递给 bind() 方法的设备的子设备。如需了解详情,请参阅“设备生命周期”。

系统会针对平台/系统总线驱动程序或代理驱动程序调用 create()。对于绝大多数驾驶员,此方法不是必需的。

在驱动程序卸载之前,系统会调用 release(),此时驱动程序在 bind() 和其他位置可能创建的所有设备都已销毁。目前,系统从不调用此方法。驱动程序一旦加载,便会在驱动程序主机进程的整个生命周期内保持加载状态。

设备生命周期

在驱动程序主机进程中,设备以 zx_device_t 结构的树的形式存在,这些结构对驱动程序而言是不可见的。这些结构是使用 device_add() 创建的,驱动程序会向其提供 zx_protocol_device_t 结构。此结构中的函数指针定义的方法是“设备操作”。各种结构和函数在 device.h 中定义

device_add() 函数会创建一个新设备,并将其作为子设备添加到所提供的父设备。该父设备必须是传入设备驱动程序的 bind() 方法的设备,或者是同一设备驱动程序创建的其他设备。

device_add() 的一个副作用是,新创建的设备将被添加到设备协调器维护的全局设备文件系统中。如果设备未实现 init() 钩子,则可以立即访问设备。

系统会在 device_add() 之后调用 init() 钩子。对于必须执行扩展初始化或探测且不希望在成功之前公开其设备(并在失败时静默移除它们)的驱动程序,此方法非常有用。驱动程序应在完成初始化后调用 device_init_reply()。此回复不一定需要从 init() 钩子调用。在此之前,设备将保持不可见状态,并且保证不会被移除。

设备会计数引用。当驱动程序使用 device_add() 创建设备时,以及当远程进程通过设备文件系统打开设备时,系统会获取引用。

从调用 device_init_reply() 或调用 device_add() 且未实现 init() 钩子的那一刻起,驱动程序主机可能会调用其他设备操作。

在设备上调用 device_async_remove() 时,系统会安排移除该设备及其子项。

移除设备分为四个部分:运行设备的 unbind() 钩子、从设备文件系统中移除设备、丢弃 device_add() 获取的引用,以及运行设备的 release() 钩子。

调用 unbind() 方法时,系统会向驱动程序发出信号,指示其应开始关闭设备,并在解除绑定完成后调用 device_unbind_reply()。解绑还充当 FIDL 事务的硬障碍。调用 Unbind 时,FDF 不允许创建任何新的 FIDL 事务或连接。如果驱动程序处理 FIDL 消息,则负责关闭或回复其解除绑定钩子中的所有未完成交易。这是一个可选钩子。如果未实现该方法,则系统会将其视为立即调用了 device_unbind_reply()。调用 device_unbind_reply 时,所有 FIDL 连接都会终止。

由于在调用子设备的 unbind() 方法时,子设备可能正在处理工作,因此父设备(已完成解绑)可能会继续代表该子设备接收设备方法调用或协议方法调用。建议在完成解除绑定之前,父设备应安排这些方法返回错误,以便在子项移除完成之前从子项进行的调用不会启动更多工作或导致意外的互动。

只有在创建驱动程序完成解绑、该设备的所有打开实例均已关闭,以及该设备的所有子级均已解绑并释放后,系统才会调用 release() 方法。这是驱动程序销毁或释放与设备关联的所有资源的最后机会。在 release() 返回后,引用该设备的 zx_device_t 无效。在此之后,调用从父设备获取的协议的任何设备方法或协议方法都是非法的,并且可能会导致崩溃。

拆解序列示例

为了说明 unbind()release() 在拆解过程中的运作方式,下面的示例展示了 USB WLAN 驱动程序通常如何处理此问题。简而言之,unbind() 调用顺序是自上而下,而 release() 顺序是自下而上。

请注意,这只是一个示例。这可能与真实 WLAN 驱动程序的实际操作不符。

假设 WLAN 设备已作为 USB 设备插入,并且在 USB 设备下已创建 PHY 接口。除了 PHY 接口之外,PHY 接口下还创建了 2 个 MAC 接口。

            +------------+
            | USB Device | .unbind()
            +------------+ .release()
                  |
            +------------+
            |  WLAN PHY  | .unbind()
            +------------+ .release()
              |        |
    +------------+  +------------+
    | WLAN MAC 0 |  | WLAN MAC 1 | .unbind()
    +------------+  +------------+ .release()

现在,我们拔下此 USB WLAN 设备。

  • USB XHCI 会检测移除操作并调用 device_async_remove(usb_device)

  • 这将导致调用 USB 设备的 unbind()。解除绑定完成后,它会调用 device_unbind_reply()

    usb_device_unbind(void* ctx) {
        // Stop interrupt or anything to prevent incoming requests.
        ...

        device_unbind_reply(usb_dev);
    }
  • 当 USB 设备完成解绑定后,系统会调用 WLAN PHY 的 unbind()。解除绑定完成后,它会调用 device_unbind_reply()
    wlan_phy_unbind(void* ctx) {
        // Stop interrupt or anything to prevent incoming requests.
        ...

        device_unbind_reply(wlan_phy);
    }
  • 当 wlan_phy 完成解绑后,系统会对其所有子级(wlan_mac_0、wlan_mac_1)调用 unbind()。
    wlan_mac_unbind(void* ctx) {
        // Stop accepting new requests, and notify clients that this device is offline (often just
        // by returning a ZX_ERR_IO_NOT_PRESENT to any requests that happen after unbind).
        ...

        device_unbind_reply(iface_mac_X);
    }
  • 移除设备的所有客户端后,如果该设备没有子项,则其引用计数将达到零,系统会调用其 release() 方法。

  • 系统会调用 WLAN MAC 0 和 1 的 release()

    wlan_mac_release(void* ctx) {
        // Release sources allocated at creation.
        ...

        // Delete the object here.
        ...
    }
  • wlan_phy 没有任何打开的连接,但仍有子设备 (wlan_mac_0 和 wlan_mac_1)。这两个引用都被释放后,其引用计数最终会达到零,并调用其 release() 方法。
    wlan_phy_release(void* ctx) {
        // Release sources allocated at creation.
        ...

        // Delete the object here.
        ...
    }
  • 现在,当 USB 设备没有子设备或打开的连接时,系统会调用其 release()