再谈重定向
- 1、实验中的有趣现象
- 2、重新理解重定向
- 3、直接完成重定向的系统调用——dup
- 4、完善自主shell——加入重定向操作
1、实验中的有趣现象
还记得我们验证文件描述符fd分配规则的实验吗?我们当时是关闭了0号文件流stdin,然后新建文件,发现新建文件的fd是0。
假如我们关闭fd == 1的文件呢?
#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){close(1);// 关闭stdoutintfd=open("log.txt",O_CREAT|O_WRONLY,0666);printf("log.txt: %d\n",fd);fflush(stdout);close(fd);return0;}本该写入到屏幕上的信息,却写入到了新建的log.txt中——这不就是输出重定向吗?
所以,我们通过关闭fd == 1的文件,再新建文件,然后用printf企图向标准输出流中做写入,结果内容写入到了文件log.txt,从而间接地完成了输出重定向的工作。
2、重新理解重定向
操作系统是只认文件描述符fd的,文件操作相关的系统调用几乎都需要fd的参与。
标准输出流stdout,底层封装的文件描述符fd是fd == 1。未来我们想要对标准输出流做写入,默认是写入到文件描述符为fd == 1的文件中的。
现在关闭了fd == 1的文件,即关闭了stdout标准输出流。然后我们新建文件log.txt,打开log.txt返回的文件描述符就是fd == 1。
而想要继续向标准输出流做写入的时候,由于操作系统只认fd == 1的文件流为标准输出流,所以就会去打开文件表fd_array中找下标为1的文件,结果找到了log.txt。
以上就是输出重定向的全过程。输出重定向的本质是:
- 做标准输出的时候,进程只会写入到文件描述符
fd == 1的文件当中,默认情况下是stdout标准输出流; - (打开文件表)
fd_array中下标为 1 的文件,由stdout变为了用户指定的文件。
同理,追加输出重定向的本质与输出重定向的非常相似。它们的唯一的区别是:
- 输出重定向,会清空历史内容;
- 追加输出重定向,不会清空,只会在文件内容的末尾继续写入。
类似的,输入重定向的本质是:
- 做标准读取的时候,进程只会读取文件描述符
fd == 0的文件,默认情况下是stdin标准输入流; - (打开文件表)
fd_array中下标为 1 的文件,由stdin变为了用户指定的文件。
3、直接完成重定向的系统调用——dup
重定向操作,本质上是(打开文件表)fd_array中file*指针的交换,即文件描述符fd作为fd_array数组的下标,对应的file*指针,的交换。
如果我们采用上面的“关闭文件再打开”的方式,显得比较麻烦。Linux为我们提供了一个操作系统——dup。
intdup2(intoldfd,intnewfd);系统调用dup的作用是:
- 关闭当前
newfd指向的文件; - 将
oldfd上的file*指针,拷贝给newfd。
所以输出重定向,可以这样用dup实现:
#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(intargc,char*argv[]){intfd=open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);// 不追加是O_TRUNC(清空)dup2(fd,1);printf("%s\n",argv[1]);close(fd);return0;}追加输出重定向,可以这样用dup实现:
#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(intargc,char*argv[]){intfd=open("log.txt",O_CREAT|O_WRONLY|O_APPEND,0666);// 追加是O_APPENDdup2(fd,1);printf("%s\n",argv[1]);close(fd);return0;}输入重定向,可以这样用dup实现:
#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){intfd=open("log.txt",O_RDONLY,0666);dup2(fd,0);inta=0;floatf=0.0f;charc=0;scanf("%d %f %c",&a,&f,&c);printf("%d %f %c\n",a,f,c);close(fd);return0;}学会使用dup之后,我们就可以继续完善我们自主实现的shell,即加上重定向操作。
4、完善自主shell——加入重定向操作
注意我们当前加入的重定向操作,是只针对普通命令,即需要bash创建子进程执行的命令,的重定向操作。
往自主shell中加入重定向操作,需要经过以下两个步骤:
- 完善执行命令函数
Execute。- 定义全局的标识符
redir_type,表示重定向操作的具体选项:- 不重定向
#define NONE_REDIR 0 - 输入重定向
#define IN_REDIR 1 - 输出重定向
#define OUT_REDIR 2 - 追加输出重定向
#define NONE_REDIR 3
- 不重定向
- 以后在
Execute中,检测redir_type发生的变化:- 若
redir_type == 0,不做任何操作 - 若
redir_type == 1,检测到需要做输入重定向,stdin流换成指定文件流dup2(fd, 0) - 若
redir_type == 2,检测到需要做输出重定向,stdout流换成指定文件流dup2(fd, 1) - 若
redir_type == 3,检测到需要做追加输出重定向,stdout流换成指定文件流dup2(fd, 1)
- 若
- 定义全局的标识符
- 添加函数
Redir,对从用户方读取的命令字符串做预处理:- 看看字符串中有没有
< > >>,以设置redir_type - 定义全局的重定向文件名
redir_filename,提取字符串中重定向操作符右侧的文件名写入redir_filename
- 看看字符串中有没有
// Execute中创建子进程的部分elseif(0==id){intfd=0;if(IN_REDIR==redir_type){fd=open(redir_filename,O_RDONLY);dup2(fd,0);}elseif(OUT_REDIR==redir_type){fd=open(redir_filename,O_CREAT|O_WRONLY|O_TRUNC,0666);dup2(fd,1);}elseif(APP_REDIR==redir_type){fd=open(redir_filename,O_CREAT|O_WRONLY|O_APPEND,0666);dup2(fd,1);}else{// do nothing}execvp(argv[0],argv);exit(12);}voidBash(){staticchar*env[256];_environ=env;LoadEnv();while(1){PrintPrompt();GetCommandLine();if(0==strlen(commandline))continue;Redir();// 获取用户输入后,先对可能包含的重定向操作做处理,再分析命令ParseCommandLine();if(CheckBuildinAndExecute())continue;Execute();}}staticvoidRedir(){char*start=commandline;char*end=&commandline[strlen(commandline)+1];// 左闭右开while(start!=end){if('>'==*start){if('>'==*(start+1)){redir_type=APP_REDIR;*start='\0';start+=2;}else{redir_type=OUT_REDIR;*start='\0';start++;}Clear_Left_Space(start);// 宏替换的技巧:1. 直接写不利于观察。2. 写成函数,要传二级指针// 过滤空格,找到重定向的文件名redir_filename=start;break;}elseif('<'==*start){redir_type=IN_REDIR;*start='\0';start++;Clear_Left_Space(start);redir_filename=start;break;}else{++start;}}}处理重定向操作的函数Redir就很有讲究。
首先是寻找重定向操作符,设置redir_type。定义char*指针start遍历输入字符串,
- 遇到
'<',就是输入重定向 - 遇到
'>',- 如果
start下一位也是'>',那么就是追加输出重定向 - 否则就只是输出重定向
- 如果
设置完redir_type之后,我们可以将重定向操作符改写成'\0',这样做是对用户输入字符串做“切割”,切出操作符之前的命令(这样后续在解析函数ParseCommandLine中,strtok遇到\0就会停下)。
再然后,就要提取redir_filename。我们可以使start跳过操作符,继续向后找文件名。而操作符之后,
- 可能直接就是文件名
- 也可能是许多前导空格
所以我们需要做一个循环操作,过滤掉空格,
- 直接写循环吗?太冗余了,代码看起来不太简洁。而且这个循环操作以后可能还要用
- 外部设计成一个函数吗?那就要传递二级指针,才不会因为传值拷贝而不能对
start进行修改
这里就涉及到一个编程技巧:将此处的循环操作设计成一个宏(替换)。
#defineClear_Left_Space(pos)do{\while(isspace(*pos))\++pos;\}while(0)宏的本质就是替换,代码在预处理阶段就会用这段循环操作替换Clear_Left_Space;同时do{}while(0)中套while循环,可以防止宏函数直接跟;造成的实际循环未执行的情况发生。
当start找到了一个非空的字符,意味着找到了文件名。就可以设置redir_filename。
添加重定向操作,还有一个重要的一点:一定要在下一次输入命令之前,重置redir_type redir_filename!
不然会发生意想不到的问题。
代码演示