目标和动机
目前,非驱动程序对驱动程序的大多数访问都由设备文件系统 (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 presubmit
、MegaCQ
和 Run-All-Tests: true
运行,以确保所有客户端都已转换。
如需迁移 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
组件。
将客户端转换为使用服务
客户端迁移涉及以下步骤:
我的客户是否需要执行额外的迁移步骤?
某些客户端迁移可能需要执行额外的步骤,具体取决于它们使用 devfs 的方式:
- 如果您的客户端使用拓扑路径(以
/dev/sys
开头)或依赖于按顺序排列的实例名称(例如/dev/class/block/000
),请按照识别服务实例中的说明正确识别客户端的所需服务实例。 - 如果您要迁移使用
DriverTestRealm
的测试,请按照使用 DriverTestRealm 提供服务中的说明操作
完成上述步骤后,您可以继续进行客户端迁移。迁移涉及 2 个步骤:
将从 dev-class
目录路由到服务的 capability 进行更改
Devfs 访问权限由 directory
capability 授予。服务功能使用标记 service
。您需要根据驱动程序到组件的路线更新多个 .cml
文件。
更改通常采用以下形式:
组件的父级
Devfs | 服务 |
---|---|
|
|
客户端组件
Devfs | 服务 |
---|---|
|
|
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
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::WireServer
或 fidl::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 文件的 Capability
和 Expose
字段中:
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 的测试客户端需要执行一些额外的步骤,才能将服务功能从被测驱动程序路由到测试代码。
在调用 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?;
然后,您需要将公开内容添加到 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?;
最后,您需要连接到 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:
示例
本指南中提供了示例链接,但为了方便您参考,我们在此处汇总了这些链接:
- 通过 overnet-usb 迁移(一站式 CL)
- 迁移 usb-peripheral
- 迁移 usb-ctrl(较旧,不建议)
调试
迁移到服务时最常见的问题是未正确关联所有功能。在日志中查找类似于以下内容的错误:
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>
会列出您的客户端使用的功能和公开的功能,并在路由失败时提供一些指示,以便了解出现了什么问题。
赞助商
如有疑问或需要了解最新状态,请与我们联系: