文件系统架构
本文旨在描述Fuchsia 文件系统的高级视图。 从文件系统的初始化开始,讨论标准文件的操作(例如打开、读取、写入等), 以及在微内核之上实现用户空间文件系统。 此外,本文档描述了 VFS 级别 遍历命名空间,该命名空间可用于与非存储实体(如系统服务)进行通信。
文件系统服务
与更常见的单核不同,Fuchsia 的文件系统完全存在于用户空间中。 它们不与内核链接或加载,它们只是实现可以显示为文件系统的服务器的用户空间进程。 因此,Fuchsia 的文件系统本身可以轻松更改——修改不需要重新编译内核。 事实上,无需重启即可更新到新的 Fuchsia 文件系统。
与 Fuchsia 上的其他服务一样, 与文件系统服务端交互的主要模式是使用句柄原语而不是系统调用来实现的。 内核对文件、目录或文件系统一无所知。 因此,文件系统客户端不能直接向内核请求“文件系统访问”。
这种架构意味着与文件系统的交互仅限于以下接口:
- 在与文件系统服务建立的通信通道上发送的消息。 对于客户端文件系统,这些通信通道可能是本地的,也可能是远程的。
- 常规初始化(预计将在每个文件系统的基础上进行大量配置; 网络文件系统需要网络访问,持久文件系统可能需要块设备访问, 内存文件系统只需要一种分配新临时页面的机制) 。
作为这个接口的好处, 任何通过通道访问的资源都可以通过实现文件或目录的预期协议使自己看起来像文件系统。 例如,“serviceFS”(本文档稍后将详细讨论)允许通过文件系统接口进行服务发现。
文件生命周期
建立连接
为了打开文件,Fuchsia 程序(客户端)使用 FIDL 向文件系统服务器发送 RPC 请求。
FIDL 定义了在文件系统客户端和服务端之间传输消息和句柄的有线格式。 Fuchsia 进程不与内核实现的 VFS 层交互,而是将请求发送到文件系统服务, 这些服务实现了文件、目录和设备的协议。 要发送这些打开请求之一,Fuchsia 进程必须通过现有句柄将 RPC 消息传输到目录; 有关此过程的更多详细信息,请参阅 “打开”的生命周期。
命名空间
在 Fuchsia 上,命名空间
是一个完全存在于客户端中的小文件系统。
在最基本的层面上,客户端将/
保存为 root 并将句柄与其关联的想法是一个非常原始的命名空间。
可以为 Fuchsia 进程提供一个任意目录句柄来表示“root”,而不是典型的单一“global”文件系统命名空间,
从而限制它们的命名空间范围。
为了限制这个范围,Fuchsia 文件系统 故意不允许通过 dotdot 访问父目录。
Fuchsia 进程可以另外将某些路径操作重定向到单独的文件系统服务端。
当客户端引用/bin
时,客户端可以选择将这些请求重定向到代表/bin
目录的本地句柄,
而不是直接向“root”目录中的“bin”目录发送请求。
与所有文件系统结构一样,命名空间在内核中是不可见的:
相反,他们在客户端一侧运行中实现(例如 libfdio),
并且被插入在大多数客户端代码和远程文件系统的句柄之间。
由于命名空间对句柄进行操作,并且大多数 Fuchsia 资源和服务都可以通过句柄访问,
因此它们是非常强大的概念。
文件系统对象(例如目录和文件)、服务、设备、包和环境(特权进程可见)都可以通过句柄使用,
并且可以在子进程中任意组合。
因此,命名空间允许在应用程序中进行可定制的资源发现。
一个进程在/svc
中观察到的服务可能与其他进程看到的相匹配,也可能不匹配,
并且可以根据应用程序启动策略进行限制或重定向。
有关用于限制过程能力的机制和策略的更多详细信息,请参阅关于 sandboxing。
传递数据
一旦建立了与文件、目录、设备或服务的连接,后续操作也将使用 RPC 消息进行传输。 这些消息使用服务器验证和理解的有线格式在一个或多个句柄上传输。
对于文件、目录、设备和服务,这些操作使用 FIDL 协议。
例如,要在文件中查找,客户端将发送一条“查找”消息, 其中包含所需位置和 FIDL 消息中的“来源”,然后将返回新的查找位置。 要截断文件,可以使用新的所需文件系统发送“截断”消息,并返回状态消息。 要读取目录,可以发送“ReadDirents”消息,并返回目录列表。 如果这些请求被发送到无法处理它们的文件系统实体,则会发送错误, 并且不会执行操作(就像发送到文本文件的“ReadDirents”消息一样)。
内存映射
对于能够支持它的文件系统,内存映射文件稍微复杂一些。 要真正“mmap”文件的一部分,客户端会发送“GetVmo”消息,并接收虚拟内存对象或 VMO 作为响应。 然后,此对象通常使用虚拟内存地址区域或 VMAR 映射到客户端的地址空间。 将文件内部“VMO”的有限视图传输回客户端需要中间消息传递层的额外工作, 因此他们可以知道他们正在传回服务器供应的对象句柄。
通过传回这些虚拟内存对象,客户端可以快速访问表示文件的内部字节,而无需实际承担往返 IPC 消息的成本。 此功能使 mmap 成为尝试高吞吐量文件系统交互的客户端的一个有吸引力的选择。
在撰写本文时,内核不支持按需分页,并且还没有连接到文件系统中。 因此,如果客户端写入“内存映射”区域,文件系统将无法合理地识别哪些页面已被触及,哪些页面未被触及。 为了应对这一限制,mmap 仅在只读文件系统上实现,例如 blobfs。
作用于路径的其他操作
除了“打开”操作之外,还有一些基于路径的操作值得讨论:“重命名”和“链接”。
与“开放”不同,这些操作实际上一次作用于多条路径,而不是单个位置。
这使得它们的使用变得复杂:如果调用rename(‘/foo/bar’, ‘baz’)
可以实现,
文件系统需要:
- 遍历两条路径,即使它们有不同的起点(这里就是这种情况;一条路径从根开始,另一条从 CWD 开始)
- 打开两个路径的父目录
- 同时对父目录和尾随路径名进行操作
为了满足这种行为,VFS 层利用了称为“cookies”的 Zircon 概念。 这些 cookie 允许客户端操作使用句柄在服务器上存储打开状态,并在以后使用相同的句柄引用它。 Fuchsia 文件系统使用这种能力来引用一个 Vnode,同时作用于另一个。
这些多路径操作执行以下操作:
- 打开父源vnode(对于“/foo/bar”,这意味着打开“/foo”)
- 打开目标父 vnode(对于“baz”,这意味着打开当前工作目录)并使用操作
GetToken
获取 vnode 令牌, 它是文件系统 cookie 的句柄。 - 向源父 vnode 发送“重命名”请求,以及源和目标路径(“bar”和“baz”),以及之前获取的 vnode 令牌。 这为文件系统提供了一种机制,可以安全地间接引用目标 vnode——如果客户端提供了无效句柄, 内核将拒绝访问 cookie 的请求,服务端可以返回错误。
文件系统生命周期
挂载
初始化 Fuchsia 文件系统时,通常会使用两个句柄创建它们:
一个句柄指向用于与挂载文件系统通信的通道
(称为“挂载点”通道——该通道的“挂载”端保存为 父Vnode中名为“remote”的字段,
另一端将连接到新文件系统的根目录),并且(可选)另一个连接底层
block device 。
一旦文件系统被初始化(从块设备读取初始状态,找到根 vnode 等),
它就会开始在挂载点通道上为 [fuchsia.io/Node
] 请求提供服务。
此时,父(挂载)文件系统将连接“pins”到 Vnode 上的远程文件系统。 VFS 层能够在观察 Vnode 时检查此远程句柄的路径: 如果检测到远程句柄,则传入请求(打开、重命名等)被转发到远程文件系统而不是底层节点。 如果用户实际上想要与挂载点节点而不是远程文件系统交互, 他们可以将“O_NOREMOTE”标志传递给“打开”操作来识别此意图。
与许多其他操作系统不同,“挂载文件系统”的概念并不存在于全局可访问的表中。 相反,问题是“存在哪些挂载点?” 只能在特定于文件系统的基础上回答——任意文件系统可能无法访问有关其他地方存在哪些挂载点的信息。
文件系统管理
有一组文件系统操作被认为与“管理”相关,
包括“卸载当前文件系统”、“查询底层块设备路径”等。
这些操作由 io.fidl 中的 DirectoryAdmin 接口定义。
与此接口的连接允许访问“文件系统范围”状态,并受访问标志ZX_FS_RIGHT_ADMIN
的限制。
此访问权限必须明确请求,并且在缺少ZX_FS_RIGHT_ADMIN
的连接上请求时不会授予。
此权限在文件系统安装后提供给根连接
—— 一个合理的管理引导点
—— 但必须由安装工具保留以传播此访问权限,或者在将文件系统的连接出售给特权较低的客户端时必须删除。
这种ZX_FS_RIGHT_ADMIN
机制(有时称为O_ADMIN
,用于 POSIX 互操作声明)
将被用于文件系统管理的显式服务取代。
它不是作为一种“隐式权限”默默地附加到有限的目录连接上,
而是由文件系统组件公开的一个单独的接口。
这将(抽象地)允许文件系统分别公开“root directory”句柄和“administraction”句柄,
而不是在同一连接上重载它们。 一旦发生这种转变,ZX_FS_RIGHT_ADMIN
(和O_ADMIN
)标志将被弃用。
当前的文件系统
由于 Fuchsia 架构的模块化特性,可以直接向系统添加文件系统。 目前,存在少数文件系统,旨在满足各种不同的需求。
MemFS:内存文件系统
MemFS用于实现对像/tmp
这样的临时文件系统的请求,
其中文件完全存在于RAM中,不会传输到底层块设备。
这个文件系统目前也用于 “bootfs”协议,
其中表示文件和目录集合的大型只读 VMO 在启动时被解包到用户可访问的 Vnode 中
(这些文件可在 /boot
中访问)。
MinFS:持久文件系统
MinFS 是一个简单的传统文件系统,能够持久存储文件。 与 MemFS 一样,它广泛使用了前面提到的 VFS 层,但与 MemFS 不同的是, 它需要额外的块设备句柄(在启动时传输到新的 MinFS 进程)。 为了便于使用,MinFS 还提供了多种工具: “mkfs”用于格式化,“fsck”用于验证, 以及“mount”和“umount”用于从命令行将 MinFS 文件系统添加和减去命名空间。
Blobfs:一个不可变的、完整性验证的包存储文件系统
Blobfs 是一个简单的平面文件系统, 针对“一次写入,然后只读”signed data 进行了优化, 例如,作为package。 除了两个小的先决条件(文件名是确定性的,文件的 Merkle 树根的内容可寻址哈希,用于完整性验证) 和文件大小的前向知识(通过在将 blob 写入之前调用“ftruncate”识别给 Blobfs storage), Blobfs 看起来像一个典型的文件系统。 它可以挂载和卸载,它似乎包含一个单一的散列平面目录, 并且可以通过“open”、“read”、“stat”和“mmap”等操作访问 blob。
ThinFS:用 Go 编写的 FAT 文件系统
ThinFS 是 Go 中 FAT 文件系统的实现。 它有双重目的:首先,证明我们的系统实际上是模块化的, 并且能够使用新颖的文件系统,而不管语言或运行时。 其次,它提供了一种读取通用文件系统的机制,可以在 EFI 分区和许多 USB 记忆棒上找到。
FVM
Fuchsia Volume Manager是一个“逻辑卷管理器”, 它在现有块设备之上增加了灵活性。 当前的功能包括添加、删除、扩展和缩小虚拟分区的能力。 为了使这些功能成为可能,fvm 在内部维护了从(虚拟分区、块)到(切片、物理块)的物理到虚拟映射。 为了使维护开销最小化,它允许分区以称为切片的块的形式收缩/增长。 切片是原生块大小的倍数。 抛开元数据不谈,设备的其余部分被分割成片。 每个切片要么是空闲的,要么属于一个且仅属于一个分区。 如果一个分片属于一个分区,那么 FVM 会维护有关哪个分区正在使用该分片的元数据, 以及该分片在该分区内的虚拟地址。
Superblock在第0块描述FVM的磁盘布局,可能看起来像
+---------------------------------+ <- 物理块 0
| 元数据 |
| +-----------------------------+ |
| | 元数据副本 1 | |
| | +------------------------+ | |
| | | 超级块 | | |
| | +------------------------+ | |
| | | 分区表 | | |
| | +------------------------+ | |
| | | 切片分配表 | | |
| | +------------------------+ | |
| +-----------------------------+ | <- 元数据的大小在超级块中描述
| | 元数据副本 2 | |
| +-----------------------------+ |
+---------------------------------+ <- 超级块描述切片的开始
| |
| 切片 1 |
+---------------------------------+
| |
| 切片 2 |
+---------------------------------+
| |
| 切片 3 |
+---------------------------------+
| |
分区表由几个虚拟分区条目(VPartitionEntry
)组成。
除了包含名称和分区标识符之外,这些 vpart 条目中的每一个都包含为此分区分配的片数。
切片分配表由紧密打包的切片条目(SliceEntry
)组成。 每个条目包含
- 分配状态
- 如果已分配,
- 它属于哪个分区
- 切片映射到分区内的逻辑切片
FVM 库可以在 这里 找到。 在 paving 中,一些分区从主机复制到目标。 所以分区和 FVM 文件本身可以在主机上创建。 为此,有主机端实用程序 这里。 可以使用 fvm-check 详细验证 FVM 设备/文件的完整性