文章目录
- 前言
- 重定向的原理
- dup函数
- 添加重定向功能到myshell
前言
了解重定向之前需要明白文件描述符的工作规则,可以看这篇文章:文件系统
最关键的一点是:在进程中,在文件描述符表中,会将最小的、没有被使用的数组元素分配给新文件。
重定向的原理
输出重定向
以下是测试代码:
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define LOG "log.txt"
int main()
{
close(1);//关闭标准输出
int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("hello world!\n");
printf("hello world!\n");
printf("hello world!\n");
printf("hello world!\n");
printf("hello world!\n");
return 0;
}
如果我们把close(1);//关闭标准输出
这句代码先注释掉,运行结果如下——打印到显示屏上。1号文件描述符对应的就是stdout文件。
那如果我们把close(1);//关闭标准输出
这句代码加上,运行结果如下——可以发现,运行后发现没有在命令行打印,并且在生成的log.txt文件中发现了5行hello world!
解析:
首先因为0,1,2号文件描述符分别对应标准输入输出和错误,我们关闭1号描述符即关闭了stdout;又因为文件描述符的分配规则:在文件描述符表中,会将最小的、没有被使用的数组元素分配给新文件。所以当我们打开新的文件log.txt的时候,会将现在空出来的1号文件描述符分配给log.txt文件;而printf默认向显示器打印,即默认输出到准输出流stdout,但是printf是根据数组下标来打印的,它并不知道对应数组下标的fd实际指向的是谁,此时1号文件描述符指向的是log.txt,所以就会发生打印输出到log.txt文件的现象,实际上着就是输出重定向。
输入重定向
首先我们向log.txt文件中,写入数据:1 2
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<stdio.h>
#define LOG "log.txt"
int main()
{
close(0);//关闭标准输入
int fd = open(LOG, O_RDONLY);
int a,b;
scanf("%d %d",&a,&b);
printf("a=%d b=%d\n",a,b);
return 0;
}
运行结果如下:
解析:
对于输入重定向,同样的方法,以O_RDONLY(只读模式)打开文件,如果关闭0号文件描述符标准输入stdin,打开文件后会将文件地址填入0号下标,此时在进程中如果使用scanf输入,就不会从默认的标准输入stdin——键盘输入,而是直接从打开的文件中读取数据并输入。
追加重定向
对于追加重定向,将输出重定向中打开文件的方式由O_TRUNC(对文件内容做清空)改为O_APPEND(追加)即可,所以输出重定向与追加重定向唯一的区别就是文件打开的方式不同。
重定向原理:
在上层无法感知的情况下,在OS操作系统内部,更改进程对应的文件描述符表中,特定下标的指向!!!这个特定下标指的就是0——标准输入,1——标准输出,2——标准错误!!!
再看一个测试,请看下列代码:
1 #include<iostream>
2 #include<cstdio>
3
4 int main()
5 {
6 //Linux下一切皆文件
7 //C
8 printf("hello printf->stdout\n");
9 fprintf(stdout,"hello fprintf->stdout\n");
10 fprintf(stderr,"hello fprintf->stderr\n");
11
12 //C++
13 std::cout << "hello cout -> cout" << std::endl;
14 std::cerr << "hello cerr -> cerr" << std::endl;
15 }
运行结果:标准输出和标准错误都会向显示器打印。
而输出重定向后,只有fprintf指定文件流stdout的与cout的打印到了文件log.txt中,而stderr与cerr还依旧向显示器文件打印。重定向时只会对标准的输出进行正常的重定向,但是标准错误不受重定向的影响。
解析:
- stdout,cout——>1,他们都是向1号文件描述符对应的文件进行打印。而输出重定向,只改变1号对应的指向,重定向后1号指向了log.txt文件,所以printf,fprintf,cout就往log.txt文件中打印了。
- stderr,cerr——>2,他们都是向2号文件描述符对应的文件进行打印。所以不影响2号。
经过以上学习,那么如何把常规消息,打印到log_normal,异常消息打印到log_error呢?
测试代码:
1 #include <unistd.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include <stdio.h>
6
7 #define LOG_NORMAL "logNormal.txt"
8 #define LOG_ERROR "logError.txt"
9
10 int main()
11 {
12 printf("hello printf->stdout\n");
13 printf("hello printf->stdout\n");
14 printf("hello printf->stdout\n");
15 printf("hello printf->stdout\n");
16 printf("hello printf->stdout\n");
17
18 fprintf(stdout,"hello fprintf->stdout\n");
19 fprintf(stdout,"hello fprintf->stdout\n");
20 fprintf(stdout,"hello fprintf->stdout\n");
21 fprintf(stderr,"hello fprintf->stdout\n");
22 fprintf(stdout,"hello fprintf->stdout\n");
23 fprintf(stdout,"hello fprintf->stdout\n");
24
25 return 0;
26 }
运行结果:所以打印信息都粘在一块了
正确写法:
1 #include <unistd.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include <stdio.h>
6
7 #define LOG_NORMAL "logNormal.txt"
8 #define LOG_ERROR "logError.txt"
9
10 int main()
11 {
12 close(1);//关闭标准输出-stdout
13 open(LOG_NORMAL, O_WRONLY | O_CREAT | O_APPEND, 0666);//这个时候1号文件描述符对应的是logNormal.txt文件
14 //所以原来应该输出到stdout显示器的文件,会被输到logNormal.txt文件中
15
16 close(2);//关闭标准错误-stderr
17 open(LOG_ERROR, O_WRONLY | O_CREAT | O_APPEND, 0666);
18
19 printf("hello printf->stdout\n");
20 printf("hello printf->stdout\n");
21 printf("hello printf->stdout\n");
22 printf("hello printf->stdout\n");
23 printf("hello printf->stdout\n");
24
25 fprintf(stdout,"hello fprintf->stdout\n");
26 fprintf(stdout,"hello fprintf->stdout\n");
27 fprintf(stdout,"hello fprintf->stdout\n");
28 fprintf(stderr,"hello fprintf->stderr\n");
29 fprintf(stdout,"hello fprintf->stdout\n");
30 fprintf(stdout,"hello fprintf->stdout\n");
31
32 return 0;
33 }
运行结果:
也可以在命令行输入以下代码,进行重定向:
./myproc 1>log.txt 2>err.txt
上述代码,我们用close函数,分别将1,2号关闭,log_normal,txt文件对应的文件描述符就变成了1,log_error文件的描述符对应的就是2,而stdout和stderr默认指向1,2号文件描述符,所以以后文件量大的话,就可以使用这种方法将错误消息都筛选到一个文件中,便于观察。
dup函数
前面讲原理时所用的方法:关闭相关的文件描述符指向的文件,然后再打开文件来实现重定向。这种方法平时不会使用,只是讲原理时使用。下面介绍重定向的实际使用方法:利用dup函数进行重定向。
dup2如果调用成功,返回newfd,否则返回-1。
> log.txt //一个大于符号+文件名即可清空这个文件
dup函数的使用方式:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<errno.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<sys/stat.h>
7 #include<fcntl.h>
8
9 #define LOG_NORMAL "logNormal.txt"
10 #define LOG_ERROR "logError.txt"
11
12 int main()
13 {
14 int fd = open(LOG_NORMAL, O_CREAT | O_WRONLY | O_APPEND, 0666);
15 if(fd<0)
16 {
17 perror("open error");
18 return 1;
19 }
20
21 dup2(fd,1); 1号文件描述符重定向到fd1
22
23 printf("hello world!\n");
24
25 close(fd);//关闭fd
26 }
运行结果:
添加重定向功能到myshell
这里的myshell指的是我们自己实现的简易命令行解释器,想学习之前实现的myshell可以看这篇文章:进程控制——简易shell
思路如下:
- 判断获取到的命令,遍历命令字符串,看看是否有
>
输出重定向,>>
追加重定向,<
输入重定向。 - 设置flag变量,用于标记重定向的类型,flag为0表示命令当中包含输出重定向,flag为1表示命令中包含追加重定向,flag为2表示输入重定向。
- 找到重定向之后,用flag标记它是哪种重定向之后,将这个位置置成’\0’,跳过一个空格后,后面跟的就是目标文件名,若flag为0,则以写的方式打开目标文件;若type值为1,则以追加的方式打开目标文件,若flag为2,则以读的方式打开目标文件。
- 若flag为0或1,则使用dup2函数实现目标文件与标准输出流的重定向;若flag为2,则使用dup2函数实现目标文件与标准输入流的重定向。
代码实现如下:
#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 flag = 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 == '>'){
flag = 0; //遇到一个'>',输出重定向
*start = '\0';
start++;
if (*start == '>'){
flag = 1; //遇到第二个'>',追加重定向
start++;
}
break;
}
if (*start == '<'){
flag = 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){
//child
if (start != NULL){
if (flag == 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 (flag == 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;
}