废弃驱动程序 devfs

目标和动机

目前,非驱动程序对驱动程序的大多数访问都由设备文件系统 (devfs) 中介。不过,devfs 将被弃用。如需详细了解弃用 devfs 的原因,请参阅 Devfs 废弃 RFC。简要总结如下:

  • 很难将 devfs 客户端限制为使用特定协议。
  • 该 API 会使驱动程序难以公开多个协议。
  • 拓扑路径和硬编码类路径会向客户端公开内部实现细节,并可能导致意外中断。
  • 由于组件管理器不知道 devfs 连接,因此无法将其用于依赖项跟踪

从 devfs 迁移到服务会改变非驱动程序访问驱动程序的方式,具体方法是从 /dev/class 中的目录切换为使用由组件管理器中介的汇总服务。

技术背景

贡献者应熟悉使用组件框架的驱动程序、FIDL 客户端和 capability 路由。

选择任务

/dev/class 中的每个条目都代表一个要迁移的类。如需查看类名称的完整列表以及它们将迁移到的服务,请参阅:src/devices/bin/driver_manager/devfs/class_names.h

每个类至少有一个驱动程序和一个客户端,有时还会有多个。理想情况下,应将类作为一项工作进行迁移,但每个客户端和驱动程序都可以作为单独的 CL 安全地迁移,而不会破坏。

请注意,某些客户端的功能会增加迁移的复杂性。如需了解您的客户是否需要执行额外的迁移步骤,请参阅:我的客户是否需要执行额外的迁移步骤

迁移 devfs 类

迁移 devfs 类分为多个步骤,这允许对非树驱动程序和客户端进行非破坏性更改。在适用的情况下,这些步骤可以合并到单个 CL 中。最后一步应始终使用 lcs presubmitMegaCQRun-All-Tests: true 运行,以确保所有客户端都已转换。

如需迁移 devfs 类,请执行以下操作:

  1. 验证 devfs 是否正在自动为您的课程宣传服务
  2. 将客户转换为使用服务
  3. 将司机转换为宣传服务的广告客户
  4. 从 devfs 中移除服务和类名称

验证 devfs 是否会自动为您的课程宣传服务

class_names.h 中查找您的类名称。检查服务和成员名称是否正确。

例如,如果您有一个类名称 echo-example,可让您访问协议 fuchsia.example.Echo,并且具有 fidl 文件:

  library fuchsia.example;

  protocol Echo { ... }

  service EchoService {
      my_device client_end:Echo;
  };

class_names.h 中的条目应如下所示:

{"echo-example", {ServiceEntry::kDevfsAndService, "fuchsia.example.EchoService", "my_device"}},

如果您对命名方式感到困惑,请参阅什么是服务成员?

如果您的服务已列出且正确无误,那么每当有具有相应类名称的设备发布到 /dev/class 文件夹时,系统都应自动通告该服务。服务功能也会从驱动程序集转移到 #core 组件。

将客户端转换为使用服务

客户端迁移涉及以下步骤:

  1. 检查客户是否需要执行额外步骤
  2. 将从 dev-class 目录路由到服务的 capability 进行更改
  3. 连接到服务实例,而不是 devfs 实例

我的客户是否需要执行额外的迁移步骤?

某些客户端迁移可能需要执行额外的步骤,具体取决于它们使用 devfs 的方式:

  • 如果您的客户端使用拓扑路径(以 /dev/sys 开头)或依赖于按顺序排列的实例名称(例如 /dev/class/block/000),请按照识别服务实例中的说明正确识别客户端的所需服务实例。
  • 如果您要迁移使用 DriverTestRealm 的测试,请按照使用 DriverTestRealm 提供服务中的说明操作

完成上述步骤后,您可以继续进行客户端迁移。迁移涉及 2 个步骤:

将从 dev-class 目录路由到服务的 capability 进行更改

Devfs 访问权限由 directory capability 授予。服务功能使用标记 service。您需要根据驱动程序到组件的路线更新多个 .cml 文件。

更改通常采用以下形式:

组件的父级

Devfs服务
 {
      directory: "dev-class",
      from: "parent",
      as: "dev-echo-example",
      to: "#timekeeper",
      subdir: "echo-example",
  },
 {
      service: "fuchsia.example.EchoService",
      from: "parent",
      to: "#timekeeper",
  },

客户端组件

Devfs服务
 use: [
  {
    directory: "dev-echo-service",
    rights: [ "r*" ],
    path: "/dev/class/echo-service",
  },
 ],
 use: [
    { service: "fuchsia.example.EchoService", },
 ],

CL 示例:

连接到服务实例,而不是 devfs 实例

使用 devfs 时,您需要连接到以下位置的协议

/dev/class/<class_name>/<instance_name>

服务只是包含协议的某个目录,由组件框架在 /svc/ 目录中按服务名称提供。因此,您可以通过以下网址连接到该服务提供的协议:

/svc/<ServiceName>/<instance_name>/<ServiceMemberName>

对于第 1 步中的示例,这将是:

/svc/fuchsia.example.EchoService/<instance_name>/my_device

对于 devfs 和服务,建议的方法是监控相应目录,以便实例显示。有各种工具可以帮助您完成此操作,您的客户很可能已经在使用其中一种工具:

  • std::filesystem::directory_iterator(不过,样式指南禁止这样做)
  • fsl::DeviceWatcher
  • 我们还专门针对服务添加了一款新工具: ServiceMemberWatcher

您可以继续使用现有工具,也可以(推荐)改用 ServiceMemberWatcher 来获享类型检查的好处。

ServiceMemberWatcher 会执行类型检查,以自动获取服务和成员名称。它可以同步和异步使用,以直接连接到单协议服务的协议。Rust 等效项为 Service

同步 C++

SyncServiceMemberWatcher<fuchsia_examples::EchoService::MyDevice> watcher;
zx::result<ClientEnd<fuchsia_examples::Echo>> result = watcher.GetNextInstance(true);

异步 C++

// Define a callback function:
void OnInstanceFound(ClientEnd<fuchsia_examples::Echo> client_end) {...}
// Optionally define an idle function, which will be called when all
// existing instances have been enumerated:
void AllExistingEnumerated() {...}
// Create the ServiceMemberWatcher:
ServiceMemberWatcher<fuchsia_examples::EchoService::MyDevice> watcher;
watcher.Begin(get_default_dispatcher(), &OnInstanceFound, &AllExistingEnumerated);
// If you want to stop watching for new service entries:
watcher.Cancel()

Rust

  let device = Service::open(fidl_examples::EchoServiceMarker)
      .context("Failed to open service")?
      .watch_for_any()
      .await
      .context("Failed to find instance")?
      .connect_to_device()
      .context("Failed to connect to device protocol")?;

示例:

备选方案:更改要监控的目录,并将服务成员文件夹添加到现有代码

您只需更改几行代码,即可更新现有代码: - 监控目录 /svc/<ServiceName> 而非 /dev/class/<class_name> - 找到实例后,连接到服务成员文件夹条目,而不是实例文件夹本身。

C++

using std::filesystem;
- constexpr char kDevicePath[] = "/dev/class/echo-example";
+ constexpr char kServiceName[] = "/svc/fuchsia.example.EchoService";
+ const std::filesystem::path kServiceMember = "my_device";
- for (auto& dev_path : std::filesystem::directory_iterator(kDevicePath)) {
+ for (auto& instance_path : std::filesystem::directory_iterator(kServiceName)) {
+     directory_entry dev_path({instance_path / kServiceMember});
  auto dev = component::Connect<i2c::Device>(dev_path.path().c_str());

  ...
}

Rust

-const ECHO_DIRECTORY: &str = "/dev/class/echo-example";
+const ECHO_DIRECTORY: &str = "/svc/fuchsia.example.EchoService";
+const ECHO_MEMBER_NAME: &str = "/my_device";
let mut dir = std::fs::read_dir(ECHO_DIRECTORY).expect("read_dir failed")?;
let entry = dir.next()
    .ok_or_else(|| anyhow!("No entry in the echo directory"))?
    .map_err(|e| anyhow!("Failed to find echo device: {e}"))?;
let path = entry.path().into_os_string().into_string()
    .map_err(|e| anyhow!("Failed to parse the device entry path: {e:?}"))?;

- fdio::service_connect(&path, server_end.into_channel())
+ fdio::service_connect(&(path + ECHO_MEMBER_NAME), server_end.into_channel())

将驱动程序转换为用于宣传服务的广告

使用服务时,您无需再使用 DdkAdd (dfv1) 或 AddOwnedChildNode (dfv2) 即可发布实例。不过,您只需随时发布服务实例即可,因为它与驱动程序实例(而非特定设备/节点)相关联。不过,在 dfv2 中完成 start hook 和 dfv1 中完成 init hook 之前,应枚举所有非动态枚举的服务实例。当然,如果您希望驱动程序绑定到您的驱动程序,则仍需要出于此目的添加设备/节点。

确定协议服务器实现

转换驱动程序在 DFv1 和 DFv2 驱动程序之间有所不同,但在这两种情况下,您都应该已经有一个类用作协议的服务器实现。它可以从 fidl::WireServerfidl::Server 继承,或者在 DFv1 中,它可以使用混入:ddk::Messageable<Protocol>::Mixin。不过,ddk::Messageable 已废弃,因此请勿在新代码中使用它。

创建 ServiceInstanceHandler

接下来,您需要创建一个 ServiceInstanceHandler:每当有人连接到您的服务时都会调用的函数。幸运的是,fidl::ServerBindingGroup 可以非常轻松地实现这一点。

将绑定组添加到服务器类:

fidl::ServerBindingGroup<fuchsia_examples::EchoService> bindings_;

然后,您可以创建 ServiceInstanceHandler。此示例中的 this 指向您在上一步中确定的服务实例。

  fuchsia_examples::EchoService::InstanceHandler handler({
      .my_device = bindings_.CreateHandler(this, fdf::Dispatcher::GetCurrent()->async_dispatcher(), fidl::kIgnoreBindingClosure),
  });

请注意,您需要为服务中的每个协议提供单独的 ServerBindingGroup 或至少 CreateHandler 调用。(大多数服务只有一个协议。)在此示例中,device 是服务定义中的成员协议的名称。

DFv1

zx::result add_result =
      DdkAddService<fuchsia_examples::EchoService>(std::move(handler));

您无需再添加设备即可宣传服务。不过,dfv1 仍然要求您在从绑定钩子返回之前至少添加 1 个设备,因此请谨慎移除所有设备。

DFv2

zx::result add_result =
    outgoing()->AddService<fuchsia_examples::EchoService>(std::move(handler));

如需停止向 devfs 投放广告,您需要删除所有 DevfsAddArgs。 您还可以删除 driver_devfs::Connector 类以及它调用的 Serve 函数。不过,请保留 fidl::ServerBindingGroup 以供您的协议使用。

- zx::result connector = devfs_connector_.Bind(async_dispatcher_);
- auto devfs = fuchsia_driver_framework::wire::DevfsAddArgs::Builder(arena)
-                 .connector(std::move(connector.value()))
-                 .class_name("echo-example");
auto offers = compat_server_.CreateOffers2();
offers.push_back(fdf::MakeOffer2<fuchsia_example::EchoService>());
zx::result result = AddChild(kDeviceName,
-                               devfs.Build(),
                             *properties, offers);

从驱动程序公开服务

您必须将该服务添加到驱动程序的 cml 文件的 CapabilityExpose 字段中:

capabilities: [
    { service: "fuchsia.examples.EchoService" },
],
expose: [
    {
        service: "fuchsia.examples.EchoService",
        from: "self",
    },
],

如果 devfs 已向客户端通告该服务,那么将上述条目添加到驱动程序的 .cml 中应该是唯一需要进行的 capability 路由更改。

清理

将所有驱动程序和客户端迁移到服务后,您可以删除 src/devices/bin/driver_manager/devfs/class_names.h 中的类名称条目。这将阻止 devfs 通告 /dev/class/<class_name> 目录以及它所代表的服务。您还可以从 src/devices/bin/driver_manager/devfs/meta/devfs-driver.cml 中移除服务 capability

附录

什么是服务成员?

服务成员是指服务中的特定协议。服务成员有自己的类型,然后可以在 ServiceMemberWatcher 等工具中使用该类型,不仅指明服务,还指明其中的特定协议。

请考虑以下 fidl 定义:

library fuchsia.example;

protocol Echo { ... }
protocol Broadcast { ... }

service EchoService {
    speaker client_end:Broadcast;
    loopback client_end:Echo;
};

以下介绍了示例中的值:

  • fuchsia.example.EchoService 是服务 capability
    • 此值在 .cml 文件中使用,是服务目录的名称
  • fuchsia_example::EchoService 是服务的 C++ 类型
  • fuchsia_example::EchoService::Speaker服务成员的 C++ 类型
    • 此类型实际上仅供 ServiceMemberWatcher 等工具使用。
    • 此服务成员将在 <instance_name> 目录中显示为 speaker
    • 连接到 /svc/fuchsia.example.EchoService/<instance_name>/speaker 会为您提供一个预期协议为 fuchsia_example::Broadcast 的通道。

将服务与 DriverTestRealm 搭配使用

使用 DriverTestRealm 的测试客户端需要执行一些额外的步骤,才能将服务功能从被测驱动程序路由到测试代码。

  1. 在调用 realm.Build() 之前,您需要调用 AddDtrExposes

    C++

    auto realm_builder = component_testing::RealmBuilder::Create();
    driver_test_realm::Setup(realm_builder);
    async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
    std::vector<fuchsia_component_test::Capability> exposes = { {
        fuchsia_component_test::Capability::WithService(
            fuchsia_component_test::Service{ {.name = "fuchsia_examples::EchoService"}}),
    }};
    driver_test_realm::AddDtrExposes(realm_builder, exposes);
    auto realm = realm_builder.Build(loop.dispatcher());
    

    Rust

    // Create the RealmBuilder.
    let builder = RealmBuilder::new().await?;
    builder.driver_test_realm_setup().await?;
    
    let expose = fuchsia_component_test::Capability::service::<ft::DeviceMarker>().into();
    let dtr_exposes = vec![expose];
    
    builder.driver_test_realm_add_dtr_exposes(&dtr_exposes).await?;
    // Build the Realm.
    let realm = builder.build().await?;
    
  2. 然后,您需要将公开内容添加到 Realm 启动参数中:

    C++

    auto realm_args = fuchsia_driver_test::RealmArgs();
    realm_args.root_driver("fuchsia-boot:///dtr#meta/root_driver.cm");
    realm_args.dtr_exposes(exposes);
    fidl::Result result = fidl::Call(*client)->Start(std::move(realm_args));
    

    Rust

    // Start the DriverTestRealm.
    let args = fdt::RealmArgs {
        root_driver: Some("#meta/v1_driver.cm".to_string()),
        dtr_exposes: Some(dtr_exposes),
        ..Default::default()
    };
    realm.driver_test_realm_start(args).await?;
    
  3. 最后,您需要连接到 Realm 的 exposed() 目录以等待服务实例:

    C++

    fidl::UnownedClientEnd<fuchsia_io::Directory> svc = launcher.GetExposedDir();
    component::SyncServiceMemberWatcher<fuchsia_examples::EchoService::MyDevice> watcher(
        svc);
    // Wait indefinitely until a service instance appears in the service directory
    zx::result<fidl::ClientEnd<fuchsia_examples::Echo>> peripheral =
        watcher.GetNextInstance(false);
    

    Rust

    // Connect to the `Device` service.
    let device = client::Service::open_from_dir(realm.root.get_exposed_dir(), ft::DeviceMarker)
        .context("Failed to open service")?
        .watch_for_any()
        .await
        .context("Failed to find instance")?;
    // Use the `ControlPlane` protocol from the `Device` service.
    let control = device.connect_to_control()?;
    control.control_do().await?;
    

示例:本部分中的代码来自以下 CL:

示例

本指南中提供了示例链接,但为了方便您参考,我们在此处汇总了这些链接:

调试

迁移到服务时最常见的问题是未正确关联所有功能。在日志中查找类似于以下内容的错误:

WARN: service `fuchsia.example.EchoService` was not available for target `bootstrap/boot-drivers:dev.sys.platform.pt.PCI0`:
    `fuchsia.example.EchoService` was not offered to `bootstrap/boot-drivers:dev.sys.platform.pt.PCI0` by parent
For more, run `ffx component doctor bootstrap/boot-drivers:dev.sys.platform.pt.PCI0`

您还可以在系统运行时检查组件路由。ffx component 提供了许多有用的工具来诊断路由问题:

  • 调用 ffx component list 可获取组件名称列表。/ 表示父->子关系,这对于了解组件拓扑非常有用。
  • 调用 ffx component capability <ServiceName> 以查看哪些人会触碰该服务
  • ffx component doctor <your_client> 会列出您的客户端使用的功能和公开的功能,并在路由失败时提供一些指示,以便了解出现了什么问题。

赞助商

如有疑问或需要了解最新状态,请与我们联系: