Lecture 03: Files and I/O
导读
POSIX 文件本体可以先看成字节序列,字节里的语义由上层程序解释。围绕这个抽象,C 标准库提供 FILE * 这样的高层 stream,内核系统调用则使用 fd 这个低层整数句柄。
本讲最值得反复对齐的是状态到底存在哪里:open file description 保存 offset 等内核状态,dup、dup2、fork 会让多个 fd 共享它;而高低层 I/O 混用的风险,本质来自用户态 buffer 与内核 offset 状态不同步。
本讲地图
| 主题 | 解决的问题 | 关键结论 |
|---|---|---|
| POSIX 文件抽象 | 文件、目录、路径是什么 | 文件是命名字节序列,目录是名字到对象的组织 |
| I/O 分层 | 应用如何到达设备 | FILE *、fd、syscall、file system、driver 层层包装 |
| 高层 stream | 为什么需要 fopen/fread/fwrite | 用户态缓冲与格式化接口让普通文件处理更方便 |
| 低层 fd | 为什么需要 open/read/write | 精确控制阻塞、offset、pipe/socket、fork/exec |
| Kernel state | fd 到底指向什么 | fd 表项指向 open file description,后者记录 offset 和对象 |
| Aliasing | 为什么 dup/fork 后读写互相影响 | 多个 fd 可共享同一个 open file description |
正文
文件接口可以一路向下拆:上层看到字节序列和 FILE *,内核看到 fd、open file description 和设备对象。状态到底存在哪里,是读懂这一讲的关键。
文件抽象
程序需要用统一方式处理磁盘文件、终端、设备、pipe、socket 等 I/O 对象。Unix/POSIX 的选择是把它们尽量放进同一套 open/read/write/close 风格里,也就是常说的 “everything is a file”。
POSIX 文件可以先理解为“文件系统中用名字引用的一组字节序列”。字节本身没有结构,文本、二进制、序列化对象的含义由应用解释。文件元数据记录大小、修改时间、权限等描述信息。目录则组织名字空间,把名字映射到文件或子目录。
每个进程有 current working directory。绝对路径从根开始,不依赖 CWD;相对路径依赖当前工作目录。chdir 改变的是进程后续解析相对路径的上下文。
“万物皆文件”强调统一接口,不代表所有对象语义完全相同。普通文件通常支持 seek,socket 和 pipe 是顺序流;终端、设备、网络连接的阻塞、flush、错误和控制语义也不同。ioctl 这类接口的存在,正说明有些设备专用控制不适合塞进标准 read/write。
I/O 分层

I/O 分层说明了 FILE *、fd、syscall、文件系统和驱动各处在什么位置。

高层 API 通过库函数包装系统调用,代价和能力边界都不同。
图里的层次可以从应用一路读到硬件。应用看到的 I/O 大致有五层:
| 层次 | 代表对象 | 作用 |
|---|---|---|
| 高层 I/O | FILE * stream | 用户态缓冲、格式化、按行读写 |
| 低层 I/O | file descriptor | 系统调用使用的整数句柄 |
| Syscall | open/read/write/close/lseek | 受控进入内核 |
| File system / driver | inode、page cache、设备驱动 | 管理对象、缓存和设备协议 |
| Device | 磁盘、终端、网卡等 | 实际数据来源或目的地 |
系统调用比普通函数调用贵,因为它要跨越用户态和内核态。因此标准库会在用户态做缓冲,把许多小读写合并成较少的大系统调用,也提供内核不愿内建的高级功能,例如按行读取和格式化文本。
内核低层接口小而稳定,便于统一各种对象;标准库高层接口好用,但隐藏了 buffer 与 flush 时机。系统程序员要知道两层各自维护什么状态,尤其不能把 FILE * 当作“文件本身”。
FILE *

FILE * 的用户态缓冲解释了为什么 fwrite 后不一定立刻被另一个读者看到。
fopen 返回的 FILE * 是 C 标准库维护的 stream 对象。它通常至少包含底层 fd、用户态 buffer、以及多线程访问时需要的 lock。常见接口包括:
| |
fread 和 fwrite 的返回值是元素个数,不是总字节数。fgetc 的返回值应接到 int,因为它要同时表达字符和 EOF。fgets(buf, n, fp) 最多读 n - 1 个字符,并自动补 \0。
下面的程序里,x 不一定变成 'b':
| |
原因是 fwrite 可能只写进了 f1 的用户态 buffer,还没有 flush 到底层 fd。若在读取前加 fflush(f1) 或关闭 f1,后续读者才更明确地看到写入结果。
高层 API 适合普通文件处理、格式化文本、按行读取和可移植代码。代价是精确控制弱,必须理解 flush、预读和 stream position。w/w+ 会截断文件,a/a+ 的写入总追加到末尾,这类模式语义在阅读代码时很容易影响结果。
FILE * 和 fd 的差别不只是接口名字:前者是用户态库对象,会带 buffer;后者是内核对象表的下标,更靠近 system call。混用两层 API 时,要先想清楚数据可能停在哪一层 buffer 里。
fd
低层接口以 fd 为中心:
| |
进程启动时通常已有 fd 0、1、2,分别对应 standard input、standard output、standard error。open 成功时返回当前进程最小的未使用 fd。read 返回实际读到的字节数,0 表示 EOF,-1 表示错误;write 也可能短写,健壮代码要循环处理。
低层 fd 适合需要精确控制 offset、阻塞、pipe/socket、non-blocking I/O、fork/exec/dup、重定向等场景。它更繁琐,但更接近 OS 真正维护的状态。
常见 flags 包括 O_RDONLY、O_WRONLY、O_RDWR、O_CREAT、O_TRUNC、O_APPEND、O_EXCL。O_CREAT 创建文件时需要第三个 mode 参数;creat(filename, mode) 可理解为 open(filename, O_CREAT | O_WRONLY | O_TRUNC, mode) 的简化形式。
fd 表

fd 是进程 fd 表的整数索引,真正的打开实例在内核里。

dup 复制 fd 号,但不复制 open file description,所以 offset 会共享。
open(path, flags) 不只是返回一个整数。内核还会创建 open file description,里面记录当前打开实例的状态。对本讲来说,最重要的是两个字段:
- 文件数据在哪里:例如 inode 或设备对象。
- 当前读写位置:也就是 file offset。
进程 fd 表把 fd 整数映射到 open file description。read、write 会按成功传输的字节数推进 offset;lseek 直接修改 offset;close 删除当前进程 fd 表项,并减少底层对象引用计数。
dup 和 dup2 复制的是 fd 表引用,而不是新建打开实例:
| |
fd 和 fd2 指向同一个 open file description,所以共享 offset。dup2(oldfd, newfd) 则把 newfd 改成指向 oldfd 的同一 open file description;若 newfd 已打开,会先关闭它。
这种别名机制正是 shell 重定向的基础:
| |
printf 仍写 stdout,但 stdout 底层 fd 1 已经指向 out.txt。
fork 与 offset
fork() 后 child 得到父进程 fd 表的副本,但表项通常指向同一批 open file description。因此父子进程可能共享 offset。若 fork 前 fd 的 offset 是 100,父子各读 100 字节,谁先读由调度决定;但它们不会都从 100 开始读,第二个读者会看到已经推进后的 offset。
close(fd) 只删除当前进程的 fd 表项,不会关闭另一个进程里的 fd。底层 open file description 要等最后一个引用关闭后才释放。
共享 open file description 很有用:父子进程可以共享 terminal、pipe、socket 或同一个文件打开实例。但在多线程进程中随意 fork 很危险,因为 child 只保留调用 fork 的那个线程;其他线程如果正持有锁或修改共享结构,会让 child 处在不一致状态。常见原则是:多线程进程里 fork 后尽快 exec。
高低层 I/O 混用也要谨慎:
| |
直觉可能以为 read(fd, y, 10) 会接着读第 10 到 19 字节,但 fread 可能已经预读更多数据到 FILE 的用户态 buffer,底层 fd offset 已经被推进很远。除非非常清楚同步策略,否则避免混用 FILE * 和 fd;若必须混用,要显式处理 fflush、fseek、fdopen、fileno 等状态同步。