开放流程的生命周期

为了全面了解 Fuchsia 上的文件系统访问,本文档深入探讨了在执行打开文件这样简单的操作时使用的每个层的详细信息。请务必注意:所有这些层都存在于用户空间中;即使在与文件系统服务器和驱动程序交互时,内核也仅用于将消息从一个组件传递到另一个组件。

拨打以下电话:

open(“foobar”);

该请求会发送到哪里?

标准库:定义了“打开”

“open”调用是由标准库提供的函数。对于 C/C++ 程序,这通常会在 unistd.h 中声明,而 libfdio 中有相应的支持定义。对于 Go 程序,Go 标准库中有一个等效(但不同)的实现。对于每种语言和运行时,开发者可以选择自己的“开放”定义。

在单体内核上,open 将是系统调用周围的轻量级 shim,其中内核可能会处理路径解析、重定向等。在该模型中,内核需要根据有关调用者的外部知识来协调对资源的访问。不过,Zircon 内核有意不提供此类系统调用。相反,客户端通过渠道访问文件系统 - 当进程初始化时,系统会为其提供一个命名空间,该命名空间是一个“绝对路径”->“句柄”映射表。从进程内部访问的所有路径都通过将请求定向到此命名空间映射来打开。

不过,在此示例中,由于涉及打开“foobar”的请求,因此使用了相对路径,传入的调用可以通过表示当前工作目录(本身表示为绝对路径和句柄)的路径发送。

标准库负责获取句柄(或多个句柄)并使其看起来像文件描述符。因此,“文件描述符表”是客户端进程中存在的一个概念(如果客户端选择使用自定义运行时,则可以纯粹将资源视为句柄 - “文件描述符”封装是可选的)。

不过,这会引发一个问题:给定文件、套接字、管道等的文件描述符,标准库会做些什么来使所有这些资源在功能上看起来相同?相应客户如何知道通过这些句柄发送哪些消息?

Fdio

一个名为 fdio 的库负责为各种资源(文件、套接字、服务、管道等)提供统一的接口。此层定义了一组函数,例如 read、write、open、close、seek 等,这些函数可用于由各种协议支持的文件描述符。每种受支持的协议都负责提供客户端代码来解读其互动的具体细节。例如,套接字可为客户端提供多个句柄;一个用于数据传输,一个用作控制平面。相比之下,文件通常仅使用一个通道来控制和传输数据(除非已额外完成请求内存映射的工作)。虽然套接字和文件都可能会收到对 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 消息的“数据”字段设置为字符串“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 对象的请求。这种行为对于与由 components 托管的 capabilities 进行交互至关重要 - 组件(默认情况下)是延迟启动的,当请求功能时启动,这是通过 open 调用完成的。此模型允许客户端立即开始发送请求,而不是在组件完成启动程序并开始处理请求时阻塞打开,然后一旦组件准备就绪,就会响应这些请求。如需详细了解此行为如何应用于功能路由,请参阅协议打开的生命周期。

总而言之,“open”调用已通过标准库,对“CWD”fdio 对象执行了操作,该对象将请求转换为 FIDL 消息,然后使用 zx_channel_write 系统调用将该消息发送到服务器。客户端可以选择使用 zx_object_wait_one 等待服务器的响应,也可以继续异步处理。无论哪种方式,都已创建了一个通道,其中一端位于客户端,另一端传输到“服务器”。

文件系统:服务器端

调度

消息从通道的客户端传输后,会保留在通道的服务器端,等待被读取。服务器由“持有通道另一端句柄的任何一方”标识,它可能与客户端位于同一进程(或不同进程)中,使用与客户端相同的运行时(或不同的运行时),并使用与客户端相同的语言(或不同的语言)编写。通过使用约定的线格式,进程间依赖关系会受到通道上发生的精简通信层的限制。

在未来的某个时间点,CWD 句柄的服务器端需要读取客户端传输的消息。此过程不是自动的,服务器需要有意等待接收句柄上的传入消息,在本例中,接收句柄是“当前工作目录”句柄。当服务器对象(文件、目录、服务等)打开时,其句柄会注册到服务器端 Zircon 端口,该端口会等待其底层句柄变为可读(表示有消息到达)或已关闭(表示它们永远不会再收到消息)。此对象会将传入的请求分派给相应的处理程序,因此称为分派器。 它负责将传入的消息重定向到回调函数,以及一些之前提供的表示打开连接的“iostate”。

对于使用 libfs 的 C++ 文件系统,此回调函数为 vfs_handler,它会接收一些关键信息:

  • 由客户端提供的 FIDL 消息(如果句柄已关闭,则由服务器人为构造,使其看起来像“关闭”消息)
  • 表示当前与句柄(作为“iostate”字段传递,如前所述)的连接的 I/O 状态。

vfs_handler 可以解读 I/O 状态以推断更多信息:

  • 文件内(或目录内,如果已使用 readdir)的查找指针
  • 用于打开底层资源的标志
  • 表示底层对象的 Vnode(可能在多个客户端或多个文件描述符之间共享)

此处理函数配备了这些信息,可充当大型“switch/case”表,根据客户端提供的“操作”字段将 FIDL 消息重定向到相应的函数。在打开的案例中,Open 序号被视为操作,因此 (1) 需要一个句柄,并且 (2)“data”字段(“foo”)被解读为路径。

VFS 层

在 Fuchsia 中,“VFS 层”是一个独立于文件系统的代码库,它可以调度和解释服务器端消息,并在适当情况下调用底层文件系统中的操作。值得注意的是,此层是完全可选的 - 如果文件系统服务器不想与此库关联,则没有义务使用它。若要成为文件系统服务器,进程只需了解 FIDL 有线格式。因此,一种语言中可能存在任意数量的“VFS”实现。目前有以下实现:

  • 树内 C++ VFS:由 Fuchsia 的“主要”文件系统 minfs 和 blobfs 使用。目前,它是所有 VFS 实现中功能最多的,但也是最难使用的。
  • 树内 Rust VFS:一些 Rust 文件系统(包括 fat32 实现)使用此功能。它更新,但目前的功能比 C++ 实现少。
  • SDK C++ VFS:面向 SDK 用户的“树内”C++ 版本的简化版本。这最常用于服务发现等简单用途。

VFS 层定义了可路由到基础文件系统的操作的接口,包括:

  • 读取/写入 Vnode
  • 从父 Vnode 中查找/创建/取消关联 Vnode(按名称)
  • 按名称重命名/链接 Vnode
  • 以及更多服务

若要实现文件系统(假设开发者想要使用共享 VFS 层),只需定义一个实现此接口的 Vnode 并链接到 VFS 层即可。这样一来,您只需付出极少的努力,即可实现“路径遍历”和“文件系统挂载”等功能,并且几乎不会出现重复的代码。为了实现与文件系统无关,VFS 层对文件系统使用的底层存储没有预先设定的概念:文件系统可能需要访问块设备、网络或仅需要内存来存储数据,但 VFS 层仅处理对路径、数据字节数组和 vnode 进行操作的接口。

路径行走

若要打开服务器端资源,系统会为服务器提供某个起始点(由所调用的句柄表示)和一个字符串路径。此路径会按“/”字符拆分为多个段,并且每个组成部分都会通过回调“查找”到基础文件系统。如果查找成功返回一个 vnode,并且检测到另一个“/”段,则该进程会继续,直到出现以下情况:(1) lookup 找不到组件;(2) 路径处理到达路径中的最后一个组件;(3) lookup 找到一个装载点 vnode,即附加了“远程”句柄的 vnode。目前,我们将忽略挂载点 vnode,尽管在有关文件系统挂载的部分中讨论了它们。

假设 lookup 成功找到了“foo”Vnode。文件系统服务器将继续调用 VFS 接口“打开”,验证是否可以使用提供的标志访问所请求的资源,然后再调用“GetHandles”询问底层文件系统是否需要其他句柄才能与 Vnode 交互。假设客户端同步请求了“foo”对象(这是默认 POSIX 打开调用的隐含含义),那么与“foo”交互所需的任何其他句柄都会打包到一个小型 FIDL 描述对象中,并传递回客户端。或者,如果“foo”未能成功打开,系统仍会返回一个 FIDL 说明对象,但“status”字段会设置为一个错误代码,以指示失败。假设“foo”打开成功。服务器将继续为“foo”创建一个“iostate”对象,并将其注册到调度程序。这样一来,服务器就可以处理对“foo”的未来调用。“Foo”已打开,客户端现在可以发送其他请求。

从客户端的角度来看,在“打开”调用开始时,路径和句柄组合通过 CWD 句柄传输到远程文件系统服务器。由于调用是同步的,因此客户端继续等待句柄上的响应。服务器正确找到、打开并初始化了相应文件的 I/O 状态后,便会发回“成功”FIDL 说明对象。客户端会读取此对象,以确定调用已成功完成。此时,客户端可以创建一个表示“foo”句柄的 fdio 对象,使用文件描述符表中的条目引用该对象,并将 fd 返回给原始“open”函数的调用者。此外,如果客户端想要向“foo”发送任何其他请求(例如“读取”或“写入”),则可以使用与已打开文件的连接直接与文件系统服务器通信,而无需在未来的请求中通过“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 |
+-------------------+