IO流起步-从fopen到文件描述符fd

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 开始

五条最值钱的结论:

  1. 程序与文件的关系,本质是进程与文件的关系;CWD 为相对路径兜底,PATH 为找命令兜底;
  2. 文件有两种状态 :磁盘文件与内存文件,fopen/open 是两者之间的桥;
  3. FILE 是结构体,内部封装 fd ;一切 f* 函数底层都是 write/read 系统调用;
  4. flags 是位标志 :| 组合、& 检测;实际权限 = mode & ~umask;
  5. fd 从 3 开始,因为 0/1/2 出生即被占用。

本文基于 2026-06-27《IO流》《IO流函数》及《open 函数使用方法》笔记整理。下一篇继续这条线:文件描述符的分配规则与重定向的本质。

相关推荐
emo_2b7 小时前
Pycharm配置conda解释器时遇到的问题及解决方法
linux·pycharm·conda
人生百态,人生如梦8 小时前
Ubuntu在低版本下使用ccswitch+claudecode(CCSwitch+Docker+Claude)连调教程
linux·ubuntu·docker·claudecode·ccswitch
byte轻骑兵8 小时前
【BlueZ】hciconfig 工具:蓝牙适配器管理实操 + 源码实现基础
linux·bluez·电脑蓝牙·嵌入式蓝牙
SKH.8 小时前
Linux驱动-系统移植
linux·服务器
根目录下的猫8 小时前
虚拟机复制过来运行时:“虚拟机使用的此版本,VMware Workstation 不支持的硬件版本。”错误解决办法,亲测可用
linux·运维·服务器·后端
yunwei378 小时前
eBPF 入门实践教程十七:编写 eBPF 程序统计随机/顺序磁盘 I/O
linux·后端·性能优化
帷幕落秋8 小时前
内存、虚拟内存与 OOM
linux·运维
dingdingfish9 小时前
TLPI 第41章 读书笔记:Fundamentals of Shared Libraries
linux·static·link·dynamic·library·tlpi
Apipi*9 小时前
30天速通Linux 第五章Linux 进程管理
linux·运维·服务器
杨云龙UP9 小时前
PostgreSQL 四种常见部署模式快速理解:单实例、流复制、repmgr、Patroni
linux·运维·数据库·postgresql·流复制·高可用ha