Open的生命周期


英文原文快照


为了提供在Fuchsia上文件系统访问的端到端图景,本文档详细介绍了在执行诸如打开文件之类的简单操作时使用的每个层的详细信息。需要注意的是:所有这些层都存在于用户空间中;即使在与文件系统服务器和驱动程序进行跨进程交互时,内核也仅用于将消息从一个组件传递到另一个组件。

一次如下的调用操作:

open(“foobar”);

请求将发向何方?

标准库:open在何处定义?

open调用是由标准库提供的函数。对于C/C++程序而言,通常在unistd.h中声明,并在libfdio中有定义支持。对于Go程序而言,Go标准库中有一个等价(但不同)的实现。对于每种语言和运行时,开发人员可以选择加入他们自己对”open”的定义。 在宏内核操作系统中,open是系统调用的一个轻量级封装(shim),内核可能会处理路径解析,重定向等操作。在该模型中,内核需要基于关于调用者的外部知识来调解对资源的访问。然而,Zircon内核在设计上就没有这样的系统调用,相反,客户通过channel来访问文件系统:当进程在初始化时,进程可以提供表示根(”/“)目录的句柄和表示当前工作目录(CWD)的句柄。或者,在更奇特的运行时中,可能不会提供这些句柄中的一个或多个。但在这个例子中,涉及打开“foobar”的请求,使用了一个相对路径,所以进来的调用可以通过表示当前工作目录的路径来进行发送。

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

但是,这里产生了一个问题:对于给文件,套接字,管道等分配的文件描述符,标准库需要做什么才能使所有这些资源在功能上看起来是一样的?客户又如何知道通过这些句柄发送什么消息?

Fdio

名为fdio的库负责为各种资源(文件,套接字,服务,管道等等)提供统一的接口。这一层定义了一组函数,例如 读取,写入,打开,关闭,搜索等等。这些函数可用于由各种协议支持的文件描述符。每个支持的协议都负责提供客户端代码来解释其交互的细节。例如,套接字为客户端提供多个句柄;一个用于数据流,另一个用作控制平面。相比之下,文件通常仅使用单个channel进行控制和数据(除非进行额外的工作来请求进行内存映射)。尽管套接字和文件可能会接收到对openwrite的调用,但它们需要以不同的方式解释这些命令。

为了本文的目的,我们将重点关注文件系统客户端使用的主要协议:RemoteIO。

RemoteIO

调用open(“foo”)的程序将调用标准库,找到与当前工作目录对应的“fdio”对象,并且需要向远程服务器发送请求以“请打开foo”。这又是如何实现的?

该程序具有以下功能: <!— One or more *handles representing a connection to the CWD

  • zx_channel_write: A system call which can send bytes and handles (over a channel)
  • zx_channel_read: A system call which can receive bytes and handles (over a channel)
  • zx_object_wait_one: A system call which can wait for a handle to be readable / writable —>
    • 一个或多个handle表示与CWD的连接
    • zx_channel_write:发送字节和句柄(通过channel)的系统调用
    • zx_channel_read:接收字节和句柄(通过channel)的系统调用
    • zx_object_wait_one:等待句柄变成可读/可写状态的系统调用

通过使用这些原语,客户端可以向CWD句柄上的文件系统服务器写入消息,服务端读取该消息,然后使用“成功”或“失败消息”来响应客户端。在服务端在处理并弄清楚实际打开什么的过程中时,在尝试读取状态消息之前,客户端可能会也可能不会选择等待。

很重要的一点是:当消息被发送或接收时,客户端和服务端就这N字节和N个句柄的解释是达成一致的:如果它们之间不一致,则可能会被丢弃(或者更糟糕的是,消息被曲解并产生意想不到的行为)。另外,如果这个协议允许客户端对服务端有任意的控制权,该通信层就可能被漏洞利用。

RemoteIO协议(RIO)描述了在两个实体之间传送时,这些有线格式的字节和句柄,实际上应该意味着什么。该协议描述了诸如“期望的句柄数”,“枚举操作”和“数据”之类的内容。在我们的例子中,open(“foo”)创建一个ZXRIO_OPEN消息,并将RIO消息的“data”字段设置为字符串“foo”。此外,如果任何标志被传递open操作(例如O_RDONLY,O_RDWR,O_CREAT等),这些标志将被放置在rio结构的“arg”字段中。但是,如果操作被改变(例如,write操作),则该消息的解释也将同时被改变。

这一层的精确字节协议是非常关键的,因为它允许在完全不同的运行时间之间进行通信:理解RIO的进程可以在C,C ++,Go,Rust,Dart(和其他)程序之间很容易地进行透明通信。

该协议虽然非常有用,但目前还必须手工进行构建,并且只能由系统内的低层次组件使用。为了利用FIDL提供的多运行时的互操作性,我们正在不断努力将Remote IO重新指定为可以被Fuchsia高级组件使用的FIDL接口。在将来,这两个协议将变得统一,FIDL绑定将自动生成并以各种语言进行类型检查。

libfdio包含了用于RIO的C/C++实现的客户端和服务端代码,并负责谨慎地验证这两端的输入和输出。

在“open”操作的例子中,Remote IO协议期望客户端将创建channel,并将channel的一端(作为句柄)传递给服务端。一旦事务完成,此channel可以用作与打开的文件进行通信的机制,就像之前与“CWD”句柄进行通信那样。

通过协议的设计,RIO的客户端而不是服务端,来提供句柄,这样的通信方式更适合于流水线。访问RIO对象可以是异步的; RIO对象的请求可以在对象实际被打开之前被传输。这种行为对于与服务交互很关键(这部分将在“ServiceFS”部分详细介绍)。

总而言之,一次“open”调用已经经过标准库,并作用于“CWD”的fdio对象,该对象将请求转换为RIO消息,使用zx_channel_write系统调用将该消息发送到服务器。 客户端可以选择使用zx_object_wait_one等待服务器的响应,或者以异步方式继续运行。无论哪种方式,都创建了一个channel,其中一端与客户端共存,另一端传输到“服务端”。 <!—

RemoteIO: Server-Side —>

RemoteIO:服务端

分派

一旦消息从channel的客户端发送出去,它就将驻留在channel的服务端并等待被读取。服务段由“持有channel另一端的人员”来标识 — 它可能与客户端位于同一个(或不同的)进程中,使用与客户端相同(或不同)的运行时,并且使用与客户相同的(或不同的编程语言)实现。通过使用商定的有线格式,进程间依赖性在channel上发生,并在精简通信层上成为瓶颈。

在未来的某个时刻,CWD句柄的服务端将需要读取客户端传输的消息。该过程不是自动完成的:服务端将需要有意等待接收句柄上的传入消息,在这种情况下,这是“当前工作目录”句柄。当服务端对象(文件,目录,服务等)被打开时,它们的句柄被注册到服务器端的Zircon端口,等待它们的底层句柄变成可读状态(意味着消息已经到达)或关闭(意味着他们永远不会收到更多的消息)。将传入请求分派给适当句柄的这个对象被称为调度器,它负责将传入的消息重定向到回调函数,以及一些以前提供的表示打开连接的“iostate”。

对于使用libfs的C ++文件系统,这个回调函数被称为vfs_handler,它接收几条关键信息:

  • 由客户端提供的RIO消息(或如果句柄已关闭并显示为“关闭”消息时,由服务器人为地构造)
  • 表示当前连接到句柄的I/O状态(以前面提到的“iostate”字段进行传递)。

vfs_handler可以通过解释I/O状态来推断附加信息:

  • 文件中的seek指针(或如果使用了readdir,在目录中的seek指针)
  • 用于打开底层资源的标志
  • 代表底层对象的Vnode(并且可以在多个客户端或多个文件描述符之间共享)

该处理函数配备了这些信息,充当了一个大型的“switch/case”表,根据客户端提供的“operation”字段将RIO消息重定向到一个合适的函数。在开放的情况下,ZXRIO_OPEN字段被认定为操作,因此(1)需要一个句柄,并且(2)’data’字段(“foo”)被解释为路径。

VFS层

在Fuchsia中,“VFS层”是独立于文件系统的代码库,它可以分派和解释服务端消息,并在适当的地方调用底层文件系统中的操作。值得注意的是,该层是完全可选的:如果文件系统服务器不想链接到这个库,它们便没有义务使用它。要成为文件系统服务器,进程仅必须了解Remote IO的有线格式,因此,一种编程语言中尽管可以有任意数量的“VFS”实现,但在撰写本文时已有两个众所周知的实现:用C++编写实现的libfs库,另一个是在Go中用编写的ThinFS的rpc包

VFS层定义了可从客户端路由到底层文件系统的操作接口,这其中包括:

  • 读/写Vnode
  • 从父Vnode查找/创建/取消链接Vnode(按名称)
  • 按名称重命名/链接Vnode
  • 以及更多……

要实现文件系统(假设开发人员想要使用共享的VFS层),只需定义一个实现此接口的Vnode并链接到VFS层。这将以最小的努力提供诸如“路径漫游”和“文件系统安装”等功能,并且几乎不会有重复的代码。为了保持文件系统无关性,VFS层没有先入为主的文件系统所使用的底层存储的概念:文件系统可能需要访问块设备,网络或存储数据的内存;但VFS层只处理作用于路径的接口,字节数据数组和Vnode。

路径游走(Path Walking)

要打开服务器端资源,服务器会提供一些起始点(由被调用句柄表示)和一个字符串路径,该路径被“/”字符分割成段,并且每个组件都通过对底层文件系统的回调来进行“查找”。如果查找成功则返回vnode,并且检测到另一个“/”段,则该过程继续直到(1)lookup找不到组件,(2)路径处理到达路径中的最后一个组件,或者(3) lookup找到一个vnode挂载点,这是一个具有附加的“远程”句柄的vnode。 现在,我们将忽略挂载点vnode,尽管它们在文件系统挂载一节中讨论。

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

从客户端的角度来看,在“Open”调用开始时,通过CWD句柄将路径和句柄组合传输到远程文件系统服务器。由于调用是同步的,客户端继续等待句柄上的响应。 一旦服务器正确找到,打开并初始化此文件的I/O状态,它就会发回一个“success”的RIO描述对象。该对象将被客户端读取,表明该调用已成功完成。此时,客户端可以创建一个表示“foo”句柄的fdio对象,将其引用到文件描述符表中的条目中,并将fd返回给称为原始“open”功能的任何对象。此外,如果客户想要发送任何额外的请求(比如“read”或“write”)到’foo’,那么他们可以通过使用与打开的文件的连接直接与文件系统服务器通信,而这不需要在未来的请求中路由到’CWD’。

Open的生命周期: 图解

  1. +----------------+
  2. | Client Program |
  3. +-----------------------------+
  4. | fd: x | fd: y |
  5. | Fdio (RIO) | Fdio (RIO) |
  6. +-------------------------+
  7. | '/' Handle | CWD Handle |
  8. +-------------------------+
  9. ^ ^
  10. | |
  11. Zircon Channels, speaking RIO State BEFORE open(‘foo’)
  12. | |
  13. v v
  14. +-------------------------+
  15. | '/' Handle | CWD Handle |
  16. +-------------------------+
  17. | I/O State | I/O State |
  18. +-------------------------+
  19. | Vnode A | Vnode B |
  20. +------------------------------+
  21. | Filesystem Server |
  22. +-------------------+
  23. +----------------+
  24. | Client Program |
  25. +-----------------------------+
  26. | fd: x | fd: y |
  27. | Fdio (RIO) | Fdio (RIO) |
  28. +-------------------------+
  29. | '/' Handle | CWD Handle | **foo Handle x2**
  30. +-------------------------+
  31. ^ ^
  32. | |
  33. Zircon Channels, speaking RIO Client Creates Channel
  34. | |
  35. v v
  36. +-------------------------+
  37. | '/' Handle | CWD Handle |
  38. +-------------------------+
  39. | I/O State | I/O State |
  40. +-------------------------+
  41. | Vnode A | Vnode B |
  42. +------------------------------+
  43. | Filesystem Server |
  44. +-------------------+
  45. +----------------+
  46. | Client Program |
  47. +-----------------------------+
  48. | fd: x | fd: y |
  49. | Fdio (RIO) | Fdio (RIO) |
  50. +-------------------------+--------------+
  51. | '/' Handle | CWD Handle | foo Handle |
  52. +-------------------------+--------------+
  53. ^ ^
  54. | |
  55. Zircon Channels, speaking RIO Client Sends RIO message to Server
  56. | | Message includes a foo handle
  57. v v (and waits for response)
  58. +-------------------------+
  59. | '/' Handle | CWD Handle |
  60. +-------------------------+
  61. | I/O State | I/O State |
  62. +-------------------------+
  63. | Vnode A | Vnode B |
  64. +------------------------------+
  65. | Filesystem Server |
  66. +-------------------+
  67. +----------------+
  68. | Client Program |
  69. +-----------------------------+
  70. | fd: x | fd: y |
  71. | Fdio (RIO) | Fdio (RIO) |
  72. +-------------------------+--------------+
  73. | '/' Handle | CWD Handle | foo Handle |
  74. +-------------------------+--------------+
  75. ^ ^
  76. | |
  77. Zircon Channels, speaking RIO Server dispatches message to I/O State,
  78. | | Interprets as open
  79. v v Finds or Creates foo
  80. +-------------------------+
  81. | '/' Handle | CWD Handle |
  82. +-------------------------+
  83. | I/O State | I/O State |
  84. +-------------------------+-------------+
  85. | Vnode A | Vnode B | Vnode C |
  86. +------------------------------+--------+
  87. | Filesystem Server |
  88. +-------------------+
  89. +----------------+
  90. | Client Program |
  91. +-----------------------------+
  92. | fd: x | fd: y |
  93. | Fdio (RIO) | Fdio (RIO) |
  94. +-------------------------+--------------+
  95. | '/' Handle | CWD Handle | foo Handle |
  96. +-------------------------+--------------+
  97. ^ ^ ^
  98. | | |
  99. Zircon Channels, speaking RIO | Server allocates I/O state for Vnode
  100. | | | Responds to client-provided handle
  101. v v v
  102. +-------------------------+--------------+
  103. | '/' Handle | CWD Handle | foo Handle |
  104. +-------------------------+--------------+
  105. | I/O State | I/O State | I/O State |
  106. +-------------------------+--------------+
  107. | Vnode A | Vnode B | Vnode C |
  108. +------------------------------+---------+
  109. | Filesystem Server |
  110. +-------------------+
  111. +----------------+
  112. | Client Program |
  113. +-----------------------------+----------+
  114. | fd: x | fd: y | fd: z |
  115. | Fdio (RIO) | Fdio (RIO) | Fdio (RIO) |
  116. +-------------------------+--------------+
  117. | '/' Handle | CWD Handle | foo Handle |
  118. +-------------------------+--------------+
  119. ^ ^ ^
  120. | | |
  121. Zircon Channels, speaking RIO | Client recognizes that foo was opened
  122. | | | Allocated Fdio + fd, open succeeds.
  123. v v v
  124. +-------------------------+--------------+
  125. | '/' Handle | CWD Handle | foo Handle |
  126. +-------------------------+--------------+
  127. | I/O State | I/O State | I/O State |
  128. +-------------------------+--------------+
  129. | Vnode A | Vnode B | Vnode C |
  130. +------------------------------+---------+
  131. | Filesystem Server |
  132. +-------------------+