
观众老爷们大家好 这里是邪修KING的独家频道 本文属于系列Linux系统篇 ------操作指令 一起学Linux的小伙伴可订阅专栏: Linux系统篇 学习定位: 这一篇只整理《基础 IO》前四部分。
目标不是马上写工程,而是把 "文件 → 系统调用 → fd → 重定向 → FILE" 这条线真正串起来。
这也是后面 MiniShell 加入重定向功能的理论基础。
一、先建立一个最重要的认识:Linux 中"文件"到底是什么?
我们以前容易把文件理解成:
磁盘上的 test.txt、a.c、a.out。
这是狭义的文件。
但 Linux 更重要的思想是:
Linux 下一切皆文件。
键盘、显示器、磁盘、网卡等,都可以被操作系统抽象成类似"文件"的对象。这样程序就可以用统一的方式进行输入输出(I/O)。
所以从操作系统角度看:
程序
↓
进程
↓
对"文件"进行操作
↓
输入 / 输出
↓
设备或磁盘
而且要注意:
真正操作文件的不是 C 语言本身,而是操作系统提供的系统调用;C 标准库只是给我们提供了更方便的接口。
二、C语言中的 fopen,和 Linux 的 open 是两套层次
以前我们写文件:
c
FILE *fp = fopen("myfile", "w");
常见的还有:
c
fclose(fp);
fread(...);
fwrite(...);
这些属于:
C 标准库函数(libc)
而 Linux 提供的更底层接口是:
c
open()
close()
read()
write()
lseek()
它们属于:
系统调用接口。
可以这样理解:
我们写的程序
│
C标准库函数
fopen / fread / fwrite
│
▼
系统调用接口
open / read / write
│
▼
Linux
│
▼
磁盘 / 键盘 / 显示器
所以不要认为:
fopen 和 open 是完全无关的东西
更准确的理解是:
库函数是在系统调用之上提供的一层更方便的封装。
三、真正需要重点理解:文件描述符 fd
这是这一章最核心的东西。
1. fd是什么?
调用:
c
int fd = open("myfile", O_RDONLY);
成功后可能得到:
c
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 → 显示器
所以:
c
read(0, buf, sizeof(buf));
可以理解:
从标准输入读取。
而:
c
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 → 清空原内容
例如:
c
open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
意思:
打开 log.txt,只写;不存在就创建;如果原来有内容就清空。
这里的 | 是按位或,用于把多个选项组合起来。
mode
只有涉及创建文件时才重点看:
0666
表示:
新创建文件的默认权限。
open返回值
成功 → 返回新的fd
失败 → -1
例如:
c
int fd = open("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
}
六、read / write / close:真正进行 IO
这三个先和 open 一起记。
open
↓
得到fd
↓
read / write
↓
close
read
核心形式:
c
ssize_t read(int fd, void *buf, size_t count);
可以理解成:
c
read(
从哪个文件读? → fd
读到哪里? → buf
最多读多少? → count
)
例如:
c
char buf[1024];
ssize_t s = read(fd, buf, sizeof(buf));
意思:
从 fd 对应的文件中读取数据,放到 buf,最多读取 1024 字节。
c
write
可以理解成:
c
write(
写到哪个文件? → fd
写什么? → buf
写多少? → count
)
例如:
c
write(1, buf, s);
意思:
把 buf 中的 s 个字节写到标准输出。
c
close
close(fd);
意思:
关闭这个文件描述符。
所以最基本的文件操作框架就是:
open
↓
fd
↓
read / write
↓
close
七、真正理解"重定向":它不是魔法,只是换了 fd 指向
这是这一章最值得你记住的一张图。
平时:
printf()
↓
stdout
↓
fd 1
↓
显示器
所以:
bash
./test
输出自然出现在屏幕。
但是如果:
bash
./test > log.txt
本质上发生了:
原来
fd 1 ─────────→ 显示器
重定向后
fd 1 ─────────→ log.txt
于是程序仍然:
c
printf("hello");
但它访问的是:
stdout
↓
fd 1
↓
log.txt
所以:
程序自己甚至不需要知道"我要输出到文件"。
它还是向 stdout 输出,只是 Shell 在运行程序之前,把 fd 1 的去向换成了文件。
PDF也通过关闭 1 后再 open 文件得到 fd=1 的实验,直观展示了为什么原本输出到显示器的内容会进入文件,这就是输出重定向。
dup2:更规范地完成重定向
接口:
c
#include <unistd.h>
int dup2(int oldfd, int newfd);
例如:
c
dup2(fd, 1);
可以理解成:
让 fd 1 也指向 fd 所指向的文件。
于是:
fd
↓
log.txt
fd 1
↓
log.txt
以后:
c
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:最后把这两个概念分清
你之前接触的是:
c
FILE *fp;
现在又出现:
c
int fd;
不要混。
可以简单理解成:
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 重定向时,你会突然发现------原来 >、>>、< 就是这么来的。