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 中宣告的服務成員名稱,其中該成員的宣告類型代表預定的通訊協定
設計
服務種類
首先,我們會思考我們希望支援的各種服務:
不重複的單一通訊協定:ONE 執行個體、ONE 通訊協定:
/svc/fuchsia.Scheduler/default/profile_provider
多個通訊協定的複合:ONE 個執行個體、MANY 個通訊協定:
/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
關鍵字可讓我們編寫服務宣告,用來將一組通訊協定定義為服務成員。舉例來說,我們可以宣告各種服務種,如下所示:
不重複的單一通訊協定:ONE 執行個體、ONE 通訊協定:
service Scheduler { fuchsia.scheduler.ProfileProvider profile_provider; };
多個通訊協定的複合:ONE 個執行個體、MANY 個通訊協定:
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 的元件架構。
語言繫結
系統會修改語言繫結,以更便利的方式連線至服務。具體來說,這類 API 會更加註重服務導向,例如:
使用單一通訊協定連線至服務的「預設」執行個體: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 S
的 service_instance<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 中,資料以目錄的形式呈現。其他系統則可能使用不同的服務探索機制。
「元件執行個體」是元件具有專屬私人沙箱的元件特定執行個體。在執行階段,它會透過傳入命名空間中的目錄開啟其他元件提供的服務執行個體。相反地,API 會在傳出目錄中呈現自己的服務執行個體,供其他元件公開。元件管理員可做為服務探索的代理程式。
- 元件執行個體經常 (但不一定) 具有「程序」的一對一執行個體。
- 元件執行器通常可以在相同程序中執行多個元件執行個體,每個元件執行個體均擁有「自己的」傳入命名空間。
慣用服務
回溯相容性
此提案將會淘汰,最終從 FIDL 中移除 Discoverable
屬性。
線路格式沒有任何變更。
如果您要引進新的資料類型或語言功能,請考慮您預期使用者會對 FIDL 定義進行哪些變更,而不會破壞產生的程式碼使用者。如果功能對產生的語言繫結設有任何新的來源相容性限制,請在這裡列出。
效能
這對於連線至服務的預設執行個體或已知 rii 的執行個體時,處理序間通訊 (IPC) 效能應該不會受到影響。
如要連線至其他執行個體 (執行個體 ID 不明 之前),系統會要求使用者列出服務的目錄並找出該執行個體,然後再進行連線。
由於服務定義必須由後端產生特定語言繫結的服務定義,因此對建構和二進位檔大小的影響最小。
安全性
本提案可讓我們強制執行更精細的存取權控管機制,因為我們可將服務分割成具有不同存取權的個別通訊協定。
本提案並未影響安全性。
測試
編譯器中的單元測試,以及相容性測試套件的變更,以檢查服務中包含的通訊協定是否能連線。
缺點、替代方案和未知
探索以下問題:
- 為何服務宣告在 FIDL 中?
- 通訊協定、服務和元件有何不同?
- 針對服務執行個體提議的平面拓撲是否充分錶達?
- 我們該如何拓展服務?
- 如果元件執行個體要公開與單一基礎邏輯資源相關的多項服務,該如何表示?
問題 1:服務宣告在 FIDL 中為何?
回應
- 我們使用 FIDL 來描述 Fuchsia 的系統 API,包括元件交換的通訊協定。
- 視情況而定,有很多方法可能會運用相同的通訊協定。以服務的形式呈現這些通訊協定的各種用途,可讓開發人員更容易針對各種情況存取正確的通訊協定組合。
- FIDL 已提供語言繫結並快速擴充,讓開發人員以一致且便利的方式存取這些服務。
討論
- [ianloic] 不過,元件資訊清單呢?何不使用 FIDL 一併描述這些方法?
- [jeffbrown] 元件資訊清單說明遠超出處理序間通訊 (IPC) 的問題
- [abdulla] 在元件資訊清單中描述服務會導致重複描述這些服務
- [ianloic] 我們可以從資訊清單中產生元件的骨架嗎?
- [drees] 在 FIDL 中加入服務宣告會設下特定結構,在其他平台上是否合理?
- [jeffbrown] 我們希望在元件之間共用服務宣告 原因是必須在元件之間共用服務,這是服務交換的協議點
- [ianloic] 超額網路的服務聲明可能相似
- [pascallouis] 根據我們目前的需求,一開始就適合採用簡單明瞭的策略。我們日後會視需要進行調整。
- [pascallouis] FIDL 會先是 Fuchsia,所以要先引進對使用者有意義的功能 就不能參考我們今天的資訊,但其他情況可長期通用以滿足其他情境的需求
- [dustingreen] 另一個檔案呢?
- [pascallouis] 這些檔案非常小且孤單,我們有機會檢查是否將這些檔案保留在 FIDL 中,而日後有需要時,移動檔案的風險就會降低
問題 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] 在選擇通訊協定組合與服務宣告時,應提供什麼指引?
- [abdulla] 通訊協定組合表示通訊協定本身高度相關,而服務表示正在共同提供一組功能 (可能不相關)
- [pascallouis] 在單一管道上組合多工通訊協定,因此訊息排序與服務的個別通訊協定會有不同的管道。
- [jeffbrown] 可以在不同位置委派,但關係不相關,組合則無法提供這項功能,且服務允許「探索」在執行階段進行「探索」,例如列出可用的通訊協定
問題 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" }, ], ... }
元件接著會在單一基礎資源之上實作這些服務,但這些服務的使用者並不需要知道這一點。