美文网首页
CSAPP : Shell Lab

CSAPP : Shell Lab

作者: leon4ever | 来源:发表于2017-12-26 16:34 被阅读1248次

    实验介绍

    完成一个简单的shell程序,总体的框架和辅助代码都已经提供好了,我们需要完成的函数主要以下几个:

    • eval: 主要功能是解析cmdline,并且运行. [70 lines]
    • builtin cmd: 辨识和解析出bulidin命令: quit, fg, bg, and jobs. [25lines]
    • do bgfg: 实现bg和fg命令. [50 lines]
    • waitfg: 实现等待前台程序运行结束. [20 lines]
    • sigchld handler: 响应SIGCHLD. 80 lines]
    • sigint handler: 响应 SIGINT (ctrl-c) 信号. [15 lines]
    • sigtstp handler: 响应 SIGTSTP (ctrl-z) 信号. [15 lines]

    难点主要在于对信号的处理,需要我们捕获信号,改变其对应的处理方式。
    其他需要注意的地方:

    1. 系统函数的返回值检查,一定要多注意有可能出错的地方;
    2. 竞争条件,fork子进程之后,如果子进程很快就结束了,而此时主进程还没addjob就会有问题,总之就是不能假设进程之间以安全的顺序执行,这里利用互斥量的思路,主进程会阻塞子进程的信号,直到addjob之后;
    3. SIGCHLD信号处理函数,考虑多个子进程结束,以及非正常结束时waitpid的返回值,后面结合课本里详细说。

    有关Shell

    我们要实现的shell有两种执行模式

    1. 如果用户输入的命令是内置命令,那么 shell 会直接在当前进程执行(例如 jobs)
    2. 如果用户输入的是一个可执行程序的路径,那么 shell 会 fork 出一个新进程,并且在这个子进程中执行该程序(例如 /bin/ls -l -d)

    第二种情况中,如果命令以& 结束,那么这个job在后台执行
    需要支持的功能:

    • job control:允许用户更改进程的前台/后台状态以及京城的状态(running, stopped, or terminated)
      • ctrl-c 会触发 SIGINT 信号并发送给每个前台进程,默认的动作是终止该进程
      • ctrl-z 会触发 SIGTSTP 信号并发送给每个前台进程,默认的动作是挂起该进程,直到再收到 SIGCONT 信号才继续
      • jobs 命令会列出正在执行和被挂起的后台任务
      • bg job 命令可以让一个被挂起的后台任务继续执行 ,fg job 命令同理

    参考课本代码

    先来看看课本上我们可以参考的代码有哪些
    P525 eval()函数原型

    void eval(char *cmdline) 
    {
        char *argv[MAXARGS]; /* Argument list execve() */
        char buf[MAXLINE];   /* Holds modified command line */
        int bg;              /* Should the job run in bg or fg? */
        pid_t pid;           /* Process id */
        
        strcpy(buf, cmdline);
        bg = parseline(buf, argv);       //解析命令行函数都提供好了
        if (argv[0] == NULL)  
        return;   /* Ignore empty lines */
    
        if (!builtin_command(argv)) { 
            if ((pid = Fork()) == 0) {   /* 子进程来执行job */
                if (execve(argv[0], argv, environ) < 0) {
                    printf("%s: Command not found.\n", argv[0]);
                    exit(0);
                }
            }
        /* 如果是前台作业,主进程需要等待子进程运行完毕 */
        if (!bg) {
            int status;
            if (waitpid(pid, &status, 0) < 0)
            unix_error("waitfg: waitpid error");
        }
        else
            printf("%d %s", pid, cmdline);
        }
        return;
    }
    

    基本功能都完成了,唯一的不足就是由于joblist的存在,需要考虑竞争条件,也就是主进程一定要先addjob,然后才能deletejob
    然后看一下信号处理函数,实验代码里已经要求了,需要对以下三个信号进行处理

    Signal(SIGINT, sigint_handler); /* ctrl-c /
    Signal(SIGTSTP, sigtstp_handler); /
    ctrl-z /
    Signal(SIGCHLD, sigchld_handler); /
    Terminated or stopped child */

    那么就来看看书上P532的示例:

    #include "csapp.h"
    
    void sigint_handler(int sig) /* SIGINT handler */   //line:ecf:sigint:beginhandler
    {
        printf("Caught SIGINT!\n");    //line:ecf:sigint:printhandler
        exit(0);                      //line:ecf:sigint:exithandler
    }                                              //line:ecf:sigint:endhandler
    
    int main() 
    {
        /* Install the SIGINT handler */         
        if (signal(SIGINT, sigint_handler) == SIG_ERR)  //line:ecf:sigint:begininstall
        unix_error("signal error");                 //line:ecf:sigint:endinstall
        
        pause(); /* Wait for the receipt of a signal */  //line:ecf:sigint:pause
        
        return 0;
    }
    

    嗯,SIGINT就是我们想自己处理的信号,然后通过sigint_handler来进行自定义的处理。(当然这示例也忒简单了)

    还有一个重要的信号不排队问题,涉及到父进程回收子进程:

    • 子进程结束的时候向父进程发生信号,但是内核的规矩是这样的:
      在任何时刻,一种类型至多只会有一个待处理信号(内核中负责维护待处理信号的pending位向量对应的特定信号类型只有一位),也就是说信号是不会排队的,如果处理信号A的过程中又来了信号B,信号B是会被阻塞的,此时又来信号C,那么信号C就被丢弃了,处理办法也很简单,每次处理信号的时候,用while循环尽可能多接收几个信号。

    书上的对应代码P539

    void handler2(int sig) 
    {
        int olderrno = errno;
        while (waitpid(-1, NULL, 0) > 0) {
            Sio_puts("Handler reaped child\n");
        }
        if (errno != ECHILD)
            Sio_error("waitpid error");
        Sleep(1);
        errno = olderrno;
    }
    

    接下来再看信号阻塞,也就是要避免父进程和子进程多job列表操作的竞争
    linux提供阻塞信号的隐式机制和显式机制

    • 隐式:默认阻塞当前处理程序正在处理的信号类型
    • 显式:使用sigprocmask函数

    具体怎么使用可以参考课本P543的promask2.c

    #include "csapp.h"
    
    void handler(int sig)
    {
        int olderrno = errno;
        sigset_t mask_all, prev_all;
        pid_t pid;
    
        Sigfillset(&mask_all);
        while ((pid = waitpid(-1, NULL, 0)) > 0) { /* Reap a zombie child */
            Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);    //阻塞所有信号
            deletejob(pid); /* 对joblist安全删除 */
            Sigprocmask(SIG_SETMASK, &prev_all, NULL);      //恢复所有信号
        }
        if (errno != ECHILD)
            Sio_error("waitpid error");
        errno = olderrno;
    }
        
    int main(int argc, char **argv)
    {
        int pid;
        sigset_t mask_all, mask_one, prev_one;
    
        Sigfillset(&mask_all);
        Sigemptyset(&mask_one);
        Sigaddset(&mask_one, SIGCHLD);
        Signal(SIGCHLD, handler);
        initjobs(); /* Initialize the job list */
    
        while (1) {
            Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); /* 阻塞 SIGCHLD */
            if ((pid = Fork()) == 0) { /* Child process */
                Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* 子进程解除阻塞 SIGCHLD */
                Execve("/bin/date", argv, NULL);
            }
            Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* 阻塞所有信号*/  
            addjob(pid);  /* 对job列表的操作安全了 */
            Sigprocmask(SIG_SETMASK, &prev_one, NULL);  /* 父进程解除阻塞 SIGCHLD */
        }
        exit(0);
    }
    

    看起来挺麻烦,实际我们可以简化的,实际写的时候只阻塞SIGCHLD即可。
    课本里面还提到一点,主程序显式等待某个信号处理程序进行,尤其是shell创建前台作业的时候,基本思路是用循环,当接收到信号时,在信号处理程序中把while条件更改来跳出循环,这样做存在资源浪费以及无法精准唤醒的问题,比较好的解决方法是sigsuspend,相当于一个原子操作,可以暂时取消阻塞SIGCHLD,然后pause接收信号,紧接着恢复阻塞。
    否则的话,本来的pause()是挂起进程直到收到信号,那么下面的代码有可能永远休眠

    while(!pid)
      pause();  //如果在while判断之后和pause之前收到信号,那就错过啦,所以需要原子操作
    

    实验代码

    经过上面对课本代码的复习,实际上好多功能都已经给出了实现方法,实验代码自然也就不难给出啦。

    1. eval()

    主要功能是对用户输入的参数进行解析并运行计算。如果用户输入内建的命令行(quit,bg,fg,jobs)那么立即执行。 否则,fork一个新的子进程并且将该任务在子进程的上下文中运行。如果该任务是前台任务那么需要等到它运行结束才返回。

    1. 注意每个子进程必须用户自己独一无二的进程组id,要不然就没有前台后台区分啦
    2. 在fork()新进程前后要阻塞SIGCHLD信号,防止出现竞争(race)这种经典的同步错误
    void eval(char *cmdline) 
    {
        char* argv[MAXARGS];
        pid_t pid;
        sigset_t mask;
        int fgorbg = parseline(cmdline,argv);
        if(argv[0] == NULL)
            return;
        sigemptyset(&mask);
        sigaddset(&mask,SIGCHLD);
        sigprocmask(SIG_BLOCK,&mask,NULL);
        if(!builtin_cmd(argv)){
            if((pid = fork()) == 0){
                sigprocmask(SIG_UNBLOCK,&mask,NULL);        //子进程也是要解除阻塞的
                if(setpgid(0,0)<0)
                     unix_error("eval: setgpid failed.\n");  
                if(execve(argv[0],argv,environ)<0){
                    printf("%s: Command not found.\n",argv[0]);
                    exit(0);
                }
            }
            if(!fgorbg)
                addjob(jobs,pid,FG,cmdline);
            else
                addjob(jobs,pid,BG,cmdline);
            sigprocmask(SIG_UNBLOCK,&mask,NULL);        //这里一定要addjob之后再解除阻塞
            if(!fgorbg) // FG job
                waitfg(pid);
            else
                printf("[%d] (%d) %s\n",pid2jid(pid),pid,cmdline);
        }
    
        return;
    }
    

    2. builtin_cmd()

    判断命令是否是内置指令,是的话立即执行,不是则返回,对单独的‘&’无视

    int builtin_cmd(char **argv) 
    {
        if(strcmp(argv[0],"quit")==0)
            {printf("exit\n");exit(0);}
        if(strcmp(argv[0],"jobs")==0)
            {
                listjobs(jobs);
                return 1;
            }
        if(strcmp(argv[0],"bg")==0 || strcmp(argv[0],"fg")==0)
            {
                do_bgfg(argv);
                return 1;
            }
        return 0;     /* not a builtin command */
    }
    

    3. do_bgfg()

    执行bg和fg指令功能

    void do_bgfg(char **argv) 
    {
        char *id = argv[1];
        struct job_t *job;
        int jobid;
        if(id == NULL){
            printf("%s command requires PID of jobid argument.\n",argv[0]);
            return;
        }
        if(id[0] == '%')
            jobid = atoi(id+1);
        if((job = getjobjid(jobs,jobid))==NULL){
            printf("Job does not exist.\n");
            return;
        }
        if(strcmp(argv[0],"bg")==0){
            job->state = BG;
            kill(-(job->pid),SIGCONT);
        }
        if(strcmp(argv[0],"fg")==0){
            job->state = FG;
            kill(-(job->pid),SIGCONT);
            waitfg(job->pid);
        }
        return;
    }
    

    kill函数的用法,向任何进程组或进程发送信号
    int kill(pid_t pid, int sig);
    参数pid的可能选择:

    1. pid大于零时,pid是信号欲送往的进程的标识。
    2. pid等于零时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
    3. pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
    4. pid小于-1时,信号将送往以-pid为组标识的进程。

    4. waitfg()

    等待前台进程完成,这里偷懒了,没用sigsuspend,还是用了比较消耗资源的方法哈哈哈。。。

    void waitfg(pid_t pid)
    {
        while(pid == fgpid(jobs));
        return;
    }
    

    5. 几个信号处理函数

    SIGINT处理比较简单,就是截获CTRL+C然后发给前台程序嘛

    void sigint_handler(int sig) 
    {
        pid_t pid = fgpid(jobs);
        int jid = pid2jid(pid);
        if(pid!=0){
            printf("Job [%d] terminated by SIGINT.\n",jid);
            deletejob(jobs,pid);
            kill(-pid,sig);
        }
        return;
    }
    

    SIGTSTP就是把CTRL+Z发给前台

    void sigtstp_handler(int sig) 
    {
        pid_t pid = fgpid(jobs);
        int jid = pid2jid(pid);
        if(pid!=0){
            printf("Job [%d] stopped by SIGINT.\n",jid);
            (*getjobpid(jobs,pid)).state = ST;;
            kill(-pid,sig);
        }
        return;
    }
    

    SIGCHLD是最麻烦的了,参考网上大牛的方法,需要考虑子进程返回的原因
    运用waitpid()函数并且用WNOHANG|WUNTRACED参数,该参数的作用是判断当前进程中是否存在已经停止或者终止的进程,如果存在则返回pid,不存在则立即返回
    通过另外一个&status参数,我们可以判断返回的进程是由于什么原因停止或暂停的。

    • WIFEXITED(status):
      如果进程是正常返回即为true,什么是正常返回呢?就是通过调用exit()或者return返回的
    • WIFSIGNALED(status):
      如果进程因为捕获一个信号而终止的,则返回true
    • WTERMSIG(status):
      当WIFSIGNALED(status)为真时,设置该值,返回导致当前状态的信号编号
    • WIFSTOPPED(status):
      如果返回的进程当前是被停止,则为true
    • WSTOPSIG(status):
      返回引起进程停止的信号
    void sigchld_handler(int sig) 
    {
        pid_t pid;
        int status,child_sig;
        while((pid = waitpid(-1, &status, WUNTRACED | WNOHANG)) > 0 ){  
            printf("Handling chlid proess %d\n", (int)pid);  
            /*handle SIGTSTP*/  
            if( WIFSTOPPED(status) )  
                sigtstp_handler( WSTOPSIG(status) );  
            /*handle child process interrupt by uncatched signal*/  
            else if( WIFSIGNALED(status) ) {  
                child_sig = WTERMSIG(status);  
                if(child_sig == SIGINT)  
                    sigint_handler(child_sig);  
            }  
            else      
                deletejob(jobs, pid);  
        }  
        return; 
    }
    

    总结

    本次Shell Lab的收获有以下几点

    1. 对Shell有了更加深刻的理解,借助实验代码实现了不带重定向的简单shell
    2. 掌握信号的正确接收处理,阻塞和解除阻塞机制,写出避免竞争的代码
    3. 父进程和子进程的fork,回收,信号传递等
    4. linux下编程规范,以及进程相关函数的使用,学到了学到了

    相关文章

      网友评论

          本文标题:CSAPP : Shell Lab

          本文链接:https://www.haomeiwen.com/subject/fmsdgxtx.html