驱动程序中的协议

什么是协议?

协议是一种严格的接口定义。

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

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

当然,其他协议对它们必须提供的功能有不同的要求。例如,块设备提供 fuchsia.hardware.block.volume.Service FIDL 服务,其 volume 协议 (fuchsia.storage.block.Block) 包含用于返回设备大小(以块为单位)和打开 I/O 会话的方法。

在许多情况下,协议用于通过利用接口的通用实现来简化驱动程序。例如,“以太网”驱动程序实现了以太网接口,并绑定到实现 Ethermac 协议的设备。某些协议利用共享内存和非 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。此附加协议用于处理所有以太网驱动程序共有的功能(以避免代码重复)。此类功能包括缓冲区管理、状态报告和管理功能。

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

此架构在多个位置重复使用,例如显示控制器、I2C 总线和串行驱动程序。

流程 / 协议映射

为了使上述讨论保持简单,我们没有讨论与驱动程序相关的进程分离。 为了了解这些问题,我们来看看其他操作系统如何处理这些问题,并将其与 Fuchsia 的方法进行比较。

在 Linux 等单内核中,许多驱动程序都在内核内实现。 这意味着它们共享相同的地址空间,实际上位于同一“进程”中。

这种方法的主要问题是故障隔离 / 利用。不良驱动程序可能会导致整个内核崩溃,因为它位于同一地址空间中,因此具有对所有内核内存和资源的特权访问权限。出于同样的原因,被破解的驱动程序也可能会带来安全威胁。

另一种极端情况是将每个驱动程序服务都放入自己的进程中,一些微内核操作系统会采用这种做法。其主要缺点是,如果一个驱动程序依赖于另一个驱动程序的服务,内核必须在两个驱动程序进程之间至少执行一次上下文切换操作(如果不是数据传输)。 虽然微内核操作系统通常旨在快速执行此类操作,但以高频率执行这些操作是不理想的。

Fuchsia 采用的方法基于驱动程序宿主的概念。驱动程序宿主是包含协议栈(即协同工作的一个或多个协议)的进程。驱动程序宿主从 ELF 共享库(称为动态共享对象 [DSO])加载驱动程序。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。该 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 文件。而是附加到给定节点的驱动程序的组件清单的网址。