RFC-0041:支援整合服務和裝置

RFC-0041:支援統一服務和裝置
狀態已接受
區域
  • FIDL
說明

介紹服務的概念,也就是一組通訊協定,其中可能會有一或多個集合例項。

作者
提交日期 (年-月-日)2019-04-08
審查日期 (年-月-日)2019-04-23

摘要

介紹服務的概念,也就是一組通訊協定,其中可能會有一或多個集合例項。

提振精神

目前,在元件架構中,服務會定義為單一通訊協定,且在 /svc 底下程序的命名空間中,只能存在該通訊協定的一個例項。這會導致我們無法描述更複雜的關係:

  • 服務會以兩種不同的形式表示,具體取決於使用者。例如,當通訊協定有兩個不同版本時,例如 FontProviderFontProviderV2
  • 服務分為兩個部分,以便根據存取層級授予功能,例如一般存取權和管理員存取權 (例如 DirectoryDirectoryAdmin),後者提供特權存取權
  • 由許多不同通訊協定組成的服務,可供不同使用者使用,例如用於電源管理的 Power,以及用於網路堆疊的 Ethernet
  • 具有多個執行個體的服務,例如提供 AudioRenderer 的多個音訊裝置,或公開 Printer 的多台印表機

提供這種彈性可讓服務更清楚表達,而無需使用 服務中樞 等替代做法。有了這種彈性,我們就能將裝置定義為服務。具體來說,我們打算將 /svc/$Protocol 從「每個程序命名空間只有一個通訊協定」演進為:

/svc/$Service/$Instance/$Member

這會改為引入兩個額外的間接路徑:服務 (例如印表機、乙太網路) 和執行個體 (例如預設、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
    
  • 服務的多個例項,使用單一通訊協定:許多例項,一個通訊協定:

    /svc/fuchsia.hardware.Block/0/device
                            .../1/device
    
  • 多個執行個體,且各自使用不同的通訊協定:多個執行個體,多個通訊協定:

    /svc/fuchsia.Wlan/ff:ee:dd:cc:bb:aa/device
                                    .../power
                  .../00:11:22:33:44:55/access_point
                                    .../power
    

語言

為了向 FIDL 引入服務概念,並支援各種版本,我們將對 FIDL 語言進行以下變更:

  1. 新增 service 關鍵字。
  2. 移除 Discoverable 屬性。

service 關鍵字可讓我們編寫服務宣告,用於定義一組通訊協定,做為服務的成員。舉例來說,我們可以宣告不同的服務版本,如下所示:

  • 單一專屬通訊協定:ONE 執行個體,ONE 通訊協定:

    service Scheduler {
      fuchsia.scheduler.ProfileProvider profile_provider;
    };
    
  • 多個通訊協定的組合:ONE 個例項,MANY 個通訊協定:

    service Time {
      fuchsia.time.Provider network;
      fuchsia.time.Provider rough;
    };
    
  • 服務的多個例項,使用單一通訊協定:許多例項,一個通訊協定:

    service Block {
      fuchsia.hardware.block.Device device;
    };
    
  • 多個執行個體,且各自使用不同的通訊協定:許多執行個體,許多通訊協定

    service Wlan {
      fuchsia.hardware.ethernet.Device device;
      fuchsia.wlan.AccessPoint access_point;
      fuchsia.hardware.Power power;
    };
    

服務宣告可能包含多個使用相同通訊協定的成員,但每個成員宣告都必須使用不同的 ID。請參閱上方的「多個通訊協定的組合」。

如果服務的執行個體可能包含另一個執行個體的不同一組通訊協定,服務宣告會宣告任何執行個體可能存在的所有可能通訊協定。請參閱上方的「多個例項,各有不同的一組通訊協定」一節。

服務宣告不會提及服務的特定例項名稱或提供服務的元件的 URI,而是交由元件架構負責,根據元件資訊清單宣告和在執行階段使用其 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();
    
  • 使用單一通訊協定連線至多個服務執行個體:多個執行個體,單一通訊協定:

    • 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();
    
  • 使用多種通訊協定連線至多個服務例項:多個例項,多種通訊協定:

    • 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 句柄可做為 Prequest<P> 傳遞一樣。這可能會以 service_instance<S> 的形式呈現,用於 service S。我們會確保這項延期是可行的,而不會在今天就開始進行。

我們會開放 (並且打算擴大) 允許的協議類型,讓更多類型的成員加入。舉例來說,我們可能會希望服務公開 VMO (handle<vmo>):

service DesignedService {
    ...
    handle<vmo>:readonly logo; // gif87a
};

導入策略

這項提案應分階段實作,以免中斷現有程式碼。

第 1 階段
  1. 修改 component_manager,讓元件 v2 支援服務的新目錄結構定義。
  2. 修改 appmgr 和 sysmgr,讓 v1 元件支援服務的新目錄結構定義。
第 2 階段
  1. 新增服務宣告支援功能。
  2. 修改語言繫結以產生服務。
第 3 階段
  1. 針對所有含有 Discoverable 屬性的通訊協定,建立適當的服務宣告。> 注意:在這個階段,我們應驗證服務的舊版和新版目錄結構定義之間,是否有任何名稱衝突。
  2. 將所有原始碼遷移至使用服務。
第 4 階段
  1. 從 FIDL 檔案中移除所有 Discoverable 屬性。
  2. 從 FIDL 和語言繫結中移除對 Discoverable 的支援。
  3. 從 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 的事先資訊,使用者必須先列出服務的目錄,並在連線前找到例項。

對建構和二進位檔大小的影響微乎其微,因為服務定義必須由特定語言繫結的後端產生。

安全性

這項提案可讓我們將服務分割為具有不同存取權的個別通訊協定,進而實施更精細的存取權控管。

這項提案對安全性沒有其他影響。

測試

編譯器中的單元測試,以及相容性測試套件的變更,以便檢查服務中包含的通訊協定是否可連線。

缺點、替代方案和未知事項

本文將探討以下問題:

問題 1:為什麼服務宣告屬於 FIDL?

回應

  • 我們會使用 FIDL 描述 Fuchsia 的系統 API,包括元件交換的通訊協定。
  • 相同的通訊協定可能會因情況而有許多用途。將這些通訊協定的各種用途視為服務,可讓開發人員更輕鬆地存取適合各種情況的通訊協定組合。
  • FIDL 已提供可輕鬆擴充的語言繫結,為開發人員提供一致且方便存取這些服務的方式。

討論

  • [ianloic] 那元件資訊清單呢?為什麼不使用 FIDL 來描述這些內容?
  • [jeffbrown] 元件資訊清單會說明超出 IPC 疑慮的概念
  • [abdulla] 在元件資訊清單中說明服務,可能會導致這些服務的說明重複
  • [ianloic] 我們可以從資訊清單產生元件的骨架嗎?
  • [drees] 將服務宣告放入 FIDL 會強制使用特定結構,這在其他平台上是否合理?
  • [jeffbrown] 我們希望服務宣告是元件的外部,因為它們需要在元件之間共用,這是服務交換的協議點
  • [ianloic] overnet 的服務聲明可能會類似
  • [pascallouis] 我們現在知道自己需要什麼,因此是否可以從簡單的做法開始?我們之後可以視需要調整。
  • [pascallouis] FIDL 是 Fuchsia 優先,因此在現有資訊的情況下,引入僅在該情境中才有意義的功能是合理的,但隨著時間推移,這些功能可能會推廣至其他情境
  • [dustingreen] 那麼獨立檔案呢?
  • [pascallouis] 這些檔案會非常小且孤立,如果我們將這些檔案保留在 FIDL 中,則可進行靜態類型檢查,如果需要,日後移動這些檔案的風險似乎很低

Q2:通訊協定、服務和元件有何不同?

回應

  • 通訊協定宣告會描述一組可能透過管道傳送或接收的訊息,以及這些訊息的二進位表示法。
  • 服務宣告會說明服務供應商以單一單位提供的能力。其中包含服務名稱和零個或多個命名成員通訊協定,用戶端可用來與這項能力互動。
    • 同一個通訊協定可能會多次出現在服務宣告的成員中;成員的名稱表示通訊協定的預期解讀方式。
      • 例如: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" },
        ],
        ...
    }
    
  • 元件會在單一基礎資源上實作這些服務,但這些服務的使用者不必瞭解這項事實。