第 4 章 文件 I/O:通用 I/O 模型 (File I/O: The Universal I/O Model)
核心结论
-
核心四调用:所有 UNIX 文件 I/O 围绕四个系统调用:
open(打开/创建文件)、read(读)、write(写)、close(关闭);它们对所有文件类型(普通文件、设备、管道、套接字)通用。 -
文件描述符 (fd):进程级非负整数,标识一个打开的文件;0=标准输入,1=标准输出,2=标准错误;通过
open获取,用close释放。 -
open() flags:访问模式
O_RDONLY/O_WRONLY/O_RDWR;创建标志O_CREAT、O_EXCL、O_TRUNC、O_APPEND;状态标志O_CLOEXEC、O_NONBLOCK、O_SYNC、O_DSYNC等。 -
内核翻译 I/O:调用
open/read/write/close时,内核把请求翻译为具体文件系统或设备驱动的操作;应用程序员不需要关心磁盘结构或设备协议。 -
open() 返回最小未用 fd:SUSv3 规定
open()成功后返回进程未使用的最小 fd 编号——这能保证特定 fd(如 0)被新打开的文件占据。 -
错误诊断与 flags 组合:
O_RDWR≠O_RDONLY | O_WRONLY(前者是值 2,后者是逻辑错误);访问模式标志是值 0/1/2,需用O_ACCMODE掩码后比较。
|
本章主旨
本章是 Linux 系统编程「文件 I/O」的入门。核心是 4 个系统调用 |
一、核心概念
本章围绕 5 个核心概念展开:文件描述符 + 四大调用 + 通用 I/O 哲学 + open() 的 flags 分类。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
文件描述符 (fd) |
进程级的非负整数;标识一个打开的文件;通过 |
§4.1;0/1/2 是 stdin/stdout/stderr;POSIX 名 |
四大核心调用 |
|
§4.1;这些是所有 UNIX 文件 I/O 的基础;std 库 |
通用 I/O (Universality) |
同一组系统调用处理所有文件类型(普通文件、设备、管道、套接字等);内核翻译 I/O 请求为具体操作。 |
§4.2;这是 UNIX「一切皆文件」哲学的核心。 |
open() flags 三大类 |
访问模式(O_RDONLY/WR/RDWR)、创建标志(O_CREAT/EXCL/TRUNC/APPEND)、状态标志(O_CLOEXEC/NONBLOCK/SYNC/DSYNC/ASYNC/DIRECT)。 |
§4.3.1;用 ` |
` 组合;SUSv3 标志在大多数 UNIX 系统上可用。 |
open() 返回最小未用 fd |
SUSv3 规定 |
二、详细笔记
4.1 文件描述符与四大调用
What:文件描述符(fd)是进程级的非负整数;标准输入/输出/错误的 fd 是 0/1/2;open/read/write/close 是四大核心调用。
Why:理解 fd 是进程级的(不是全局的),才能理解「fork 后子进程继承父进程的 fd」和「dup/dup2 复制 fd」等概念(详见第 5 章)。
How:四大调用签名(§4.1):
// 摘自《The Linux Programming Interface》第 4 章
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, ... /* mode_t mode */);
// 成功返回新 fd;失败返回 -1
// flags: O_RDONLY (0) / O_WRONLY (1) / O_RDWR (2) / O_CREAT / O_TRUNC / O_APPEND 等
ssize_t read(int fd, void *buffer, size_t count);
// 成功返回实际读到的字节数(0 表示 EOF);失败返回 -1
ssize_t write(int fd, const void *buffer, size_t count);
// 成功返回实际写入的字节数;可能 < count(short write)
int close(int fd);
// 成功返回 0;失败返回 -1
When:
-
写程序处理任何 I/O——从命令行参数读文件、写日志、socket 通信——都用这一组。
-
想绕过 stdio 缓冲(
fopen)——直接用open/read/write;第 13 章详述缓冲差异。
Example:cp 命令的简化实现(Listing 4-1):
// 摘自《The Linux Programming Interface》第 4 章 fileio/copy.c
int inputFd = open(argv[1], O_RDONLY);
int outputFd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
char buf[1024];
ssize_t numRead;
while ((numRead = read(inputFd, buf, sizeof(buf))) > 0)
if (write(outputFd, buf, numRead) != numRead)
fatal("couldn't write whole buffer");
4.2 通用 I/O 模型
What:UNIX I/O 的核心是「同一组系统调用处理所有文件类型」——无论普通文件、终端、管道、套接字、设备。
Why:这是 UNIX 「小工具组合」哲学的基础——./copy test test.old 与 ./copy /dev/tty b.txt 用同一份代码。
How:通用 I/O 通过内核翻译实现:
-
应用调用
open/read/write/close。 -
内核根据 fd 对应的「文件表项」找到对应文件系统或设备驱动。
-
内核调用驱动实现的对应操作(vtable 形式)。
-
驱动执行实际 I/O(磁盘块、设备寄存器、网络栈)。
When:
-
写跨文件类型的工具(如
cat、dd、tee)——完全用通用 I/O。 -
需要特定文件类型的能力(如文件锁、终端特殊字符)——用
fcntl()或ioctl()(详见第 5 章、第 63 章)。
Example:以下 4 个 cp 用法都合法(§4.2):
$ ./copy test test.old # 普通文件→普通文件
$ ./copy a.txt /dev/tty # 普通文件→终端(直接显示)
$ ./copy /dev/tty b.txt # 终端→普通文件(读用户输入)
$ ./copy /dev/pts/16 /dev/tty # 伪终端对拷
4.3 open() flags 详解
What:open() 的 flags 参数是位掩码,组合多个标志;分为访问模式、创建标志、状态标志三类。
Why:flags 决定了「文件是创建还是打开」「写入是覆盖还是追加」「是否在 exec 时关闭」——这是后续所有文件操作的基石。
How:flags 分类(§4.3.1):
| 类别 | 标志 | 作用 |
|---|---|---|
访问模式 |
|
三选一,值为 0/1/2 |
创建标志 |
|
文件不存在则创建;需提供 mode 参数 |
创建标志 |
|
与 O_CREAT 一起用;文件已存在则失败(原子创建) |
创建标志 |
|
打开时截断到 0 字节 |
创建标志 |
|
写入总是追加到文件末尾 |
状态标志 |
|
exec 时自动关闭 fd(防止泄漏给子进程) |
状态标志 |
|
非阻塞 I/O |
状态标志 |
|
同步写入(强制数据到磁盘) |
状态标志 |
|
I/O 就绪时发信号(signal-driven I/O) |
状态标志 |
|
绕过 buffer cache 直接 I/O |
状态标志 |
|
|
状态标志 |
|
不跟随符号链接 |
状态标志 |
|
pathname 必须是目录 |
状态标志 |
|
不让 pathname 成为控制终端 |
When:
-
创建新文件——
O_CREAT | O_WRONLY | O_TRUNC,提供 mode(如0644)。 -
原子创建——
O_CREAT | O_EXCL;若文件已存在则失败。 -
日志追加——
O_WRONLY | O_CREAT | O_APPEND;多进程并发写不交错。 -
exec 时关闭——
O_CLOEXEC;多线程程序避免 fd 泄漏给exec()后的子进程。 -
高性能数据库——
O_DIRECT+O_SYNC;绕过 page cache。
Example:常见 open() 模式:
// 摘自《The Linux Programming Interface》第 4 章
// 1) 只读打开已有文件
int fd = open("startup", O_RDONLY);
// 2) 读写打开,不存在则创建,截断
int fd = open("myfile", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
// 3) 追加写日志(多进程并发安全)
int fd = open("w.log", O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
4.4 open() 返回最小未用 fd
What:SUSv3 规定 open() 成功后返回进程未使用的最小 fd 编号;这意味着可以强制新文件占据特定 fd(如 0)。
Why:某些程序假设某些 fd(如 0 = stdin)已打开;通过「先 close 再 open」可以保证这一假设。
How:强制 fd 0:
// 摘自《The Linux Programming Interface》第 4 章
// 确保文件作为 fd 0 打开
if (close(STDIN_FILENO) == -1)
errExit("close");
int fd = open(pathname, O_RDONLY);
// 此时 fd == 0(因为 0 是最小未用 fd)
When:
-
守护进程把 fd 0/1/2 重定向到
/dev/null或日志文件。 -
第 5 章的
dup2()提供更灵活的方式(不依赖 close+open 的时序)。
Example:守护进程典型模式:
// 摘自《The Linux Programming Interface》第 4 章
// 守护进程关闭 0/1/2,然后重新打开到 /dev/null 或日志
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
int fd0 = open("/dev/null", O_RDONLY); // 必为 0
int fd1 = open("/dev/null", O_WRONLY); // 必为 1
int fd2 = open("/dev/null", O_WRONLY); // 必为 2
4.5 mode 参数与文件权限
What:open() 的 mode 参数仅在 O_CREAT 时生效;指定新文件的权限位;实际权限还受进程 umask 影响。
Why:mode 与 umask 共同决定新文件权限——理解两者关系才能写出「权限正确」的程序。
How:
| 常量 | 值 | 含义 |
|---|---|---|
|
0400 |
owner 读 |
|
0200 |
owner 写 |
|
0100 |
owner 执行 |
|
0040 |
group 读 |
|
0020 |
group 写 |
|
0010 |
group 执行 |
|
0004 |
other 读 |
|
0002 |
other 写 |
|
0001 |
other 执行 |
实际权限 = mode & ~umask。例如 mode = 0666 & ~umask 0022 = 0644。
When:
-
创建公共可读写文件——
mode = 0666;umask 通常是0022,实际权限0644。 -
创建私密文件——
mode = 0600;不受 umask 影响。 -
创建可执行文件——
mode = 0755或0750。
Example:
// 摘自《The Linux Programming Interface》第 4 章
// 公共读写文件:实际权限受 umask 影响
int fd = open("shared.log", O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
// 0666
// 私密文件:mode 已经是 0600,umask 通常不再削减
int fd = open("secret.key", O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR);
// 0600
三、关键图表
|
非可视化条目(标准 fd 与 flags 速查)
|
四、思维导图
mindmap
root((第 4 章 通用 I O 模型))
文件描述符
进程级整数
0 1 2 标准 fd
POSIX 名称
四大调用
open
read
write
close
通用 I O
所有文件类型
内核翻译
一切皆文件
open flags
访问模式
RDONLY WRONLY RDWR
创建标志
CREAT EXCL TRUNC APPEND
状态标志
CLOEXEC NONBLOCK SYNC
DIRECT ASYNC NOATIME
mode 与权限
S_IRUSR 等
umask 影响
实际权限计算
返回最小 fd
SUSv3 保证
强制特定 fd
五、重点与易错点
-
O_RDWR ≠ O_RDONLY | O_WRONLY:访问模式是值 0/1/2 而不是位标志;后者是逻辑错误(得到值 3,无意义)。
-
O_CREAT 必须提供 mode 参数:否则 mode 从栈取「垃圾值」,新文件权限不可预测。
-
O_EXCL 必须与 O_CREAT 一起用:单独
O_EXCL无意义;这是「原子创建文件」的正确方式(详见第 5 章 race condition)。 -
open() 返回最小未用 fd:可被
dup2、fcntl F_DUPFD绕过;不要假设「我刚 close 的 fd 一定是下一个」。 -
fd 是进程级的:fork 后子进程继承父进程的 fd 副本;不同进程可以有相同 fd 值指向不同文件。
-
read 返回 0 表示 EOF:不是错误;不要当成
-1处理。 -
read/write 返回 ssize_t:是有符号的;可能小于请求的 count(short read/write),必须循环处理。
-
read 可能被信号中断(EINTR):要么重试,要么用
SA_RESTART(详见第 20 章)。 -
write 在普通文件上不保证立即落盘:数据进入 page cache;
fsync才能强制落盘(详见第 13 章)。 -
O_APPEND 的真正价值是「原子性」:多进程并发追加不会互相覆盖;这是
lseek + write做不到的(详见第 5 章)。 -
O_CLOEXEC 是多线程程序的必备:避免 fd 泄漏给 exec 后的子进程;用
fcntl F_SETFD也可但有竞态。 -
跨章衔接:第 5 章展开
fcntl、dup/dup2、pread/pwrite、readv/writev;第 13 章展开缓冲机制;第 14 章展开文件系统。 -
常见陷阱:
open("file", O_WRONLY)创建失败时 mode 参数被忽略;若想创建必须显式O_CREAT。