【Linux指南】基础IO系列(五):重定向原理与 dup2 系统调用 —— 改变 IO 流向的魔法

在 Linux 终端中,你一定用过这样的命令:ls -l > log.txt(把列表输出写到文件)、cat < input.txt(从文件读入内容)、./a.out 2>&1(把错误输出和正常输出放一起)------ 这些都是重定向 。但你有没有想过:为什么>能改变输出的去向?2>&1又是什么原理?这篇文章会从 fd 的本质出发,带你揭开重定向的面纱:先讲手动实现重定向的底层逻辑,再讲更优雅的dup2系统调用,最后实战不同类型的重定向(输出、追加、输入、标准错误),让你彻底明白 "改变 IO 流向" 到底是怎么回事。

文章目录

    • [一、重定向的 "现象引入":我们每天都在用的 IO 魔法](#一、重定向的 “现象引入”:我们每天都在用的 IO 魔法)
    • [二、重定向的底层原理:基于 fd 的 "指针替换"](#二、重定向的底层原理:基于 fd 的 “指针替换”)
    • [三、dup2 系统调用:重定向的 "官方工具"](#三、dup2 系统调用:重定向的 “官方工具”)
      • [3.1 dup2 的函数解析](#3.1 dup2 的函数解析)
        • [1. 函数原型](#1. 函数原型)
        • [2. 参数含义](#2. 参数含义)
        • [3. 工作流程(关键!)](#3. 工作流程(关键!))
        • [4. 形象比喻](#4. 形象比喻)
      • [3.2 dup2 的核心优势(对比手动重定向)](#3.2 dup2 的核心优势(对比手动重定向))
      • [3.3 实战 1:用 dup2 实现输出重定向(模拟`>`)](#3.3 实战 1:用 dup2 实现输出重定向(模拟>))
      • [3.4 实战 2:用 dup2 实现追加重定向(模拟`>>`)](#3.4 实战 2:用 dup2 实现追加重定向(模拟>>))
    • 四、常见重定向类型的完整实现
      • [4.1 输入重定向(模拟`cat < input.txt`)](#4.1 输入重定向(模拟cat < input.txt))
      • [4.2 标准错误重定向(模拟`./a.out 2> error.txt`)](#4.2 标准错误重定向(模拟./a.out 2> error.txt))
      • [4.3 标准错误合并到标准输出(模拟`./a.out 1> log.txt 2>&1`)](#4.3 标准错误合并到标准输出(模拟./a.out 1> log.txt 2>&1))
    • 五、重定向的关键注意事项(避坑指南)
      • [5.1 重定向必须在 "子进程" 中执行(避免影响父进程)](#5.1 重定向必须在 “子进程” 中执行(避免影响父进程))
      • [5.2 缓冲类型变化导致的 "数据丢失"(经典坑)](#5.2 缓冲类型变化导致的 “数据丢失”(经典坑))
      • [5.3 进程替换不影响重定向(关键特性)](#5.3 进程替换不影响重定向(关键特性))
    • [六、实战衔接:自定义 Shell 中重定向的初步思路](#六、实战衔接:自定义 Shell 中重定向的初步思路)
      • [6.1 步骤 1:解析命令行中的重定向符号](#6.1 步骤 1:解析命令行中的重定向符号)
      • [6.2 步骤 2:子进程中执行重定向](#6.2 步骤 2:子进程中执行重定向)
      • [6.3 步骤 3:父进程等待子进程](#6.3 步骤 3:父进程等待子进程)
    • 七、总结

一、重定向的 "现象引入":我们每天都在用的 IO 魔法

在讲原理前,先回顾重定向的日常用法 ------ 这些 "改变 IO 流向" 的操作,本质都是在修改 fd 对应的文件。

重定向符号 作用描述 示例命令 底层效果(fd 变化)
> 输出重定向:覆盖写入目标文件 ls -l > log.txt fd=1(stdout)从 "显示器" 指向 "log.txt"
>> 追加重定向:在目标文件末尾追加 echo "test" >> log.txt fd=1 指向 "log.txt",打开时带O_APPEND标志
< 输入重定向:从文件读取输入,而非键盘 cat < input.txt fd=0(stdin)从 "键盘" 指向 "input.txt"
2> 标准错误重定向:把错误输出写入目标文件 ./a.out 2> error.txt fd=2(stderr)从 "显示器" 指向 "error.txt"
2>&1 标准错误合并到标准输出:错误和正常输出同路 ./a.out 1> log.txt 2>&1 fd=2 的指向复制 fd=1(即都指向 "log.txt")

这些命令看似神奇,但底层逻辑都围绕一个核心:修改进程fd_array中某个 fd 对应的struct file指针 ------ 比如把 fd=1 的指针从 "显示器文件" 改成 "磁盘文件",后续所有写 fd=1 的操作(如printfwrite(1, ...))都会指向新文件。

二、重定向的底层原理:基于 fd 的 "指针替换"

要理解重定向,必须结合第四篇讲的fd 分配规则(最小未使用原则)。我们先通过 "手动实现输出重定向" 的代码,直观感受重定向的本质 ------ 核心就是 "关闭目标 fd → 打开新文件(占用目标 fd) → 后续操作指向新文件"。

2.1 手动实现输出重定向(模拟ls -l > log.txt

我们知道,stdout对应的 fd=1,默认指向显示器。如果我们手动关闭 fd=1,再打开新文件,根据 "最小未使用原则",新文件的 fd 会变成 1------ 此时write(1, ...)printf(默认写 fd=1)会把数据写到新文件,而不是显示器。

代码实现:
c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    // 1. 关闭stdout(fd=1)------此时fd_array[1]变为NULL
    close(1);
    printf("这段文字不会显示(stdout已关闭)\n"); // 写fd=1失败,无输出

    // 2. 打开新文件log.txt,带O_WRONLY|O_CREAT|O_TRUNC(覆盖写)
    // 由于fd=1是最小未使用的,新文件的fd会是1
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd == -1) { perror("open"); return 1; }
    printf("fd=%d\n", fd); // 此时printf写fd=1(log.txt),不会显示在终端

    // 3. 验证:用write(1, ...)写数据,会写入log.txt
    const char *msg = "手动重定向:ls -l的模拟输出\n";
    write(1, msg, strlen(msg));

    // 4. 关闭文件
    close(fd);
    return 0;
}
运行结果验证:
bash 复制代码
# 编译运行
gcc redirect_manual.c -o redirect_manual
./redirect_manual

# 查看log.txt(关键!内容会在这里)
cat log.txt
# 输出:
# fd=1
# 手动重定向:ls -l的模拟输出
原理拆解(关键步骤):
  1. 关闭 fd=1 :进程的fd_array[1]从 "指向显示器的 struct file" 变为NULL
  2. 打开新文件 :内核找最小未使用的 fd(此时是 1),将log.txtstruct file*存到fd_array[1]
  3. 写操作转向 :后续printf(默认写 fd=1)、write(1, ...)都会通过fd_array[1]指向log.txt,数据写入文件而非显示器。

2.2 为什么手动重定向不常用?------ 有两个坑

手动关闭 fd 再打开的方式能实现重定向,但实际开发中很少用,因为有两个关键问题:

  1. 竞态条件 :关闭 fd 后、打开新文件前,若有其他线程 / 信号触发 IO 操作(如printf),会写向无效 fd(导致错误);
  2. 恢复困难:若后续想恢复 fd 的原始指向(如把 fd=1 恢复为显示器),需要提前保存原始 fd------ 手动操作繁琐。

为了解决这些问题,Linux 提供了专门的 dup2系统调用,能安全、高效地实现重定向。

三、dup2 系统调用:重定向的 "官方工具"

dup2的核心作用是 "复制一个 fd 的指向,覆盖到另一个 fd"------ 比如把 "文件 fd=3" 的指向复制到 "fd=1",让 fd=1 和 fd=3 指向同一个文件。它会自动处理 "新 fd 已打开" 的情况,避免手动操作的坑。

3.1 dup2 的函数解析

1. 函数原型
c 复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);
2. 参数含义
参数名 含义 要求
oldfd 「源 fd」:已打开的、有效的文件描述符(要复制的指向) 必须是有效的 fd(否则返回 - 1,错误码EBADF
newfd 「目标 fd」:要被覆盖的 fd(复制后,newfd 指向 oldfd 对应的文件) 可以是已打开或未打开的 fd
3. 工作流程(关键!)

dup2执行时,会按以下步骤操作,确保安全:

  1. 检查 oldfd 有效性 :若oldfd无效(未打开或已关闭),直接返回-1,不修改newfd
  2. 处理 newfd 与 oldfd 相等 :若newfd == oldfd,无需复制(两者已指向同一文件),直接返回newfd
  3. 关闭 newfd(若已打开) :若newfd已打开,先调用close(newfd)关闭它(避免 fd 泄漏);
  4. 复制指向 :将oldfd对应的struct file*指针,复制到newfdfd_array[newfd]中;
  5. 返回结果 :成功返回newfd,失败返回-1(错误码存于errno)。
4. 形象比喻

dup2(oldfd, newfd)就像 "钥匙复制":

  • oldfd是 "原钥匙",对应一把 "门"(文件);
  • newfd是 "新钥匙",复制后也能开同一把 "门";
  • 如果 "新钥匙" 之前开另一把 "门"(已打开),会先把那扇门关上(close(newfd)),再复制。

3.2 dup2 的核心优势(对比手动重定向)

对比维度 手动重定向(close+open) dup2 系统调用
安全性 有竞态条件(关闭 fd 后、打开前可能出错) 原子操作(关闭 + 复制一步完成,无竞态)
恢复难度 需提前保存原始 fd,手动恢复 可提前用 dup 保存原始 fd(如dup(1)保存 stdout)
代码复杂度 需写多个步骤(close、open、判断) 一行代码搞定,逻辑清晰

3.3 实战 1:用 dup2 实现输出重定向(模拟>

需求:把printfwrite(1, ...)的输出重定向到log.txt,用dup2实现。

代码实现:

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    // 1. 打开目标文件log.txt(覆盖写),得到oldfd
    int oldfd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (oldfd == -1) { perror("open"); return 1; }

    // 2. 关键:用dup2复制oldfd(log.txt)的指向到newfd=1(stdout)
    // 此时fd=1的指向从"显示器"变为"log.txt"
    int ret = dup2(oldfd, 1);
    if (ret == -1) { perror("dup2"); close(oldfd); return 1; }

    // 3. 验证:写fd=1的操作会写入log.txt
    printf("dup2输出重定向:printf的内容\n"); // 默认写fd=1
    const char *msg = "dup2输出重定向:write的内容\n";
    write(1, msg, strlen(msg)); // 显式写fd=1

    // 4. 关闭文件(oldfd可关闭,fd=1仍指向log.txt)
    close(oldfd);

    // 5. 可选:恢复fd=1为显示器(需提前保存原始fd)
    // int stdout_backup = dup(1); // 打开前保存
    // dup2(stdout_backup, 1); // 恢复
    // close(stdout_backup);

    return 0;
}

运行结果

bash 复制代码
./dup2_redirect
cat log.txt
# 输出:
# dup2输出重定向:printf的内容
# dup2输出重定向:write的内容

关键说明

  • oldfdlog.txt的 fd(比如 3),dup2(oldfd, 1)后,fd=1 和 fd=3 都指向log.txt
  • 关闭oldfd后,fd=1 仍有效(因为struct file有引用计数,dup2会让引用计数 + 1,close(oldfd)仅 - 1);
  • 若想恢复 fd=1 为显示器,需在dup2前用dup(1)保存原始 fd(dup(1)会创建一个新 fd,指向显示器)。

3.4 实战 2:用 dup2 实现追加重定向(模拟>>

追加重定向和输出重定向的区别,仅在于打开文件时是否带O_APPEND标志 ------O_APPEND会让写入操作从文件末尾开始,而非覆盖开头。

代码实现:

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    // 1. 打开文件时带O_APPEND(追加模式)
    int oldfd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    if (oldfd == -1) { perror("open"); return 1; }

    // 2. dup2复制到fd=1
    if (dup2(oldfd, 1) == -1) { perror("dup2"); close(oldfd); return 1; }

    // 3. 写入内容(会追加到log.txt末尾)
    printf("追加重定向:这是新追加的内容\n");
    close(oldfd);

    return 0;
}

运行结果

bash 复制代码
# 先运行之前的输出重定向,再运行这个追加重定向
./dup2_redirect
./dup2_append
cat log.txt
# 输出:
# dup2输出重定向:printf的内容
# dup2输出重定向:write的内容
# 追加重定向:这是新追加的内容

四、常见重定向类型的完整实现

除了输出和追加重定向,输入重定向(<)、标准错误重定向(2>)、错误合并(2>&1)也是常用场景。我们逐个实现,覆盖所有核心用法。

4.1 输入重定向(模拟cat < input.txt

输入重定向的本质:把 fd=0(stdin,默认指向键盘)的指向改成 "输入文件",后续read(0, ...)scanf(默认读 fd=0)会从文件读入,而非键盘。

代码实现:

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define BUF_SIZE 1024

int main() {
    // 1. 准备输入文件(提前创建input.txt,写入内容)
    // echo "输入重定向的测试内容" > input.txt

    // 2. 打开输入文件(只读模式),得到oldfd
    int oldfd = open("input.txt", O_RDONLY);
    if (oldfd == -1) { perror("open"); return 1; }

    // 3. dup2:把oldfd(input.txt)的指向复制到fd=0(stdin)
    if (dup2(oldfd, 0) == -1) { perror("dup2"); close(oldfd); return 1; }

    // 4. 验证:read(0, ...)会从input.txt读入,而非键盘
    char buf[BUF_SIZE] = {0};
    ssize_t read_cnt = read(0, buf, BUF_SIZE - 1);
    if (read_cnt > 0) {
        printf("从文件读入的内容:%s", buf); // 这里的printf仍写显示器(fd=1未改)
    }

    close(oldfd);
    return 0;
}

运行结果

bash 复制代码
# 准备输入文件
echo "输入重定向的测试内容" > input.txt

# 编译运行
gcc dup2_input.c -o dup2_input
./dup2_input
# 输出:
# 从文件读入的内容:输入重定向的测试内容

4.2 标准错误重定向(模拟./a.out 2> error.txt

标准错误(stderr)对应 fd=2,默认指向显示器。标准错误重定向的本质:把 fd=2 的指向改成 "错误文件",后续fprintf(stderr, ...)write(2, ...)会把错误信息写入文件。

代码实现:

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    // 1. 打开错误文件(覆盖写)
    int oldfd = open("error.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (oldfd == -1) { perror("open"); return 1; }

    // 2. dup2:把oldfd复制到fd=2(stderr)
    if (dup2(oldfd, 2) == -1) { perror("dup2"); close(oldfd); return 1; }

    // 3. 验证:错误输出会写入error.txt
    fprintf(stderr, "标准错误重定向:这是错误信息\n"); // 默认写fd=2
    write(2, "标准错误重定向:write的错误信息\n", strlen("标准错误重定向:write的错误信息\n"));

    // 正常输出仍写显示器(fd=1未改)
    printf("这是正常输出,显示在终端\n");

    close(oldfd);
    return 0;
}

运行结果

bash 复制代码
./dup2_stderr
# 终端显示:这是正常输出,显示在终端

# 查看错误文件
cat error.txt
# 输出:
# 标准错误重定向:这是错误信息
# 标准错误重定向:write的错误信息

4.3 标准错误合并到标准输出(模拟./a.out 1> log.txt 2>&1

2>&1是最常用的 "错误合并" 写法,本质是:先把 fd=1 重定向到文件,再把 fd=2 的指向复制到 fd=1------ 最终 fd=1 和 fd=2 都指向同一个文件,正常输出和错误输出都写入文件。

注意顺序:必须先重定向 fd=1,再重定向 fd=2!

如果写成2>&1 1> log.txt,会先让 fd=2 指向 "显示器"(fd=1 的原始指向),再把 fd=1 指向文件 ------ 此时 fd=2 仍指向显示器,错误输出不会写入文件。

代码实现:

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    // 步骤1:先把fd=1重定向到log.txt(覆盖写)
    int log_fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (log_fd == -1) { perror("open log"); return 1; }
    if (dup2(log_fd, 1) == -1) { perror("dup2 1"); close(log_fd); return 1; }

    // 步骤2:再把fd=2复制到fd=1(此时fd=1已指向log.txt)
    if (dup2(1, 2) == -1) { perror("dup2 2"); close(log_fd); return 1; }

    // 验证:正常输出和错误输出都写入log.txt
    printf("正常输出:这会写入log.txt\n");          // fd=1 → log.txt
    fprintf(stderr, "错误输出:这也会写入log.txt\n"); // fd=2 → log.txt(复制fd=1的指向)

    close(log_fd);
    return 0;
}

运行结果

bash 复制代码
./dup2_merge
# 终端无输出(所有输出都写入log.txt)

cat log.txt
# 输出:
# 正常输出:这会写入log.txt
# 错误输出:这也会写入log.txt

五、重定向的关键注意事项(避坑指南)

重定向虽然好用,但有几个容易踩的坑,必须重点注意:

5.1 重定向必须在 "子进程" 中执行(避免影响父进程)

如果在父进程(如 Shell)中执行重定向,会修改父进程的 fd 指向 ------ 比如 Shell 的 fd=1 被改成文件后,后续所有命令的输出都会写入文件,无法恢复。

正确做法:在子进程中执行重定向(如 Shell 创建子进程后,子进程先重定向,再执行命令)------ 子进程退出后,父进程的 fd 不受影响。

代码示例:子进程执行重定向

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid == -1) { perror("fork"); return 1; }

    if (pid == 0) {
        // 子进程:执行重定向+命令
        int fd = open("child_log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if (fd == -1) { perror("open"); exit(1); }
        dup2(fd, 1); // 子进程的fd=1指向文件

        // 执行命令(这里用printf模拟)
        printf("子进程重定向:这会写入child_log.txt\n");
        close(fd);
        exit(0);
    } else {
        // 父进程:等待子进程,fd=1仍指向显示器
        waitpid(pid, NULL, 0);
        printf("父进程:这会显示在终端\n"); // 不受子进程重定向影响
    }

    return 0;
}

运行结果:

bash 复制代码
./child_redirect
# 终端显示:父进程:这会显示在终端

cat child_log.txt
# 输出:子进程重定向:这会写入child_log.txt

5.2 缓冲类型变化导致的 "数据丢失"(经典坑)

C 库 IO(如printf)的缓冲类型会根据输出目标变化:

  • 输出到显示器:行缓冲 (遇\n或缓冲区满时刷新);
  • 输出到文件(重定向后):全缓冲 (缓冲区满或fflush/fclose时刷新)。

如果重定向后未刷新缓冲区,数据可能留在用户态缓冲中,导致文件中无内容。

经典错误示例:

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    close(1);
    int fd = open("bug.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd == -1) { perror("open"); return 1; }

    printf("重定向bug:这段文字不会写入文件"); // 无\n,全缓冲未刷新
    close(fd); // 未fflush,缓冲区数据丢失

    return 0;
}

运行结果

bash 复制代码
./buffer_bug
cat bug.txt
# 无输出(数据留在C库缓冲中,未写入文件)

解决方法:

  1. 手动调用fflush(stdout)刷新缓冲;
  2. 在字符串末尾加\n(仅行缓冲有效,重定向后全缓冲无效,不推荐);
  3. 用系统 IO(write)代替 C 库 IO(系统 IO 无用户态缓冲)。

修复后的代码:

c 复制代码
// 方法1:fflush
printf("重定向修复:这段文字会写入文件");
fflush(stdout); // 强制刷新缓冲

// 方法2:用write(推荐,无缓冲问题)
write(1, "重定向修复:write无缓冲问题\n", strlen("重定向修复:write无缓冲问题\n"));

5.3 进程替换不影响重定向(关键特性)

子进程执行exec系列函数(如execvp)替换代码时,fd 相关的资源(fd_arraystruct file)不会被替换 ------ 因为这些资源存储在进程的 PCB 中,而exec仅替换进程的代码段和数据段。

这意味着:子进程先重定向,再exec执行命令,命令的 IO 流向会继承重定向后的 fd 指向。

代码示例:exec 后重定向仍有效

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid == -1) { perror("fork"); return 1; }

    if (pid == 0) {
        // 子进程:先重定向,再exec执行ls命令
        int fd = open("ls_log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if (fd == -1) { perror("open"); exit(1); }
        dup2(fd, 1); // fd=1指向文件

        // exec替换为ls命令,ls的输出会写入ls_log.txt
        execlp("ls", "ls", "-l", NULL); // 若exec失败才会执行下面的代码
        perror("execlp failed");
        exit(1);
    } else {
        waitpid(pid, NULL, 0);
        printf("ls的输出已写入ls_log.txt\n");
    }

    return 0;
}

运行结果

bash 复制代码
./exec_redirect
cat ls_log.txt
# 输出:ls -l的结果(如total 48 -rwxr-xr-x 1 user user 8968 Nov 26 15:00 exec_redirect ...)

六、实战衔接:自定义 Shell 中重定向的初步思路

在之前实现的 "微型 Shell" 中,要添加重定向功能,核心是 "解析命令行中的重定向符号→子进程中执行 dup2 重定向→执行命令"。这里给出关键步骤,为后续完整实战铺垫:

6.1 步骤 1:解析命令行中的重定向符号

Shell 读取用户命令(如ls -l > log.txt)后,需要先拆分命令:

  • 拆分规则:从后往前遍历命令行,识别>/>>/<等符号;
  • 拆分结果:左侧为 "命令部分"(如ls -l),右侧为 "目标文件"(如log.txt),记录重定向类型(输出 / 追加 / 输入)。

简化解析代码:

c 复制代码
#include <string.h>
#include <ctype.h>

// 重定向类型:无重定向/输出/追加/输入
#define NONE_REDIR 0
#define OUTPUT_REDIR 1
#define APPEND_REDIR 2
#define INPUT_REDIR 3

int redir_type = NONE_REDIR;
char redir_file[1024] = {0};

// 去除字符串前后空格(辅助函数)
void trim_space(char *str) {
    if (!str) return;
    char *start = str;
    while (isspace(*start)) start++;
    char *end = str + strlen(str) - 1;
    while (end >= start && isspace(*end)) end--;
    *(end + 1) = '\0';
    memmove(str, start, end - start + 2);
}

// 解析命令行中的重定向符号
void parse_redir(char *cmd) {
    int len = strlen(cmd);
    // 从后往前遍历,优先识别>>(两字符),再识别>和<(单字符)
    for (int i = len - 1; i >= 0; i--) {
        if (cmd[i] == '>' && i > 0 && cmd[i-1] == '>') {
            // 追加重定向>>
            redir_type = APPEND_REDIR;
            cmd[i-1] = '\0'; // 截断命令部分(如"ls -l>>log.txt"→"ls -l")
            trim_space(cmd + i + 1); // 提取目标文件(log.txt)
            strncpy(redir_file, cmd + i + 1, sizeof(redir_file)-1);
            return;
        } else if (cmd[i] == '>') {
            // 输出重定向>
            redir_type = OUTPUT_REDIR;
            cmd[i] = '\0';
            trim_space(cmd + i + 1);
            strncpy(redir_file, cmd + i + 1, sizeof(redir_file)-1);
            return;
        } else if (cmd[i] == '<') {
            // 输入重定向<
            redir_type = INPUT_REDIR;
            cmd[i] = '\0';
            trim_space(cmd + i + 1);
            strncpy(redir_file, cmd + i + 1, sizeof(redir_file)-1);
            return;
        }
    }
}

6.2 步骤 2:子进程中执行重定向

Shell 创建子进程后,根据解析出的redir_typeredir_file,用dup2执行重定向,再exec执行命令:

c 复制代码
void child_exec(char *cmd) {
    // 1. 解析重定向
    parse_redir(cmd);

    // 2. 根据重定向类型执行dup2
    if (redir_type == OUTPUT_REDIR) {
        int fd = open(redir_file, O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if (fd == -1) { perror("open"); exit(1); }
        dup2(fd, 1);
        close(fd);
    } else if (redir_type == APPEND_REDIR) {
        int fd = open(redir_file, O_WRONLY | O_CREAT | O_APPEND, 0666);
        if (fd == -1) { perror("open"); exit(1); }
        dup2(fd, 1);
        close(fd);
    } else if (redir_type == INPUT_REDIR) {
        int fd = open(redir_file, O_RDONLY);
        if (fd == -1) { perror("open"); exit(1); }
        dup2(fd, 0);
        close(fd);
    }

    // 3. 解析命令参数(如"ls -l"→argv = ["ls", "-l", NULL])
    char *argv[64] = {0};
    int argc = 0;
    argv[argc++] = strtok(cmd, " ");
    while (argv[argc++] = strtok(NULL, " "));
    argc--;

    // 4. 执行命令
    execvp(argv[0], argv);
    perror("execvp failed");
    exit(1);
}

6.3 步骤 3:父进程等待子进程

父进程无需执行重定向,仅需等待子进程退出,保证 Shell 的 fd 不受影响:

c 复制代码
int main() {
    char cmd[1024] = {0};
    while (1) {
        // 显示提示符(如[user@host dir]$)
        printf("[user@localhost shell]$ ");
        fflush(stdout);

        // 读取用户命令
        fgets(cmd, sizeof(cmd), stdin);
        cmd[strcspn(cmd, "\n")] = '\0'; // 去除换行符

        // 创建子进程
        pid_t pid = fork();
        if (pid == -1) { perror("fork"); continue; }
        if (pid == 0) {
            child_exec(cmd); // 子进程执行重定向+命令
        } else {
            waitpid(pid, NULL, 0); // 父进程等待
        }
    }
    return 0;
}

七、总结

这篇我们彻底搞懂了重定向的原理和实现:

  • 本质:重定向是 "修改 fd 对应的 struct file 指针",让 IO 操作指向新文件 / 设备;
  • 核心工具dup2系统调用 ------ 原子操作复制 fd 指向,避免手动重定向的坑;
  • 常见类型 :输出(>)、追加(>>)、输入(<)、错误合并(2>&1),实现逻辑仅差打开文件的 flags;
  • 关键注意:子进程执行重定向(不影响父进程)、缓冲刷新(避免数据丢失)、进程替换不影响 fd。

我们还初步衔接了自定义 Shell 的重定向功能,给出了解析命令和执行重定向的核心代码 ------ 下一篇会在此基础上,完整实现 "支持重定向的微型 Shell",并结合之前的进程控制知识,打通 "进程创建→重定向→命令执行" 的全流程。

下一篇主题:缓冲区机制 ------Linux IO 效率的核心密码------ 缓冲区像一个 "数据中转站",默默影响着 IO 的效率和正确性,下篇文章会带你彻底搞懂缓冲区

相关推荐
吴烦恼的博客2 小时前
RK3588-kernel BringUp记录(二)
linux·kernel
阿正的梦工坊2 小时前
深入理解 Playwright 自动化脚本中的三个关键配置参数:无头模式,XVFB和持久化上下文
运维·自动化
-ONLY-¥2 小时前
HAProxy+Nginx高可用集群实战指南
linux
十年一梦实验室2 小时前
【Gemini & Nano banana】根据(F-35隐身战机)机器人与自动化产线机械、电气、软件及整体布局方案设计绘制综合方案图
运维·机器人·自动化
两点王爷2 小时前
在离线的Ubuntu机器中安装docker
运维·docker·容器
FS_Marking2 小时前
10G CWDM/DWDM SFP+光模块选购指南
运维·网络
线束线缆组件品替网2 小时前
Amphenol网线组件RJE1Y12305152401线束选型指南替代方案解析
服务器·数码相机·电脑·音视频·电视盒子·智能电视
Totoro-wen2 小时前
H20*8卡服务器装机指南
运维·服务器
盘古信息IMS2 小时前
2026年WMS系统选型指南:制造企业如何构建高度适配的智能仓储中枢?
运维·制造·devops