为了全面了解 Fuchsia 上的文件系统访问,本文将深入探讨执行一些简单操作(例如打开文件)时所用各个层的详细信息。请务必注意:所有这些层都存在于用户空间中;即使与文件系统服务器和驱动程序交互,内核也只是用于将消息从一个组件传递到另一个组件。
系统会向以下各项发出调用:
open(“foobar”);
该请求会发送到哪里?
标准库:定义“open”的地方
“open”调用是由标准库提供的函数。对于 C/C++ 程序,这通常在 unistd.h
中声明,而后者在 libfdio 中有后备定义。
对于 Go 程序,Go 标准库中提供了等效(但不同的)实现。对于每种语言和运行时环境,开发者都可以选择采用自己的“开放”定义。
在单体内核中,open
将是系统调用周围的轻量级 shim,内核可能会处理路径解析、重定向等。在该模型中,内核需要根据有关调用方的外部知识来中介对资源的访问。不过,Zircon 内核故意不提供此类系统调用。相反,客户端通过通道访问文件系统 - 在初始化进程时,系统会为其提供一个命名空间,该命名空间是一个“绝对路径”->“句柄”映射表。通过将请求定向到此命名空间映射,系统会打开从进程内访问的所有路径。
不过,在此示例中,涉及打开“foobar”的请求使用了相对路径,因此可以通过表示当前工作目录的路径(本身表示为绝对路径和句柄)发送传入调用。
标准库负责获取一个(或多个)句柄,并使其看起来像文件描述符。因此,“文件描述符表”是客户端进程中存在的一个概念(如果客户端选择使用自定义运行时,则可以将其资源纯粹视为句柄,而“文件描述符”封装是可选的)。
不过,这引发了一个问题:给定文件、套接字、管道等的文件描述符,标准库会做些什么来使所有这些资源在功能上看起来相同?该客户端如何知道要通过这些句柄发送哪些消息?
Fdio
名为 fdio 的库负责为各种资源(文件、套接字、服务、管道等)提供统一接口。此层定义了一组函数(例如读取、写入、打开、关闭、定位等),这些函数可用于由各种协议支持的文件描述符。每个受支持的协议都负责提供客户端代码来解读其互动细节。例如,套接字会向客户端提供多个句柄;一个用于数据流,另一个用于控制平面。与之相反,文件通常仅使用单个通道来传输控制信息和数据(除非执行了额外的工作来请求内存映射)。虽然套接字和文件都可能会收到对 open
或 write
的调用,但它们需要以不同的方式解读这些命令。
在本文档中,我们将重点介绍文件系统客户端使用的主协议:FIDL。
FIDL
调用 open("foo")
的程序将调用标准库,找到与当前工作目录对应的“fdio”对象,并且需要向远程服务器发送请求以“请打开 foo”。如何实现这一点?该计划提供以下工具:
- 一个或多个表示与 CWD 的连接的句柄
- zx_channel_write:一种系统调用,可发送字节和句柄(通过通道)
- zx_channel_read:一种系统调用,可通过通道接收字节和句柄
- zx_object_wait_one:一种系统调用,可等待句柄变为可读/可写
使用这些基元,客户端可以将消息写入 CWD 句柄上的文件系统服务器,服务器可以读取该消息,然后在写回客户端时以“成功”或“失败消息”进行响应。在服务器努力确定要实际打开的内容时,客户端可以选择等待,也可以选择直接尝试读取状态消息。
在传输或接收消息时,客户端和服务器必须就对这些 N 字节和 N 个句柄的解释达成一致:如果它们之间存在分歧,消息可能会被丢弃(更糟糕的是,会扭曲为意外行为)。此外,如果此协议允许客户端对服务器进行任意控制,则此通信层将很容易被利用。
FIDL IO 协议介绍了这些字节和句柄在两个实体之间传输时实际上应是指什么。该协议描述了“预期句柄数量”“枚举的操作”和“数据”等内容。在本例中,open("foo")
会创建一个 Open
消息,并将 FIDL 消息的“data”字段设置为字符串“foo”。此外,如果向 open 传递任何标志(例如 O_RDONLY, O_RDWR, O_CREAT
等),这些标志将放置在 FIDL 结构的“arg”字段中。但是,如果操作被更改(例如,更改为 write
),则对此消息的解释将会改变。
此层级的确切字节一致性至关重要,因为它允许在完全不同的运行时之间进行通信:了解 FIDL 的进程可以在 C、C++、Go、Rust、Dart 程序(及其他程序)之间透明地轻松通信。
libfidl 包含 FIDL 的 C/C++ 实现的客户端和服务器端代码,并负责自动验证两端的输入和输出。
对于 open
操作,FIDL 协议要求客户端创建一个通道并将一端(作为句柄)传递给服务器。事务完成后,此通道可用作与打开的文件通信的机制,就像之前与“CWD”句柄进行通信一样。
通过将协议设计为让 FIDL 客户端(而不是服务器)提供句柄,通信更适合通过流水线进行。对 FIDL 对象的访问可以是异步的;在实际打开 FIDL 对象之前,可以传输对该对象的请求。这种模型允许客户端立即开始发送请求,而不是在组件完成启动过程并开始处理请求时阻塞打开,然后在组件准备就绪后响应这些请求。如需详细了解此行为如何应用于 capability 路由,请参阅协议打开的生命周期。
总的来说,“open”调用已通过标准库,对“CWD”fdio 对象执行操作,该操作将请求转换为 FIDL 消息,然后使用 zx_channel_write
系统调用将消息发送到服务器。客户端可以选择使用 zx_object_wait_one
等待服务器响应,也可以异步继续处理。无论哪种方式,都已创建一个通道,其中一个端位于客户端,另一端传输到“服务器”。
文件系统:服务器端
调度
消息从通道的客户端传输完毕后,便会位于通道的服务器端,等待读取。服务器由“持有通道另一端句柄的任何实体”标识 - 它可以与客户端位于同一(或不同)进程中,使用与客户端相同(或不同的)运行时,并使用与客户端相同(或不同的)语言编写。通过使用商定的线格格式,进程间依赖项会在通过通道进行的薄通信层出现瓶颈。
在未来的某个时间点,CWD 句柄的服务器端端点将需要读取客户端传输的消息。此过程并非自动进行的,服务器需要在接收句柄(在本例中为“当前工作目录”句柄)上有意等待传入消息。打开服务器对象(文件、目录、服务等)时,其句柄会注册到服务器端 Zircon 端口,该端口会等待其底层句柄可读取(表示有消息到达)或关闭(表示它们永远不会再收到消息)。此对象会将传入请求分派给适当的句柄,称为调度程序。它负责将传入消息重定向到回调函数,以及之前提供的一些表示打开连接的“iostate”。
对于使用 libfs 的 C++ 文件系统,此回调函数称为 vfs_handler
,它会接收一些关键信息:
- 由客户端提供的 FIDL 消息(如果句柄已关闭,则由服务器人为构造,使其看起来像“关闭”消息)
- 表示与句柄的当前连接的 I/O 状态(作为前面提到的“iostate”字段传递)。
vfs_handler
可以解读 I/O 状态以推断其他信息:
- 文件(或目录,如果已使用 readdir)中的光标
- 用于打开底层资源的标志
- Vnode,代表底层对象(可以在多个客户端或多个文件描述符之间共享)
此处理程序函数配备了这些信息,可充当一个大型“switch/case”表,根据客户端提供的“operation”字段将 FIDL 消息重定向到适当的函数。在打开的情况下,系统会将 Open
序数视为操作,因此 (1) 应提供句柄,并且 (2)“data”字段(“foo”)会被解读为路径。
VFS 层
在 Fuchsia 中,“VFS 层”是一个与文件系统无关的代码库,可以调度和解释服务器端消息,并在适当情况下调用底层文件系统中的操作。值得注意的是,此层完全是可选的 - 如果文件系统服务器不想与此库链接,则无义务使用它。如需成为文件系统服务器,进程必须仅了解 FIDL 传输格式。因此,一种语言中可能会有任意数量的“VFS”实现。目前有以下实现:
- 树内 C++ VFS:由 Fuchsia 的“主要”文件系统 minfs 和 blobfs 使用。它目前拥有所有 VFS 实现中最多的功能,但也可能是最难用的。
- 树内 Rust VFS:某些 Rust 文件系统(包括 fat32 实现)会使用此 VFS。它是较新的实现,目前提供的功能比 C++ 实现要少。
- SDK C++ VFS:面向 SDK 用户的“树内”C++ 版本的简化版。这最常用于服务发现等更简单的用途。
VFS 层定义了可路由到底层文件系统的操作的接口,包括:
- 对 Vnode 执行读写操作
- 从父 Vnode 中查找/创建/解除关联 Vnode(按名称)
- 按名称重命名/关联 Vnode
- 等等
如需实现文件系统(假设开发者想要使用共享 VFS 层),只需定义一个 Vnode 来实现此接口并链接到 VFS 层。这样,您只需付出极少的努力,几乎无需重复代码,即可提供“路径遍历”和“文件系统挂载”等功能。为了实现与文件系统无关,VFS 层对文件系统使用的底层存储没有先入为主的概念:文件系统可能需要访问块设备、网络或内存来存储数据,但 VFS 层只处理对路径、数据字节数组和 vnode 执行操作的接口。
路径漫步
如需打开服务器端资源,系统会向服务器提供一些起始点(由调用的句柄表示)和一个字符串路径。此路径会被“/”字符拆分为多个分段,系统会通过对底层文件系统的回调来“查找”每个组件。如果查找成功返回了 vnode,并且检测到另一个“/”段,则该过程会继续进行,直到 (1) lookup
找不到组件;(2) 路径处理到达路径中的最后一个组件;或 (3) lookup
找到 mountpoint vnode,该 vnode 是附加了“远程”句柄的 vnode。目前,我们将忽略挂载点 vnode,但文件系统挂载部分中对其进行了介绍。
假设 lookup
已成功找到“foo” Vnode。文件系统服务器将继续调用 VFS 接口“Open”,验证是否可以使用提供的标志访问请求的资源,然后调用“GetHandles”询问底层文件系统是否需要其他句柄来与 Vnode 交互。假设客户端同步请求“foo”对象(默认 POSIX open 调用暗含其中),与“foo”交互所需的任何其他句柄都会封装到一个小型 FIDL 说明对象中并传回客户端。或者,如果“foo”未能打开,系统仍会返回一个 FIDL 说明对象,但“status”字段会设置为错误代码,表示失败。假设“foo”打开成功。服务器将继续为“foo”创建一个“iostate”对象,并向调度程序注册该对象。这样一来,服务器就可以处理对“foo”的后续调用。“Foo”已打开,客户端现在可以发送其他请求了。
从客户端的角度来看,在“Open”调用开始时,系统会通过 CWD 句柄将路径和句柄组合传输到远程文件系统服务器。由于调用是同步的,因此客户端继续等待句柄的响应。服务器正确找到、打开并初始化此文件的 I/O 状态后,会发回一个“成功”FIDL 描述对象。客户端将读取此对象,标识调用已成功完成。此时,客户端可以创建一个代表“foo”句柄的 fdio 对象,使用文件描述符表中的条目引用它,并将 fd 返回给调用原始“open”函数的用户。此外,如果客户端想要向“foo”发送任何其他请求(例如“read”或“write”),则可以使用与打开的文件的连接直接与文件系统服务器通信,而无需在未来的请求中通过“CWD”进行路由。
打开内容的生命周期:图表
+----------------+
| Client Program |
+----------------+
| fd: x | fd: y |
| Fdio (FIDL)| Fdio (FIDL)|
+-------------------------+
| '/' Handle | CWD Handle |
+-------------------------+
^ ^
| |
Zircon Channels, speaking FIDL State BEFORE open(‘foo’)
| |
v v
+-------------------------+
| '/' Handle | CWD Handle |
+-------------------------+
| I/O State | I/O State |
+-------------------------+
| Vnode A | Vnode B |
+-------------------------+
| Filesystem Server |
+-------------------+
+----------------+
| Client Program |
+-------------------------+
| fd: x | fd: y |
| Fdio (FIDL)| Fdio (FIDL)|
+-------------------------+
| '/' Handle | CWD Handle | **foo Handle x2**
+-------------------------+
^ ^
| |
Zircon Channels, speaking FIDL Client Creates Channel
| |
v v
+-------------------------+
| '/' Handle | CWD Handle |
+-------------------------+
| I/O State | I/O State |
+-------------------------+
| Vnode A | Vnode B |
+-------------------------+
| Filesystem Server |
+-------------------+
+----------------+
| Client Program |
+-------------------------+
| fd: x | fd: y |
| Fdio (FIDL)| Fdio (FIDL)|
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
^ ^
| |
Zircon Channels, speaking FIDL Client Sends FIDL message to Server
| | Message includes a ‘foo’ handle
v v (and waits for response)
+-------------------------+
| '/' Handle | CWD Handle |
+-------------------------+
| I/O State | I/O State |
+-------------------------+
| Vnode A | Vnode B |
+-------------------------+
| Filesystem Server |
+-------------------+
+----------------+
| Client Program |
+-------------------------+
| fd: x | fd: y |
| Fdio (FIDL)| Fdio (FIDL)|
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
^ ^
| |
Zircon Channels, speaking FIDL Server dispatches message to I/O State,
| | Interprets as ‘open’
v v Finds or Creates ‘foo’
+-------------------------+
| '/' Handle | CWD Handle |
+-------------------------+
| I/O State | I/O State |
+-------------------------+-------------+
| Vnode A | Vnode B | Vnode C |
+------------------------------+--------+
| Filesystem Server |
+-------------------+
+----------------+
| Client Program |
+-------------------------+
| fd: x | fd: y |
| Fdio (FIDL)| Fdio (FIDL)|
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
^ ^ ^
| | |
Zircon Channels, FIDL | Server allocates I/O state for Vnode
| | | Responds to client-provided handle
v v v
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
| I/O State | I/O State | I/O State |
+-------------------------+--------------+
| Vnode A | Vnode B | Vnode C |
+------------------------------+---------+
| Filesystem Server |
+-------------------+
+----------------+
| Client Program |
+-----------------------------+----------+
| fd: x | fd: y | fd: z |
| Fdio (FIDL)| Fdio (FIDL)| Fdio (FIDL) |
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
^ ^ ^
| | |
Zircon Channels, speaking FIDL | Client recognizes that ‘foo’ was opened
| | | Allocated Fdio + fd, ‘open’ succeeds.
v v v
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
| I/O State | I/O State | I/O State |
+-------------------------+--------------+
| Vnode A | Vnode B | Vnode C |
+------------------------------+---------+
| Filesystem Server |
+-------------------+