文件系统架构


英文原文快照


本文旨在描述Fuchsia文件系统的高层次视图,包括文件系统的初始化和标准操作(如Open、Read、Write等)的讨论,以及在微内核上实现用户空间文件系统的特殊行为。此外,本文档描述了通过VFS层的命名空间访问,该访问可用于与非存储实体(例如系统服务)进行通信。

文件系统即服务

与更常见的宏内核不同,Fuchsia的文件系统完全位于用户空间内,它没有链接到内核也没有加载内核,而仅仅只是用户空间内的可以作为文件系统出现的服务进程。因此,Fuchsia的文件系统本身可以轻松地被更改——而修改不需要重新编译内核。实际上,无需重新启动即可更新到新的Fuchsia文件系统。

与Fuchsia上的其他本地服务一样,使用句柄原语而不是系统调用的方式成为与文件系统服务交互的主要模式,内核既不了解文件、目录,也不了解文件系统本身。因此,文件系统的客户程序无法直接向内核请求“访问文件系统”。

因此,此架构意味着与文件系统的交互仅限于以下接口:

  • 在与文件系统服务建立的通信通道(channel)上发送的消息。对于客户端文件系统,这些通信信道可以是本地或远程的。
  • 初始化的例程(需要在每个文件系统的基础上进行大量配置:网络文件系统需要网络访问权限,持久化文件系统可能需要访问块设备,而内存文件系统只需要某机制来分配新的临时页面即可)。

此接口设计的一个好处是,可通过通道访问的任何资源都可以通过实现文件或目录的预期协议的方式,使它们看起来像文件系统。例如,”serviceFS”(在本文档后面会更详细地讨论)允许通过文件系统接口进行服务发现。

文件的生命周期

建立连接

为了打开文件,Fuchsia应用程序(客户端)通过名为RemoteIO的协议将RPC请求发送到文件系统服务上。RemoteIO定义了用于在文件系统客户端和服务之间传输消息和句柄的有线格式。Fuchsia进程不是与内核实现的VFS层交互,而是向实现了文件,目录和设备的协议的文件系统服务发送请求。为了向其中之一的实体发送Open请求,Fuchsia进程必须通过现有句柄将RPC消息传输到目录上; 关于此过程的更多详细信息,请参阅文件打开操作的生命周期

命名空间

在Fuchsia上,命名空间是一个小型的文件系统,它完全存在于客户端中。在最基本的层次上,客户程序以”root”形式保存”/“并将句柄与其关联的想法是一个非常原始的命名空间。可以为Fuchsia进程提供一个任意的目录句柄来表示”root”,而不是典型的单一“全局”文件系统命名空间,从而限制了其命名空间的范围。为了限制该作用域,Fuchsia文件系统有意设计为不允许通过”dotdot”方式来访问父目录(英文原文)

Fuchsia进程还可以将某些路径操作重定向到单独的文件系统服务上。当客户端引用 “/bin”时,客户程序可以选择将这些请求重定向到表示”/bin”目录的本地句柄上,而不是直接向”root”目录中的”bin”目录发送请求。命名空间,像其他所有文件系统结构一样,对内核是不可见的:相反,它们被实现为客户端的运行时(例如libfdio)或,客户端代码和直接使用句柄操纵远程文件系统之间的部分。

由于命名空间在句柄上操作,并且大多数Fuchsia资源和服务都可通过句柄访问,因此它们是非常强大的概念。文件系统对象(如目录和文件)、服务、设备、程序包和环境(对特权进程可见)都可以通过句柄来操纵,并可以在子进程中任意组合。因此,命名空间允许在应用程序中进行可自定义的资源发现,使得一个进程在”/svc”中看到的服务可能与其他进程看到的不一样,同时可以根据应用程序启的动策略进行限制或重定向。

有关限制进程capability的机制和策略的更多详细信息,请参阅沙箱有关的文档。

传递数据

建立连接后,无论是文件、目录、设备还是服务,都可以使用RPC消息传输后续操作。这些消息使用服务能够验证和理解的有线格式,在一个或多个句柄上传输。对于文件、目录和设备,这些操作使用RemoteIO协议,而在服务的情况下,这些操作则使用FIDL协议。尽管有计划将所有操作统一到FIDL协议中。

例如,为了在文件中执行seek操作,客户端需要在RIO消息中发送带有目标位置和”whence”的ZXRIO_SEEK消息,请求将返回新的seek位置。为了对文件执行truncate操作,可以对所需的新文件系统发送FDIO_TRUNCATE消息,请求将返回状态消息。为了读取目录,可发送ZXRIO_READDIR消息,请求将返回返回目录列表。如果将这些请求发送到无法处理它们的文件系统实体上(例如发送到文本文件的ZXRIO_READDIR消息),则并不会执行该操作,并返回错误消息。

内存映射

对于能够提供支持的文件系统,内存映射文件稍微会复杂一些。为了能实际地mmap文件的一部分,客户端发送FDIO_MMAP消息,并接收虚拟内存对象(VMO)作为响应。而后,此对象通常通过虚拟内存地址区域(VMAR)映射文件到客户端的地址空间上。将文件内部”VMO”的有限视图传回客户端需要中间消息传递层的额外工作,因此它们可感知到它们正在传回服务产生的对象句柄。

通过传回这些虚拟内存对象,客户端可以快速访问文件的内部数据,而不会实际承受往返IPC消息的成本。此功能使得mmap成为试图在文件系统交互上实现高吞吐量客户端的有吸引力选项。

在撰写本文时,内核尚不支持按需分页,并且尚未应用到文件系统上。因此,如果客户端向“内存映射”区域写入,则文件系统无法正确地识别哪些页面是存在的和哪些页面尚未存在。为了应对这种限制,mmap只实现在只读文件系统上,例如blobfs。

其他作用于路径上的操作

除了open操作之外,还有一些值得讨论的基于路径的操作:renamelink。与open操作不同,这些操作实际上同时作用于多个路径上,而不是单个位置上。这使得它们的使用变得复杂:如果调用rename('/foo/bar', 'baz'),文件系统则需要找到一种可行的方法,以达到以下目的: <!— * Traverse both paths, even when they have distinct starting points (which is the case this here; one path starts at root, and other starts at the CWD)

  • Open the parent directories of both paths
  • Operate on both parent directories and trailing pathnames simultaneously —>

  • 遍历两条路径,即使它们具有不同的起点(这里的情况是:一条路径从根目录开始,另一条路径从当前目录CWD开始)

  • 打开两个路径的父目录
  • 同时在父目录和尾路径名(即文件名)上进行操作

为了满足这种行为,VFS层利用了一种名为”cookies”的Zircon概念,这些cookie允许客户端的操作使用句柄在服务器上存储对象的打开状态,而稍后可使用相同的句柄引用它。Fuchsia文件系统使用此功能来引用一个Vnode,同时作用于另一个之上。

这些多路径操作执行以下操作:

  • 打开源路径的父vnode(对于路径/foo/bar,则意味着打开/foo
  • 打开目标路径的父vnode(对于baz,这意味着打开当前工作目录)并使用IOCTL_DEVMGR_GET_TOKEN操作获取vnode的token,该token是文件系统cookie的句柄。
  • 向源路径的父vnode发送rename请求,并同时发送相应的源路径和目标路径(barbaz),以及先前获取的vnode token。这样就为文件系统间接安全地引用目标路径的vnode提供了一种机制——如果客户端提供无效句柄,内核将拒绝对cookie访问的请求,并从服务端返回错误。

文件系统生命周期

挂载

初始化Fuchsia文件系统时,通常使用两个句柄来创建它们:其中一个用于与挂载文件系统通信的通道(被称“挂载点”通道——此通道的”挂载”端保存为在父Vnode中名为remote的字段,另一端连接到新文件系统的根目录)句柄,以及(可选的)另一个和底层block device交互的句柄。一旦文件系统初始化完成(从块设备读取初始状态,找到根vnode等),它就会在挂载点通道上标记一个信号(ZX_USER_SIGNAL0)。这将通知其(已挂载的)父文件系统:子文件系统已准备就绪可以使用。此时,在初始化时传递给文件系统的通道可用于发送文件系统请求,如”Open”等操作。

此时,(已挂载的)父文件系统将与远程文件系统的连接“固定到”Vnode上。在观察Vnode时,可依路径行走的VFS层将检查此远程句柄:如果检测到远程句柄,则传入相应的请求(打开,重命名等)将转发到远程文件系统而不是底层的节点上。如果用户实际上想要与挂载节点而不是远程文件系统进行交互,它们可以将O_NOREMOTE标志传递给open操作作为标识。

与许多其他操作系统不同,”挂载文件系统”的概念并不存在于全局可访问表中。相反,“存在什么样的挂载点?”这样的问题只能在特定于文件系统的基础上进行回答——任意文件系统可能无法访问有关其他文件系统存在的挂载点信息。

当前的文件系统

由于Fuchsia架构的模块化特性,将文件系统添加到操作系统中变得非常简单。目前,存在一些特定文件系统,旨在满足各种不同的需求。

MemFS:内存中的文件系统

MemFS用于实现对/tmp这样的临时文件系统的请求。其中文件系统中的文件完全存在于RAM中,而不是传输到底层块设备上。此文件系统当前也用于”bootfs”协议,其中表示文件和目录集合的大型只读VMO在引导时会将其打包到用户可访问的vnode上(使得这些文件可在/boot中进行访问)。

MinFS:持久化文件系统

MinFS是一个简单的传统文件系统,能够持久化存储文件。与MemFS一样,它广泛使用前面提到的VFS层,但与MemFS不同的是,它需要一个块设备的附加句柄(在启动时传输到新MinFS进程上)。为了便于使用,MinFS还提供了各种工具:用于格式化的”mkfs”,用于验证的”fsck”,以及用于从命令行向命名空间添加或删除MinFS文件系统的”mount”和”umount”。

Blobfs:一个不可变的,完整性验证的程序包存储文件系统

Blobfs是一个简单,扁平化的文件系统,它针对“一次写入,然后只读”的签名数据(英文原文),如程序包(英文原文)进行了优化。除了两个小的先决条件(文件名是确定性的,可寻址的文件Merkle树的根节点哈希值,其用于完整性验证)和文件大小的先决知识(在将blob写入存储体之前通过调用ftruncate识别为Blobfs)之外,Blobfs看起来像一个典型的文件系统。它可以被挂载和卸载,并包含单一扁平的哈希目录,并可以通过openreadstatmmap等操作访问blob。

ThinGS:用Go编写的FAT文件系统

ThinFS是FAT文件系统的一个Go语言实现。它的存在有两个目的:首先,证明了我们的系统实际上是模块化的,并能够使用新型的文件系统,而与其实现语言或运行时无关;其次,它提供了一种在EFI分区和许多USB存储器上读取通用文件系统的机制。

FVM

TODO: smklein