System Level I/O¶
本章重点:如何正确使用 Unix/Linux 的底层 I/O,理解 Linux 内核如何管理文件(fd / open file table / vnode)。
学习目标¶
- 能写出 robust read/write
- 能解释 short count 发生原因
- 画出 descriptor table → open file table → v-node table 的结构
- 理解 fork 为什么共享文件偏移
- 能解释为何网络 I/O 必须用 Unix I/O / RIO 而不是 stdio
- 能解释为什么 stdio 对网络不安全(缓冲 + 格式化问题)
- 能正确使用 dup2 实现重定向
- 实践:可以写一个 mini-cp 小项目
Unix I/O¶

关于 Unix I/O, 最重要的是理解 unix 对 "file" 的抽象:everything is a file.
在操作系统和系统编程中,文件不仅仅指磁盘上的文件,它可以是任何文件对象(file object),包括:
- 磁盘文件
- 终端设备(tty)
- 网络 socket
- 管道(pipe)
- 其他特殊设备
Unix 的文件系统是树状结构,用 Directory hierarchy 的层次结构管理这些文件。无论真实底层是什么设备,都通过这个树结构把文件暴露给用户和程序。
每个新创建的进程都会自动继承某些“已经打开的文件描述符(File Descriptor)”,任何进程一开始,就自动拥有这 3 个打开的文件对象:stdin(fd = 0), stdout(fd = 1), stderr(fd = 2)
为什么 printf() 输出不需要调用 open()
shell 是一个进程,它有:stdin(fd = 0), stdout(fd = 1), stderr(fd = 2)这三个 fd。当你在 shell 输入 ./a.out, shell 调用 fork() 创建程序进程,子进程会继承父进程所有打开的文件描述符。
所以新程序也继承了:fd = 0, fd = 1, fd = 2
所以 printf() 不需要 open, 可以直接写到 fd=1(stdout)。
open & close¶
一个进程如何 open & close 文件?(注意 file 的概念是一切 object file,不仅用于普通文件,也适用于 socket、管道、设备等各种文件对象。)
在 Linux/C 中,进程打开文件通常调用:
其中,filename 是要打开的文件路径;flags 是文件打开方式(读/写?);mode 是文件权限(只在创建文件时生效)。
调用 open() 后,系统会返回一个 整数 fd,代表该进程内部的文件描述符,用于后续的读写操作。
fd 的分配规则¶
每个进程维护一个文件描述符表(fd table),表中记录了当前进程所有打开的文件。
open() 会从 fd 表 从 0 开始扫描,找到第一个没有被占用的槽位,返回这个最小可用的 fd
fd 并不是文件本身,而是一个索引,指向内核维护的文件表项。
文件表项中会记录:文件位置(offset)、访问模式(flags)、指向 vnode / inode 的指针
通过 fd,进程可以使用 read(), write(), lseek() 等系统调用操作文件。
关闭文件:close()¶
当进程不再需要访问文件时,需要关闭文件:
关闭文件会发生:
-
释放 fd 表中的槽位
使该 fd 可以被下一次 open() 使用
-
减少文件表项引用计数
如果引用计数降到 0,内核会释放与文件对象相关的资源(缓冲区、锁等)
-
刷新缓冲区(对写文件)
确保数据写入磁盘
关闭文件是系统资源管理的重要环节,如果忘记 close(),可能导致:fd 泄漏,系统可用文件数耗尽。
Robust I/O¶
Higher-Level I/O 目的是提供缓冲读写,减少对内核的系统调用次数。常见形式有 RIO 和 Standard I/O。
Robust I/O 是 CSAPP 中封装的一套 高层次 I/O,它提供 缓冲读写,目的是减少对内核的系统调用次数,并解决一些 Unix I/O 在实际开发中容易遇到的问题。
为什么需要 RIO?
Unix 系统调用如 read() 和 write() 在遇到网络或设备 I/O 时,可能出现 short count,也就是系统调用返回的字节数比请求的少。
如果直接使用 read() 或 write(),程序需要手动循环处理这些情况 while (left > 0) 。
RIO 封装了这种循环逻辑,保证应用程序读取到预期数量的字节。
rio_readn 是要么给你 n 字节,要么 EOF/出错。
rio_writen 是要么写完 n 字节,要么出错。
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
/* rio_readn: try to read n bytes from fd into usrbuf.
* Returns:
* >0 : actual number of bytes read (can be < n only on EOF)
* 0 : EOF (only possible if n==0 or encountered EOF immediately)
* -1 : error (errno set)
*/
ssize_t rio_readn(int fd, void *usrbuf, size_t n) {
size_t nleft = n; /* bytes left to read */
ssize_t nread; /* bytes read by last read() */
char *bufp = usrbuf; /* current position in user buffer */
while (nleft > 0) {
nread = read(fd, bufp, nleft);
if (nread < 0) {
if (errno == EINTR) /* read was interrupted by signal */
nread = 0; /* retry the read */
else
return -1; /* other error -> give up */
} else if (nread == 0) /* EOF */
break; /* stop reading */
nleft -= nread;
bufp += nread;
}
return (ssize_t)(n - nleft); /* return how many bytes we actually read */
}
/* rio_writen: write exactly n bytes from usrbuf to fd.
* Returns:
* >0 : number of bytes written (should be n on success)
* -1 : error (errno set)
*/
ssize_t rio_writen(int fd, void *usrbuf, size_t n) {
size_t nleft = n; /* bytes left to write */
ssize_t nwritten; /* bytes written by last write() */
char *bufp = usrbuf; /* current position in user buffer */
while (nleft > 0) {
nwritten = write(fd, bufp, nleft);
if (nwritten <= 0) {
if (nwritten < 0 && errno == EINTR) /* write interrupted */
nwritten = 0; /* retry */
else
return -1; /* error */
}
nleft -= nwritten;
bufp += nwritten;
}
return (ssize_t)n; /* all bytes written */
}
Standard I/O (higher-level)¶
我们常用的 C 标准库提供的输入输出函数,如 scanf, printf, fgets, fputs 等,是 standard I/O library 提供的 higher level 输入输出函数。
对比 Unix I/O, standard I/O lib 把打开的文件看成是 stream,并提供格式化输入输出。
为什么网络不能使用 printf?
文件共享¶

一个文件可能被不同的进程以不同方式、不同时间打开、读写、关闭。
- Descriptor tables:每个进程都有自己的一张 Descriptor Table。 每个 FD(文件描述符)不是文件是指向 file table entry 的指针
- Open file table:一个“打开实例”的视角(offset, flags, refcount)
- V-node table:文件的元数据(权限、大小、磁盘存储)
fork() 与文件偏移¶
当父进程调用 fork() 创建子进程时,父子进程共享同一个内核文件表项。文件偏移量(file offset)是内核级共享的。
结果就是,父进程读写文件时,偏移量前进;子进程读写同一个 fd,也会影响偏移量!
如果希望父子进程互不干扰,需要各自 open() 文件,或在子进程里 dup()/close() 后重建 fd