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 中的启动钩子和 dfv1 中的初始化钩子之前进行枚举。当然,如果您希望驱动程序绑定到您的驱动程序,您仍然需要为此目的添加设备/节点。

确定协议服务器实现

转换驱动程序在 DFv1 和 DFv2 驱动程序之间有所不同,但在这两种情况下,您都应该已经有一个充当协议服务器实现的类。它可以继承自 fidl::WireServerfidl::Server,或者在 DFv1 中,它可以继承自 mixin:ddk::Messageable<Protocol>::Mixin不过,ddk::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 仍要求您在从绑定钩子返回之前至少添加 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 中移除服务功能

附录

什么是服务会员?

服务成员是指服务中的特定协议。服务成员有自己的类型,然后可以在 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,以设置驱动程序测试 realm:

    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. 连接到 realm 的 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> 将列出您的客户端使用和公开的功能,并在路由失败时提供一些错误指示。

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

赞助商

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