shell lab 实现详解

这次的CSAPP的实验是要自己实现一个shell(外壳),即自己实现一个命令行,要自己实现一个简单的我们在linux上常用的shell,想想就让人兴奋呢。
这次的实验环境,已经给我们搭好了程序的基本框架,只需要完成shell里面的几个关键函数就可以,相关的单元测试试验都已经给出,该试验涉及的内容包括:解析命令行参数,理解并使用(fork,execve,waitpid)常见的多进程函数,了解linux进程组,以及前台进程和后台进程的相关概念,理解linux的信号机制(包括发送信号,接受信号,阻塞信号等)。
先上相关的实验指导书的链接书地址
http://csapp.cs.cmu.edu/public/labs.html
本次shell实验要求我们主要填充的函数如下所示:
eval: Main routine that parses and interprets the command line. [70 lines]

builtin cmd: Recognizes and interprets the built-in commands: quit, fg, bg, and jobs. [25 lines]

dobgfg: Implements the bg and fg built-in commands. [50 lines]

waitfg: Waits for a foreground job to complete. [20 lines]

sigchld handler: Catches SIGCHILD signals. [80 lines]

sigint handler: Catches SIGINT (ctrl-c) signals. [15 lines]

sigtstp handler: Catches SIGTSTP (ctrl-z) signals. [15 lines]

同时试验给了15个测试文件和标准输出,需要我们编写的程序在测试要和标准输出基本相同。
测试的命令为:

make test01
make test02
...
make test16

标准输出文件为:tshref.out
实验中需要注意的几个关键点:
1. 读懂并理解CSAPP第8章异常控制流的每一个词。(这一点很关键,课本上给出了例如fork函数,execval函数,signal信号机制的讲解,并且给出了很多函数的初版函数,可以在此的基础上进行修改和理解。)
2. 利用你的测试文件来跟踪你的进展,你成功测试到那个测试文件说明的程序进展如下所示。
3. waitpid(回收僵尸进程,使用在sigchld_handler中) ,kill(发送信号给指定进程,函数原型为kill(pid,信号类型)如果pid为负的说明给该进程组的所有进程发送信息,该函数使用在sigint_handler和sigtstp_handler中),execve(让进程执行指定的程序,这个函数结合fork使用,让新fork出来的进程执行指定的外部命令),setpgid(设置进程进新的进程组,这个函数在后面会进行说明),sigprocmask将信号添加进阻塞进程中进行处理,阻塞之后要记得解阻塞哦。
4. 在kill函数中发送SIGINT和SIGTSTP信号的时候,记得使用-pid,给整个进程组发送信号。
5. 下面是waitfg和sigchld_handler的推荐写法:
-waitfg:利用忙等待来调用sleep(0),当然忙等待要针对前台进程而言。
-sigchld_handler:最好只使用一次waitpid,不然容易出现竞争条件。
6. 关于在eval里面要使用sigprocmask函数在addjob之前阻塞住SIGCHLD,不然在子进程执行完的时候如果还没有执行addjob,就会给父进程shell发送SIGCHLD,这时候调用sigchld_handler在addjob之前deletejob一个根本不存在的job,容易出现竞争条件。同时,阻塞之后要及时释放锁,释放锁的地方有三个地方,
一是:在子进程执行execve,即子进程执行其他程序的时候需要把锁还原。
二是:在父进程addjob之后要及时归还锁。
三是:不能把sigprocmask阻塞进程在build_cmd判断外面,不然会出现锁住了没有归还锁的情况。
7. 程序中不要使用/bin/vi,/bin/emacs,这些会改变shell本身设置的程序,尽量使用/bin/echo , /bin/ps,这些简单的程序。
8. 当按下ctrl-c 或者ctrl-z的时候,内核会发送SIGINT或者SIGTSTP信号给所有前台进程组,因为shell是前台进程,那么从shell中fork出的子进程都属于shell这个前台进程组的,那么我们每次SIGINT或者SIGTSTP信号都会给我们的所有进程,这明显是不合理的。
所以当我们在每次fork出子进程的时候,都要利用setpgid(0,0)把新加的进程添加到新的进程组中。

  1. 过几天经过测试的时候,发现自己忘记了测试test16,现在把test16补完,test16要求把子进程自己给自己发送SIGINT信号和SIGSTSP信号,我们之前只讨论过通过ctrl-z和ctrl-c向shell前台发送现在。那么,对于子进程自己给自己发送SIGINT信号和SIGSTSP信号的唯一改变就是,shell外壳不会主动调用sigint_handler和sigstsp_handler,只会在调用发送信号函数kill()的时候,将进程终止或者停止的时候,调用sigchld_handler回收僵尸进程。那么,在sigchld_handler根据得到信号调用sigint_handler和sigstsp_handler显得很重要。但是,我们通过ctrl-z和ctrl-c而调用sigint_handler和sigstsp_handler发送kill信号之后也会调用sigchld_handler,在于sigchld_handler里面是通过ctrl-z和ctrl-c还是调用kill函数发送的判断就显的很重要。

接下来是解题攻略的说明:
以下是eval函数体内容:
以下的Fork函数(),Sigaddset等都是封装了相关异常检测机制的函数进去

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*/
    sigset_t mask;
    strcpy(buf,cmdline);   
    bg=parseline(cmdline,argv);
    //跳过空白指令
    if(argv[0]==NULL)
    {
        return;
    }

    //Prevented from the completing condition, add the thing



    if(!builtin_cmd(argv))
    {


        //Prevented from the completing condition, add the thing
        //必须放在builtin_cmd里面,因为如果放在外面,在执行内部命令的时候,就不会释放这些锁了
        Sigemptyset(&mask);
        Sigaddset(&mask,SIGCHLD);
        Sigprocmask(SIG_BLOCK,&mask,NULL);

        if((pid=Fork())==0)         //创建子进程,Running the child process.
        {
            Sigprocmask(SIG_UNBLOCK,&mask,NULL);  //UNBLOCK the order
            if(setpgid(0,0)<0)
            {
                unix_error("eval: setpgid failed.\n");
            }
            Execve(argv[0],argv,environ);    //孩子进程自己执行他的应用程序
        }


        //Parent process:add new jobs and printf message
        else
        {
            if(bg)
                addjob(jobs,pid,BG,cmdline);
            else
                addjob(jobs,pid,FG,cmdline);
            Sigprocmask(SIG_UNBLOCK,&mask,NULL);    //UNBLOCK the order

            //父进程等待前置进程结束
            if(!bg)
            {
                waitfg(pid);
            }
            else
            {
                printf("[%d] (%d) %s",pid2jid(pid),pid,cmdline);
            }
        }
    }
}

eval需要注意的点为:1.判断是否为内部函数,如果是内部函数,直接执行built_cmd函数就行。如果不是内部函数,需要Fork出一个新的进程去执行相应的函数。
2. 函数框架提供了parseline命令行解析工具,即可以通过该解析函数来得到命令行参数。如果不是内部函数,首先要先将SIGCHLD信号阻塞住,以防出现竞争条件。
3. 子进程解决信号阻塞,并执行相关函数。
4. 父进程要判断子进程是前台进程还是后台进程,如果是前台进程,则调用waitpid来等待前台进程,如果是后台进程,则打印出相关进程信息。同时,把新添加的进程利用addjob添加到工作组中。

以下是判断是否是内部命令的函数

int builtin_cmd(char **argv) 
{
    if(!strcmp(argv[0],"quit"))   //退出命令
    {
        exit(0);
    }
    else if(!strcmp(argv[0],"&"))     /* Ignore singleton & */
    {
        return 1;
    }
    else if(!strcmp(argv[0],"jobs"))    //job order
    {
        listjobs(jobs);
        return 1;
    }
    else if(!strcmp(argv[0],"bg") || !strcmp(argv[0],"fg"))
    {
        do_bgfg(argv);
        return 1;
    }
    return 0;
}

这个就不多解释了

下面是waitpid函数内容,该函数是为前台进程而准备

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

下面是较为关键的三个信号处理函数的。
首先是处理孩子进程,即僵尸进程的函数

/* 
 * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
 *     a child job terminates (becomes a zombie), or stops because it
 *     received a SIGSTOP or SIGTSTP signal. The handler reaps all
 *     available zombie children, but doesn't wait for any other
 *     currently running children to terminate.  
 */
void sigchld_handler(int sig) 
{
    int status;
    pid_t pid;
    //Waiting for/ handling all of the child processes according to their status
    while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0)   /* Reap a zombie child */
    {
        if(WIFSTOPPED(status))
        {
             sigtstp_handler(-pid);
        }
        //WIFSIGNALED表示因为未被捕获的信号而中断,适用于子进程自己给自己发送KILL而中断
        else if(WIFSIGNALED(status))
        {
            //表示进程自己给自己发信号而造成的程序中止
              sigint_handler(-pid);
        }
        else if(WIFEXITED(status))
        {
            deletejob(jobs,pid);      /* Delete the child from the job list */
        }
    }
    if(errno!=ECHILD)
      unix_error("waitpid error");
    return;
}

用while循环来避免信号阻塞的问题,为了回收所有的僵尸进程
说明一下waitpid的几个参数的含义
status表示中止进程或者停止进程的原因,WNOHANG | WUNTRACED表示立即返回,如果等待集合中没有进程被中止或停止返回0,否则返回进程的pid,
下面的几种宏的意思为:WIFSTOPPED(status):表示如果进程是因为停止的信号而停止,那么返回true. WIFSIGNALED(status):表示进程如果是因为未捕获的信号而中止,返回true。WIFEXITED(status):表示进程通过调用exit()或者return正常结束,则返回true。那么,明显的对于停止或者中止的程序,我们分别调用其的异常处理函数,即handler。传入的参数为-pid,这一点很关键,利用传入的参数为-pid可以有效的区别是因为ctrl-z或者ctrl-c或者通过kill函数发送的信号的区别。


下面是sigint_handler

/* 
 * sigint_handler - The kernel sends a SIGINT to the shell whenver the
 *    user types ctrl-c at the keyboard.  Catch it and send it along
 *    to the foreground job.  
 */
void sigint_handler(int sig) 
{
    pid_t pid=fgpid(jobs);
    int jid=pid2jid(pid);

    //只处理前台进程
    if(pid!=0)
    {
        //说明是进程通过kill函数发送的信号, 通过sigchld_handler发的信号
        if(pid==-sig)
        {
            printf("Job [%d] (%d) terminated by signal %d\n",pid2jid(-sig),-sig,2);
            deletejob(jobs,-sig);
        }
        // when sig<0, send SIGINT singal to all foreground process
        else if(sig==SIGINT)
        {
            kill(-pid,SIGINT);
            printf("Job [%d] (%d) terminated by signal %d\n",jid,pid,sig);
            deletejob(jobs,pid);
        }
        //只触发一次sigint_handler,实现对进程的发送信息,打印结果,删除任务

    }
    return;
}

收到中断信号后,先做的事是判断传入的参数
1. 如果参数为SIGINT,那么就是通过ctrl-z或者ctrl-c发送的信号,所做的操作就为如下三点:
1.1. 发送SIGINT进程中止信号到前台进程组
1.2. 打印中止信号信息
1.3. 删除任务。
2. 如果参数为负数,说明是通过sigchld_handler调用的函数,那么有两种情况
1.1. sig==-前台进程的进程号,说明并没有执行过deletejob或者改变进程状态,说明是进程自己发送的信号,这个时候只需要做两件事。
1.1.1. 打印中止信号信息。
1.1.2. 删除任务。
1.2. sig!=-前台进程的进程号,说明该进程已经不为前台进程,说明ctrl-z引起的kill函数调用sigchld,然后再跳用sigtstp_handler处理函数。此时,不需要进行任何操作。


下面是sigtstp_handler

/*
 * sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
 *     the user types ctrl-z at the keyboard. Catch it and suspend the
 *     foreground job by sending it a SIGTSTP.  
 */
void sigtstp_handler(int sig) 
{
    pid_t pid=fgpid(jobs);
    int jid=pid2jid(pid);
    //send fg job/ related process group signal
    if(pid!=0)
    {
        //通过ctrl-z发送信号
        if(sig==20)
        {
            printf("Job [%d] (%d) Stopped by signal %d\n",jid,pid,sig);
            getjobpid(jobs,pid)->state=ST;
            kill(-pid,SIGTSTP);
        }
        //通过自己发送kill函数发送信号
        else if(pid==-sig)
        {
            printf("Job [%d] (%d) Stopped by signal %d\n",jid,pid,20);
            getjobpid(jobs,pid)->state=ST;
        }
        //其他情况说明是已经停止的进程通过sigchild发送信息
    }
    return;
}

对于判断中止信号来源的判断内容与sigint_handler类似,该函数的主要内容如下所示,打印停止信息,将该进程的状态改为ST停止状态,然后发送SIGSTSP信号给前台进程。

最后是比较难的,dofgbg函数

void do_bgfg(char **argv) 
{
    struct job_t* StpJob;
    char* id=argv[1];
    int jid;
    pid_t pid;



    //如果命令不存在
    if(id==NULL)
    {
        printf("%s command requireds pid or %%jobid argument\n",argv[0]);
        return;
    }



    //如果命令表示的是job
    if(id[0]=='%')
    {
        jid=trunce(id);
        if(!(StpJob=getjobjid(jobs,jid)))
        {
            printf("%s:No such job\n",id);
            return;
        }
    }

    // For a PID
    else if(isdigit(id[0]))
    {
        pid=trunce(id);
        if(!(StpJob=getjobpid(jobs,pid)))
        {
            printf("(%d):No such process\n",pid);
            return;
        }
    }
    else
    {
        printf("%s: argument must be a PID or %%jobid\n",argv[0]);
        return;
    }

    //发送continue信息
    if(kill(-(StpJob->pid),SIGCONT)<0)
    {
        if(errno!=ESRCH)
        {
            unix_error("kill error");
        }
    }

    //FG和BG执行两种操作

    if(!strcmp(argv[0],"bg"))
    {
        StpJob->state=BG;
        printf("[%d] (%d) %s",StpJob->jid,StpJob->pid,StpJob->cmdline);
    }
    else if(!strcmp(argv[0],"fg"))
    {
        StpJob->state=FG;
        waitfg(StpJob->pid);
    }
    else
    {
        printf("bg/fg error:%s\n",argv[0]);
    }
    return;
}

这个函数先做的是命令错误判断,fg和bg后面是否有参数,参数是否符合%+数字或者数字,所表示的进程是否为正在运行的进程,做完这些判断之后,根据如果是%号,说明取的是工作组号,如果直接是数字说明取的是进程号,根据工作组号和进程号获取对应的job结构体,接下来如果是bg,说明要恢复成后台进程,即改变job的state;如果是fg,说明要恢复成前台进程,即改变job的state,然后调用waitfg;等前台进程运行结束。

下面是我自己编写的用于截取工作号和进程号的函数:

/*从字符串的右边截取n个字符*/  
char * right(char *dst,char *src, int n)  
{  
    char *p = src;  
    char *q = dst;  
    int len = strlen(src);  
    if(n>len) n = len;  
    p += (len-n);   /*从右边第n个字符开始,到0结束,很巧啊*/  
    while((*(q++) = *(p++))); 
    return dst;  
}  


//用于获取pid,jid的数字
int trunce(char *src)
{

    int len=strlen(src);
    char * dst=malloc(sizeof(char)*len*2);
    if(src[0]=='%')
        right(dst,src,len-1);
    else
        memcpy(dst,src,len);
    int result=atoi(dst);
    free(dst);
    return result;
}

完整的代码和实验指导书会在我的github上放出:
https://github.com/HBKO/CMUlab/tree/master

你可能感兴趣的:(CSAPP-深入理)