【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 的效率和正确性,下篇文章会带你彻底搞懂缓冲区

相关推荐
A小辣椒4 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒8 小时前
TShark:基础知识
linux
AlfredZhao10 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式