shell lab 实现详解

来源:互联网 发布:洛天依软件如何解压 编辑:程序博客网 时间:2024/05/22 17:02

这次的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 test01make 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

原创粉丝点击