本文档旨在简要介绍 Fuchsia 文件系统,从其初始化、标准文件系统操作(例如打开、读取、写入等)的讨论,以及实现用户空间文件系统的怪癖。此外,本文档还介绍了如何在命名空间中进行 VFS 级遍历,该遍历可用于与非存储实体(例如系统服务)进行通信。
文件系统是服务
与更常见的单体内核不同,Fuchsia 的文件系统完全位于用户空间中。它们不会与内核关联或加载到内核中;它们只是实现可显示为文件系统的服务器的用户空间进程。因此,Fuchsia 的文件系统本身可以轻松更改,无需重新编译内核即可进行修改。
图 1:典型的文件系统进程框图。
与 Fuchsia 上的其他原生服务器一样,与文件系统服务器交互的主要模式是使用句柄基元(而非系统调用)实现的。内核对文件、目录或文件系统一无所知。因此,文件系统客户端无法直接向内核请求“文件系统访问权限”。
这种架构意味着与文件系统的互动仅限于以下接口:
- 通过与文件系统服务器建立的通信通道发送的消息。这些通信通道可能是客户端文件系统的本地通道,也可能是远程通道。
- 初始化例程(预计将按文件系统进行大量配置;网络文件系统需要网络访问权限,永久性文件系统可能需要块设备访问权限,内存中文件系统只需要一种机制来分配新的临时页面)。
此接口的一个优势是,通过通道访问的任何资源都可以通过实现文件或目录的预期协议,使自己看起来像文件系统。以类似于文件系统的结构提供其capabilities。
文件生命周期
建立连接
如需打开文件,Fuchsia 程序(客户端)会使用 FIDL 向文件系统服务器发送 RPC 请求。
FIDL 定义了在文件系统客户端和服务器之间传输消息和句柄的线格格式。Fuchsia 进程会向文件系统服务发送请求,而不是与内核实现的 VFS 层交互,这些文件系统服务会实现文件、目录和设备的协议。如需发送其中一个打开请求,Fuchsia 进程必须通过现有句柄将 RPC 消息传输到目录;如需详细了解此过程,请参阅打开文档的生命周期。
命名空间
在 Fuchsia 上,命名空间是完全位于客户端内的一小部分文件系统。在最基本的层面上,客户端将“/”保存为根目录并将句柄与其相关联的想法是一种非常原始的命名空间。您可以为 Fuchsia 进程提供任意目录句柄来表示“根”,而不是典型的单个“全局”文件系统命名空间,从而限制其命名空间的范围。为了限制此范围,Fuchsia 文件系统故意不允许通过点点访问父级目录。
Fuchsia 进程还可能会将某些路径操作重定向到单独的文件系统服务器。当客户端引用“/bin”时,客户端可以选择将这些请求重定向到代表“/bin”目录的本地句柄,而不是直接将请求发送到“root”目录下的“bin”目录。与所有文件系统结构一样,命名空间在内核中不可见:而是在客户端运行时(例如 libfdio)中实现,并在大多数客户端代码与远程文件系统的句柄之间插入。
由于命名空间对句柄进行操作,并且大多数 Fuchsia 资源和服务都可以通过句柄访问,因此它们是非常强大的概念。文件系统对象(例如目录和文件)、服务、设备、软件包和环境(对特权进程可见)都可以通过句柄使用,并且可以在子进程中任意组合。因此,命名空间支持在应用中进行可自定义的资源发现。一个进程在“/svc”中观察到的服务可能与其他进程看到的服务不一致,并且可以根据应用启动政策受到限制或重定向。
如需详细了解用于限制进程功能的机制和政策,请参阅沙盒文档。
传递数据
建立与文件、目录、设备或服务的连接后,后续操作也会使用 RPC 消息传输。这些消息通过一个或多个句柄传输,使用服务器可以验证和理解的线格格式。
对于文件、目录、设备和服务,这些操作使用 FIDL 协议。
例如,如需在文件中跳转,客户端会在 FIDL 消息中发送包含所需位置和“whence”的 Seek
消息,系统会返回新的跳转位置。如需截断文件,可以发送包含所需新文件系统的 Truncate
消息,并返回状态消息。如需读取目录,可以发送 ReadDirents
消息,系统会返回目录项列表。如果这些请求发送到无法处理它们的文件系统实体,系统会发送错误消息,并且不会执行操作(例如,发送到文本文件的 ReadDirents
消息)。
内存映射
对于能够支持内存映射的文件系统,内存映射文件会稍微复杂一些。如需实际“mmap”文件的一部分,客户端会发送“GetVmo”消息,并接收虚拟内存对象 (VMO) 作为响应。然后,此对象通常使用虚拟内存地址区域 (VMAR) 映射到客户端的地址空间。将文件内部“VMO”的受限视图传回给客户端需要中间消息传递层执行额外的工作,以便它们知道自己传回的是服务器供应商提供的对象句柄。
通过传回这些虚拟内存对象,客户端可以快速访问表示文件的内部字节,而无需实际承担往返 IPC 消息的开销。这一特性使 mmap 成为尝试在文件系统交互中实现高吞吐量的客户端的理想之选。
对路径执行的其他操作
除了“打开”操作之外,还有一些基于路径的其他操作值得讨论:“重命名”和“链接”。与“打开”不同,这些操作实际上会同时作用于多个路径,而不是单个位置。这会使其使用变得复杂:如果调用了“rename(‘/foo/bar’, ‘baz’)”,文件系统需要想出一种方法来:
- 遍历这两个路径,即使它们具有不同的起点(本例就是如此;一个路径从根目录开始,另一个路径从 CWD 开始)
- 打开这两个路径的父目录
- 同时对父级目录和尾随路径名进行操作
为了实现此行为,VFS 层利用了一种名为“cookie”的 Zircon 概念。这些 cookie 允许客户端操作使用句柄在服务器上存储打开状态,并稍后使用相同的句柄引用该状态。Fuchsia 文件系统使用此功能在对一个 Vnode 执行操作时引用另一个 Vnode。
这些多路径操作会执行以下操作:
- 打开父级源 vnode(对于“/foo/bar”,这意味着打开“/foo”)
- 打开目标父 vnode(对于“baz”,这意味着打开当前工作目录),并使用操作
GetToken
(即文件系统 Cookie 的句柄)获取 vnode 令牌。 - 向来源父 vnode 发送“重命名”请求,以及来源和目标路径(“bar”和“baz”)以及先前获取的 vnode 令牌。这为文件系统提供了一种机制,以便安全地间接引用目标 vnode。如果客户端提供无效的句柄,内核将拒绝访问 Cookie 的请求,并且服务器可以返回错误。
文件系统生命周期
安装
Fshost 负责在系统上装载文件系统。在撰写本文时,我们正在进行更改,以使文件系统作为组件运行(尽管 fshost 仍会控制这些文件系统的挂载)。我们会尽可能使用静态路由。请参阅 fuchsia.fs.startup/Startup 协议。
文件系统管理
有一系列文件系统操作被视为与“管理”相关,包括“卸载当前文件系统”。这些操作由 admin.fidl 中的 fs.Admin 接口定义。文件系统会导出此服务以及对文件系统根目录的访问权限。
当前文件系统
由于 Fuchsia 架构的模块化特性,因此可以轻松地向系统添加文件系统。目前,存在一些文件系统,旨在满足各种不同的需求。
MemFS:内存中文件系统
MemFS 用于实现对临时文件系统(如 /tmp
)的请求,其中文件完全存在于 RAM 中,不会传输到底层块设备。此文件系统目前也用于“bootfs”协议,其中代表一组文件和目录的大型只读 VMO 会在启动时展开为用户可访问的 Vnode(这些文件在 /boot
中可用)。
MinFS:一种持久性文件系统
MinFS 是一种简单的传统文件系统,能够永久存储文件。与 MemFS 一样,它会广泛使用前面提到的 VFS 层,但与 MemFS 不同的是,它需要额外的块设备句柄(在启动时传输到新的 MinFS 进程)。为方便使用,MinFS 还提供了各种工具:“mkfs”用于格式化,“fsck”用于验证,以及“mount”和“umount”用于通过命令行向命名空间添加和移除 MinFS 文件系统。
Blobfs:可验证完整性的不可变软件包存储文件系统
Blobfs 是一种简单的扁平文件系统,专为“写入一次,然后只读”已签名数据(例如软件包)进行了优化。除了两个小的前提条件(文件名,即文件 Merkle 树根的确定性内容可寻址哈希,用于完整性验证)和对文件大小的预先了解(通过在将 blob 写入存储空间之前调用“ftruncate”来向 Blobfs 标识)之外,Blobfs 看起来就像一个典型的文件系统。它可以装载和卸载,似乎包含一个包含哈希的单个扁平目录,并且可以通过“open”“read”“stat”和“mmap”等操作访问 blob。
FVM
Fuchsia 卷管理器是一种“逻辑卷管理器”,可在现有块设备上增加灵活性。当前功能包括添加、移除、扩展和缩减虚拟分区。为了实现这些功能,FVM 在内部维护从(虚拟分区、块)到(slice、物理块)的物理到虚拟映射。为了最大限度地减少维护开销,它允许分区以称为“slice”的块进行缩减/扩容。切片是原生块大小的倍数。除了元数据之外,设备的其余部分会被划分为 slice。每个 slice 要么是空闲的,要么属于一个且只有一个分区。如果某个 slice 属于某个分区,FVM 会维护有关哪个分区正在使用该 slice 以及该 slice 在该分区中的虚拟地址的元数据。
FVM 的磁盘布局如下所示,并已在此处声明。
+---------------------------------+ <- Physical block 0
| metadata |
| +-----------------------------+ |
| | metadata copy 1 | |
| | +------------------------+ | |
| | | superblock | | |
| | +------------------------+ | |
| | | partition table | | |
| | +------------------------+ | |
| | | slice allocation table | | |
| | +------------------------+ | |
| +-----------------------------+ | <- Size of metadata is described by
| | metadata copy 2 | | superblock
| +-----------------------------+ |
+---------------------------------+ <- Superblock describes start of
| | slices
| Slice 1 |
+---------------------------------+
| |
| Slice 2 |
+---------------------------------+
| |
| Slice 3 |
+---------------------------------+
| |
分区表由多个虚拟分区条目 (VPartitionEntry
) 组成。除了包含名称和分区标识符之外,这些 vpart 条目中的每个条目还包含为此分区分配的 slice 数量。
slice 分配表由紧密打包的 slice 条目 (SliceEntry
) 组成。每个条目包含
- 分配状态
- (如果已分配),
- 它所属的分区以及
- 该 slice 映射到分区中的哪个逻辑 slice
如需查看 FVM 库,请点击此处。在铺设期间,系统会将某些分区从主机复制到目标设备。因此,分区和 FVM 文件本身可能会在主机上创建。为此,请使用此处提供的主机端实用程序。您可以使用 fvm-check 详细验证 FVM 设备/文件的完整性