目標和動機
目前,非驅動程式存取的大多數驅動程式庫,都是由裝置檔案系統 (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 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
目錄路由至服務的能力
directory
能力會授予 Devfs 存取權。服務功能使用標記 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
會執行類型檢查,自動取得服務和成員名稱。這項 API 可同步和非同步使用,直接連線至單一通訊協定服務的通訊協定。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()
荒漠油廠
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());
...
}
荒漠油廠
-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 鉤子和 dfv1 中的 init 鉤子。當然,如果您希望驅動程式綁定至自己的驅動程式庫,仍需為此新增裝置/節點。
找出通訊協定伺服器實作項目
轉換驅動程式庫方式在 DFv1 和 DFv2 驅動程式之間有所不同,但在兩種情況下,您都應該已擁有一個類別,用來做為通訊協定的伺服器實作項目。它可以繼承 fidl::WireServer
或 fidl::Server
,或是在 DFv1 中使用 mixin: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
,即可完成所需的能力轉送變更。
清除
所有驅動程式和用戶端都已遷移至服務後,您可以刪除 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
是 service member 的 C++ 類型- 這類型實際上只會由
ServiceMemberWatcher
等工具使用。 - 這個服務成員會以
speaker
的形式顯示在<instance_name>
目錄中 - 連線至
/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());
荒漠油廠
// 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?;
接著,您需要將公開資訊新增至領域啟動參數:
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));
荒漠油廠
// 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?;
最後,您需要連線至領域的
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);
荒漠油廠
// 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 周邊裝置
- 遷移 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>
會列出用戶端使用的功能和公開的功能,並在路由失敗時提供一些錯誤指示。
贊助商
如有任何問題或想瞭解最新進度,請與我們聯絡: