IO 流起步:从 fopen 到文件描述符 fd
时间回到 2026-06-27。进程控制刚收尾,正式进入「IO 流」章节------
也就是那个贯穿后文所有话题(文件系统、缓冲区、重定向)的起点:
进程和文件,到底是怎么"接上头"的?
本篇覆盖:C 库文件接口 vs 系统调用接口、FILE 与 fd 的关系、open 的 flags 与 umask。
一、视角切换:程序与文件的关系 = 进程与文件的关系
开篇先把最核心的一句话立住:
文件 = 文件的内容 + 文件的属性。
而"谁去打开文件、谁去找路径",答案都是:进程。
所以「程序操作文件」这个说法,剥开看就是**「进程操作文件」**。整个操作系统的四大管理(进程管理、文件管理、内存管理、驱动管理)在这里交汇。
由此立刻能回答一个铺垫已久的问题:为什么 PCB 里要存 CWD(当前工作目录)?
- 文件系统需要路径来定位文件,而相对路径的解析基准就是 CWD------没有 CWD,"相对"二字无从谈起;
- 一切以
-bash进程的 CWD 为起点"衍生万物"; - 注意区分:运行可执行文件时找命令看的是环境变量
PATH;进程打开/创建文件时解析路径看的是 CWD。两条机制各管一段,设计者刻意让它们互补连接。
二、为什么文件操作函数都"往内存里搬"?
冯·诺依曼架构的老规矩:CPU 只从内存读取数据,直接读磁盘太慢。
所以:
fopen等一切文件操作函数,本质都是把文件(至少是把需要的部分)加载进内存;- 进程本身也是如此------二进制文件被加载进内存,CPU 才能执行。
加载属性还是加载内容? 看需求,所以有对应的函数。于是文件有了两种状态:
| 状态 | 位置 | 例子 |
|---|---|---|
| 磁盘文件(未打开) | 磁盘上,躺平 | 上篇博客的 Ext 文件系统部分 |
| 内存文件(已打开) | 被进程打开,驻留内存 | 本篇的主角 |
而"一个进程可以打开很多个文件"(1 : n )------最起码每个进程都得打开自己的二进制文件;100 个进程就至少 100 个。文件一多就要管理,管理方式又双叒叕是那套:先描述(struct file{})、再组织(链表的增删查改),和进程管理如出一辙。
三、两套接口:C 标准库 与 系统调用
| 层 | 函数 | 返回/标识 |
|---|---|---|
| C 语言标准(库函数) | fopen / fclose / fread / fwrite / fputs |
FILE*(文件指针/文件句柄) |
| 系统调用标准(Linux 本身) | open / close / read / write |
int fd(文件描述符) |
关系一句话:库函数封装了系统调用 ------无论 C 还是 C++ 的文件接口,底层最终都是 open/close/write。
3.1 FILE 的本质:一个结构体
FILE* 不是什么神秘的东西,它就是一个结构体 ,里面封装了 fd:
stdin里封装的是 0(标准输入)stdout里封装的是 1(标准输出)stderr里封装的是 2(标准错误)
也就是说:fputs、scanf 这些函数的底层全部都是 write() ,而 write() 只认 0、1、2 这样的整数编号。
3.2 为什么从键盘打印的内容会"换行"?------ 文件即 char 数组
一个非常好用的心智模型:
文件在我看来,就是一维 char 数组
char[]。
\n也只是数组里的一个下标位置的字符;- 内存里存的就是字面量
'\n'; - 打印到屏幕时,输出函数把它"转换"成换行动作------换行是显示器(驱动)的解读,不是文件里存了个"换行魔法"。
顺带一个 C 语言约定:\0 是字符串结束标记,但操作系统不认识 \0------文本读写函数默认不会把它写进文件。文件是文件,C 字符串是 C 字符串,两层标准别混。
四、open:flags、mode 与 umask
c
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
4.1 访问模式:三选一,互斥
| 标志 | 含义 |
|---|---|
O_RDONLY |
只读 |
O_WRONLY |
只写 |
O_RDWR |
读写 |
4.2 附加标志:按位或 | 随意叠加
| 标志 | 含义 |
|---|---|
O_CREAT |
文件不存在则创建,此时必须传第三个参数 mode |
O_EXCL |
配合 O_CREAT:文件已存在直接报错(防止覆盖) |
O_TRUNC |
打开时清空文件内容 |
O_APPEND |
写操作永远追加到末尾 |
O_NONBLOCK |
非阻塞打开 |
flags 的设计思想 :这些宏本质都是只有一个 bit 为 1 的整数 。传参时用按位或 | 组合(遵守交换律,顺序无所谓),内核里用按位与 & 逐位检测------这就是"位标志(bit flag)"这一经典 C 设计。fopen 的 mode 参数最后也被翻译成这套 flags。
4.3 权限与 umask
新建文件的初始权限并不等于你写的 mode:
实际权限 = mode & ~umask
- 一般
mode设为0666; - 每个系统有自己的默认掩码(通常
0002),想把权限"给足"可以用umask(0)临时清零; - 这就是为什么创建出来的文件权限总比你写的少几位。
4.4 为什么第一个打开的文件 fd 是 3?
因为 0、1、2 在进程启动时就已经被 stdin/stdout/stderr 占用了 ------进程一睁眼就自带三个标准流。所以你 open 的第一个文件自然从 3 开始编号。(这个细节后面讲重定向时威力无穷,此处先埋个种子。)
五、fopen 的 mode 与 shell 重定向的对应
| fopen mode | 行为 | shell 里的影子 |
|---|---|---|
"w" |
存在则清空,不存在则创建 | > 文件名 |
"a"(append) |
追加到结尾 | >> 文件名(如 echo "x" >> f) |
"r" |
只读打开 | < 输入重定向 |
你会发现 shell 的重定向符号和 C 的文件模式是一一对应的------因为 shell 的重定向本来就是"打开文件 + 换描述符指向"这套机制的用户级语法糖。
六、小结
冯诺依曼架构:CPU 只碰内存
↓
打开文件 = 把文件载入内存(内容/属性看需求)
↓
一个进程 : n 个文件 → struct file{} + 链表管理(先描述再组织)
↓
两套接口:FILE*(库) ↔ fd(系统调用),库函数封装系统调用
↓
stdin=0, stdout=1, stderr=2 → 自己 open 的从 3 开始
五条最值钱的结论:
- 程序与文件的关系,本质是进程与文件的关系;CWD 为相对路径兜底,PATH 为找命令兜底;
- 文件有两种状态 :磁盘文件与内存文件,
fopen/open是两者之间的桥; - FILE 是结构体,内部封装 fd ;一切
f*函数底层都是write/read系统调用; - flags 是位标志 :
|组合、&检测;实际权限 =mode & ~umask; - fd 从 3 开始,因为 0/1/2 出生即被占用。
本文基于 2026-06-27《IO流》《IO流函数》及《open 函数使用方法》笔记整理。下一篇继续这条线:文件描述符的分配规则与重定向的本质。