驱动程序堆栈性能

本文档旨在简要介绍在 Fuchsia 中编写新驱动程序或与现有驱动程序交互的性能方面的良好做法和错误做法。

本文档介绍了以下主题:

API 定义

为新的驱动程序堆栈编写 API 时,应考虑一些关键的性能数据分析。驱动程序堆栈 API 属于应用或驱动程序类别。

应用 API 用于为常规非驱动程序组件提供对硬件资源的访问权限,而驱动程序 API 用于让驱动程序相互通信。例如,fuchsia.hardware.network 是用于与网络驱动程序交互的应用 API,fuchsia.hardware.network.driver 则是为网络设备定义较低级别的驱动程序 API。

设备驱动程序 API 通常是 fuchsia.hardware.* banjo 和 fuchsia.hardware.* FIDL API。

避免同步操作

快速路径中的工作单元应避免期望同步完成,尤其是(但并非专门)操作需要跨越进程边界时。对于 FIDL API,这种设计可能会导致新的工作单元直到最后一个工作单元完成才能启动,这意味着调用方必须空闲,直到可以安全请求下一个工作单元。

从性能的角度来看,慢路径操作(例如设置、拆解和配置)可以执行同步操作。

鼓励批处理

当 API 定义明确定义一批工作(而不是始终传输单个单元)时,我们鼓励 API 用户通过其应用或驱动程序尝试批量处理。

批处理的一个重要功能是减少 API 需要执行一定数量的工作的次数。对于跨进程边界的 API,这会直接减少系统调用和调度开销,从而提高性能。

此外,如果 API 定义本身批量提供工作单元,设备驱动程序可以通过硬件加速更轻松地将一批工作项合并为一个单元。例如,许多设备驱动程序使用 DMA 将工作与其驱动的特定硬件加入队列。如果一次收到一批工作项,或许可以减少硬件的事务数量。如果没有明确确定的批处理边界,设备驱动程序会被迫更频繁地与硬件进行交互,或依靠启发法(例如轮询间隔)来减轻硬件通信负担。

避免使用数据副本

对于网络、音频或视频等高带宽或低延迟应用,避免数据复制尤为重要。较大的载荷应尽可能跨越 VMO 中的 API 边界。一种常见策略是在设置过程中协商有限数量的 VMO,并在操作期间交换对这些 VMO 中的区域的引用。

阐明流控制

如果 API 可以进行批处理,并且未按照上述建议设置严格的同步操作预期,那么控制 API 服务器和客户端之间的信息流对于正确性和性能都非常重要。

在常规操作中,API 的流控制定义必须使系统能够执行的工作总量保持一定的不变性(在定义时进行固定或在设置时预先协商),让所有部分都能够执行工作,而不会阻止其等待对应的部分。

例如,fuchsia.hardware.network 在设置期间通过定义有限数量的“工作单元”(即网络数据包)来强制执行流控制。任何时候,相关各方都知道在仅使用本地维护的状态时,还可以跨 API 边界推送多少数据包。

用于订购的账号

在某些应用中,所有工作单元都必须按照既定顺序执行,以确保正确性。然而,在其他应用中,某些排序限制可能会减弱或完全取消。

当无需按确切顺序执行工作项流时,API 可以反映这一点,以便驱动程序和应用轻松识别可以并行处理的工作。

例如,网络数据包通常需要保持排序以免破坏应用协议,但通常仅对每个应用流实例(称为“数据流”)而言才是如此。网络适配器中的常见策略是定义一定数量的数据包队列,并将每个应用数据流确定性地分配给其中一个队列。然后,网络堆栈可以安全地并行处理队列,而无需查看数据包的内容。观察此类常用的硬件设施并在 API 中充分利用这些设施,可以带来有意义的性能提升。

实现

本部分列出了在实现提供或使用设备驱动程序 API 的代码时要观察的性能模式和反模式。实现可以是驱动程序本身,也可以是与设备驱动程序提供的服务交互的应用。

编写快速路径上的代码时应始终注意以下内容。请注意,对于设备驱动程序实现,快速路径的一部分通常位于中断线程中。

  • 避免分配内存或创建 zircon 对象。快速路径上使用的资源应始终预先分配。在使用 FIDL 绑定(尤其是 hlcpp)或 fit::function 之类的库时,可能要特别小心,因为此类库可能会导致隐式分配。
  • 注意系统调用。Syscall 的运行成本可能很高,因此必须格外小心,避免在对性能要求较高的操作中粗暴调用内核。完全消除系统调用可能无法实现,但减少每单位工作量的调用次数(例如批处理)可以大大减少系统负载。
  • 快速归还共享资源。如果 API 定义了一个共享资源的有限池(例如共享内存区域),则应尽快重复使用这些资源或将其返回到“可用”池,最好是批量处理。
  • 尽可能以只读(而非读写)方式映射 VMO。以只读方式映射 VMO 可减少需要针对 DMA 执行的缓存操作数量。例如,使用 DMA 写入可能具有脏缓存行的共享 VMO 时,必须在 DMA 之前和之后刷新缓存并使缓存失效。另一方面,对于只读映射,缓存行已知始终是干净的,这意味着它们只需在 DMA 完成后被清空和失效。