什么是协议?
协议是严格的接口定义。
以太网驱动程序发布了符合 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
文件。而是附加到指定节点的驱动程序组件清单的网址。