| 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具有单个协议的服务的多个实例:多个 实例,一个 协议:
/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 语言进行以下更改:
- 添加
service关键字。 - 移除
Discoverable属性。
借助 service 关键字,我们可以编写服务声明,并使用该声明将一组协议定义为服务的成员。
例如,我们可以按如下方式声明不同类型的服务:
单个唯一协议:一个 实例,一个 协议:
service Scheduler { fuchsia.scheduler.ProfileProvider profile_provider; };多个协议的组合:一个 实例,多个 协议:
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; };
服务声明可以有多个使用相同协议的成员,但每个成员声明都必须使用不同的标识符。 请参阅上文的“多个协议的组合”。
当服务实例可能包含与另一个实例不同的协议集时,服务声明会声明任何实例中可能存在的所有协议。 请参阅上文的“具有不同协议集的多个实例”。
服务声明不会提及服务特定实例的名称或提供服务的组件的 URI,这由组件框架根据组件清单声明和运行时对其 API 的使用来决定。
语言绑定
我们将修改语言绑定,以便更方便地连接到服务。 具体而言,它们将更加面向服务,例如:
连接到具有单个协议的服务的“default”实例:一个 实例,一个 协议:
- 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();连接到具有多个协议的服务的“default”实例:一个 实例,多个 协议:
- 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 句柄可以
作为 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 上,它表示为一个目录。 其他系统可能会使用不同的服务发现机制。
组件实例 是具有自己的专用沙盒的组件的特定实例。在运行时,它通过在其传入命名空间 中打开目录来使用其他组件提供的服务实例。 相反,它通过在传出目录 中显示自己的服务实例来将其公开给其他组件。 组件管理器 充当服务发现的代理。
- 组件实例通常(但并非总是)与进程 一对一。
- 组件运行程序通常可以在同一进程中运行多个组件实例,每个实例都有自己的传入命名空间。
服务的惯用用法
向后兼容性
此提案将弃用并最终从 FIDL 中移除 Discoverable 属性。
线路格式没有任何变化。
如果您要引入新的数据类型或语言功能,请考虑您希望用户对 FIDL 定义进行哪些更改,而不会破坏生成代码的用户。 如果您的功能对生成的语言绑定施加了任何新的源代码兼容性 限制,请在此处列出这些限制。
性能
连接到服务的默认实例或已知实例时,这应该不会对 IPC 性能产生影响。
如需连接到其他实例(其中实例 ID 事先未知 ),用户需要列出服务的目录并找到 实例,然后才能进行连接。
对 build 和二进制文件大小的影响很小,因为服务定义必须由后端为特定语言绑定生成。
安全
此提案将允许我们强制执行更精细的访问权限控制,因为我们可以将服务拆分为具有不同访问权限的单独协议。
此提案对安全性没有其他影响。
测试
编译器中的单元测试,以及对兼容性测试套件的更改,以检查是否可以连接到服务中包含的协议。
缺点、替代方案和未知因素
探讨了以下问题:
- 为什么服务声明属于 FIDL?
- 协议、服务和组件之间有什么区别?
- 建议的服务实例的扁平拓扑是否具有足够的表现力?
- 我们应该如何随着时间的推移扩展服务?
- 如果组件实例希望公开与单个底层逻辑资源相关的多个服务,该如何表达?
问题 1:为什么服务声明属于 FIDL?
响应
- 我们使用 FIDL 来描述 Fuchsia 的系统 API,包括组件交换的协议。
- 根据具体情况,同一协议可能会以多种方式使用。 将这些协议的各种用途表示为服务,可让开发者更轻松地访问每种情况下的正确协议集。
- FIDL 已经提供了语言绑定,可以轻松扩展这些绑定,为开发者提供一致且便捷的方式来访问这些服务。
讨论
- [ianloic] 但组件清单呢? 为什么不使用 FIDL 来描述这些清单?
- [jeffbrown] 组件清单描述的概念远远超出了 IPC 的范畴
- [abdulla] 在组件清单中描述服务会导致重复描述这些服务
- [ianloic] 我们能否从组件清单生成组件的框架?
- [drees] 将服务声明放在 FIDL 中会强制使用特定结构,这在其他平台上是否有意义?
- [jeffbrown] 我们希望服务声明位于组件外部,因为它们需要在组件之间共享,这是服务交换的协议点
- [ianloic] overnet 的服务声明可能类似
- [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(namespace) 和 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" }, ], ... }然后,组件将在单个底层资源之上实现这些服务,但这些服务的使用者无需了解这一事实。