Linux 重定向和缓冲区

📅 2026/7/2 17:43:36
Linux 重定向和缓冲区
第一篇文件描述符与重定向理解 fd、struct_file、dup2 和重定向的本质一、预备知识1.1 三个默认流每个进程启动时内核会默认打开三个文件文件流文件描述符对应设备stdin0键盘stdout1显示器stderr2显示器1.2 核心概念fd 和 struct_file文件描述符fd是什么fd 是一个非负整数是操作系统为了管理已打开文件而分配的索引号。你可以把它理解为进程访问文件的“门牌号”。cint fd open(log.txt, O_WRONLY); // fd 3struct_file 是什么struct_file是 Linux内核中描述一个已打开文件的数据结构。当进程调用open()时内核会创建一个struct_file对象记录文件路径、偏移量、权限、操作函数指针等信息把这个对象的地址放入进程的files_struct数组中返回这个数组的索引——也就是fdtext进程 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的地址存入该位置返回这个 fdcclose(0); // 释放 fd0 int fd open(log.txt, O_WRONLY); // fd 0最小未使用二、重定向的实现原理2.1 什么是重定向重定向 修改 fd 数组中的指针让某个 fd 指向另一个 struct_file。2.2 初识重定向关闭 1stdout新打开的文件会占用 fd 1cint 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);示例cint 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。 第二篇缓冲区深度解析理解语言层缓冲区、内核缓冲区、刷新策略一、引子一个现象cint 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的 typedefcstruct _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 实验代码cint 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中的内容textHello 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 效率。不同的刷新策略适配不同的设备场景。