再谈重定向
1、实验中的有趣现象
还记得我们验证文件描述符fd分配规则的实验吗?我们当时是关闭了0号文件流stdin,然后新建文件,发现新建文件的fd是0。
假如我们关闭fd == 1的文件呢?
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1); // 关闭stdout
int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
printf("log.txt: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}

本该写入到屏幕上的信息,却写入到了新建的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。
c
int dup2(int oldfd, int newfd);
系统调用dup的作用是:
- 关闭当前
newfd指向的文件; - 将
oldfd上的file*指针,拷贝给newfd。
所以输出重定向,可以这样用dup实现:
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); // 不追加是O_TRUNC(清空)
dup2(fd, 1);
printf("%s\n", argv[1]);
close(fd);
return 0;
}
追加输出重定向,可以这样用dup实现:
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666); // 追加是O_APPEND
dup2(fd, 1);
printf("%s\n", argv[1]);
close(fd);
return 0;
}
输入重定向,可以这样用dup实现:
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_RDONLY, 0666);
dup2(fd, 0);
int a = 0;
float f = 0.0f;
char c = 0;
scanf("%d %f %c", &a, &f, &c);
printf("%d %f %c\n", a, f, c);
close(fd);
return 0;
}
学会使用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
- 看看字符串中有没有
c
// Execute中创建子进程的部分
else if (0 == id)
{
int fd = 0;
if (IN_REDIR == redir_type)
{
fd = open(redir_filename, O_RDONLY);
dup2(fd, 0);
}
else if (OUT_REDIR == redir_type)
{
fd = open(redir_filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
dup2(fd, 1);
}
else if (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);
}
c
void Bash()
{
static char *env[256];
_environ = env;
LoadEnv();
while (1)
{
PrintPrompt();
GetCommandLine();
if (0 == strlen(commandline))
continue;
Redir(); // 获取用户输入后,先对可能包含的重定向操作做处理,再分析命令
ParseCommandLine();
if (CheckBuildinAndExecute())
continue;
Execute();
}
}
c
static void Redir()
{
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;
}
else if ('<' == *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进行修改
这里就涉及到一个编程技巧:将此处的循环操作设计成一个宏(替换)。
c
#define Clear_Left_Space(pos) do{\
while(isspace(*pos))\
++pos; \
}while(0)
宏的本质就是替换,代码在预处理阶段就会用这段循环操作替换Clear_Left_Space;同时do{}while(0)中套while循环,可以防止宏函数直接跟;造成的实际循环未执行的情况发生。
当start找到了一个非空的字符,意味着找到了文件名。就可以设置redir_filename。
添加重定向操作,还有一个重要的一点:一定要在下一次输入命令之前,重置redir_type redir_filename!

不然会发生意想不到的问题。