023 基础 IO —— 重定向

重定向

相关好文 | CSDN

1. 什么是重定向?

重定向本质上就是操作文件描述符(每一个打开的文件或设备在内核中都有一个编号,称为文件描述符),即 修改标准输入/输出/错误 这三个文件描述符(file descriptor,简称 FD)的指向,让它们 不再指向默认终端(屏幕、键盘),而是指向 文件、设备或者其他地方

文件描述符编号 描述 默认指向
0 标准输入 stdin 键盘
1 标准输出 stdout 屏幕
2 标准错误 stderr 屏幕

image-20250426191149734

比如常见的 shell 命令:

1
ls > output.txt

背后的本质动作是:

  1. 关闭文件描述符 1(stdout)
  2. 打开或创建 output.txt
  3. 将描述符 1 指向 output.txt 文件

这样,所有本应该显示在屏幕上的内容,都会写入 output.txt 文件


2. 常见的重定向符号

符号 含义 功能描述 示例 效果
> 输出重定向(覆盖),stdout 把标准输出写入到指定文件,若文件存在则 清空 后写入,否则创建 ls > out.txt ls 的 stdout 写入(或覆盖)out.txt
>> 输出追加,stdout 把标准输出追加到指定文件末尾,若文件不存在则创建 echo hi >> out.txt hi 追加到 out.txt 末尾
< 输入重定向,stdin 从指定文件读取内容作为标准输入 wc -l < in.txt in.txt 作为 stdin 传给 wc -l
<< Here-Document(多行输入) 在脚本中内联一段文本作为标准输入,直到遇到结束标识符

3. 使用 dup2 系统调用

1. dup2 是什么?

dup2 是一个系统调用,用于复制文件描述符。本质作用就是:让两个文件描述符指向同一个内核打开文件表项。经常用于做输入输出重定向。

2. 函数原型

1
2
#include <unistd.h>
int dup2(int oldfd, int newfd);

参数:

  • oldfd 已经打开的文件描述符(比如文件、管道等)
  • newfd 要复制到的新文件描述符(比如 0/1/2)

返回值:成功: 返回 newfd失败: 返回 -1,并设置 errno 错误号(需要 perror 打印)

3. dup2 做了什么事情?(内部流程)

假设调用 dup2(oldfd, newfd)

  • 如果 oldfd == newfd:直接返回 newfd什么也不做(高效优化);
  • 如果 newfd 已经打开,会 先关闭 newfd(防止资源泄漏);
  • 然后 让 newfd 指向 oldfd 指向的内核文件表项

注意:

1
2
3
         ┌──────────────┐
oldfd → │ 内核打开文件表 │ ← newfd
└──────────────┘

不是简单复制数字,而是让它们“指向同一块内核资源”!dup2 后,oldfdnewfd 同时指向同一个内核文件表结构。修改一边,另一边也受影响(因为指的是同一份数据)。

4. 代码实验验证

如何 将 printf 打印内容重定向到一个文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>              // printf, perror
#include <unistd.h> // dup2, close
#include <fcntl.h> // open

int main()
{
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); // 打开一个文件,准备写入(如果不存在就会自动创建)
if (fd < 0)
{
perror("open failed");
return 1;
}

close(1); // 关闭标准输出 1(stdout)

dup2(fd, 1); // 将打开的文件描述符复制到 1(标准输出)

printf("这行文字被写入到 output.txt 文件中啦!\n"); // 现在所有的 printf 都不会打印到屏幕,而是写到 output.txt
dprintf(fd, "这是直接用 fd 写的一行文字!\n"); // 可以继续用 fd 写数据(可选)

close(fd); // 关闭文件
return 0;
}

运行示例:

1
2
3
gcc dup2_1.c -o dup2_1
./dup2_1
cat output.txt

image-20250426193916749

你会看到 output.txt 文件里出现了程序输出的内容,而不是显示在屏幕上!

5. 标准错误也能重定向

如果想把错误信息也重定向,可以:

1
dup2(fd, STDERR_FILENO); // 把标准错误(2)也重定向到 fd

这样 perrorfprintf(stderr, ...) 的内容也会写进文件!


6. minishell 重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#include <stdio.h>
#include <fcntl.h>
#include <ctype.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEN 1024 // 命令最大长度
#define NUM 32 // 命令拆分后的最大个数
int main()
{
int type = 0; // 0 >, 1 >>, 2 <
char cmd[LEN]; // 存储命令
char* myargv[NUM]; // 存储命令拆分后的结果
char hostname[32]; // 主机名
char pwd[128]; // 当前目录
while (1)
{
//获取命令提示信息
struct passwd* pass = getpwuid(getuid());
gethostname(hostname, sizeof(hostname) - 1);

getcwd(pwd, sizeof(pwd) - 1);
int len = strlen(pwd);
char* p = pwd + len - 1;

while (*p != '/')
{
p--;
}

p++;

printf("[%s@%s %s]$ ", pass->pw_name, hostname, p); // 打印命令提示信息

fgets(cmd, LEN, stdin); // 读取命令
cmd[strlen(cmd) - 1] = '\0';

char* start = cmd; // 实现重定向功能
while (*start != '\0')
{
if (*start == '>')
{
type = 0; // 遇到一个'>',输出重定向
*start = '\0';
start++;

if (*start == '>')
{
type = 1; // 遇到第二个'>',追加重定向
start++;
}
break;
}
if (*start == '<')
{
type = 2; // 遇到'<',输入重定向
*start = '\0';
start++;
break;
}
start++;
}
if (*start != '\0') // start位置不为'\0',说明命令包含重定向内容
{
while (isspace(*start)) // 跳过重定向符号后面的空格
{
start++;
}
}
else
{
start = NULL; // start设置为NULL,标识命令当中不含重定向内容
}

//拆分命令
myargv[0] = strtok(cmd, " ");
int i = 1;
while (myargv[i] = strtok(NULL, " "))
{
i++;
}

pid_t id = fork(); // 创建子进程执行命令
if (id == 0)
{
if (start != NULL) // 重定向符号后面有内容
{
if (type == 0) // 输出重定向
{
int fd = open(start, O_WRONLY | O_CREAT | O_TRUNC, 0664); // 以写的方式打开文件(清空原文件内容)
if (fd < 0)
{
error("open");
exit(2);
}

close(1);
dup2(fd, 1); // 重定向
}
else if (type == 1) // 追加重定向
{
int fd = open(start, O_WRONLY | O_APPEND | O_CREAT, 0664); // 以追加的方式打开文件
if (fd < 0)
{
perror("open");
exit(2);
}
close(1);
dup2(fd, 1); // 重定向
}
else // 输入重定向
{
int fd = open(start, O_RDONLY); // 以读的方式打开文件
if (fd < 0)
{
perror("open");
exit(2);
}
close(0);
dup2(fd, 0); // 重定向
}
}

execvp(myargv[0], myargv); // child进行程序替换
exit(1); // 替换失败的退出码设置为1
}

//shell
int status = 0;
pid_t ret = waitpid(id, &status, 0); // shell等待child退出
if (ret > 0)
{
printf("exit code:%d\n", WEXITSTATUS(status)); // 打印child的退出码
}
}
return 0;
}

4. 1(stdout) VS 2(stderr)

还是那句话,在类 Linux 中,每个进程启动时都会默认打开三个 文件描述符(file descriptor),分别对应三个“数据通道(stream)”:

描述符 名称(英文) 默认指向 常用符号
0 标准输入(stdin) 键盘(或重定向的输入文件) <
1 标准输出(stdout) 终端屏幕(或重定向的输出文件) >
2 标准错误(stderr) 终端屏幕(或重定向的错误文件) 2>

1. 基本重定向:分开输出

  • 1>file1.txtstdout 写入 file1.txt
  • 2>file2.txtstderr 写入 file2.txt
1
2
3
./mytest 1>file1.txt 2>file2.txt	# 执行名为 mytest 的程序,并进行输出重定向
# 任何正常输出(如 printf、cout 等)都会进入 file1.txt
# 任何错误信息(如 error: …、segfault 报错)都会进入 file2.txt

如果只写 >file1.txtShell 会把它当作 1>file1.txt 处理,因为不加数字时默认重定向文件描述符 1(stdout)。

2. 将 stderr 重定向到 stdout 已指向的文件

有时希望把 stderr 合并到与 stdout 相同的目标中,例如:

1
./mytest >all.txt 2>&1

这里的执行顺序和含义要注意:

  1. >all.txt 先把 stdout (1) 重定向到 all.txt
  2. 2>&1 再把 stderr (2) “重定向到(&)” 描述符 1 当前所指向的地方。

最终,all.txt 会包含正常输出和错误输出,顺序按照程序真正写入时的先后混合在一起。

注意顺序

  • 如果写成 ./mytest 2>&1 >all.txt,则会先将 stderr 重定向到 原先 的 stdout(通常还是终端),然后再把 stdout 重定向到 all.txt,结果是:
    • stderr 仍然打印到终端
    • stdout 写入 all.txt

3. 追加模式(append)

  • >> :向文件末尾追加(append),不覆盖原文件
  • 2>>:同理,对 stderr 追加
1
2
./mytest >>out.log 2>>err.log
./mytest >>all.log 2>&1 # 追加模式下合并所有输出

4. Bash 的简写特性

Bash 提供了更简洁的写法,将 stdout 与 stderr 同时重定向:

  • &>:等价于 >file 2>&1
  • &>>:等价于 >>file 2>&1
1
2
./mytest &>both.log      # 将 stdout 和 stderr 一次性写入 both.log(覆盖)
./mytest &>>both.log # 将 stdout 和 stderr 追加到 both.log

5. 形象比喻

可把 stdout(1)比作“普通信件通道”,stderr(2)比作“特别邮件通道”:

  1. 各走各的信道
    • 1>normal.txt 2>error.txt
    • 相当于把普通信件放到 A 信箱,特别邮件放到 B 信箱。
  2. 合并邮件
    • >all.txt 2>&1
    • 先把普通信件都放进 C 信箱,再把特别邮件也投入到 C 信箱。

5. 总结

重定向 = 文件描述符指向改变。通过 close + dup2 等系统调用,把标准输入输出 重新绑定 到文件或设备上。

  1. 单独重定向
    • cmd 1>out.txt 2>err.txt
  2. 合并到同一文件
    • 覆盖: cmd >all.txt 2>&1cmd &>all.txt
    • 追加: cmd >>all.txt 2>&1cmd &>>all.txt
  3. 顺序关键
    • 2>&1 始终要写在将 stdout 重定向之后,才能“接上”正确的目标。