驅動程式 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 目錄路由至服務的能力

directory 能力會授予 Devfs 存取權。服務功能使用標記 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 會執行類型檢查,自動取得服務和成員名稱。這項 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::WireServerfidl::Server,或是在 DFv1 中使用 mixin:ddk::Messageable<Protocol>::Mixinddk::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,即可完成所需的能力轉送變更。

清除

所有驅動程式和用戶端都已遷移至服務後,您可以刪除 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::Speakerservice member 的 C++ 類型
    • 這類型實際上只會由 ServiceMemberWatcher 等工具使用。
    • 這個服務成員會以 speaker 的形式顯示在 <instance_name> 目錄中
    • 連線至 /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());
    

    荒漠油廠

    // 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. 接著,您需要將公開資訊新增至領域啟動參數:

    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?;
    
  3. 最後,您需要連線至領域的 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:

範例

指南中已提供相關範例的連結,但為了方便參考,我們在此列出這些範例:

偵錯

遷移至服務時最常見的問題,就是無法正確連結所有功能。請在記錄中尋找類似下列的錯誤:

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> 會列出用戶端使用的功能和公開的功能,並在路由失敗時提供一些錯誤指示。

贊助商

如有任何問題或想瞭解最新進度,請與我們聯絡: