驱动程序中的协议

什么是协议?

协议是严格的接口定义。

以太网驱动程序发布了符合 ZX_PROTOCOL_ETHERNET_IMPL 的接口。这意味着它必须提供一组在数据结构中定义的函数(在本例中为 ethernet_impl_protocol_ops_t)。

这些函数对于实现该协议的所有设备来说都是通用的。例如,所有以太网设备都必须提供用于查询接口 MAC 地址的功能。

其他协议当然对它们必须提供的功能有不同的要求。 例如,块设备将发布符合“块实现协议”(ZX_PROTOCOL_BLOCK_IMPL) 的接口并提供 block_protocol_ops_t 定义的函数。例如,此协议包含一个函数,用于返回设备的大小(以块为单位)。

在许多情况下,协议用于利用接口的通用实现让驱动程序变得更简单。例如,“块”驱动程序实现通用块接口,并绑定到实现块核心协议的设备,而“以太网”驱动程序则对以太网接口和以太网协议执行相同的操作。某些协议(例如此处提到的两个协议)使用共享内存和非 RPC 信号,以实现更高的效率、更短的延迟时间和更高的吞吐量。

类表示关于设备实现接口或协议的承诺。设备存在于设备文件系统中的拓扑路径下,例如 /sys/platform/pci/00:02:00/e1000。如果它们是特定类,它们也会在 /dev/class/CLASSNAME/... 下显示为别名。e1000 驱动程序会实现 Ethermac 接口,因此它也会显示在 /dev/class/ethermac/000 中。类目录中的名称具有唯一性,但意义上没有,并且是按需分配的。

协议示例:

  • PCI 根协议 (ZX_PROTOCOL_PCIROOT)
  • PCI 设备协议 (ZX_PROTOCOL_PCI),以及
  • 以太网实现协议 (ZX_PROTOCOL_ETHERNET_IMPL)。

方括号内的名称是对应协议的 C 语言常量,仅供参考。

依赖于平台与独立于平台

我们在上文中提到,ZX_PROTOCOL_ETHERNET_IMPL “接近”客户端所使用的函数,但移除了一步。这是因为客户端和驱动程序之间还有一个协议 ZX_PROTOCOL_ETHERNET。此附加协议用于处理所有以太网驱动程序通用的功能(以避免代码重复)。此类功能包括缓冲区管理、状态报告和管理功能。

这实际上是“依赖于平台”与“不依赖于平台”的分离;通用代码存在于与平台无关的部分中(一次),而特定于驱动程序的代码在依赖平台的部分中实现。

这种架构在多个位置重复。例如,对于块设备,硬件驱动程序绑定到总线(例如,PCI)并提供 ZX_PROTOCOL_BLOCK_IMPL 协议。独立于平台的驱动程序绑定到 ZX_PROTOCOL_BLOCK_IMPL,并发布面向客户端的协议 ZX_PROTOCOL_BLOCK

显示控制器、I2C 总线和串行驱动程序也会看到此代码。

流程 / 协议映射

为简单起见,我们并未讨论进程分离,因为它与驱动程序相关。 如需了解这些问题,我们来看一下其他操作系统如何处理这些问题,并将其与 Fuchsia 方法进行比较。

在单体式内核(例如 Linux)中,许多驱动程序都是在内核中实现的。也就是说,它们共享相同的地址空间,并且实际上位于同一“进程”中。

这种方法的主要问题是故障隔离 / 利用。不良驱动程序可能会取出整个内核,因为它位于同一地址空间内,因此具有对所有内核内存和资源的特权访问。司机遭到入侵也会造成安全威胁。

某些微内核操作系统会使用另一种极端情况,即将每项驱动程序服务放入自己的进程中。其主要缺点是,如果一个驱动程序依赖于另一个驱动程序的服务,则内核必须至少在两个驱动程序进程之间实现上下文切换操作(如果也不支持数据传输)。虽然微内核操作系统通常设计为能够快速执行此类操作,但并不希望以高频率执行这些操作。

Fuchsia 采用的方法基于驱动程序主机的概念。驱动程序主机是包含协议堆栈(即一个或多个协同工作的协议)的进程。驱动程序主机会从 ELF 共享库(称为动态共享对象,或 DSO)加载驱动程序。

协议堆栈可以有效地为设备创建完整的“驱动程序”,包括依赖于平台的组件和不依赖于平台的组件,在独立的进程容器中。

对于高级读取器,请查看 Fuchsia 命令行中提供的 driver dump 命令。它会显示设备树,并显示进程 ID、DSO 名称和其他实用信息。

以下是经过精心优化的版本,其中仅显示 PCI 以太网驱动程序部分:

1. [root]
2.    [sys]
3.       <sys> pid=1416 /boot/driver/bus-acpi.so
4.          [acpi] pid=1416 /boot/driver/bus-acpi.so
5.          [pci] pid=1416 /boot/driver/bus-acpi.so
            ...
6.             [00:02:00] pid=1416 /boot/driver/bus-pci.so
7.                <00:02:00> pid=2052 /boot/driver/bus-pci.proxy.so
8.                   [e1000] pid=2052 /boot/driver/e1000.so
9.                      [ethernet] pid=2052 /boot/driver/ethernet.so

从上文中可以看出,进程 ID 1416(第 3 至 6 行)是由 DSO bus-acpi.so 实现的高级配置与电源接口 (ACPI) 驱动程序。

在主要枚举期间,ACPI DSO 检测到 PCI 总线。这会导致系统发布带有 ZX_PROTOCOL_PCI_ROOT 的父项(第 5 行,导致出现 [pci] 条目),进而导致驱动程序主机加载 bus-pci.so DSO 并绑定到它。DSO 是我们在上述讨论中一直提到的“基本 PCI 驱动程序”。

在绑定过程中,基本 PCI 驱动程序枚举了 PCI 总线,并找到了以太网卡(第 6 行检测总线 0、设备 2、功能 0,显示为 [00:02:00])。(当然,还找到了许多其他设备,为简单起见,我们已将其从上述列表中移除)。

检测到此设备后,基本 PCI 驱动程序会发布一个包含 ZX_PROTOCOL_PCI 以及设备的 VID 和 DID 的新父级。此外,还创建了一个新的驱动程序主机(进程 ID 2052),并使用 bus-pci.proxy.so DSO(第 7 行)加载了该主机。此代理充当从新驱动程序主机 (pid 2052) 到基本 PCI 驱动程序 (pid 1416) 的接口。

此时,系统会决定将设备驱动程序“切断”到自己的进程,新的驱动程序主机和基础 PCI 驱动程序现在位于两个不同的进程中。

然后,新驱动程序主机 2052 会查找匹配的子项(第 8 行上的 e1000.so DSO;系统将其视为匹配项,因为它具有 ZX_PROTOCOL_PCI 以及正确的 VID 和 DID)。 该 DSO 会发布一个 ZX_PROTOCOL_ETHERNET_IMPL,以绑定到匹配的子级(第 9 行的 ethernet.so DSO;由于它具有 ZX_PROTOCOL_ETHERNET_IMPL 协议,因此被视为匹配项)。

此链未显示出最终的 DSO (ethernet.so) 发布了一个 ZX_PROTOCOL_ETHERNET,这是客户端可以使用的代码段,因此当然无需涉及其他“设备”绑定。

驱动程序框架版本 2 (DFv2)

如果启用了驱动程序框架版本 2,driver dump 将显示略有不同的树。

$ driver dump
[root] pid=4766 fuchsia-boot:///#meta/platform-bus.cm
   [sys] pid=4766
      [platform] pid=4766
         [pt] pid=4766 fuchsia-boot:///#meta/platform-bus-x86.cm
            [acpi] pid=4766
               [acpi-pwrbtn] pid=4766 fuchsia-boot:///#meta/hid.cm
               ...
            [PCI0] pid=4766 fuchsia-boot:///#meta/bus-pci.cm
               [bus] pid=4766
                 ...
                 [00_04_0] pid=4766 fuchsia-boot:///#meta/virtio_ethernet.cm
                    [virtio-net] pid=4766 fuchsia-boot:///#meta/netdevice-migration.cm
                       [netdevice-migration] pid=4766 fuchsia-boot:///#meta/network-device.cm
                          [network-device] pid=4766
        ...

需要指出的是,节点(设备在 DFv2 中称为节点)没有与之关联的 .so 文件。而是附加到指定节点的驱动程序组件清单的网址。