摘要
在应用和外围硬件之间传输大量数据时,尽可能减少数据经过的复制次数非常重要。例如,假设某个应用想要从组件永久性存储空间读取文件。为此,应用会请求将文件读取到文件系统,而文件系统可能需要向块设备发送请求。根据块分区拓扑,请求可能需要经过多个层级的驱动程序,最终才能到达可以执行读取操作的驱动程序。
对上述问题采用简单的方法可能会导致通过 Zircon 通道在应用和硬件之间的每个层中发送 FIDL 消息,从而导致数据的多个副本。由于这种做法效率低下,我们不会这样做。遵循整个行业中已确立的模式,我们将消息拆分为两个平面:控制平面和数据平面。通过控制平面发送的消息体积小且发送费用低,而数据平面中的消息包含大量数据,复制费用高昂。通过控制平面发送的消息通常使用基于 Zircon 通道构建的 FIDL 协议。数据平面中的消息通过共享内存基元 Zircon VMOs 发送。
考虑到这一点,简单的实现可能会选择为每个通过控制平面传输的交易创建一个新的 VMO,直到它到达发出 DMA 的驱动程序,从而实现将数据放入 VMO 的应用与最终驱动程序之间零次复制的预期目标。不过,由于以下两个原因,这种做法可能无法提供足够的性能:
- 为了发出 DMA 请求,必须先固定内存,这需要调用内核并视需要在 IOMMU 中设置页面映射。
- 如果最终驱动程序需要将请求复制到特殊缓冲区(因为并非所有硬件都支持 DMA),则必须将 VMO 映射到其进程中,或者调用内核以复制内存。
由于这两种方法都很昂贵,因此我们需要更好的方法:使用预注册的 VMO。其工作原理是让应用发送一次性控制消息,以便在堆栈中的最终驱动程序中注册 VMO。对此消息的响应会返回一个标识符,该标识符日后可用于引用 VMO。控制消息应仅引用此标识符,而不是附加 VMO 句柄。注册后,堆栈中的最终驱动程序可以执行一次成本较高的固定或映射操作,并缓存结果。
VMO 标识符备注
为了确保我们不会成为混淆代理攻击的牺牲品,我们必须对 VMO 标识符保持与内核对句柄保持的相同不变性。为此,每个层级的 VMO 标识符都必须是客户端的唯一标识符,并且每个层级都必须验证该标识符是否有效。更具体地说,使用 koid 作为标识符仍然需要服务器检查客户端是否注册了具有该 koid 的 VMO。
为了减少往返次数,可以允许客户端在注册 API 中为 VMO 标识符命名,以便高效使用一次性 VMO。或者,该协议可以声明始终将 VMO 的 koid 用作标识符。
Zircon FIFO
为了进一步提升性能,某些协议可能还会选择为其控制平面使用 FIFO。FIFO 的复杂性较低,因此开销较小。其局限性之一是无法传输句柄。因此,必须使用 VMO 注册模式才能使用 FIFO。(请注意,仍需使用渠道来执行注册。)
媒体库
这种模式可能会给负责维护 VMO 与标识符之间的映射的驱动程序增加很多复杂性。我们创建了一个库来帮助实现,该库位于 //src/lib/vmo_store 下。如需查看示例用法,请参阅 //src/connectivity/network/drivers/network-device/device。
该模式的缺点
对于吞吐量较低的情况,此模式过于复杂,应尽量避免。
VMO 注册会导致一次性操作变成 2 次往返。如果一次性 VMO 很常见,FIDL 协议应确保除了预注册的 VMO 之外,还能继续使用一次性 VMO。您还可以允许客户在注册期间提供 VMO 的标识符,以缓解此问题。
预注册的 VMO 可能会导致内存“泄漏”情况,即客户端会不断注册 VMO 并忘记取消注册。此外,如果服务器在管理客户端时不小心,可能会忘记清理可能已与服务器断开连接的客户端所属的已注册 VMO。
通过会将 VMO 固定的驱动程序预注册的 VMO 会导致为 VMO 提供后备的页面无法再分页。
特定于驱动程序的注意事项
由于某些驱动程序驻留在同一驱动程序主机进程中,并且我们采用了迷你驱动程序模式(即将通用逻辑提升到“核心”驱动程序),因此似乎显而易见的做法是在核心驱动程序(而非特定于设备的驱动程序)中执行 VMO 注册。不过,这样做并不明智,原因如下:
- 核心驱动程序需要知道是否由设备专用驱动程序执行固定或映射操作。
- 固定需要访问平台总线或 PCI 驱动程序提供的总线事务发起方 (BTI) 句柄。向上传递 BTI 句柄是反模式。
- 如果需要映射,这意味着原始缓冲区会通过 FIDL 传递。这是一种反模式,因为在这种情况下,在驱动程序之间发送数据时无法避免复制数据。
- 在任何情况下,如果操作是异步的(大多数都是),则核心驱动程序将负责确保在 VMO 仍在使用时不会取消固定/取消映射 VMO。在关闭和暂停等测试不太充分的情况下,这一点尤为重要。