目標與動機
目前非驅動程式對大多數驅動程式庫的存取權,都是由裝置檔案系統 (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的測試,請按照「Using Services with DriverTestRealm」一文中的操作說明進行。
完成上述步驟後,即可繼續進行用戶端遷移。遷移作業包含 2 個步驟:
將從 dev-class 目錄路由傳送的能力變更為服務
Devfs 存取權是由 directory 能力授予。服務功能使用標記 service。視驅動程式庫到元件的路徑而定,您需要更新多個 .cml 檔案。
變更通常會採用以下形式:
元件的父項
| Devfs | 服務 |
|---|---|
|
|
用戶端元件
| Devfs | 服務 |
|---|---|
|
|
範例 CL:
如要進一步瞭解轉送功能,請參閱「Connect 元件」頁面。如果遇到問題,請參閱「疑難排解」一節和下方的「偵錯」一節。
連線至服務執行個體,而非 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()
荒漠油廠
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 中的啟動掛鉤和 dfv1 中的 init 掛鉤之前,應先列舉所有非動態列舉的服務執行個體。當然,如果您希望驅動程式繫結至驅動程式,仍需為此目的新增裝置/節點。
找出通訊協定伺服器實作項目
轉換驅動程式庫方式因 DFv1 和 DFv2 驅動程式而異,但無論是哪種情況,您都應該已有做為通訊協定伺服器實作項目的類別。這項屬性可能會從 fidl::WireServer 或 fidl::Server 繼承,或在 DFv1 中使用混入: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 檔案,包括 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是服務成員的 C++ 型別- 這種類型實際上只會用於
ServiceMemberWatcher等工具。 - 這個服務成員會以
speaker顯示在<instance_name>目錄中 - 連線至
/svc/fuchsia.example.EchoService/<instance_name>/speaker會提供預期通訊協定fuchsia_example::Broadcast的管道。
- 這種類型實際上只會用於
搭配 DriverTestRealm 使用服務
如要使用 DriverTestRealm 測試用戶端,請按照下列步驟,將受測驅動程式庫的服務能力傳送至測試程式碼。
在選項中選取
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());荒漠油廠
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?;連線至領域的
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);荒漠油廠
// 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:
範例
本指南中已提供許多範例,但為了方便參考,我們在此彙整了所有範例:
- 透過 overnet-usb 遷移 (所有 CL 都在同一個位置)
- Migrating usb-peripheral
- 遷移 usb-ctrl (較舊,不建議)
偵錯
遷移至服務時,最常見的問題是未正確連結所有功能。在記錄中尋找類似下列內容的錯誤:
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>會列出用戶端使用及公開的功能,並在路由失敗時指出問題所在。
贊助者
如有任何問題或想瞭解最新進度,請與我們聯絡: