模拟shell程序

要求:
  1. 能识别 >, < 的输入输出重定向。
  2. 能识别出管道操作。
  3. 支持多重管道: 比如cat | cat | cat | cat
  4. 支持管道和重定向的混合。
  5. 解决管道输入输出重定向和管道输出重定向和文件重定向共存的问题。

分析:
  • 简单指令

        此类指令无重定向, 无管道, 则其执行方式应该是主进程创建一个子进程, 将指令字符串组装成字符串数组后再添加调用exec函数即可。

  • 带有重定向的指令

方案一:

        > 和 < 直接当做命令行参数传递给exec函数。经过测试, exec并不能解析重定向符号

方案二:

         手动打开文件然后后dup2了。

  • 带有管道的指令

         将指令从管道符号拆分成多条指令, 每条指令分给一个子线程, 并让一个管道分隔的两个子进程一个获得一个管道的读端, 一个获得进程写端, 且将写端的进程的标准输出重定向到写端的文件描述符。如下图所示:

实现
/**
 *  完成一个模拟shell的程序。
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


char commands[1024];            /* 主进程读入的一个整的指令*/
char parts[100][100][1024];     /* 第一维度表示子进程, 第二维度表示该进程指令参数, 第三个维度为参数为具体的值*/
char *vector[100][100];         /* 第一维度表示子进程, 每个进程的vector*/
char input_file[100][1024];     /* 第一维度表示子进程, 输入重定向的文件*/
char output_file[100][1024];    /* 第一维度表示子进程, 输出重定向的文件*/
int input_flag[100] = {0};      /* 第一维度表示子进程, 输入重定向标记*/
char output_flag[100] = {0};    /* 第一维度表示子进程, 输出重定向标记*/
int  pipe_no[100][2];           /* 两个进程共享一对管道的文件描述符*/
char child_command[1024];       /* 临时存放一个子进程的指令*/
int child_process_cnt = 0;      /* 子进程的数量*/

/**
 * 读入指令, 并对指令进行特殊处理去掉结尾的\n
 */
char * readCommand()
{
    char *ret = fgets(commands, 1024, stdin);
    if(ret)
    {
        char * t = strchr(commands, '\n');
        *t = '\0';
    }
    return ret;
}

/**
 * 将参数列表进行解析, 使用strsep函数即可。
 * 将解析结果形成一个vector, 提供给execv函数使用。
 * 解析正确返回0, 解析失败返回-1。
 */
int parse_command()
{
    // 先将大的指令差分成子进程指令
    char *bbuf = commands;
    char *pp = strsep(&bbuf, "|");
    int pro_no = 0;
    while(pp)
    {
        strcpy(child_command, pp);
#ifdef DEBUG
        printf("-%s-\n", child_command);
#endif

        char *buf = child_command;
        char *p = NULL;
        p = strsep(&buf, " ");
        int index = 0, i = 0;
        // 将参数解析的到part当中
        while(p)
        {
            // 可能产生空串的情况
            if(strcmp(p, "") == 0)
            {
                p = strsep(&buf, " ");
                continue;
            }
#ifdef DEBUG
            printf("#%s#\n", p); 
#endif
            // 遇见输入重定向标记, 则获取输出文件名   
            if(strcmp(p, "<") == 0) 
            {
                input_flag[pro_no] = 1;
                p = strsep(&buf, " ");
                if(!p || strcmp(p, "") == 0) return -1;
                if(strcmp(p, "<") == 0)
                {
                    // 标准输出当中执行, 不能连续出现两次 <
                    printf("- myshell: synax error near unexpected token '<'");
                    return -1;
                }
                strcpy(input_file[pro_no], p);
            }
            else if(strcmp(p, ">") == 0)
            {
                output_flag[pro_no] = 1;
                p = strsep(&buf, " ");
                if(!p || strcmp(p, "") == 0) return -1;
                if(strcmp(p, ">") == 0)
                {
                    printf("- myshell: synax error near unexpected token '>'");
                    return -1;
                }
                strcpy(output_file[pro_no], p);
            }
            else strcpy(parts[pro_no][index++], p);
            p = strsep(&buf, " ");
        }

        // 使用part形成vector
        for(i = 0; i < index; ++i)
        {
            vector[pro_no][i] = parts[pro_no][i];
        }
        vector[pro_no][i] = NULL;
        pp = strsep(&bbuf, "|");

        pro_no += 1;

        // 统计子进程的数量
        child_process_cnt += 1;
    }
    return 0;
}

/**
 * 关闭管道的文件描述符
 */
void closePipe()
{
    int i = 0;
    for(i = 0; i < child_process_cnt -1; ++i)
    {
        int ret1 = close(pipe_no[i][0]);
        int ret2 = close(pipe_no[i][1]);

        /* 此处允许error
        if(ret1 < 0 || ret2 < 0)
        {
            printf("pipe close error!\n");
            exit(1);
        }
        */
    }
}

int main()
{
    int son_flag = 0;       /* 子进程标记*/
    system("stty erase ^H");
    while(1)
    {
        printf("simulate Shell # ");
        readCommand();
        son_flag = 0;
        if(strcmp(commands, "exit") == 0) 
            break;

        parse_command();                 
#ifdef DEBUG
        printf("子进程的数量 %d\n", child_process_cnt);
#endif
        int i = 0;
        for(i = 0; i < child_process_cnt; ++i) 
        {

            // 创建新的管道
            if(child_process_cnt > 1 && i != child_process_cnt - 1)
            {
                int ret = pipe(pipe_no[i]);
                
                if(ret < 0)
                {
                    printf("create pipe error\n");
                    exit(1);
                }
            }
            
            // 释放之前的管道
            if(i >= 2)
            {
                close(pipe_no[i - 2][0]);
                close(pipe_no[i - 2][1]);
            }
            int child_pid = fork();
            if(child_pid == 0)
                break;
            
            
        }
        
        if(i < child_process_cnt)
        {
#ifdef DEBUG
        printf("我是 %d 号进程, 我执行的指令是 #%s#\n", i, parts[i][0]);
#endif
            // 先进行文件的重定向操作
            if(input_flag[i] == 1)
            {
                int fd = open(input_file[i], O_RDONLY);
                if(fd < 0)
                {
                    printf("- myshell: %s: No such file or directory\n", input_file);
                    exit(1);               
                }
                dup2(fd, STDIN_FILENO);
            }
            
            if(output_flag[i] == 1 && i == child_process_cnt - 1)
            {
                int fd = open(output_file[i], O_WRONLY | O_CREAT | O_TRUNC, 0644);
                if(fd < 0)
                {
                    printf("- myshell: %s :cant't create the file \n", output_file);
                    exit(1);
                }
                dup2(fd, STDOUT_FILENO);   
            }
            
            // 管道处理
            if(child_process_cnt > 1)
            {
                if(i == 0)
                {
                    close(pipe_no[i][0]);                       /* 关闭读端*/
                    dup2(pipe_no[i][1], STDOUT_FILENO);         /* 标准输出重定向到写端*/
                }
                else if(i == child_process_cnt - 1)
                {
                    close(pipe_no[i - 1][1]);                   /* 关闭写端*/
                    dup2(pipe_no[i - 1][0], STDIN_FILENO);      /* 标准输入重定向到读端*/
                }
                else
                {
                    close(pipe_no[i - 1][1]);                   /* 关闭上一个管道写端*/
                    dup2(pipe_no[i - 1][0], STDIN_FILENO);      /* 重定向上一个管道为输入*/
                    
                    close(pipe_no[i][0]);                       /* 关闭读端*/
                    dup2(pipe_no[i][1], STDOUT_FILENO);         /* 标准输出重定向到写端*/
                }
            }

            int ret = execvp(parts[i][0], vector[i]);
            if(ret < 0)
            {
                printf("%s : command not found or params error\n", parts[0]);
                exit(1);
            }
        }
        else
        {
            // 父进程回收子进程完毕后, 清理子进程数量标记, 子进程重定向标记, 关闭pipe
            closePipe();
            int ret = 0;
            do
            {
                ret = wait(NULL);
            }while(ret > 0);

            child_process_cnt = 0;
            memset(input_flag, 0, sizeof(input_flag));
            memset(output_flag, 0, sizeof(output_flag));
        }
    }
    return 0;
}

##### 难点, 发现, 总结:
  • fgets读取字符串结尾会会添加\n;

        fgets 在结尾添加\n 会导致strsep 分离出来的最后一个字符串的结尾有\n, 会导致execvp 函数解析参数发生错误。

  • 终端输入会出现 ^H, ^[[D, ^[[C 的控制字符

        通过命令stty erase ^H 即可解决。 但是每次重新启动终端该问题还是存在, 因此每次启动模拟shell的程序的时候, 就执行一次这个指令即可。

  • 多次重定向:

        如下指令多次指定了输入输出重定向, 则其只会对最后一次那个输入输出重定向有效。

cat < a.c < b.c 
ps > a.dat > b.dat
  • strsep的坑

        经过测试 strsep 在开始和结尾处遇见 分隔符的时候会额外产生空串, 在对管道进行分割时, 会产生 &nbsp指令指令 >&nbsp; 的情况, 因此在对使用 &nbsp 作为分割符的时候, 需要跳过空串的情况, 以及重定向的文件名不能是空串的判断。

  • 如何管理管道的文件描述符

        经过分析, 只有在子进程数量 > 1的情况下才要创建管道, 以及对管道描述符进行管理, 那么我们子进程分为三种情况, 第一个子进程, 中间的子进程, 和最后一个子进程。

        第一个子进程关闭输出端, 并将其标准输出重定向到输入端。

        中间子进程关闭与上一个子进程之间的管道的输入端, 并将标准输入重定向管道的输出端。 关闭与下一个子进程之间管道输出端, 并将其标准输出重定向到管道的输入端。

        对于最后一个子进程关闭输入端, 并将其标准输入重定向到管道的输出端。

        难点问题: 确保每个管道我只i要保留一个输入和输出端, 因为像 cat grep 等指令是会循环阻塞等待输入的, 如果其对应管道有多个写端, 则上一个指令结束 此指令也不会结束的。

  • 管道输出重定向和文件输出重定向冲突

        比如指令:

ps -aux > 1.dat | cat

        ps的标准输出是重定向到文件也可以重定向到管道的写端, 重定向到管道会覆盖到文件。但是对于上方指令, 我们不能创建出来一个文件, 需要注意。

  • 管道输入重定向和文件输入重定向的冲突

        比如指令:

cat | cat < 1.dat

        分析系统shell的表现个人,猜测文件输入会覆盖管道输入, 造成输出结果为第二个指令的进程从1.dat当中读入数据输出后结束, 第一个子进程会等待从标准输入读入, 输出到管道写端, 但是此管道没有读端了, 因此内核向此进程发送一个SIGPIPE信号, 导致此进程结束, 指令解析完毕。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章