| RFC-0041:支援統一服務和裝置 | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 介紹服務的概念,也就是通訊協定的集合,其中可能有一或多個集合的執行個體。 |
| 作者 | |
| 提交日期 (年-月-日) | 2019-04-08 |
| 審查日期 (年-月-日) | 2019-04-23 |
摘要
介紹服務的概念,也就是通訊協定的集合,其中可能有一或多個集合的執行個體。
提振精神
在元件架構中,服務目前定義為單一通訊協定,且該通訊協定在 /svc 底下程序的命名空間中只能有一個執行個體。因此我們無法描述更複雜的關係:
- 以兩種不同形式表示的服務,視消費者而定,例如有兩個不同版本的通訊協定,如
FontProvider和FontProviderV2 - 將服務拆分為兩部分,根據存取層級授予功能,例如一般存取權與管理員存取權 (如
Directory和DirectoryAdmin),後者提供特殊存取權 - 這項服務包含許多不同的通訊協定,供不同消費者使用,例如用於電源管理的
Power,以及用於網路堆疊的Ethernet - 有多個執行個體的服務,例如提供
AudioRenderer的多個音訊裝置,或公開Printer的多部印表機
提供這項彈性可更清楚地表達服務,而不必使用服務中心等解決方法。有了這項彈性,我們就能將裝置定義為服務。具體來說,我們計畫演進 /svc/$Protocol,這表示「每個程序命名空間只能有一個通訊協定」:
/svc/$Service/$Instance/$Member
這會額外導入兩個間接層:服務 (例如印表機、乙太網路) 和執行個體 (例如 default、deskjet_by_desk、e80::d189:3247:5fb6:5808)。通訊協定的路徑會包含下列部分:
$Service:服務的完整類型,如 FIDL 中所宣告$Instance:服務執行個體的名稱,其中「default」慣例上用於表示可用的偏好 (或唯一) 執行個體$Member:FIDL 中聲明的服務成員名稱,其中該成員的聲明類型表示預期通訊協定
設計
服務的風味
首先,請考量我們打算支援的各種服務:
單一不重複的通訊協定:一個執行個體,一個通訊協定:
/svc/fuchsia.Scheduler/default/profile_provider多種通訊協定的組合:一個執行個體,多個通訊協定:
/svc/fuchsia.Time/default/network .../rough單一通訊協定的多個服務執行個體:MANY 個執行個體,ONE 個通訊協定:
/svc/fuchsia.hardware.Block/0/device .../1/device多個執行個體,具有不同的通訊協定集:MANY 個執行個體、MANY 個通訊協定:
/svc/fuchsia.Wlan/ff:ee:dd:cc:bb:aa/device .../power .../00:11:22:33:44:55/access_point .../power
語言
為了向 FIDL 導入服務概念並支援各種風格,我們將對 FIDL 語言進行下列變更:
- 新增
service關鍵字。 - 移除
Discoverable屬性。
service 關鍵字可讓我們編寫服務宣告,用來將一組通訊協定定義為服務成員。舉例來說,我們可以宣告不同類型的服務,如下所示:
單一不重複的通訊協定:一個執行個體,一個通訊協定:
service Scheduler { fuchsia.scheduler.ProfileProvider profile_provider; };多種通訊協定的組合:一個執行個體,多個通訊協定:
service Time { fuchsia.time.Provider network; fuchsia.time.Provider rough; };單一通訊協定的多個服務執行個體:MANY 個執行個體,ONE 個通訊協定:
service Block { fuchsia.hardware.block.Device device; };多個執行個體,具有不同的通訊協定集:MANY 個執行個體、MANY 個通訊協定
service Wlan { fuchsia.hardware.ethernet.Device device; fuchsia.wlan.AccessPoint access_point; fuchsia.hardware.Power power; };
服務聲明可能有多個使用相同通訊協定的成員,但每個成員聲明都必須使用不同的 ID。請參閱上方的「多種通訊協定的複合體」。
如果服務的執行個體可能包含與其他執行個體不同的通訊協定集,服務宣告會宣告任何執行個體中可能出現的所有通訊協定。請參閱上文「多個執行個體,使用不同通訊協定組合」一節。
服務聲明不會提及服務特定例項的名稱,或提供服務的元件 URI,這類資訊會留給元件架構,根據元件資訊清單聲明和執行階段的 API 使用情形決定。
語言繫結
語言繫結將會修改,讓您更方便連線至服務。具體來說,這些模型會更著重於服務,例如:
連線至服務的「default」執行個體,使用單一通訊協定:ONE 執行個體、ONE 通訊協定:
- C++:
Scheduler scheduler = Scheduler::Open(); ProfileProviderPtr profile_provider; scheduler.profile_provider().Connect(profile_provider.NewRequest());- Rust:
let scheduler = open_service::<Scheduler>(); let profile_provider: ProfileProviderProxy = scheduler.profile_provider();連線至服務的「預設」執行個體,並使用多種通訊協定:ONE 執行個體,MANY 通訊協定:
- C++:
Time time = Time::Open(); ProviderPtr network; time.network().Connect(&network); ProviderPtr rough; time.rough().Connect(&rough);- Rust:
let time = open_service::<Time>(); let network = time.network(); let rough = time.rough();使用單一通訊協定連線至多個服務例項:MANY 例項,ONE 通訊協定:
- C++:
Block block_0 = Block::OpenInstance("0"); DevicePtr device_0; block_0.device().Connect(&device_0); Block block_1 = Block::OpenInstance("1"); DevicePtr device_1; block_1.device().Connect(&device_1);- Rust:
let block_0 = open_service_instance::<Block>("0"); let device_0 = block_0.device(); let block_1 = open_service_instance::<Block>("1"); let device_1 = block_1.device();連線至服務的多個例項,使用多種通訊協定:MANY 例項、MANY 通訊協定:
- C++:
Wlan wlan_a = Wlan::OpenInstance("ff:ee:dd:cc:bb:aa"); DevicePtr device; wlan_a.device().Connect(&device); Power power_a; wlan_a.power().Connect(&power_a); Wlan wlan_b = Wlan::OpenInstance("00:11:22:33:44:55"); AccessPoint access_point; wlan_b.access_point().Connect(&access_point); Power power_b; wlan_b.power().Connect(&power_b);- Rust:
let wlan_a = open_service_instance::<Wlan>("ff:ee:dd:cc:bb:aa"); let device = wlan_a.device(); let power_a = wlan_a.power(); let wlan_b = open_service_instance::<Wlan>("00:11:22:33:44:55"); let access_point = wlan_b.access_point(); let power_b = wlan_b.power();
以下說明建議的函式簽章。
請注意,Open() 和 OpenInstance() 方法也會接受選用參數,用於指定命名空間。根據預設,系統會使用程序的全域命名空間 (可使用 fdio_ns_get_installed 擷取)。
// Generated code.
namespace my_library {
class MyService final {
public:
// Opens the "default" instance of the service.
//
// |ns| the namespace within which to open the service or nullptr to use
// the process's "global" namespace as defined by |fdio_ns_get_installed()|.
static MyService Open(fdio_ns_t* ns = nullptr) {
return OpenInstance(fidl::kDefaultInstanceName, ns);
}
// Opens the specified instance of the service.
//
// |name| the name of the instance, must not be nullptr
// |ns| the namespace within which to open the service or nullptr to use
// the process's "global" namespace as defined by |fdio_ns_get_installed()|.
static MyService OpenInstance(const std::string& instance_name,
fdio_ns_t* ns = nullptr);
// Opens the instance of the service located within the specified directory.
static MyService OpenAt(zxio_t* directory);
static MyService OpenAt(fuchsia::io::DirectoryPtr directory);
// Opens a directory of available service instances.
//
// |ns| the namespace within which to open the service or nullptr to use
// the process's "global" namespace as defined by |fdio_ns_get_installed()|.
static fidl::ServiceDirectory<MyService> OpenDirectory(fdio_ns_t* ns = nullptr) {
return fidl::ServiceDirectory<MyService>::Open(ns);
}
// Gets a connector for service member "foo".
fidl::ServiceConnector<MyService, MyProtocol> foo() const;
// Gets a connector for service member "bar".
fidl::ServiceConnector<MyService, MyProtocol> bar() const;
/* more stuff like constructors, destructors, etc... */
}
繫結程式碼:
/// FIDL bindings code.
namespace fidl {
constexpr char[] kDefaultInstanceName = "default";
// Connects to a particular protocol offered by a service.
template <typename Service, typename Protocol>
class ServiceConnector final {
public:
zx_status_t Connect(InterfaceRequest<Protocol> request);
};
// A directory of available service instances.
template <typename Service>
class ServiceDirectory final {
public:
// Opens a directory of available service instances.
//
// |ns| the namespace within which to open the service or nullptr to use
// the process's "global" namespace as defined by |fdio_ns_get_installed()|.
static ServiceDirectory Open(fdio_ns_t* ns = nullptr);
// Gets the underlying directory.
zxio_t* directory() const;
// Gets a list of all available instances of the service.
std::vector<std::string> ListInstances();
// Opens an instance of the service.
Service OpenInstance(const std::string& name);
// Begins watching for services to be added or removed.
//
// Invokes the provided |callback| to report all currently available services
// then reports incremental changes. The callback must outlive the returned
// |Watcher| object.
//
// The watch ends when the returned |Watcher| object is destroyed.
[[nodiscard]] Watcher Watch(WatchCallback* callback,
async_dispatcher_t* dispatcher = nullptr);
// Keeps watch.
//
// This object has RAII semantics. The watch ends once the watcher has
// been destroyed.
class Watcher final {
public:
// Ends the watch.
~Watcher();
};
// Callback invoked when service instances are added or removed.
class WatchCallback {
public:
virtual void OnInstanceAdded(std::string name) = 0;
virtual void OnInstanceRemoved(std::string name) = 0;
virtual void OnError(zx_status_t error) = 0;
};
}
語言繫結會進一步擴充這些功能,提供便利的方法來疊代服務執行個體,並監看是否有新的執行個體可用。
服務演進
如要發展服務,我們可以隨著時間推移新增通訊協定。 為維持來源相容性,請勿移除現有通訊協定,否則使用者可能會依據語言繫結從服務產生的程式碼,因此來源相容性可能會中斷。
由於服務中的所有通訊協定實際上都是選用,因此可能會在執行階段提供,也可能不會提供,且元件應為此情況而建構,這簡化了我們在演進服務時面臨的一系列問題:
- 隨時可將通訊協定成員新增至服務
- 應避免移除通訊協定成員 (為了確保來源相容性)
- 如要重新命名通訊協定成員,請新增通訊協定成員,並退出現有通訊協定成員
為了發展服務本身,我們也設有類似的限制。服務不一定會存在於元件的命名空間中,而且服務可能會顯示在命名空間中的多個不同位置,因此:
- 隨時可以新增服務
- 應避免移除服務 (為了確保來源相容性)
- 重新命名服務時,系統會複製服務並使用新名稱,同時保留原始服務副本 (用於來源相容性)
可能的擴充功能
我們預期 service 執行個體最終會成為「第一類」,並允許成為訊息的一部分,就像 protocol P 控制代碼可做為 P 或 request<P> 傳遞一樣。
這可能採用 service_instance<S> 的形式,例如 service S。我們會確保可以延長期限,您今天不必為此採取任何行動。
我們開放 (並計畫) 擴大成員類型,不只允許通訊協定。舉例來說,我們可能希望服務公開 VMO (handle<vmo>):
service DesignedService {
...
handle<vmo>:readonly logo; // gif87a
};
導入策略
這項提案應分階段實作,以免破壞現有程式碼。
第 1 階段
- 修改 component_manager,讓元件 v2 支援新的服務目錄結構。
- 修改 appmgr 和 sysmgr,讓元件 v1 支援服務的新目錄結構定義。
第 2 階段
- 新增服務聲明支援功能。
- 修改語言繫結,產生服務。
第 3 階段
- 針對具有
Discoverable屬性的所有通訊協定,建立適當的服務宣告。> 注意:在這個階段,我們應確認服務的新舊目錄結構定義之間不會發生名稱衝突。 - 將所有原始碼遷移至使用服務。
第 4 階段
- 從 FIDL 檔案中移除所有
Discoverable屬性。 - 從 FIDL 和語言繫結中移除對
Discoverable的支援。 - 從 component_manager、appmgr 和 sysmgr 移除對舊目錄結構定義的支援。
說明文件和範例
我們需要擴充 FIDL 教學課程,說明服務宣告的使用方式,以及服務宣告與通訊協定的互動方式。接著,我們會說明服務的不同結構:單例與多例,以及如何使用語言繫結。
詞彙解釋
通訊協定宣告會說明可透過管道傳送或接收的一組訊息,以及這些訊息的二進位表示法。
服務聲明:服務供應商以單元形式提供的能力。其中包含服務名稱,以及零個或多個具名成員通訊協定,用戶端會使用這些通訊協定與能力互動。
同一個通訊協定可能會多次顯示為服務宣告的成員,而成員的名稱會指出通訊協定的預期解讀方式:
service Foo {
fuchsia.io.File logs;
fuchsia.io.File journal;
};
元件宣告會說明可執行的軟體單元,包括元件二進位檔的位置,以及元件打算使用、公開或提供給其他元件的功能 (例如服務)。
這項資訊通常會編碼為套件中的元件資訊清單檔案:
// frobinator.cml
{
"uses": [{ "service": "fuchsia.log.LogSink" }],
"exposes": [{ "service": "fuchsia.frobinator.Frobber" }],
"offers": [{
"service": "fuchsia.log.LogSink",
"from": "realm",
"to": [ "#child" ]
}],
"program": { "binary": ... }
"children": { "child": ... }
}
服務執行個體是符合指定服務宣告的能力。在 Fuchsia 上,這會以目錄的形式呈現。其他系統可能會使用不同的服務探索機制。
元件例項是元件的特定例項,具有專屬的私有沙箱。在執行階段,這個元件會透過開啟傳入命名空間中的目錄,使用其他元件提供的服務例項。反之,它會將自己的服務例項呈現在外送目錄中,供其他元件使用。元件管理員會做為服務探索的代理程式。
- 元件執行個體通常 (但並非絕對) 與程序是一對一的關係。
- 元件執行器通常可以在同一程序中執行多個元件例項,每個例項都有自己的傳入命名空間。
服務的慣用語用法
回溯相容性
這項提案將淘汰 Discoverable 屬性,並最終從 FIDL 中移除。
線路格式沒有任何異動。
如果您要導入新的資料型別或語言功能,請考量使用者應對 FIDL 定義進行哪些變更,才不會導致產生的程式碼無法運作。如果您的功能對產生的語言繫結設有任何新的來源相容性限制,請在此列出。
效能
連線至服務的預設執行個體或先驗的執行個體時,這應該不會影響 IPC 效能。
如要連線至其他執行個體 (執行個體 ID 事先未知),使用者必須先列出服務的目錄並找出執行個體,才能連線。
對建構和二進位檔大小的影響極小,因為服務定義必須由後端為特定語言繫結產生。
安全性
這項提案可讓我們強制執行更精細的存取權控管,因為我們可以將服務拆分成具有不同存取權的獨立通訊協定。
這項提案不會對安全性造成其他影響。
測試
編譯器中的單元測試,以及相容性測試套件的變更,可檢查服務中包含的通訊協定是否可連線。
缺點、替代方案和未知事項
我們將探討下列問題:
- 為什麼服務宣告屬於 FIDL?
- 通訊協定、服務和元件有何不同?
- 服務執行個體的建議平面拓撲是否具有足夠的表達能力?
- 如何隨著時間延長服務?
- 如果元件執行個體想要公開與單一基礎邏輯資源相關的多項服務,該如何表示?
問 1:為什麼服務宣告屬於 FIDL?
回應
- 我們使用 FIDL 描述 Fuchsia 的系統 API,包括元件交換的通訊協定。
- 視情況而定,同一組通訊協定可能有多種用途。 將這些通訊協定的各種用途表示為服務,可讓開發人員在不同情況下,更輕鬆地存取合適的通訊協定組合。
- FIDL 已提供語言繫結,可輕鬆擴充,為開發人員提供一致且便利的服務存取方式。
討論
- [ianloic] But what about 元件 manifests? 何不也使用 FIDL 描述這些項目?
- [jeffbrown] 元件 manifests describe concepts that go well beyond IPC concerns
- [abdulla] describing services in 元件 manifests would lead to duplication of the description of those services
- [ianloic] could we generate the skeleton of a 元件 from its manifest?
- [drees] putting service declarations in FIDL is imposing a specific structure, does this make sense on other platforms?
- [jeffbrown] we want declarations of services to be external to components because they need to be shared between components, it is the point of agreement for service exchange
- [ianloic] service declarations for overnet likely to be similar
- [pascallouis] Is it is good to start simple based on what we know we need now. 日後可視需要調整。
- [pascallouis] FIDL 是 Fuchsia 的第一項功能,因此根據我們目前掌握的資訊,導入僅適用於該情境的功能是合理的,但隨著時間推移,這些功能可能會適用於其他情境
- [dustingreen] what about a separate file?
- [pascallouis] those files would be very small and lonely, opportunities for static type checking if we keep them in FIDL, seems low risk to move it later if needed
問 2:通訊協定、服務和元件有何不同?
回應
- 通訊協定宣告會說明一組可透過管道傳送或接收的訊息,以及這些訊息的二進位表示法。
- 服務聲明:服務供應商以單元形式提供的能力。其中包含服務名稱,以及零或多個具名成員通訊協定,用戶端可透過這些通訊協定與能力互動。
- 同一個通訊協定可能會多次顯示為服務宣告的成員;成員名稱會指出通訊協定的預期解讀方式。
- 例如:
service Foo { fuchsia.io.File logs; fuchsia.io.File journal; };
- 例如:
- 同一個通訊協定可能會多次顯示為服務宣告的成員;成員名稱會指出通訊協定的預期解讀方式。
元件宣告會說明可執行的軟體單元,包括元件二進位檔的位置,以及元件打算使用、公開或提供給其他元件的功能 (例如服務)。
這類資訊通常會編碼為套件中的元件資訊清單檔案。範例:
// frobinator.cml { "uses": [{ "service": "fuchsia.log.LogSink" }], "exposes": [{ "service": "fuchsia.frobinator.Frobber" }], "offers": [{ "service": "fuchsia.log.LogSink", "from": "realm", "to": [ "#child" ]}], "program": { "binary": ... } "children": { "child": ... } }
服務執行個體是符合特定服務宣告的能力。在 Fuchsia 上,這會以目錄的形式呈現。其他系統可能會使用不同的服務探索機制。
元件例項是元件的特定例項,具有自己的私有沙箱。在執行階段,它會透過開啟傳入命名空間中的目錄,使用其他元件提供的服務執行個體。反之,它會將自己的服務例項呈現在輸出目錄中,供其他元件使用。元件管理員會做為服務探索的代理程式。
- 元件執行個體通常 (但並非絕對) 與程序是一對一的關係。
- 元件執行器通常可以在同一程序中執行多個元件例項,每個例項都有自己的傳入命名空間。
討論
- [ianloic] what guidance should we offer for choosing 通訊協定 composition vs. service declarations?
- [abdulla] 通訊協定組合表示通訊協定本身高度相關,而服務則表示共同提供一組功能 (可能不相關)
- [pascallouis] compose multiplexes protocols over a single 管道 so has implications for message ordering vs. individual protocols of a service have different channels
- [jeffbrown] can delegate in different places, not related, composition doesn't get you this functionality, services allow "discovery" at runtime, e.g. listing which protocols are available
問 3:服務執行個體的建議平面拓撲是否充分表達?
回應
- 平面拓撲結構易於使用,因為不需要以遞迴方式遍歷路徑來找出所有執行個體。這會影響易用性和效能。
- 如果相關資訊編碼在執行個體名稱中 (例如
/svc/fuchsia.Ethernet/rack.5,port.9/packet_receiver),平面拓撲的表達能力與階層式拓撲相同。 - 您可以使用 Open()、Open(命名空間) 和 OpenAt(directory),從不同位置存取服務。 換句話說,並非所有服務都必須來自程序全域命名空間中的「/svc」。如有需要,這項功能可讓您建立任意服務拓撲。
第 4 季:我們應如何隨著時間延長服務?
回應
- 我們可以將新成員新增至現有的服務宣告。 新增成員不會破壞來源或二進位檔的相容性,因為每個成員實際上都是選用項目 (嘗試連線至通訊協定是可能會失敗的作業)。
- 我們可以從服務宣告中移除現有成員。 移除 (或重新命名) 現有成員可能會破壞來源和二進位檔的相容性,因此可能需要審慎規劃遷移作業,以減輕負面影響。
- 服務文件應清楚說明服務的預期用途或實作方式,特別是當這類用途不明顯時,例如說明服務中已淘汰並預計移除的功能。
預期版本控管模式:隨著通訊協定演進,在服務中新增成員。通訊協定列舉 (列出目錄) 可讓用戶端探索支援的項目。範例:
在第 1 版中...
service Fonts { FontProvider provider; }; protocol FontProvider { GimmeDaFont(string font_name) -> (fuchsia.mem.Buffer ttf); };在第 2 版中,增量更新...
service Fonts { FontProvider provider; FontProvider2 provider2; }; protocol FontProvider2 { compose FontProvider; GetDefaultFontByFamily(string family) -> (string family); };在第 3 版中,我們徹底重新設計了...
service Fonts { [Deprecated] FontProvider provider; [Deprecated] FontProvider provider2; TypefaceChooser typeface_chooser; } protocol TypefaceChooser { GetTypeface(TypefaceCriteria criteria); }; table TypefaceCriteria { 1: Family family; 2: Style style; 3: int weight; };
問 5:如果元件執行個體想公開與單一基礎邏輯資源相關的多項服務,該如何表示?
回應
元件會定義透過元件資訊清單公開的多項服務。範例:
// frobinator.cml { ... "exposes": [ { "service": "fuchsia.frobinator.Fooer" }, { "service": "fuchsia.frobinator.Barer" }, ], ... }然後,這個元件會在單一基礎資源上實作這些服務,但這些服務的使用者不必知道這項事實。