📁 第一篇:文件描述符与重定向
理解 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 = 3struct_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 效率。不同的刷新策略适配不同的设备场景。