一步一步实现自己的shell程序(三)---IO重定向和管道

代码见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号管道的读端,将标准输出重定向到第一个管道的读端;第二个进程则将标准输入重定向到第一个管道的读端,将标准输出重定向到第二个管道的写 这样每个进程都从前一个进程的输出中读取数据,然后将自身的输出传递给下一个进程,直到最后一个进程输出到终端屏幕。需要注意的是,由于父进程创建了n - 1个管道,fork函数创建的进程会复制所有管道,因此每个子进程都有n-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中多个命令的管道通信。

猜你喜欢

转载自blog.csdn.net/wutao1530663/article/details/62421957