学习定位:这一篇只整理《基础 IO》前四部分。
目标不是马上写工程,而是把 “文件 → 系统调用 → fd → 重定向 → FILE” 这条线真正串起来。
这也是后面 MiniShell 加入重定向功能的理论基础。
一、先建立一个最重要的认识:Linux 中“文件”到底是什么?
我们以前容易把文件理解成:
磁盘上的 test.txt、a.c、a.out。
这是狭义的文件。
但 Linux 更重要的思想是:
Linux 下一切皆文件。
键盘、显示器、磁盘、网卡等,都可以被操作系统抽象成类似“文件”的对象。这样程序就可以用统一的方式进行输入输出(I/O)。
所以从操作系统角度看:
程序 ↓ 进程 ↓ 对“文件”进行操作 ↓ 输入 / 输出 ↓ 设备或磁盘而且要注意:
真正操作文件的不是 C 语言本身,而是操作系统提供的系统调用;C 标准库只是给我们提供了更方便的接口。
二、C语言中的 fopen,和 Linux 的 open 是两套层次
以前我们写文件:
FILE*fp=fopen("myfile","w");常见的还有:
fclose(fp);fread(...);fwrite(...);这些属于:
C 标准库函数(libc)
而 Linux 提供的更底层接口是:
open()close()read()write()lseek()它们属于:
系统调用接口。
可以这样理解:
我们写的程序 │ C标准库函数 fopen / fread / fwrite │ ▼ 系统调用接口 open / read / write │ ▼ Linux │ ▼ 磁盘 / 键盘 / 显示器所以不要认为:
fopen 和 open 是完全无关的东西
更准确的理解是:
库函数是在系统调用之上提供的一层更方便的封装。
三、真正需要重点理解:文件描述符 fd
这是这一章最核心的东西。
1. fd是什么?
调用:
intfd=open("myfile",O_RDONLY);成功后可能得到:
fd=3这个 3 就叫:
文件描述符(file descriptor,fd)
它本质上就是:
一个小整数,用来代表当前进程打开的某个文件。
PDF进一步解释了它的底层关系:进程内部有文件描述符表,fd本质上就是这张表的下标,通过这个下标可以找到对应的打开文件对象。
可以想成:
当前进程 │ ▼ 文件描述符表 下标 指向 0 ───→ 标准输入 1 ───→ 标准输出 2 ───→ 标准错误 3 ───→ myfile 4 ───→ another.txt所以以后看到:
read(fd, ...) write(fd, ...) close(fd)你要想到:
“拿着这个数字,告诉操作系统我要操作哪个打开的文件。”
四、0、1、2:Shell、重定向的钥匙
Linux 进程默认打开三个文件描述符:
0 → stdin → 标准输入 1 → stdout → 标准输出 2 → stderr → 标准错误一般情况下:
0 → 键盘 1 → 显示器 2 → 显示器所以:
read(0,buf,sizeof(buf));可以理解:
从标准输入读取。
而:
write(1,buf,strlen(buf));就是:
向标准输出写。
这也是为什么下面这个程序:
char buf[1024]; ssize_t s = read(0, buf, sizeof(buf)); if(s > 0) { write(1, buf, s); }本质上就是:
键盘 ↓ fd 0 ↓ read ↓ buf ↓ write ↓ fd 1 ↓ 显示器这里一定注意:read/write 的第一个参数就是 fd。
五、open:如何打开一个文件?
最重要的接口:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);三个核心参数:
pathname
即"myfile"
表示:
我要打开/创建哪个文件。
flags
告诉操作系统:
我想以什么方式打开?
最常见:
O_RDONLY → 只读 O_WRONLY → 只写 O_RDWR → 读写三者必须选一个。
还可以组合:
O_CREAT → 不存在就创建 O_APPEND → 追加写 O_TRUNC → 清空原内容例如:
open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);意思:
打开 log.txt,只写;不存在就创建;如果原来有内容就清空。
这里的 | 是按位或,用于把多个选项组合起来。
mode
只有涉及创建文件时才重点看:
0666
表示:
新创建文件的默认权限。
open返回值 成功 → 返回新的fd 失败 → -1例如:
intfd=open("myfile",O_RDONLY);if(fd<0){perror("open");}六、read / write / close:真正进行 IO
这三个先和 open 一起记。
open ↓ 得到fd ↓ read / write ↓ closeread
核心形式:
ssize_tread(intfd,void*buf,size_tcount);可以理解成:
read(从哪个文件读? → fd 读到哪里? → buf 最多读多少? → count)例如:
charbuf[1024];ssize_ts=read(fd,buf,sizeof(buf));意思:
从 fd 对应的文件中读取数据,放到 buf,最多读取 1024 字节。
write可以理解成:
write(写到哪个文件? → fd 写什么? → buf 写多少? → count)例如:
write(1,buf,s);意思:
把 buf 中的 s 个字节写到标准输出。
closeclose(fd);意思:
关闭这个文件描述符。
所以最基本的文件操作框架就是:
open ↓ fd ↓ read / write ↓ close七、真正理解“重定向”:它不是魔法,只是换了 fd 指向
这是这一章最值得你记住的一张图。
平时: printf() ↓ stdout ↓ fd 1 ↓ 显示器所以:
./test输出自然出现在屏幕。
但是如果:
./test>log.txt本质上发生了:
原来fd 1 ─────────→ 显示器
重定向后fd 1 ─────────→ log.txt
于是程序仍然:
printf("hello");但它访问的是:
stdout ↓ fd 1 ↓ log.txt所以:
程序自己甚至不需要知道“我要输出到文件”。
它还是向 stdout 输出,只是 Shell 在运行程序之前,把 fd 1 的去向换成了文件。
PDF也通过关闭 1 后再 open 文件得到 fd=1 的实验,直观展示了为什么原本输出到显示器的内容会进入文件,这就是输出重定向。
dup2:更规范地完成重定向
接口:
#include<unistd.h>intdup2(intoldfd,intnewfd);例如:
dup2(fd,1);可以理解成:
让 fd 1 也指向 fd 所指向的文件。
于是:
fd ↓ log.txt fd 1 ↓ log.txt以后:
printf(...)最终走 stdout → fd 1,自然就进入 log.txt。
PDF的 MiniShell 重定向实现也是在子进程中先 open() 文件,再通过 dup2(fd, 0/1) 替换标准输入或标准输出。
八、把这一章和前面的进程控制串起来
这就是你现在最应该形成的总框架:
Shell │ 用户输入命令 │ ▼ 解析命令 │ ▼ fork() ┌──────┴──────┐ │ │ 父进程 子进程 │ │ │ 判断有没有重定向 │ │ │ open() │ │ │ dup2() │ │ │ ▼ │ exec() │ │ │ ▼ │ ls / cat ... │ │ │ ▼ └──── wait ← exit你可以把它理解成:
前一章的 fork + exec + wait,这一章又给子进程加上了 open + dup2,于是 Shell 就开始拥有真正的重定向能力。
例如:
ls -l > log.txt实际上就是:
Shell │ ├─ fork │ ├─ 子进程 │ │ │ ├─ open("log.txt", ...) │ │ │ ├─ dup2(fd, 1) │ │ │ └─ exec("ls") │ └─ 父进程 wait这就是为什么你之前学的 MiniShell 没有白学。
九、FILE 和 fd:最后把这两个概念分清
你之前接触的是:
FILE*fp;现在又出现:
intfd;不要混。
可以简单理解成:
FILE * ↓ C标准库提供的“高级文件对象” ↓ 内部最终还是要依靠 ↓ fd ↓ Linux系统调用PDF明确指出:C 库的 IO 函数最终还是通过 fd 访问文件,因此 FILE 结构体内部必然包含与 fd 相关的信息。
所以:
高级 ↑ FILE *fp │ │ C标准库 ↓ int fd │ │ 系统调用 ↓ Linux内核 ↓ 文件/设备这条关系记住就够了。
至于 PDF 后面讲的:
FILE 内部怎么维护缓冲区、为什么 fork 后 printf 会出现重复输出……
这些都属于缓冲区部分,你现在既然还没上这节,先完全跳过。
🧠 最后:6个问题,检验你是不是真的理解了
建议你先别看答案,自己在脑子里说一遍。
① 为什么 open() 返回的是一个整数 fd,而不是一个 FILE *?
答案:因为 open() 是 Linux 系统调用层面的接口,它返回的是当前进程文件描述符表中的一个下标;FILE * 是 C 标准库提供的更高层对象。
② 为什么 Linux 默认有 0、1、2?
答案:
0 → stdin → 标准输入
1 → stdout → 标准输出
2 → stderr → 标准错误
它们让程序可以用统一的 fd 进行输入输出。
③ 为什么 write(1, buf, size) 能输出到显示器?
答案:因为当前进程中 fd=1 默认对应标准输出,而标准输出通常指向终端显示器。
④ ./test > log.txt 为什么能让程序输出到文件?
答案:Shell 在程序执行前改变了标准输出 fd 1 的指向,让它指向 log.txt,所以程序继续向 stdout/fd1 写时,数据就进入文件。
⑤ 为什么 dup2(fd, 1) 能实现重定向?
**答案:**因为它让新的 fd=1 指向与 fd 相同的打开文件对象,因此程序后续向标准输出 1 写入时,就会写入那个文件。
⑥ FILE * 和 fd 到底是什么关系?
答案:
FILE * ↓ C标准库层 ↓ 底层依靠 fd ↓ Linux系统调用 ↓ 内核所以不是两个完全独立的 IO 系统,而是不同层次的接口。
📌 这一篇最终只记这一张图
Linux 基础 IO │ ┌───────────┴───────────┐ ▼ ▼ 文件是什么 怎么操作文件 │ │ Linux下一切皆文件 C库 / 系统调用 │ ┌─────────────┴─────────────┐ ▼ ▼ C标准库 系统调用 fopen/fclose/fwrite open/read/write │ │ └──────────┬────────────────┘ ▼ fd │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ 0 1 2 stdin stdout stderr │ │ │ 输入 输出 错误 │ ▼ dup2(fd,1) │ ▼ 重定向 │ ▼ ./test > log.txt你现阶段真正需要形成的认知链:
文件 → 操作系统管理 → 系统调用 → fd → 0/1/2 → dup2 → 重定向 → FILE 是更高层的封装
而且这一次不要急着写代码。你老师让你先“理解吸收”是非常合理的:现在你能把这张图和前面的 fork → exec → wait 串起来,等老师后面真正开始做 MiniShell 重定向时,你会突然发现——原来 >、>>、< 就是这么来的。