IO重定向

第一部分:重定向的本质

1. 核心规则:最小分配原则

Linux 在 open 一个文件时,有一个铁律:

给新文件分配的 fd,永远是当前 files_struct****数组中 最小的、未被占用的 下标。

2. 手动实现重定向 ( The "Hack" Way )

利用这个规则,我们可以玩一个魔术:

  1. 我们知道 printf 默认是往 stdout (也就是 fd 1) 打印数据。
  2. 如果我们先 close(1),把 1 号下标空出来。
  3. 然后立刻 open("log.txt", ...)
  4. 根据"最小分配原则",系统会把 1 号下标 分配给 log.txt
  5. 此时,printf 依然傻傻地往 fd 1 写数据,但 fd 1 已经不再指向显示器,而是指向了 log.txt

代码验证

复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    // 1. 关闭标准输出 (显示器)
    close(1); 

    // 2. 打开新文件
    // 系统发现 1 号坑是空的,于是把 fd 1 给到了 log.txt
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);

    // 3. 正常打印
    // printf 底层是 write(1, ...),它不知道 1 号变了
    printf("fd: %d\n", fd); 
    printf("hello redirection\n");

    // 4. 刷新缓冲区 (重要!如果是文件,默认是全缓冲,不刷新可能写不进去)
    fflush(stdout); 

    close(fd);
    return 0;
}

现象 :屏幕上什么都没有,但 cat log.txt 会发现内容都在里面。这就是 > 的雏形。


第二部分:dup2 系统调用

手动 closeopen 这种方法有风险(比如多线程环境下可能有竞争,或者代码写起来麻烦)。Linux 提供了一个专门的系统调用来做这件事:dup2

1. 函数原型
复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);
2. 核心逻辑 (面试必问)

很多人容易搞混参数顺序。记忆口诀: newfd****成为 oldfd****的副本

  • 动作
    1. 如果 newfd 已经被打开了,先把它 close 掉。
    2. 把内核数组中 oldfd 指向的那个 file 结构体指针,复制newfd 的下标位置。
    3. 结果newfdoldfd 现在同时指向 同一个文件(原来 oldfd 打开的那个文件)。
    4. 通常我们会让 oldfd 是刚打开的文件(如 fd 3),newfd 是 1。这样 1 就指向了 3 指向的文件。
3. 代码实战
复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0) { perror("open"); return 1; }

    // 【核心】把 fd(3) 的内容复制给 1
    // 此时 1 号下标也指向了 log.txt
    dup2(fd, 1); 

    printf("This will go to file!\n");
    fprintf(stdout, "This too!\n");

    // 现在 1 和 3 都指向 log.txt,关闭 3 不影响 1
    close(fd); 

    return 0;
}

第三部分:标准输出 (1) vs 标准错误 (2)

我们在 Linux 命令中常看到 > log.txt 2>&1,这是什么意思?

  • stdout (1):正常的打印信息。
  • stderr (2):专门用于打印错误信息。
  • 区分意义
    • 当我们执行 ./program > log.txt 时,Shell 只把 fd 1 重定向到了文件。
    • 此时 fd 2 依然指向显示器。
    • 好处:程序正常跑的日志写文件里,程序报错的信息直接打在屏幕上让你看到。

如何把错误也写进文件? ./program > log.txt 2>&1

  • 先把 1 重定向到文件。
  • 再把 2 重定向到 1(也就是 2 也指向文件)。

第四阶段: 缓冲区 (Buffer) 的坑

1. 现象:Fork 导致的"双倍快乐"

看下面这段诡异的代码:

复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main() {
    // C库函数
    const char *s1 = "hello printf\n";
    printf("%s", s1);

    // 系统调用
    const char *s2 = "hello write\n";
    write(1, s2, strlen(s2));

    // 创建子进程
    fork();
    return 0;
}

实验:

  1. 直接运行 (./test):屏幕上打印两行,非常正常。
  2. 重定向运行 (./test > log.txt):打开 log.txt,你会发现:
  • hello write 出现 1 次
  • hello printf 竟然出现了 2 次
2. 原理揭秘:缓冲策略的改变

这跟 fork 无关,跟 C 语言标准库 ( FILE**) 的缓冲策略** 有关。

  • C 库缓冲区策略
  • 无缓冲:立刻刷新。
  • 行缓冲 (Line Buffered) :遇到 \n 才刷新。(显示器默认是行缓冲)。
  • 全缓冲 (Full Buffered) :缓冲区填满才刷新。(普通文件默认是全缓冲)。

分析案发现场

  1. 直接运行时(向显示器写):
  • printf 遇到 \n,触发行缓冲 ,立马把数据刷给内核(write)。
    • fork 时,C 库缓冲区是空的。父子进程各自退出,没啥可刷的。
    • write 是系统调用,直接写内核。

重定向时(向文件写):

  • printf 虽然有 \n,但因为目标变成了普通文件,策略变为全缓冲 。数据暂存 在 C 库的用户级缓冲区里,没有 刷给内核。
    • 退出时
    • 写时拷贝 :子进程复制了父进程的内存,包括那个没刷新的 C 库缓冲区
    • Fork 发生:父进程创建子进程。
    • write 直接写内核,不受影响(先写进去了)。
  • 父进程退出,刷新自己的缓冲区 -> 写入一次 "hello printf"。
    • 子进程退出,刷新自己的缓冲区 -> 写入一次 "hello printf"。

结论

  • 库函数(printf, fwrite)自带用户级缓冲区,操作文件时是全缓冲。
  • 系统调用(write)没有用户级缓冲区。
  • fork 会拷贝用户级缓冲区的数据。
相关推荐
大柏怎么被偷了2 小时前
【Linux】重定向与应用缓冲区
linux·服务器·算法
Awesome Baron2 小时前
Python批量测试脚本的一般结构
笔记
航Hang*2 小时前
第3章:复习篇——第1节:创建和管理数据库
开发语言·数据库·笔记·sql·sqlserver
阿华hhh2 小时前
数据结构(树)
linux·c语言·开发语言·数据结构
Bruce_Liuxiaowei2 小时前
Mac_Linux 查询网站IP地址:4个核心命令详解
linux·tcp/ip·macos
大聪明-PLUS2 小时前
硬件断点:它们在 Linux 中的用途和工作原理
linux·嵌入式·arm·smarc
爱吃番茄鼠骗2 小时前
Linux操作系统———UDP/IPC网络编程
linux·网络·udp
Starry_hello world2 小时前
Linux 线程(2)
linux
Promise4852 小时前
关于使用wsl实现linux移植(imux6ull)的网络问题
linux·服务器·网络