| RFC-0263:将驱动程序通信迁移到服务 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 将驱动程序和客户端从与 devfs 连接迁移到使用服务。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2024-08-25 |
| 审核日期(年-月-日) | 2024-10-28 |
问题陈述
目前,非驱动程序对几乎所有驱动程序的访问都由设备文件系统 (devfs) 介导。遗憾的是,devfs 存在以下几个缺点:
- 很难将 devfs 的客户端限制为特定协议。
- 该 API 使驱动程序难以公开多个协议。
- 拓扑路径和硬编码的类路径会向客户端公开内部实现细节,并可能导致意外中断。
- 由于组件管理器不知道 devfs 连接,因此无法将其用于依赖项跟踪。此外,虽然目前任何驱动程序和客户端都可以从 devfs 迁移到聚合服务,但驱动程序作者在执行此类迁移时目前面临着重大挑战。
摘要
我们建议通过迁移此类访问权限来使用由组件管理器中介的聚合服务,从而改变非驱动程序访问驱动程序的方式。此迁移将同时包含拓扑路径 (/dev/sys/) 和类路径 (/dev/class/)。在此过程中,使用 fuchsia.device/Controller 协议的客户端对驱动程序的任何剩余访问权限也将被移除。此 RFC 将介绍在迁移到聚合服务时面临的挑战,并提出解决方案以确保驱动程序作者不会受到不当影响。我们还希望为驱动程序访问提供更高的标准化程度,以改善代码健康状况并降低编写驱动程序和客户端的难度。
利益相关方
辅导员:
审核者:
- cja@
- suraj@
- csuter@
- jfsulliv@
- 组件框架团队
已咨询:
- 驱动程序团队 / 驱动程序作者
共同化:
此 RFC 已通过驱动程序框架团队的设计审核。
背景
直到最近,Fuchsia 中的驱动程序还不是组件,非驱动程序与驱动程序通信的唯一方式是通过 devfs。驱动程序管理器会维护 devfs 并填充两个与此相关的分支:/dev/sys/ 和 /dev/class。
- /dev/sys/ 从主板驱动程序开始填充,表示驱动程序管理器中父子绑定关系的层次结构。此分支中的路径称为“拓扑路径”。具有“dev-topological”目录访问权限的组件可以打开拓扑路径。
- /dev/class/ 由驱动程序在添加子设备时指定类名称来填充。然后,驱动程序管理器会向 /dev/class/
/ 添加与驱动程序对应的条目。这样一来,输出类似的驱动程序就可以归为一组,例如,所有摄像头都可以在 /dev/class/camera 中有一个条目。此分支中的路径称为“类路径”。 具有“dev-class”目录访问权限的组件可以打开类路径,而该权限通常会进一步限制为仅可访问特定类文件夹。
任一分支中的驱动程序条目都允许对驱动程序进行两种类型的访问:device_controller 和 device。
- device_controller 提供了一个 fuchsia.device/Controller 接口,该接口允许组件查询、绑定和重新绑定驱动程序。
- 设备向驱动程序提供特定于驱动程序的接口,该接口实际上与驱动程序在添加子设备时指定的类名称相关联。例如,目前添加类名称为“camera”的设备的所有驱动程序都提供 fuchsia.hardware.camera 接口,并且连接到 /dev/class/camera/000(如果存在)将实例化与该驱动程序的 fuchsia.hardware.camera 接口的连接。
在过去几年中,我们一直致力于隔离和移除 fuchsia.device/Controller 接口的使用,而移除这些使用将被视为超出此 RFC 的范围。此处提及该接口是为了实现完整性,并说明不会通过聚合服务提供类似的接口。
设计初衷
此迁移的主要目标是移除用于访问驱动程序的 devfs 系统,并将其替换为更标准化的基于服务的连接模型。这种方法具有许多优势,其中许多优势都与 devfs 的不足之处有关:
- 提高了依赖项和关联关系的可见性。通过让客户端通过服务功能进行连接,组件管理器可以跟踪连接并维护准确的依赖关系图。使用 devfs 时无法实现这一点,因为其结构对组件管理器来说在很大程度上是不透明的。
- 服务提供了一种记录更完善、更灵活的方式来导出驱动程序的功能。使用 devfs 时,每个设备实例只能提供一种协议,并且协议类型永远不会明确定义。对于聚合服务,可以提供多种协议,每种协议都会在服务定义中明确列出。
- 更明确且严格控制的访问权限。组件必须指定其请求的服务类型,这与 devfs 中的拓扑路径不同,后者可允许组件访问任何驱动程序。
- 移除对拓扑路径的依赖。拓扑路径可让您了解驱动程序管理器如何装载每个驱动程序。因此,对拓扑路径进行硬编码表示依赖于驱动程序框架的内部运作,而这正是我们希望避免的。
此外,devfs 访问可以通过多种方式实现,包括同步和异步,以及通过类路径和拓扑路径。我们希望提供一种统一的方法来连接到驱动程序和非驱动程序提供的服务,这应该可以改善代码健康状况并降低编写驱动程序和客户端的难度。
要求
迁移必须为所有当前的 devfs 用户提供解决方案,以免丢失任何功能。 此外,驱动程序和客户端必须能够执行软迁移,添加服务并连接到服务,而无需提供服务的所有客户端和驱动程序同时迁移。
设计
驱动程序框架工作
每个客户端或驱动程序从 devfs 的迁移都必须单独处理。不过,驱动程序框架需要执行一些工作来帮助解决上述问题。
类名称 <-> 协议 / 服务映射
为了便于转换,系统会在 C++ 标头中整理类名称、协议名称和服务名称之间的映射。目前还没有明确的映射关系,客户端和驱动程序之间只是隐式约定每个类对应于特定的 FIDL 协议。明确指定此映射关系可实现以下步骤:
- 确保
/dev/class/<classname>的客户端和服务器就一组协议达成一致 - 允许客户从连接到
/dev/class/<classname>迁移到fuchsia.hardware.<protocolname>。
此映射将需要为每个类名称创建一个服务(目前有大约 200 个可能的类名称)。对于目前仅通过拓扑路径访问的服务,可能还需要在此映射中创建其他条目。此映射仅在迁移期间有效。迁移完每个类后,可以移除映射中的相应条目。
公交信息传播
移除拓扑路径后,驱动程序管理器将负责将总线信息从总线驱动程序传播到所有后代驱动程序,以用于识别服务实例。借助驱动程序库,您可以更轻松地存储和检索此信息。
启用软迁移:
当驱动程序和客户端从 devfs 迁移到服务时,我们必须确保一方能够处理或提供这两种连接类型,以提供连接的连续性。此外,我们必须确保另一侧不提供这两种连接类型,以防止出现重复条目。例如,如果驱动程序 A 和 B 都向客户端 C 和 D 提供 Foo 连接,那么如果驱动程序 A 简单地切换到服务,客户端就必须在 devfs 和服务之间进行选择,然后只能看到一个产品。解决方案是多步迁移。有多种方法可以实现这种软迁移,但需要在其中一方进行一些额外的工作。 无论选择哪种方案,迁移阶段都可以按课程推进,以简化发布流程,同时还允许不同课程自定义其迁移策略。
建议的选项:通过 devfs 自动宣传服务
修改 devfs 以针对每个 devfs 条目自动宣传服务。服务类型将通过类名称来指示,使用前面指示的类名称到服务的映射。在同一项更改中,devfs 还会将与映射的类名称对应的所有服务功能公开给引导组件。为类名启用服务后,客户端可以转换为使用服务,然后驱动程序切换到广告服务。驱动程序迁移只需移除 DevfsAddArgs,并改为调用 AddService。最大的缺点是,在转换的类类型的所有客户端上,驱动程序转换会被阻止。
迁移顺序如下: 1) 更改 devfs 以宣传服务 2) 客户端转换为服务 3) 驱动程序宣传服务并移除 devfs
替代方案 1:创建客户端工具:DeviceAndServiceWatcher
创建一个工具,当给定类名称 / 服务名称时,该工具会连接到聚合服务和 /dev/class/
迁移顺序如下: 1) 添加 DeviceAndServiceWatcher 2) 客户端迁移为使用 DeviceAndServiceWatcher 3) 驱动程序宣传服务并移除 devfs 4) 在所有迁移完成后,从 DeviceAndService Watcher 中移除 devfs 逻辑
替代方案 2:延迟移除 devfs
让驱动程序在宣传服务后继续提供 devfs 条目。 客户端转换后,驱动程序可以移除 devfs 代码。这样做的好处是不会阻止驱动程序转换,不过后续仍需对驱动程序进行更改。此外,这还有几个缺点:新的 AddService 代码不会立即得到执行,因此当客户端最终切换到使用服务时,除了驱动程序必须进行的更改(以移除 devfs 代码)之外,可能还需要对驱动程序进行其他更改。
迁移顺序如下: 1) 驱动程序添加服务 2) 客户端转换为服务 3) 驱动程序移除 devfs
实现
对于与特定类名称交互的每组驱动程序和客户端,此迁移可以逐步完成。需要提供一些工具和库,驱动程序框架将执行多次迁移,以确保库满足迁移的驱动程序和客户端的需求。在确保迁移过程足够符合人体工程学后,我们将添加类名称迁移作为开放任务,供更广泛的 Fuchsia 社区参与。系统会识别出需要更多关注的特定类/接口,并需要采取更协调一致的方法。这包括仍在使用 fuchsia.device/Controller 接口的任何客户端,在这些客户端迁移之前,需要先解决此问题。所有驱动程序和客户端都迁移到服务后,即可移除 devfs 的内部实现。
迁移机制
为了进一步细分迁移的实现方式,下面列出了转换特定类别的驱动程序的步骤:
1) 添加了类名称 <-> 协议 / 服务映射
- 必须根据当前使用情况推断出类与 FIDL 协议的映射。
- 然后,需要创建一个服务来封装该协议,并将类到服务的映射记录在相应的标头中。这样一来,devfs 就可以开始宣传该服务了。
- 然后,需要更新 CML 文件,以将服务路由到客户端。
2) 客户端迁移
客户端迁移的复杂程度各不相同,因为客户端可能使用拓扑路径或类路径。客户迁移可分为两个阶段:确定所需的服务提供商,以及连接到该服务。使用类路径的客户端可能已完成第一步。
确定所需的服务提供商
如果客户端执行以下任何操作,则必须实现此迁移步骤:
- 使用拓扑路径
- 使用硬编码的类路径(例如 /dev/class/camera/000)
- 连接到给定 devfs 条目的
device_controller并调用GetTopologicalPath()。
在上述每种情况下,客户端都从服务/FIDL 接口的提供方(或看起来是)同步已知的情况迁移到聚合服务,其中实例将异步添加和移除,并且实例名称不能用于标识。
此类客户可选择以下几种方案:
- 连接到第一个可用的服务实例。对于仅需要一个服务提供方(例如电源控制器)的简单工具和测试,这通常就足够了。这还允许客户端保持连接是同步的假设。这通常不是首选解决方案,因为它忽略了系统的异步特性。
- 查询每个服务实例的信息,以确定它是否是正确的实例。您可以通过以下两种方式实现这一目标:
- 连接到每个服务实例的 FIDL 接口。许多 FIDL 协议已指定有关设备的信息(例如,
fuchsia.camera/Control协议提供GetInfo调用)。如果所提供的协议目前不包含身份识别信息,则可以对其进行修改以添加此类特定于设备的信息。 - 向服务添加额外的
GetInfo协议,该协议可以包含设备识别信息。将身份识别接口与主要通信接口分离具有以下几项优势:- 识别接口可以主要由驱动程序库处理,因为它提供了一组静态信息。
- 识别接口最好能处理多个并发连接,以帮助进行查询,而大多数驱动程序无法处理与其他接口的多个并发连接。
- 分离识别逻辑可让未来的工作更轻松地将识别步骤集成到框架中。
GetInfo接口的主要缺点是需要从驱动程序中获取更多信息才能填充,因此 devfs 更难自动提供。在这种情况下,驱动程序需要修改其 DevfsAddArgs 以添加相关数据,而 devfs 会处理该接口的服务。驱动程序必须先执行此操作,然后客户端才能转换为服务。
- 连接到每个服务实例的 FIDL 接口。许多 FIDL 协议已指定有关设备的信息(例如,
- 未来可能会提供一种选项,用于向组件添加标记以指明有关特定设备的信息。然后,客户端可以查询与每个服务实例关联的标记。
设备特定信息和总线信息:
有许多信息可以帮助客户端在连接到聚合服务时识别正确的驱动程序。此信息可能包括设备 ID (DID)、产品 ID (PID)、供应商 ID (VID),以及传递给驱动程序的任何数量的元数据。不过,在从拓扑路径迁移时,客户端如果不使用拓扑路径,可能会丢失三条信息:驱动程序名称(驱动程序已知)、总线类型和总线 ID/地址。为了弥补总线信息的丢失,驱动程序框架将添加从总线驱动程序向所有后代传播总线类型和地址的功能。目前,此功能以元数据的形式提供给多种类型的总线驱动程序(例如 i2c),因此实现将仅涉及将总线类型和地址标记为特定的元数据条目,并确保它始终传播到子级。然后,驱动程序可以使用上述选项之一向客户端提供公交信息。
连接到服务提供商
一旦客户端确定了要连接到的服务实例,连接本身就与连接到类路径非常相似。
- 客户端必须确保它有权访问相应功能 - 它可以将“dev-class”功能替换为它期望的服务功能。根据客户端的拓扑位置,可能需要在多个文件中执行此操作。另请注意:某些分片包含“dev-class”功能的传播,因此可能并不总是很明显该功能来自何处。
- 对于 devfs 和服务,理想的连接方法都涉及 DeviceWatcher。此库会异步通知客户端新的服务提供程序实例。最大的变化应该是被监控的位置。
3) 驱动程序迁移
如上所述,如果使用服务中的 GetInfo 接口作为驱动程序,则会进行广告宣传,驱动程序需要修改其 DevfsAddArgs 以添加相关数据,而 devfs 会处理该接口的服务。这必须由驱动程序完成,然后客户端才能转换为服务。
驱动程序迁移涉及调用 AddService,并可能将所服务的协议封装在服务中。如果使用 GetInfo 接口,驱动程序将提供一个包含要提供服务的静态信息的驱动程序库,而不是在 DevfsAddArgs 中传递该信息。在添加 AddService 调用的同时,驱动程序将从 NodeAddArgs 条目中移除 devfs_args。这将停用与驱动程序关联的所有 devfs 条目。
性能
此次迁移对效果的影响微乎其微。与组件建立连接通常仅在组件的生命周期内完成一次,并且连接类型的差异很小。
工效学设计
在设计此迁移时,我们一直将人体工学问题放在首位。主要问题涉及通过拓扑路径移除访问权限:
- 如果不使用拓扑路径,则更难区分某些驱动程序,因为客户端必须连接到每个服务才能确定它是否是所需的服务。通过提供客户端库,让客户端能够提供条件函数,从而帮助选择正确的设备,而无需添加样板代码,即可克服这一缺点。
- 驱动程序可能需要添加包含身份识别信息的接口或函数。还可以提供客户端库来帮助驱动程序处理任何
GetInfo协议。 - 您将无法再使用 shell 通过 /dev/sys 目录浏览设备树。我们希望其他 ffx 工具能够满足这些需求。 此外,还需要额外的路由才能将每项服务功能路由到客户端。
总而言之,我们认为此次迁移带来的积极收益远超额外的复杂性。
向后兼容性
我们为使用 devfs 的驱动程序和客户端提供软迁移路径,但转换某个类后,我们将停止对该类的 devfs 支持。我们的目标是移除所有旧版接口实例,因此我们不打算在初始迁移期结束后提供任何向后兼容性支持。
所有驱动程序和客户端迁移完毕后,系统会移除 devfs,这意味着任何其他客户端都无法通过 devfs 访问驱动程序。
安全注意事项
此变更将通过以下几种方式来提高安全性:
- 客户端将无法再使用拓扑路径,这种路径可提供对子树中任何驱动程序的不受限制的访问权限。
- 只要客户端有权访问驱动程序,就无法再使用 fuchsia.device/Controller 接口
- 系统将更明确地跟踪每个客户端可访问的服务和协议,并且系统和组件关闭将变得更加透明,因为可以明确跟踪依赖项。
- 现在,公交信息将与司机分享,而之前不会。 这些信息将以枚举形式进行编码,以限制发布 PII 的可能性。虽然司机不需要知道其父级公交车的信息,但我们认为与司机分享此信息不会带来安全风险。
隐私注意事项
不适用
测试
此迁移的测试将主要包括端到端测试和冒烟测试,以及任何相关的主机测试,因为如果出现任何错误,客户端将无法连接到其服务提供商。由于此变更会影响许多主机测试,因此我们建议迁移者手动测试所有受影响的主机测试。我们正借此机会(不再依赖拓扑路径)来改进设备枚举测试。设备枚举测试将不再检查特定路径,而是检查是否正在加载所有驱动程序软件包。
使用 DriverTestRealm 的测试可能会受到迁移的影响,并且在迁移方面应与其他客户端同等对待。需要添加路由,以适应使用 DriverTestRealm 的任何测试。
文档
我们计划在迁移过程中详细说明如何迁移一类驱动程序和客户端。我们未来还将提供有关如何以异步方式连接到服务的示例和最佳实践。随着 devfs 的使用被淘汰,我们将清理文档以移除对它的引用,并确保所有驱动程序文档都描述了广告和连接到服务。考虑到驱动程序作者会越来越多地处理功能路由,我们计划扩充当前文档,以说明如何确保功能路由到正确的组件,以及如何调试功能路由方面经常遇到的问题。
缺点、替代方案和未知因素
缺点
从 devfs 迁移到服务的主要缺点是,服务连接现在将是强类型,并且功能路由现在必须指定要路由的服务。这两项更改都使系统在设置方面变得不太宽容,但也提供了更多检查来确保系统得到正确使用。
迁移到服务模式的另一个缺点是组件连接的稳定性可能会降低。汇总服务相对较新,可能存在一些通过此迁移暴露出来的意外极端情况。此外,此迁移还涉及驱动程序从由驱动程序框架团队管理的系统迁移到由其自己的团队支持的组件框架。团队之间始终存在沟通不畅的风险,这可能会导致所需功能失去支持。
替代方案
实施部分讨论了多种替代迁移方案。此迁移的主要替代方案是不执行迁移。虽然新驱动程序和客户端仍会优先使用服务,但 devfs 可以保留下来供旧驱动程序使用。这样做会使添加新组件变得更加困难,并会继续使关机流程复杂化,从而增加我们的 flake 数量。保留 devfs 还会继续阻碍我们清理驱动程序生命周期,并会因我们被迫绕过旧版系统而导致团队进展缓慢。