> For the complete documentation index, see [llms.txt](https://book.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://book.bsdcn.org/4.3-bsd-unix-cao-zuo-xi-tong-she-ji-yu-shi-xian/di-yi-bu-fen-gai-lan/2.6.-io-xi-tong.md).

# 2.6 I/O 系统

UNIX I/O 系统的基本模型是一个既可随机访问又可顺序访问的字节序列。典型的 UNIX 用户进程中没有访问方法，也没有控制块。

不同的程序期望不同程度的结构，但内核不把结构强加给 I/O。例如，正文文件的惯例是以单个换行字符（ASCII 行进给字符）分隔的 ASCII 字符行，但内核对这个惯例一无所知。对多数程序而言，这一模型进一步简化为一个数据字节流，即 I/O 流。正是这单一的共同数据形式，使 UNIX 特有的、以工具为基础的方法行得通 \[Kernighan & Pike, 1984]。一个程序的 I/O 流几乎可以喂给任何其他程序。（不要把这种传统的 UNIX I/O 流与第八版的流 I/O 系统或由它衍生的 System V Release 3 STREAMS 混为一谈；后两者在那些系统中也是作为传统 I/O 流来访问的。）

**描述符与 I/O**

UNIX 进程用描述符（descriptor）引用 I/O 流。描述符是一个小的无符号整数。对描述符可以施加 `read` 或 `write` 系统调用来传输数据；`close` 系统调用可以释放任何描述符。

描述符代表内核支持的下层对象，由按对象类型各异的系统调用创建。在 4.3BSD 中，描述符可以代表三类对象：文件、管道和套接字。

* 文件（file）是一个至少有一个名字的字节线性数组。文件一直存在，直到它的所有名字都被显式删除，而且没有进程持有它的描述符。用 `open` 系统调用打开一个文件的名字，就产生该文件的一个描述符。I/O 设备按文件访问。
* 管道（pipe）与文件一样是字节线性数组，但只用作 I/O 流，而且是单向的。它没有名字，因此不能用 `open` 打开；它由 `pipe` 系统调用创建，该调用返回两个描述符，其中一个接受输入，输入被可靠、不重复、按顺序地送到另一个描述符。
* 套接字（socket）是用于进程间通信的暂时对象；只要还有进程持有引用它的描述符，它就存在。套接字由 `socket` 系统调用创建，该调用返回它的一个描述符。套接字有多种，支持不同的通信语义，如数据的可靠投递、消息保序和保持消息边界。

4.2BSD 之前的 UNIX 系统中，管道用文件系统实现；4.2BSD 引入套接字后，管道改用套接字重新实现。

内核为每个进程保存一张描述符表；内核用这张表把描述符的外部表示（一个小的无符号整数）翻译成内部表示。（描述符只是这张表的索引。）进程的描述符表继承自父进程，因此对描述符所指对象的访问权也一并继承。进程获得描述符的主要途径有：打开或创建对象，以及从父进程继承。此外，套接字 IPC 还允许在消息中把描述符传给无关进程。

每个有效的描述符表项又都指向一个 file 结构，其中包含一个以字节计的文件偏移量（file offset），从对象开头算起。读写操作从这个偏移量开始，每次传输数据后更新它。对允许随机访问的对象，文件偏移量还可以用 `lseek` 系统调用设置。普通文件允许随机访问，有些设备也允许；管道和套接字不允许。

进程终止时，内核收回该进程使用的全部描述符。如果进程持有对象的最后一个引用，就通知对象的管理者，让它做必要的清理，例如最终删除文件或释放套接字。

**描述符管理**

多数进程在开始运行时就期望有三个已经打开的描述符：描述符 0、1、2，分别通称标准输入、标准输出和标准错误。通常，登录进程把这三个描述符关联到用户的终端（见 3.4 节），用户运行的进程再经 `fork` 和 `exec` 继承它们。于是，程序读标准输入就能读到用户键入的内容，写标准输出就能把输出送到用户的屏幕。标准错误描述符也打开供写入，用于错误输出；标准输出则用于普通输出。

这些描述符（以及其他描述符）可以映射到终端以外的对象；这称为 I/O 重定向，所有标准 shell 都允许用户这样做。shell 只需关闭描述符 1（标准输出）再打开想要的输出文件，得到新的描述符 1，就把程序的输出定向到了文件。类似地，关闭描述符 0 再打开文件，标准输入就可以重定向为来自文件。

管道使一个程序的输出可以成为另一个程序的输入，两个程序都无需改写、甚至无需重新连接。源程序的描述符 1（标准输出）不再写向终端，而是设为管道的输入描述符；汇端程序的描述符 0（标准输入）不再引用终端键盘，而是引用管道的输出。这样得到的两个进程加连接管道的组合称为管道线。管道线可以是由管道连接的任意长的进程序列。

`open`、`pipe` 和 `socket` 系统调用产生的新描述符取当前可用作描述符的最小编号。要让管道线工作，必须提供某种机制把这样的描述符映射到 0 和 1。`dup` 系统调用创建一个指向同一文件表项的描述符副本。新描述符同样取最小编号；但如果先把想要的描述符关闭，`dup` 就能完成所需的映射。不过要小心：如果想要描述符 1，而描述符 0 恰好也被关闭了，得到的结果将是描述符 0。为避免这个问题，系统提供了 `dup2` 系统调用；它类似 `dup`，但多一个参数指明想要的描述符编号（若它已打开，`dup2` 会先关闭再重新使用）。

**文件**

UNIX 中的普通文件是有一个或多个名字的字节序列。`open` 系统调用以文件的名字和一个权限模式为参数，模式指明文件是为读、为写还是读写打开。这个系统调用也可以用来创建新的空文件（另有 `creat` 系统调用专司此职）。若 `open` 设置了适当的模式，就可以对得到的描述符施加 `read` 和 `write` 系统调用。描述符可用 `close` 系统调用关闭。

`link` 系统调用接受一个文件的名字，为它另造一个名字。文件名可以用 `unlink` 系统调用删除。当一个文件的最后一个名字被删除（而且打开它的最后一个进程关闭它）时，文件即被删除。

文件按层次组织在目录中。目录是一类文件，但与普通文件不同，目录的结构由系统强加。进程可以像读普通文件一样读目录，但只有系统才能修改目录。目录由 `mkdir` 系统调用创建、由 `rmdir` 系统调用删除。4.2BSD 之前，`mkdir` 和 `rmdir` 是靠一连串 `link` 和 `unlink` 系统调用实现的。专门添加创建和删除目录的系统调用，理由有三：

* 操作可以做成原子的。系统崩溃时，目录不会像用一连串 `link` 操作那样留下半成品。
* 运行联网文件系统时，文件和目录的创建与删除需要以原子方式规定，才能串行化。
* 非 UNIX 文件系统可能不支持 `link` 操作。它们也许支持目录的概念，却多半不会像 UNIX 文件系统那样用链接来创建和删除目录。因此，只有显式给出目录创建和删除请求，它们才能创建和删除目录。

`chown` 系统调用设置文件的属主和属组，`chmod` 更改保护属性。对文件名施加 `stat` 可以读回这些属性。`fchown`、`fchmod` 和 `fstat` 系统调用则对描述符而非文件名做同样一组操作。`rename` 系统调用可以给文件换一个新名字，替换文件原有的名字之一。与目录的创建、删除操作一样，`rename` 系统调用加入 4.2BSD 是为了给本地文件系统中的改名提供原子性。后来证明，显式地向外来文件系统和网络上输出改名操作也很有用。

`truncate` 系统调用加入 4.2BSD，允许把文件截短到任意偏移量。添加这个调用主要是支持 Fortran 运行时库——它的语义是把随机访问文件的末尾设在程序最后访问的位置。没有 `truncate` 的话，缩短文件的唯一办法是把想要的部分复制到新文件、删除旧文件、再把副本改成原来的名字。这样做不仅慢，而且文件系统满时库可能会失败。

文件系统有了缩短文件的能力后，内核顺势用它缩短大型空目录。缩短空目录的好处是，创建或删除名字时内核搜索目录的时间随之减少。

其他一些 UNIX 系统提供 FIFO。FIFO 本质上是一个在文件系统中有名字的管道，最后一个引用它的描述符关闭后它也不消失。既然 POSIX 有 FIFO，将来的 4BSD 发行版或许也会包含它们。关于 POSIX 从 4.3BSD **吸收** 文件系统设施的讨论，见 1.2 节。

**设备**

硬件设备有文件名，用户可以用与普通文件相同的系统调用访问它们。内核能识别设备特殊文件（device special file，简称特殊文件）并确定它指哪个设备，多数进程无须自己操心这一点。终端、打印机和磁带机都当作字节流访问，与 UNIX 磁盘文件无异。这样，设备的依赖性和特殊性尽量留在内核里，甚至在内核内部，它们大多也隔离在设备驱动中。

设备特殊文件由 `mknod` 系统调用创建。另有一个系统调用 `ioctl`，用于操纵特殊文件的下层设备参数。可做的操作因设备而异。这个系统调用让设备的特殊性质有处可访，而不是把语义硬塞进其他系统调用。例如，磁带机上有一个写带尾标记的 `ioctl`，而不用一个特殊或修改版的 `write`。

**套接字 IPC**

4.2BSD 内核引入了一种比管道更灵活的 IPC 机制，以套接字为基础。套接字是通信的一个端点，由描述符引用，就像文件或管道一样。两个进程可以各建一个套接字，再把这两个端点连接起来，得到一条可靠的字节流。连接建立后，进程就可以像使用管道那样读写套接字的描述符。套接字的透明性使内核能把一个进程的输出重定向为另一台机器上另一个进程的输入。管道与套接字的一个重大区别是：管道需要一个共同的父进程来建立通信通道，而套接字之间的连接可以由两个无关进程建立，它们甚至可以位于不同的机器上。

套接字机制要求扩展传统的 UNIX I/O 系统调用，以提供相关的命名和连接语义。开发者没有重载既有接口，而是在既有接口无须改动即可胜任的范围内沿用它们，并设计新接口处理新增语义。字节流型连接沿用 `read` 和 `write` 系统调用，同时新增六个系统调用用于发送和接收带地址的消息（如网络数据报）。写消息的系统调用有 `send`、`sendto` 和 `sendmsg`；读消息的有 `recv`、`recvfrom` 和 `recvmsg`。事后看来，每类的前两个都是第三个的特例；`recvfrom` 和 `sendto` 大概本该作为 `recvmsg` 和 `sendmsg` 的库接口来实现。

**分散/聚集 I/O**

除传统的 `read` 和 `write` 系统调用外，4.2BSD 引入了分散/聚集 I/O 的能力。分散输入用 `readv` 系统调用，让一次读入的数据落到几个不同的缓冲区；反过来，`writev` 系统调用让几个不同的缓冲区在一次原子写中写出。与 `read` 和 `write` 传递单个缓冲区指针和长度参数不同，进程传入的是一个指向缓冲区和长度数组的指针，外加描述数组长度的计数。

这一设施让进程地址空间不同部位的缓冲区无需复制到单一连续缓冲区即可原子地写出。当下层抽象基于记录时，原子写是必需的，例如磁带机每个写请求输出一个带块。一次读入分到几个缓冲区也很方便（比如记录头读进一处、数据读进另一处）。虽然分散数据完全可以靠先读进一个大缓冲区再把各块复制到目的地来模拟，但这种场合内存到内存的复制开销常常会让受影响程序的运行时间翻倍不止。

正如 `send` 和 `recv` 本可以用 `sendto` 和 `recvfrom` 的库接口实现一样，用 `readv` 模拟 `read`、用 `writev` 模拟 `write` 也是可能的。但 `read` 和 `write` 的使用频繁得多，为模拟它们付出的额外开销并不划算。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://book.bsdcn.org/4.3-bsd-unix-cao-zuo-xi-tong-she-ji-yu-shi-xian/di-yi-bu-fen-gai-lan/2.6.-io-xi-tong.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
