📁 第一篇:文件描述符与重定向
理解 fd、struct_file、dup2 和重定向的本质
一、预备知识
1.1 三个默认流
每个进程启动时,内核会默认打开三个文件:
| 文件流 | 文件描述符 | 对应设备 |
|---|---|---|
stdin |
0 | 键盘 |
stdout |
1 | 显示器 |
stderr |
2 | 显示器 |
1.2 核心概念:fd 和 struct_file
文件描述符(fd)是什么?
fd 是一个非负整数 ,是操作系统为了管理已打开文件而分配的索引号。
你可以把它理解为:进程访问文件的"门牌号"。
c
int fd = open("log.txt", O_WRONLY); // fd = 3
struct_file 是什么?
struct_file 是 Linux 内核中描述一个已打开文件的数据结构。
当进程调用 open() 时,内核会:
-
创建一个
struct_file对象,记录文件路径、偏移量、权限、操作函数指针等信息 -
把这个对象的地址放入进程的
files_struct数组中 -
返回这个数组的索引 ------也就是 fd
text
进程 PCB
└── files_struct
├── fd[0] ──→ struct_file (stdin → 键盘)
├── fd[1] ──→ struct_file (stdout → 显示器)
├── fd[2] ──→ struct_file (stderr → 显示器)
├── fd[3] ──→ struct_file (log.txt → 磁盘文件)
└── ...
fd 和 struct_file 的关系
| 概念 | 本质 | 比喻 |
|---|---|---|
| fd | 整数(数组下标) | 门牌号 |
| struct_file | 内核数据结构 | 房间 |
进程通过 fd 找到对应的 struct_file,进而操作文件。
1.3 fd 的分配规则
当调用 open() 打开新文件时,内核会:
-
扫描
files_struct中的fd数组 -
找到当前未被使用的最小 fd
-
把新建的
struct_file的地址存入该位置 -
返回这个 fd
c
close(0); // 释放 fd=0
int fd = open("log.txt", O_WRONLY); // fd = 0(最小未使用)
二、重定向的实现原理
2.1 什么是重定向?
重定向 = 修改 fd 数组中的指针,让某个 fd 指向另一个 struct_file。
2.2 初识重定向
关闭 1(stdout),新打开的文件会占用 fd = 1:
c
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("printf, fd:%d\n", fd);
fprintf(stdout, "fprintf, fd:%d\n", fd);
return 0;
}
运行结果:屏幕上没有输出,log.txt 中写入了两行内容。
这就是重定向的本质------通过改变文件描述符的指向,改变数据的输出目标。
2.3 dup2 函数
dup2(oldfd, newfd):让 newfd 成为 oldfd 的副本 ,即让 newfd 指向 oldfd 所指向的文件。
c
#include <unistd.h>
int dup2(int oldfd, int newfd);
示例:
c
int main() {
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1); // 将 stdout(1) 重定向到 fd 指向的文件
printf("Hello Linux!\n"); // 写入 log.txt
fprintf(stdout, "Hello World!\n"); // 写入 log.txt
return 0;
}
记忆技巧: dup2(fd, 1) 表示"将 1 重定向到 fd"。
2.4 重定向的本质总结
text
重定向前:
fd[1] ──→ struct_file (显示器)
重定向后(dup2(fd, 1)):
fd[1] ──→ struct_file (log.txt)
重定向就是修改 fd 数组中某个槽位的指针,让它指向另一个 struct_file。
📦 第二篇:缓冲区深度解析
理解语言层缓冲区、内核缓冲区、刷新策略
一、引子:一个现象
c
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("printf, fd:%d\n", fd);
// fflush(stdout); // 注释掉
close(fd);
return 0;
}
运行结果:log.txt 内容为空。
加上 fflush(stdout) 后,内容出现。
这说明数据没有直接写入文件,而是先存在了某个地方------这个地方就是缓冲区。
二、缓冲区是什么?
2.1 本质:结构体
在 C 语言中,FILE 实际上是 _IO_FILE 的 typedef:
c
struct _IO_FILE {
int _flags;
char* _IO_read_ptr;
char* _IO_read_end;
char* _IO_read_base;
char* _IO_write_base;
char* _IO_write_ptr;
char* _IO_write_end;
char* _IO_buf_base;
char* _IO_buf_end;
int _fileno; // 对应的文件描述符
};
每个打开的文件都有自己的缓冲区 ,缓冲区中记录了对应的 _fileno(文件描述符)。
2.2 缓冲区体系
text
用户代码
↓
语言层缓冲区(printf / fprintf / fflush)
↓
内核层缓冲区(write / read)
↓
磁盘 / 显示器 / 网络
| 层级 | 作用 |
|---|---|
| 用户层缓冲区 | 减少频繁的系统调用,提升用户体验 |
| 语言层缓冲区 | 减少与内核的交互次数,提升性能 |
| 内核层缓冲区 | 减少磁盘 I/O 次数,提升系统整体效率 |
三、缓冲区的刷新策略
| 策略 | 说明 | 典型场景 |
|---|---|---|
| 立即刷新 | 每写入一点就立刻刷新 | 极少使用 |
| 行刷新 | 遇到换行符 \n 时刷新 |
显示器(为符合人眼阅读习惯) |
| 全缓冲 | 缓冲区满了才刷新 | 普通文件(如写入磁盘) |
| 特殊情况刷新 | 进程退出、调用 fflush、exit 等 |
进程终止时 |
为什么显示器用行刷新?
如果一次性全部刷新出来,人眼看不完;如果 1 个字符 1 个字符地打印,体验又太差。所以显示器采用行刷新。
四、为什么缓冲区存在?
为了减少系统调用,提升效率。
直接和 OS 交互(系统调用)成本很高,因为:
-
涉及用户态/内核态切换
-
OS 忙着调度、回收资源
所以:
-
语言层缓冲区:把多次小写入合并成一次大写入,减少系统调用
-
内核层缓冲区:把多次磁盘 I/O 合并成一次,减少磁盘操作
类比: 自己翻山越岭送礼物,一次只能送一件;用快递公司批量运输,效率高得多。
五、综合实验:fork 与缓冲区
5.1 实验代码
c
int main() {
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
printf("Hello Linux!\n");
fprintf(stdout, "Hello World!\n");
char* message = "Hello C++!\n";
write(1, message, strlen(message));
fork(); // 创建子进程
return 0;
}
5.2 运行结果
log.txt 中的内容:
text
Hello Linux!
Hello World!
Hello C++!
Hello Linux!
Hello World!
5.3 现象分析
| 函数 | 写入位置 | 打印次数 | 原因 |
|---|---|---|---|
printf |
语言层缓冲区 | 2 次 | 子进程复制了父进程的缓冲区,进程退出时各刷新一次 |
fprintf |
语言层缓冲区 | 2 次 | 同上 |
write |
内核层缓冲区 | 1 次 | 系统调用直接写入内核,不经过语言层缓冲区 |
5.4 核心结论
printf/fprintf等库函数使用语言层缓冲区,write等系统调用直接写入内核层缓冲区。
fork创建子进程时,会复制父进程的语言层缓冲区内容,导致多打印一份。
六、关键结论
| 问题 | 答案 |
|---|---|
| 缓冲区是什么? | 结构体 (FILE / _IO_FILE),每个文件都有独立的缓冲区 |
| 缓冲区为什么存在? | 减少系统调用,提升效率 |
| 缓冲区如何工作? | 根据刷新策略(行刷新 / 全缓冲 / 立即刷新)决定何时写入内核 |
printf 和 write 的区别? |
printf 写语言层缓冲区,write 直接写内核缓冲区 |
fork 后为什么多打印? |
子进程复制了父进程的语言层缓冲区 |
七、总结
缓冲区体系图
text
用户代码
↓
语言层缓冲区(printf / fprintf)
↓ (fflush / exit 触发刷新)
内核层缓冲区(write / read)
↓ (OS 调度)
磁盘 / 显示器 / 网络
一句话总结
缓冲区是介于用户代码和内核之间的"中转站",通过批量处理减少系统调用,提升 I/O 效率。不同的刷新策略适配不同的设备场景。