Devfs 迁移指南

目标和动机

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

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

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

技术背景

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

选择任务

/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 目录路由的功能更改为服务
  3. 连接到服务实例,而不是 devfs 实例

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

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

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

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

将从 dev-class 目录路由的功能更改为服务

devfs 访问权限由 directory 功能授予。服务功能使用标记 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>::Mixinddk::Messageable 已废弃,因此请勿在新代码中使用它。

创建 ServiceInstanceHandler

接下来,您需要创建一个 ServiceInstanceHandler:一个在有人连接到您的服务时调用的函数。幸运的是,fidl::ServerBindingGroup 使此操作非常简单。

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

fidl::ServerBindingGroup<fuchsia_examples::Echo> 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 仍然要求您在从绑定 hook 返回之前至少添加 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 中应该是唯一需要的功能路由更改。

清理

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

附录

什么是服务成员?

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

请考虑以下 fidl 定义:

library fuchsia.example;

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

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

下面介绍了示例中的值:

  • fuchsia.example.EchoService 是服务功能
    • 这在 .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. 在选项中使用 driver_exposes 设置驱动程序测试领域:

    C++

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

    Rust

    let echo_capability = fuchsia_component_test::Capability::service::<fuchsia_examples::EchoServiceMarker>().into();
    let exposed_capabilities = vec![echo_capability];
    
    // Create the RealmBuilder.
    let builder = RealmBuilder::new().await?;
    builder.driver_test_realm_setup(Options::new().driver_exposes(exposed_capabilities), {}).await?;
    
    // Build the Realm.
    let realm = builder.build().await?;
    
  2. 连接到领域的 exposed() 目录以等待服务实例:

    C++

    fidl::UnownedClientEnd<fuchsia_io::Directory> svc_dir{
      realm.component().exposed().unowned_channel()->get()};
    component::SyncServiceMemberWatcher<fuchsia_examples::EchoService::Device> watcher(
        svc_dir);
    // Wait indefinitely until a service instance appears in the service directory
    zx::result<fidl::ClientEnd<fuchsia_examples::Echo>> echo_client =
        watcher.GetNextInstance(false);
    

    Rust

    // Connect to the `Device` service.
    let device = client::Service::open_from_dir(realm.root.get_exposed_dir(), fuchsia_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")?;
    

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

示例

示例已在整个指南中链接,但在此处进行了编译,以便于参考:

调试

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

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

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

  • 调用 ffx component list 以获取组件名称列表。/ 表示父级 -> 子级关系,这有助于了解组件拓扑。
  • 调用 ffx component capability <ServiceName> 以查看谁触及该服务
  • ffx component doctor <your_client> 将列出您的客户端使用 和公开的功能,并在路由失败时提供一些错误指示。

如需了解详情,请参阅连接组件 页面,尤其是在“问题排查”下。

赞助商

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