代码见github
基本知识
IO重定向
Unix系统中,每个程序都有三个数据流,分别为:
- 标准输入stdin——-需要处理的数据流
- 标注输出stdout——–结果数据流
- 标准错误输出stderr—–错误消息流
默认的三个数据流的文件描述符分别为0,1,2,默认的标准输入为终端键盘IO,标准输出和错误输出都是为终端屏幕。而IO重定向就是将三个数据流定向到别的文件描述符。
最低可用文件描述符
什么是文件描述符? 简单来说就是打开文件的一个索引号。Unix系统中,把打开文件保持在一个数组中,文件描述符即为某文件在此数组中的索引。而最低可用文件描述符的意思就是,每当系统打开一个新文件,则分配一个目前可用的最小的文件描述符用于该文件。每个Unix程序默认打开0,1,2三个文件描述符,其实它们对应的文件就是键盘设备和终端屏幕设备的设备文件。
如何实现IO重定向
系统调用dup函数
int dup(int oldfd);
dup函数的作用是复制oldfd文件描述符给一个最低可用文件描述符。如果我们想将标准输入重定向到新的文件描述符fd,那么我们可以先close(0)关闭标准输入文件描述符,然后调用函数dup(fd),系统则会默认使用最低可用文件描述符指向fd文件描述符对应的文件。最后再关闭fd文件描述符就完成了IO重定向,如下图所示:
dup2函数
int dup2(int oldfd,int newfd);
系统调用dup2函数则将dup函数的多个操作合并为一个原子操作,
- dup2(oldfd,newfd)
相当于:
- close(newfd)
- newfd = dup(oldfd)
在shell中为程序IO重定向
shell程序能够很轻易的实现和下图类似的IO重定向,’>’代表输出重定向,’<’则代表输入重定向。
wutao@wutao-K43SV:~/git/shell/my_shell2$ ls > file
wutao@wutao-K43SV:~/git/shell/my_shell2$ cat file
a.out
built_in.c
controlflow.c
data
env
file
...
那么shell程序是如何实现IO重定向的呢?关键就在于fork函数和exec函数之间的空隙。exec函数的功能只是利用磁盘上的一个新程序替换了当前进程的正文段(.text),数据段(.data,.bss),堆段(heap)和栈段(stack)。而环境变量和一些进程属性不在其中,则会由新程序(非新进程)继承。而我们关注的文件描述符默认是继承的,除非特地用fcntl函数设置了执行时关闭标志。我们可以在fork函数之后,exec函数之前进行IO重定向,具体的实现就是在每次执行exec函数前,判断命令行中是否包含’<’或’>’标志,若有则需要IO重定向,因此我们新建一个redirect.c模块处理重定向需求,execute函数只需要添加一个函数调用:
...
else if(pid == 0){
signal(SIGINT,SIG_DFL);
environ = var_to_environ();
redirect(arglist);//增加一行
execvp(arglist[0],arglist);
...
redirect函数对当前命令行进行遍历,判断是否包含”>”,”<”或者”>>”参数,如果有则进行对应的处理。需要注意的是,如果参数中包含重定向的符号,在传递给exec函数之前必须把该符号删除,这里我们写了一个delete_io_symbol函数用以删除某一位置的重定向符号以及其后的文件名参数,具体代码见github:
void redirect(char **args){
/*
purpose: determines if io redirect symbol in cmd and process it
details: it is necessary to delete the symbol in the args,or exec() would raise a error
*/
char *cp;
int mode = 0;
for(int i = 0;args[i];++i){
if(strcmp(args[i],">") == 0){
*args[i] = '\0';
output_redirect(args[i + 1],mode);
}
else if(strcmp(args[i],">>")== 0){
mode = 1;
output_redirect(args[i + 1],mode);
delete_io_symbol(args,i);
}
else if(strcmp(args[i],"<") == 0)
input_redirect(args[i + 1]);
else
continue;
delete_io_symbol(args,i);
}
}
管道
管道是Unix系统进程通信的一种形式。管道有两个特点:
- 一般管道的数据只能在一个方向上流动
- 管道只能在具有公共祖先的两个进程之间使用。通常管道由一个父进程创建,调用fork函数后,这个管道就能在父进程和子进程之间使用了
创建管道
管道是通过调用pipe函数创建,参数是一个大小为2的int数组,由该参数返回两个文件描述符:fd[0]为读打开,fd[1]为写打开。即fd[1]的输出是fd[0]的输入,如下图所示:
#include<unistd.h>
int pipe(int fd[2]);
使用fork函数创建新进程时,也会将父进程的管道复制,就像这样:
因此如果我们想从父进程写数据,子进程读书据,那么就可以关闭父进程管道中的fd[0],关闭子进程管道中的fd[1],这样就形成了父进程写子进程读的单向数据流动管道。
如何在Shell中实现管道
shell中允许将多个进程之间用管道连接,例如可以使用下面的命令将ls的输出作为wc的输入,然后将wc的输出作为wc -l的输入,最后输出到终端屏幕的是wc -l 统计wc输出的行数:
wutao@wutao-K43SV:~/git/shell/my_shell2$ ls | wc | wc -l
1
那么如何在shell中实现多个进程的管道呢?如下图所示,我们要实现n个进程的管道通信需要有n - 1个管道,然后分别关闭进程中不需要的管道。实践中,我们在父进程中创建n - 1个管道后,执行n次fork函数,根据进程的序号关闭不同的管道,并进行IO重定向。比如第一个进程只需要关闭1号管道的读端,将标准输出重定向到第一个管道的读端;第二个进程则将标准输入重定向到第一个管道的读端,将标准输出重定向到第二个管道的写
void do_pipe(int pfd[][2],int process_index,int pipe_num,int pid){
int j;
if (pid != 0){//父进程关闭所有管道
for(int i = 0;i < pipe_num;i++){
close(pfd[i][0]);
close(pfd[i][1]);
}
}
else{//根据进程序号关闭和重定向不同的管道
if(process_index == 0){
dup2(pfd[0][1], STDOUT_FILENO);
close(pfd[0][0]);
close(pfd[0][1]);
for (j=1; j<pipe_num; j++) {
close(pfd[j][0]);
close(pfd[j][1]);
}
}
else if(process_index == pipe_num){
dup2(pfd[pipe_num-1][0],STDIN_FILENO);
close(pfd[pipe_num-1][1]);
close(pfd[pipe_num - 1][0]);
for(j = 0;j <pipe_num -1;j++){
close(pfd[j][0]);
close(pfd[j][1]);
}
}
else{
dup2(pfd[process_index - 1][0],STDIN_FILENO);
close(pfd[process_index - 1][1]);
dup2(pfd[process_index][1],STDOUT_FILENO);
close(pfd[process_index][0]);
for(j = 0;j <pipe_num;j++){
if(j !=process_index || j!=process_index -1){
close(pfd[j][0]);
close(pfd[j][1]);
}
}
}
}
}
小结
这篇博客介绍了IO重定向和管道的基本知识,有了这些基础,我们就能实现Shell中多个命令的管道通信。