Linux 重定向和缓冲区

📁 第一篇:文件描述符与重定向

理解 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() 时,内核会:

  1. 创建一个 struct_file 对象,记录文件路径、偏移量、权限、操作函数指针等信息

  2. 把这个对象的地址放入进程的 files_struct 数组中

  3. 返回这个数组的索引 ------也就是 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() 打开新文件时,内核会:

  1. 扫描 files_struct 中的 fd 数组

  2. 找到当前未被使用的最小 fd

  3. 把新建的 struct_file 的地址存入该位置

  4. 返回这个 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 时刷新 显示器(为符合人眼阅读习惯)
全缓冲 缓冲区满了才刷新 普通文件(如写入磁盘)
特殊情况刷新 进程退出、调用 fflushexit 进程终止时

为什么显示器用行刷新?

如果一次性全部刷新出来,人眼看不完;如果 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),每个文件都有独立的缓冲区
缓冲区为什么存在? 减少系统调用,提升效率
缓冲区如何工作? 根据刷新策略(行刷新 / 全缓冲 / 立即刷新)决定何时写入内核
printfwrite 的区别? printf 写语言层缓冲区,write 直接写内核缓冲区
fork 后为什么多打印? 子进程复制了父进程的语言层缓冲区

七、总结

缓冲区体系图

text

复制代码
用户代码
    ↓
语言层缓冲区(printf / fprintf)
    ↓  (fflush / exit 触发刷新)
内核层缓冲区(write / read)
    ↓  (OS 调度)
磁盘 / 显示器 / 网络

一句话总结

缓冲区是介于用户代码和内核之间的"中转站",通过批量处理减少系统调用,提升 I/O 效率。不同的刷新策略适配不同的设备场景。