狂自私

导航

实现一个简单的shell

使用已学习的各种C函数实现一个简单的交互式Shell,要求:
1、给出提示符,让用户输入一行命令,识别程序名和参数并调用适当的exec函数执行程序,待执行完成后再次给出提示符。
2、该程序可识别和处理以下符号:
1) 简单的标准输入输出重定向:仿照例 "父子进程ls | wc -l",先dup2然后exec。
2) 管道(|):Shell进程先调用pipe创建管道,然后fork出两个子进程。一个子进程关闭读端,调用dup2将写端赋给标准输出,另一个子进程关闭写端,调用dup2把读端赋给标准输入,两个子进程分别调用exec执行程序,而Shell进程把管道的两端都关闭,调用wait等待两个子进程终止。
实现步骤:
1. 接收用户输入命令字符串,拆分命令及参数存储。(自行设计数据存储结构)
2. 实现普通命令加载功能
3. 实现输入、输出重定向的功能
4. 实现管道
5. 支持多重管道

   

以上。

   

出于简单,我假设我们输入的命令字符串是符合要求,没有错误的。

我们要实现的有:普通命令;输入输出重定向;单个管道。有四种情况:ls -ahl 单个命令;ls -alh > a.txt 输出重定向;ls -ahl | grep root 管道;cat < a.txt输出重定向。其实更具体细分还有命令带参数和不带参数的情况。情况有这几种,我们应该用标志将他们区分,所以,储存命令的数据结构就很重要了。这是我设计的结构体:

typedef struct My_order

{

    char *argv[32]; //命令以及参数、文件

    int pipe;

    int right;

    int left;

} My_order;

我将其命名为My_order。现在我们要做的事是解析用户输入的命令字符串:ls -ahl | grep root 。理想情况下,我们应该将其拆分为 ls 、-ahl、|、grep、root这些字符串。该怎么拆分呢?观察命令字符串:命令参数之间用空格隔开的,我们可以利用这个特性。但是我们要自己造轮子么?不用,C库函数为我们提供了一个字符串分割函数strtok():

原型:char *strtok(char *restrict s1,const char * restrict s2);

描述:该函数把s1字符串分解为单独的记号。s2字符串包含了作为记号分隔符的字符。按顺序调用该函数。第一次调用时,s1应指向待分解的字符串。函数定位到非分隔符后的第一个记号分隔符,并用空字符替换它。函数返回一个指针,指向存储第一个记号的字符串。若未找到,返回NULL。再次调用strtok查找字符串中的更多记号。每次调用都返回指向下一个记号的指针。未找到返回NULL。

于是,我们像下面这样调用该函数就可以完美的解决问题了。

int resolve_order(My_order *my_order, char p[])

{

    //先初始化

    my_order->pipe = my_order->left = my_order->right = 0;

    for (int i = 0; i != 32; i++)

    {

        my_order->argv[i] = NULL;

    }

 

    int i = 0;

    int option = 0;

 

    my_order->argv[i] = strtok(p, " ");

    while (my_order->argv[++i] = strtok(NULL, " "))

    {

        if (strcmp(my_order->argv[i], " | ") == 0)

        {

            my_order->pipe++;

        }

        else if (strcmp(my_order->argv[i], ">") == 0)

        {

            my_order->right++;

        }

        else if (strcmp(my_order->argv[i], "<") == 0)

        {

            my_order->left++;

        }

    }

    return 0;

}

当命令字符串中有管道,输入输出重定向符的时候,相应的值就要增加。但是最多也只能是1,再多的话,我这个简单的shell就不能胜任了。即像这样的命令:cat|cat|cat我是解决不了的。

我们上面的示例命令有管道,所以我们要用到pipe函数,建立管道,使进程之间能够相互通讯。但是我们第一步是要创建进程,不多,一个就够了,使用fork()函数。但是在此之前我们还有问题要解决:是子进程解决管道前面的命令呢还是父进程先解决?子进程和父进程谁先执行?这里废话一点:以前有个牛人(抱歉不记得是谁了,若是知道请告知)做了个实验:观察父子进程谁先被执行,最后得出的结论是绝大部分情况下是父进程先抢到CPU资源。但是这并没有理论支撑。计算机科学没有理论来支持这个结论。(当故事听就好哈,不要较真,本人还是萌新。)虽然有大牛得出这样的结论来了,但是我还是没有遵循这个结论。^_^。所以我让子进程去执行管道前面的命令了,    哎。还好我写了这个博客,不然我会闹大笑话。必须要两个子进程,一个不行,除非我就执行这一个管道命令。exec族函数的一大特点是什么?执行完成指定程序之后根本就不回来!意味着这个进程死掉了,无论是父进程还是子进程都会被回收掉。所以还是要两个子进程,这里要注意的是,使用兄弟进程进行通讯的时候父进程应该使用waitpid函数进行非阻塞回收。但是在我的实现上依旧有那种阻塞情况发生,是在是不懂怎么回事。不过这不重要。(玛德,废话真多。)代码:

void my_pipe(My_order *my_order)

{

    int fd[2];

    int p_ret = pipe(fd);//fd[0]->r;fd[1]->w

    if (-1 == p_ret)

    {

        perror("pipe error ");

        exit(1);

    }

 

    int i = 0;

    int pid;

    for (; i != 2; i++)

    {

        if (!(pid = fork()))

        {

            break;

        }

    }

    if (0 == i)

    {

        if (strlen(my_order->argv[1]) > 1)

        {

            close(fd[1]);

            dup2(fd[0], STDIN_FILENO);

            execlp(my_order->argv[3], my_order->argv[3], my_order->argv[4], NULL);

        }

        else

        {

            close(fd[1]);

            dup2(fd[0], STDIN_FILENO);

            execlp(my_order->argv[2], my_order->argv[2], my_order->argv[3], NULL);

        }

    }

    else if (1 == i)

    {

        if (strlen(my_order->argv[1]) > 1)//有参数

        {

            close(fd[0]);

            dup2(fd[1], STDOUT_FILENO);

            execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);

        }

        else

        {

            close(fd[0]);

            dup2(fd[1], STDOUT_FILENO);

            execlp(my_order->argv[0], my_order->argv[0], NULL);

        }

    }

    else

    {

        waitpid(-1, NULL, WNOHANG);

        waitpid(-1, NULL, WNOHANG);

    }

 

    return 0;

}

我写的不够严谨,都没有什么错误检查。别像我这样写,要检查错误,检查函数返回值。

比如就是万一有用户这样写 ls -alh | a.txt 虽然这样在真正的shell也不能通过,但是别人有错误提示啊。

其实有管道这个是整个程序中最难的部分。接下来的重定向其实很简单的。进过我的测试(用我那点可怜的知识)发现,重定向无非三种正确(的简单的)情况:命令>命令;命令>文件;命令<文件。前面的部分全是命令,后面的就稍微有点不同。那么问题来了:如何判断后面的是文件还是命令?以有无后缀区分?但是在Linux中后缀是方便我们识别的而不是系统的刚需啊。我也经常看到gcc main.c  -o a这样的命令啊。(别喷别喷)没事,大部分的Linux命令都在/bin目录下呢。简单的实现也无需考虑那么多,现在就是我们需要去查看目录中有无对应字符串内容的命令。读目录也很简单啊,我的博客前几篇(忘了哪一篇了)介绍了读取指定目录获取文件数目内容。我们稍微变换一下就可以用来区分文件or命令了:

int get_dirfile(char *name) //命令存在返回0;不存在返回-1;

{

    DIR *dir = opendir(" / bin");

    struct dirent *di;

 

    while ((di = readdir(dir)) != NULL)

    {

        if (strcmp(di->d_name, name) == 0)

        {

            return 0;

            break;

        }

    }

    return -1;

}

//是命令就执行。是文件就打开(创建)。打开文件也很简单嘛:

int open_file(char p[])

{

    int o_ret = open(p, O_RDWR | O_CREAT | O_TRUNC, 0644);

    if (o_ret == -1)

    {

        perror("open file error ");

        exit(1);

    }

 

    return o_ret;

}

 

相关的函数、宏若不知道意思,请参阅前几篇(也忘了是哪一篇了)的介绍。

接下来,就要解决重定向了。dup2函数一定是需要的(我也在前几篇介绍了的),这里就不介绍了。接下来就很简单了,就是每个命令就要确定一下参数有无。

int exec_order(My_order *my_order)

{

    if ((my_order->pipe == 0) && (my_order->left == 0) && (my_order->right == 0))

    {

        if (!fork())

        {

            if (my_order->argv[1] != NULL)

                execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);

            else

                execlp(my_order->argv[0], my_order->argv[0], NULL);

        }

        else

        {

            wait(NULL);

        }

    }

    else if ((my_order->pipe == 1) && (my_order->left == 0) && (my_order->right == 0))

    {

        my_pipe(my_order);

    }

    else if ((my_order->pipe == 0) && (my_order->left == 1) && (my_order->right == 0))

    {

        if (!fork())

        {

            execlp(my_order->argv[0], my_order->argv[0], my_order->argv[2], NULL);

        }

        else

        {

            wait(NULL);

        }

    }

    else if ((my_order->pipe == 0) && (my_order->left == 0) && (my_order->right == 1))

    {

        if (!fork())

        {

            if (strlen(my_order->argv[1]) > 1)

            {

                int fd = open_file(my_order->argv[3]);

                dup2(fd, STDOUT_FILENO);//执行之后,标准输入就指向了fd

                execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);

                close(fd);

            }

            else

            {

                int fd = open_file(my_order->argv[2]);

                dup2(fd, STDOUT_FILENO);

                execlp(my_order->argv[0], my_order->argv[0], NULL);

                close(fd);

            }

 

        }

        else

        {

            wait(NULL);

        }

    }

}

其实这里有个小问题,就是像ps这样的命令参数是没有-的,直接就是ps a这样。为了简便,先这样吧。

main函数就很简单了。

int main(void)

{

    while (1)

    {

        My_order my_order;

        char p[32] = { '\0' };

        puts("GYJ_LoveDanDan@desktop:—————————————— - ");

        gets(p);

        //char p[8] = { "ls -alh | grep lovedan " };

        resolve_order(&my_order, p);

        exec_order(&my_order);

    }

    return 0;

}

写个这程序,真的是,感觉到了自己真是菜鸡。最开始的任务其实有这个:

你的程序应该可以处理以下命令:
ls-l-R>file1
cat<file1|wc-c>file1
注:表示零个或多个空格,表示一个或多个空格

5. 支持多重管道:类似于cat|cat|cat

我最开始为了解析字符串,操碎了心,眼看就要成功了,但是因为我用来储存的数据结构不好用于执行execlp函数,就放弃了,几经波折,我看透了。自己砍了要求,很勉强的实现了这个四不像shell。我想问人,没人回答我,我想查资料,没找到。这也许就是小说中散修和宗门的区别吧。

posted on 2018-04-05 20:44  狂自私  阅读(1793)  评论(0编辑  收藏  举报