Linux系统篇(二十三) 基础 IO:从“文件”到“文件描述符”,彻底理解重定向


观众老爷们大家好 这里是邪修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 重定向时,你会突然发现------原来 >、>>、< 就是这么来的。

相关推荐
Yeniden1 小时前
Java 后端从零到企业级:第1篇 Java 基础语法——变量、数据类型与运算符
java·开发语言·apache
云雀衔光1 小时前
MCP + 应用生成:让 AI 直接产出可交互的应用
java·人工智能·测试工具·microsoft·交互·ai编程
灵境(虚幻知音)1 小时前
效能评估指标体系构建:方法、流程与模板
android·数据库
Nuanyt1 小时前
JVM常见核心知识梳理02 类加载 字节码技术 双亲委派 Java内存模型 JMM 并发底层 volatile synchronized 常见排障与调优工具
java·开发语言·jvm
光影少年2 小时前
React18 对RN 的影响
android·前端·react.js·ios·前端框架
正仪2 小时前
systemd 是什么?
linux·运维·windows
Shan12052 小时前
经典算法题示例与详解:飞地的数量(二)
java·数据结构·算法
暖核2 小时前
华为云运维三件套实战:云审计 + 云监控 + 云日志完整操作指南
linux·运维·华为云
一嘴一个橘子2 小时前
java - redis 缓存雪崩
java