本文档介绍了迄今为止引导组件框架的设计原则。
如果本文档中包含某项原则,则表示组件框架已经支持该原则,或者该原则是原始愿景的一部分(即使在现今的系统中尚未完全实现)。本文档旨在促进对相关原则(而非应该是什么)的共同理解。这些原则的依据或对新原则的建议(框架最初设计时并无这些建议)不在本文档的讨论范围内。迁移到组件 v2 后,我们希望修改这些原则,使其与自组件框架首次设想以来了解到的知识保持一致。将本文档视为历史上下文的记录,作为改进这些过程的起点。
背景
我们将原则定义为关于通常正确的系统属性的基本声明。原则源自系统的设计目标。尽管通常情况下如此,但在某些情况下,可能无法完全满足原则,例如:
- 不符合现代标准的技术债务。
- 需要一般规则例外的特殊用例。
- 提供比生产环境中适合的访问权限更强大或更特权的开发者钩子。
有几种策略可以提高系统维护某个原则的程度,这些策略并不相互排斥:
- 让工具或运行时强制执行不可变性,以确保不可能违反原则。
- 设计的 API 应能够自然鼓励用户以遵循该原则与系统进行交互。
- 构建与原则一致的更高级别的框架。
- 在对系统进行更改时要勤加谨慎,并根据原则评估这些更改。
- 通过文档和示例推广最佳做法。
本文档中的一些原则专门针对组件框架。还有一些是组件框架也遵循的常规 Fuchsia 原则。
原则
最小权限
根据最小权限原则,组件只应获得在系统中执行其角色所需的最低功能。
示例:sysmgr
提供了组件 v1 中的反模式示例。v1 API 支持实例化配置了自己的一组功能的封闭式子领域。不过,sysmgr
定义了一个大区,其中包含几乎所有系统功能,这使得 v1 组件很容易违反最小权限。
目前实现的功能
组件框架提供功能路由作为访问权限控制的主要形式。它要求每个功能*都在路由中明确声明,以便为子级授予该功能。父组件采用功能路由,为其子组件定义沙盒。
未实现的愿景
最初的想法是用基于角色的访问权限控制来补充功能路由。不过,这种想法从未越过早期的头脑风暴。
组件管理器想要支持灵活的隔离政策,这是另一种权限控制机制。下文中的隔离部分提供了更多信息。
无环境授权机构
如果程序可以在没有明确地授予对象访问权限(例如,通过名称或编号引用对象)的情况下对某个对象执行操作,系统就会显示环境权限。您可能很难确保该程序访问对象的正当理由,因为它本身可以生成名称。
相比之下,使用基于功能的安全性,程序只有在可以访问某个对象时,才能对该对象执行操作。权能可以转移但无法伪造,因此无需 Ambient 授权即可安全地进行访问权限委托。
Fuchsia 以及类似的组件框架应在没有环境权限的情况下运行。
目前实现的功能
由于 capability 仅由父级提供 *,因此没有全局命名空间可从中获取功能或对对象执行操作。示例:组件的外观(如其 POSIX 文件系统)衍生自父组件对组件的沙盒的声明。
组件框架(在一些特权 API 的情况下除外)[*][#peer-exception] 不会向组件透露其对等方的身份。这可以防止组件滥用对等身份来构建 Ambient 授权方。
框架功能是一种受限的 Ambient 授权形式,因为任何组件都可以访问它们。不过,这种氛围授权是安全的,因为这些功能的作用域被限定在组件自己的领域。*
未实现的愿景
组件框架环境允许父级预配默认继承的功能,它在安全性与工效学设计之间达到了一种折中方式,从而形成一种氛围授权。虽然组件能够替换传递给子级的环境,但大多数情况下它们不会替换,而且与常规的功能路由不同,组件无法了解构成环境的功能。具体而言,使用环境来预配运行程序和解析器。这表明框架中可能存在一个缺口,无法同时安全地路由功能,也符合人体工程学。
普遍性
组件是 Fuchsia 软件的基础构建块。通用是所有用户空间软件都应作为组件运行的原则。
这种原则具有强弱形式。弱项形式是所有软件都应在组件中运行。强形式是,属于较大子系统一部分的程序应该是组件,而不是进程。此外,各个程序可能会决定将子模块拆分为多个组件。
目前实现的功能
截至 2022 年 4 月,系统的大部分内容都将作为 v2 组件运行。
截至 2022 年 4 月,大多数用户体验组件(在模块化下运行)仍为 v1,但有一个进行中的项目可以将其迁移到 v2。
驱动程序和文件系统就是进程,但我们计划将它们变为组件。
未实现的愿景
目前还不清楚这种强有力的原则是否适用于所有位置。 例如:
- Chromium 中的进程只是进程,而非组件。但是,目前还不清楚它们是否应该作为组件。
- Shell 程序如今是进程,我们不知道它们是否会成为组件。
对于何时将程序表示为组件或表示流程,我们从未制定过正式的指导意见。
独立性
在 Fuchsia 设备上,可能会有多个相同(或不同)组件实例同时运行。组件实例在以下维度上是独立的:
- 身份:每个实例都与其他实例区分开来。
- 沙盒:每个实例都有专为该实例量身定制的命名空间。
- 存储:假设每个实例需要存储空间(例如授权、邮箱、数据、缓存和其他状态),都有自己的专用存储位置。
- 生命周期:每个实例都可以独立启动或停止。
- 子项:每个实例都有自己的子项列表,并负责配置其子项的运行环境。
目前实现的功能
您可以为一个组件创建多个实例。组件实例带有自己的状态、子级和生命周期。
如今,系统有一些概念映射到了在不同上下文中使用的“组件标识”:
- 组件的绝对名称。用于功能许可名单。
- 组件的相对名称。在组件事件的载荷中提供,由
archivist
和test_manager
用于标识客户端。 - 实例 ID。用于永久存储。这样,组件的拓扑位置可以在不丢失存储空间的情况下更改。
- 组件的网址。在解析组件时使用,有时用作组件实例身份的代理(例如使用 Cobalt)。
未实现的愿景
授权(持久保留功能)和邮箱(基本上是不需要发送者和接收者运行的组件之间的消息总线)未实现。
“组件标识”的含义尚未完全解决。如上一部分中所述,组件框架具有各种身份概念,这些概念之间彼此不一致。例如,在什么情况下,在更新/移动/重新创建某个组件时,该组件会被视为“相同”组件?
隔离
组件可能拥有敏感信息和特权,组件框架负责保护其机密性、完整性和可用性。为此,组件框架使用各种机制来隔离组件并防止它们相互干扰。具体而言:
- 组件框架应防止组件读取或修改彼此的内部状态。
- 组件框架不应向组件透露其对等设备的真实身份。应提供本地身份信息(例如子女姓名)、经过混淆处理的身份信息,或者完全不提供身份信息。
目前实现的功能
由于组件通过功能进行交互,因此它们不知道谁在另一端。
与运行程序合作的组件框架支持各种用于隔离组件状态的机制,这些机制可以自行定义或使用 Zircon 中的状态:
- 进程隔离:某些运行程序在单独的进程中执行组件,以防止它们访问彼此的内存和句柄。
- ELF 运行程序会实现进程隔离:每个组件都会接收自己的进程和作业。
- 但是,并非所有运行程序都提供相同的隔离保证。Dart 运行程序(其中多个组件实例共享同一进程)提供的内存隔离保证较弱。
- 存储空间隔离:组件无法直接访问彼此的隔离永久性存储空间,并且通常也不会获得共享可变存储空间的访问权限。
- 内存隔离:组件无法直接访问彼此的专用内存(尽管它们可能会决定交换共享 VMO)。
组件框架不会透露组件对等实体的身份。*
未实现的愿景
最初的想法是,组件框架会定义一种容器类型(称为“隔室”),以充当运行时隔离边界。这属于基本组件关系之一。通过配置分区边界,产品所有者可以在安全性和性能之间进行权衡。
我们通过针对特定用例(devhost、fshost)的自定义解决方案实现了进程中共置组件的关键性能优势,但通常无法在组件框架级别使用。一开始,人们认为 Flutter 会成为一个重要的用例,因为 Flutter 程序可以从并置中受益(这样每个 Flutter 程序就不必自带运行时)。然而,由于 Flutter 和 Dart 中缺少稳定的 ABI,并且没有一个非常需要的用例,这一点被证明很难实现。
有一个由来已久的思路是,组件框架可以为组件提供经过混淆处理的令牌,以在本地标识其对等组件。这称为“混淆名称”:组件管理器可以获取组件之间的相对名称,对其进行哈希处理(可能使用 Nonce 的形式),然后向客户端呈现哈希值。由于这是从相对称谓派生的,因此其本质上就因具体实例而异。
问责
系统资源是有限的。计算设备上仅有这么多内存、磁盘或 CPU 时间。组件框架应跟踪组件使用资源的方式,以确保资源得到高效使用,并确保在不再需要这些资源时或在系统超额订阅时更急需用于其他目的时可回收资源。
使用资源是有原因的。一般来说,系统中的每个资源都必须以某种方式加以说明,以便系统确保它们得到有效使用。
每个组件的存在都是有原因的。父组件实例负责通过销毁不再使用的子项来确定其子项是否存在。父级在为子级设置资源约束时也发挥了作用。
每一个组件的运行都有其原因。组件框架会在组件有工作要执行时启动组件实例,例如响应来自其他组件的传入服务请求,并在相应需求消失(或优先级低于竞争相同资源的其他需求)时停止这些实例。
目前实现的功能
组件框架目前对问责制的支持是基本支持。
组件框架要求每个组件都属于一个父组件。
组件管理器启动组件实例来响应请求(单次运行组件除外)。组件管理器绝不会主动停止组件,除非组件被销毁或系统关闭期间。
组件管理器会回收已销毁的动态组件的资源,并在该组件删除后在后台清除其存储空间。
组件框架支持 eager
选项,该选项会使组件实例自动启动。关于此特征与问责制愿景如何契合,存在很大的不确定性。启动 eager 组件时没有明确的“原因”,组件管理器永远不会重启 eager 组件。另一方面,从用户体验的角度来看,这是运行“守护程序”类型的组件的一种简单便捷的方式,我们尚未想出更好的解决方案。
未实现的愿景
最初,人们对问责制的愿景要宏伟得多。
每个正在运行的进程都必须属于至少一个组件实例,该实例的功能当前正被使用、最近已在使用或很快将被使用;任何离群值都被视为无缘无故运行并立即停止。
对于隔离存储,“资源必须出于某种原因使用”这一说法目前是正确的,但我前面介绍过。我们对资源管理系统的愿景是,系统上使用的每个资源都将归因于特定的组件实例。此归因可用于公开指标以进行诊断、强制执行资源限制以及平衡负载。我们可以跟踪的资源示例包括存储设备、内存、CPU、GPU、功率或带宽。我们日后可能会实现此功能或其中一部分。
“每个组件的运行都有原因”目前仅部分实现。大多数组件都是为响应访问其公开功能的请求而启动的。不过,组件管理器不会主动终止未使用的组件或消耗过多资源的组件。具体而言,测量组件“未使用”的时间是已知的一大难题,因为组件管理器只会代理服务发现的引入阶段 - 在客户端与提供程序之间建立连接后,组件管理器就会开始处理。
最初计划构建“延迟通信”框架。这将使组件能够分派稍后传递给接收组件的消息或工作项,从而放宽对组件何时需要运行的限制,并为组件运行时提供更多启动和停止组件的余地。具体而言,有人提出了以下系统:
- 工作调度器:仅在满足特定条件后才调度工作。
- 邮箱:允许组件将邮件发布到一个用于保留邮件的“邮箱”,直到接收组件准备好处理邮件。
幻觉
组件框架应提供相关机制来保持连续性假象:用户通常不应担心如何重启软件,因为即使用户重新启动或更换设备,软件也会从上次停止的位置自动恢复。
错觉的保真度取决于以下属性在重启后得以保留的程度:
- 状态:保留组件实例的用户可见状态。
- 功能:保留授予组件实例的权限。
- 结构:保留协作组件实例之间的关系,以便它们可以根据需要重新建立通信。
- 行为:保留组件实例的运行时行为。
实际上,这种幻象并不完美。如果存在软件升级、不确定性、错误、故障和网络服务的外部依赖项,系统无法保证能够忠实复制。
虽然让组件永久运行似乎更简单,但最终会耗尽资源,因此需要通过立即停止不太重要的组件来平衡其工作集大小(请参阅问责制)。
目前实现的功能
一般来说,即使组件停止运行,组件仍会继续存在。可将这一点与进程的工作方式进行比较。
如果某个组件重启,传入、传出或通过该组件路由的功能将保持不变。但是,与这些功能的连接不会在重启后保留。根据功能的不同,新实例的行为方式可能与旧实例不同,或者可能无法获取多个实例。
组件框架支持为静态组件实例使用永久性存储空间。即使组件的拓扑位置发生变化,也可以保留其存储空间。
未实现的愿景
系统中有许多组件无法容忍重启。通过搜索使用 reboot_on_terminate
功能的组件,可以找到其中许多功能。
对于如何构建能够在重启时恢复状态的组件,或者在其中一个依赖项不可用时如何操作,没有任何标准的设计模式。
有关如何配置组件的重启政策存在一些未解决的问题。与之相关的是,重启服务器后,在组件之间重新建立连接的时机和方法存在一些疑问。
组件无法保留功能(也称为“授权”)。如果它们重启,则必须重新获取。组件也无法保留收到的消息或将消息的调度推迟到晚些时候。但是,有一个称为“邮箱”的消息队列架构的想法,它可以支持这一点。
如果某个组件已订阅事件并在其队列中存在一些未处理的事件时终止,则会丢失这些事件。
组件不支持挂起/休眠。
首选声明式 API,而非命令式 API
这一原则也称为“首选静态而非动态”。
与动态、命令式和基于运行时的 API 相比,组件框架逐渐灌输给静态、声明式和组装时间的 API。
这并不是说所有组件框架 API 都是声明性的,一个完全静态的系统不是很有用!不过,一般而言,如果可以静态描述组件定义或行为的某个方面,则应提出该要求。
采用声明式方式具有以下优势:
- 可访问性:开发者和安全专家可以访问系统的结构和安全政策。原则上,也可以通过各种方式向最终用户介绍它们,例如“模块 X 想要访问麦克风...”
- 对齐:安全边界明确标记,并与成熟的架构抽象保持一致。在这里,我们将它们放置在组件实例的边界上。
- 可审核性:声明可方便人工审核。 将最敏感的声明(例如基于角色的访问权限控制政策)集中起来也有很大帮助。
- 可测试性:可以轻松评估修改声明的预期结果和实际结果。
- 不可变性:权威声明可以嵌入到系统的信任链中并经过验证。
目前实现的功能
声明式 API 的愿景是通过 CML 实现的。CML 静态描述组件的输入和输出(功能路由)、组合(子级)和执行信息。
组合起来,Fuchsia build 中的组件清单构成了“组件实例树”,可以使用主机端工具 (scrutiny
),甚至只需检查源文件即可探索该树。此外,scrutiny
的 verify routes
插件还会在 CQ 上自动运行,用于验证静态拓扑中的所有路由均完好无损。*
安全政策许可名单是声明式 API 的另一个部分。
组件框架 API 的某些部分是命令式的,但仅在有充分的理由时是必需的。示例包括:集合、动态优惠、RealmBuilder 以及从集合进行服务聚合。
未实现的愿景
组件框架 API 的动态部分不够完善。过去,许多此类问题都被委托给会话框架,但后来会话框架已停用。总体而言,当前产品在动态组件配置方面要求不高。但是,当 Fuchsia 采用第三方或更开放的产品(如工作站)时,情况可能会发生变化。
组件框架 API 往往要么主要是静态的,要么主要是动态的;两者之间几乎没有太多关系(服务聚合是一个例外情况)。在某些情况下,使用主要是静态但将某些方面委托给运行时的 API,或者主要为动态但受静态描述的“上限”限制的 API 会很有用。
沙盒
组件实例不知道其sandbox中的服务实际来自何处。它们会感知由其沙盒定义的主观现实。因此,除非为组件实例提供了一些外部认证方法,但可能存在隐蔽边信道,否则组件实例应该无法区分它们是在“测试”沙盒还是“真实”沙盒中运行。
家长对孩子拥有很大的控制权:
- 虽然父组件实例知道其每个子组件实例的网址,但反过来却不行。
- 当组件请求使用功能时,它们无法控制这些功能的来源。组件必须信任其父级提供了来自可信来源的功能。沙盒支持一种称为“递归对称”的属性。这是一种思路,如果我们将子树的根设为其与完整拓扑的同态的根,则组件拓扑的子树可被视为与完整组件拓扑同构。具体而言,应该可以在组件拓扑的子树中运行单独的系统副本,而不会破坏系统。
这一原则的推论是,组件管理器应该没有全局单例。
目前实现的功能
沙盒化这一愿景在很大程度上得以实现。定义子树时,它的行为非常类似于完整拓扑:
- 由于组件无法访问其父级的相关信息,因此“无法判断”它属于树的哪一部分。例如,无法区分组件是在测试环境中运行还是在生产环境中运行的(除非它通过与所提供的功能之间的交互推断出这一事实)。
- 具有
framework
来源的所有 capability 均已限定作用域。 - 组件框架提供了许多内置功能,但您可以使用其他来源的功能来替换这些功能。这包括运行程序和解析器。
- 当您订阅的范围限定为子树根的事件时,事件的“名称”相对于根。
- 可以运行 Archiveist 的嵌套实例。发送到此归档人的选择器与此归档管理员订阅的事件相关。
未实现的愿景
组件框架 API 的某些部分使用称谓:
系统关闭 API 尽管可以将其作用域限定为某个子树,但始终会关闭整个树。
有一种想法是,您可以对子树运行 ffx component relative
,但这并未实现。
与此相关的一个相关理念从未实现,即应该可以在树中运行组件管理器的嵌套实例,并且应该可以使用父树在嵌套组件管理器下“组合”子树。
递归对称性最强大,可支持“提升”拓扑的子树在不同设备上运行,并且所有保留的状态均完好无损。
封装
与 OOP 一样,封装是指从所包含组件中隐藏组件的内部结构或数据。具体而言,这意味着组件实例应该只直接了解其子级组件(它们自行实例化的组件),而不能直接了解其子级的后代,而不了解它们自己的祖先。
目前实现的功能
组件忠于封装。父级可以访问其子级的身份和公开功能,但不能访问其孙级,除非一些特权 API。*
如沙盒中所述,没有任何防御措施来防范可能会为子级提供已破解功能的恶意父级。这是设计使然;在基于功能的系统中,父项支配其子项是正常现象。
未实现的愿景
通常,子项的内部状态与其父项的内部状态是隔离开来的,就像其他任何两个组件实例一样。不过,家长可以采用一些巧妙的方法来规避这个问题。例如,父级可以注入一个行为大部分类似于 ELF 运行程序的伪 ELF 运行程序,但它会向组件进程注入一个会渗漏组件私有内存的线程。
松散耦合
松散耦合有助于更轻松地改进组件。组件架构会将常用 IPC 协议和数据格式后的大多数组件实现细节(例如用于实现组件的编程语言)抽象化。
组件使用某项功能时,应明确声明需要该功能满足的约束条件。只要 capability 提供程序满足这些限制,就应该有可能用一种实现取代另一种实现。此属性称为“可替换性”。此类限制条件的示例如下:
当组件按名称从其命名空间请求功能时,要使用的实现选择取决于组件的祖先,因为它们为组件设置环境。与组件明确请求绑定到这些功能的特定实现(尽管有时可能)相比,这种针对功能发现的“按意义调用”方法使系统更具动态性且更具可配置性。
目前实现的功能
功能是组件的输入和输出。在很大程度上,组件之间的交互可以按照它们之间路由的功能来描述,从这个意义上讲,组件依赖于彼此的接口,而不是其实现。
不过,当组件实例化子组件时,它会通过指定网址来选择子组件。在这种情况下,组件可能需要子级的特定实现。这是一种更紧密的耦合,但有一定的自由度,因为网址是相对于解析器进行解析的,而解析器则最终决定要将网址解析到哪个组件。
组件框架会隐藏对等方的身份,这支持可替换性。
未实现的愿景
组件仅依赖于功能名称,而不依赖于任何版本信息。这样可使组件与版本紧密耦合。平台 ABI 版本将解决部分问题。
目前尚不清楚组件网址的依赖项在多大程度上符合此原则。
可更新性
组件可以独立于其他组件进行更新。
组件二进制文件和资源可以在停止临近使用时立即提取、缓存和移除,从而为其他组件释放存储空间。
系统会对软件包进行签名以验证其真实性和完整性,因此,您可以安全地从任何可用来源(包括来自其他 Fuchsia 设备)再次检索软件包。
目前实现的功能
在 eng build 中,重新启动组件时,其运行时信息(即软件包、二进制文件和命名空间)会从软件包服务器更新到最新版本。(不过,否则其清单不会更新。)
组件终止时,系统会舍弃组件的运行时资源(二进制文件和软件包)。
未实现的愿景
有一个 RFC 已获准用于即时更新,这允许在 OTA 之外更新软件包。不过,为将此更新流程与组件框架集成,仍需完成一些工作。
组件管理器绝不会逐出清单的缓存副本。在 eng build 中,这可能会导致组件的运行时状态与其子项或功能路由之间不一致。
易用性
组件架构的基本要素应便于开发者学习和应用。
组件架构提供数量相对较少的通用基元,这些基元有效满足了 Fuchsia 所有架构层(从设备驱动程序到第三方最终用户应用)的软件组合需求。所有组件都使用相同的基元,尽管它们可能会因其各自的角色而收到不同的功能。
组件架构还避免对特定于产品的要求做出假设,例如产品是否具有界面或其工作原理。这样一来,我们便无需针对每次出现的新用例重复劳动。
组件框架有责任让用户轻松充分利用组件框架。下面提供了一些实现方法:
- 设计 API,使其能够自然地引导用户朝着正确的方向前进。
- 发布无障碍、全面且最新的文档。
- 提供与实际用户问题相关的范例。
目前实现的功能
我们在 //docs/concepts/components 和 //docs/development/components 下提供了参考文档。请参阅组件入门指南。
我们在//示例下提供了一些基本示例。
未实现的愿景
总体而言,许多组件框架 API 并不像本来那样优雅或方便用户使用。
迭代组件以及在测试之外验证正确性的开发者经验非常有待改进。我们有一些针对基于运行时的组件探索工具的想法,目前尚未涉足。
我们需要编写更多文档,尤其是方法式文档。此外,一些现有文档可能会受益于一些他人的喜爱。我们的示例相当基础,仅限于 C++ 和 Rust。我们或许可以从更复杂或真实的示例中受益。
愿景存在缺口
组件框架对 Zircon 的某些部分提供抽象,但这些抽象与 Zircon 不同,而且不会捕获 Zircon 提供的所有功能。例如:
- Zircon 具有用于构建所有类型对象的政策控制功能。组件框架仅为其中一部分组件(例如进程对象创建)提供管道。
- Zircon 具有组件框架不支持的功能类型(例如 eventpairs、套接字)。
- 强大的框架功能始终是可用于获取 Zircon 对象的“工厂”。无法路由单例 Zircon 对象。
组件框架几乎无法提升与使用传统程序模型编写的软件的兼容性。这可能是有意为之,但我们将来可能会决定纳入其中一些功能。
附录
框架功能
框架(通过组件管理器)允许访问父级未明确授予的一些功能,例如:
fuchsia.component.Realm
:允许组件控制其子项的生命周期。- 组件级范围的事件流,允许组件接收关于子级的生命周期事件。- 通过
/pkg
访问自己的软件包,每个组件都无需请求即可获得它
不过,由于框架功能从未提供从容器环境访问功能这一不变性,这些功能不会违反最小权限、无环境授权或封装。
公开对等方信息的 API
有一些特权 API(例如领域级事件流)会公开有关组件的内部信息,例如其名称、网址或传出目录。但是,这些 API 处于锁定状态,只能由非生产组件或具有特殊特权的组件(如 archivist
或 debug_data
)使用。
拓扑的静态验证的限制
可以进行静态验证的拓扑数量存在限制。当探索到达集合时,通常必须停止,因为集合的内容是运行时确定的。