023 基础 IO —— 重定向

重定向

相关好文 | CSDN

1. 什么是重定向?

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

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

比如常见的 shell 命令:

bash 复制代码
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. 函数原型

c 复制代码
#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 指向的内核文件表项

注意:

cpp 复制代码
          ┌──────────────┐
 oldfd →  │ 内核打开文件表 │ ← newfd
          └──────────────┘

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

4. 代码实验验证

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

c 复制代码
#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;
}

运行示例:

bash 复制代码
gcc dup2_1.c -o dup2_1
./dup2_1
cat output.txt

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

5. 标准错误也能重定向

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

c 复制代码
dup2(fd, STDERR_FILENO); // 把标准错误(2)也重定向到 fd

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


6. minishell 重定向

cpp 复制代码
#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
bash 复制代码
./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 相同的目标中,例如:

bash 复制代码
./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 追加
bash 复制代码
./mytest >>out.log 2>>err.log
./mytest >>all.log 2>&1       # 追加模式下合并所有输出

4. Bash 的简写特性

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

  • &>:等价于 >file 2>&1
  • &>>:等价于 >>file 2>&1
bash 复制代码
./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 重定向之后,才能"接上"正确的目标。
相关推荐
小米里的大麦35 分钟前
026 inode 与软硬链接
linux
₯㎕星空&繁华2 小时前
Linux-地址空间
linux·运维·服务器·经验分享·笔记
风铃7772 小时前
c/c++ Socket+共享内存实现本机进程间通信
linux·c语言
lsnm4 小时前
【LINUX网络】HTTP协议基本结构、搭建自己的HTTP简单服务器
linux·运维·服务器·c语言·网络·c++·http
杜大帅锅4 小时前
Linux搭建ftp服务器
linux·运维·服务器
运维自动化&云计算5 小时前
Centos虚拟机硬盘报错,根分区满,已用显示为负40G
linux·运维·centos
Web极客码5 小时前
在Ubuntu 22.04上安装远程桌面服务
linux·运维·ubuntu
sqmeeting5 小时前
QT6 如何在Linux Wayland 桌面系统抓屏和分享屏幕
linux·qt
大白同学4216 小时前
【Linux】编辑器vim的使用
linux·编辑器·vim