CSAPP Lab:Shell Lab——理解进程控制的秘密

本次实验目的是完成一个简单的shell程序,解析命令行参数,理解并使用(fork,execve,waitpid)常见的多进程函数,了解linux进程组,以及前台进程和后台进程的相关概念,理解linux的信号机制(包括发送信号,接受信号,阻塞信号等)。实验提示以及详情请阅读CMU的实验指导:http://csapp.cs.cmu.edu/public/labs.html 。

我们要完成的shell并不是从0开始,实验本身已经帮你完成了一部分内容,并且提供一些工具函数,我们要做的是实现一下这几个核心函数:

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]

eval

这是shell程序的核心函数,我们需要在这里完成以下几个事情:

1.调用parseline,生成argv以及判断是否是后台进程。

2.调用builtin_cmd判断是否是内建命令,如果是则已经在该方法中执行,shell直接返回,否则创建进程执行。

3.fork之前要注意屏蔽SIGHLD信号,否则有可能在addjob之前就调用deletejob造成竞争。

4.需要在fork后解锁SIGHLD的信号。

/* 
 * eval - Evaluate the command line that the user has just typed in
 * 
 * If the user has requested a built-in command (quit, jobs, bg or fg)
 * then execute it immediately. Otherwise, fork a child process and
 * run the job in the context of the child. If the job is running in
 * the foreground, wait for it to terminate and then return.  Note:
 * each child process must have a unique process group ID so that our
 * background children don't receive SIGINT (SIGTSTP) from the kernel
 * when we type ctrl-c (ctrl-z) at the keyboard.  
*/
void eval(char *cmdline) 
{
    char *argv[MAXARGS];
    char buf[MAXLINE];
    int isBg;
    pid_t pid;
    sigset_t mask;

    strcpy(buf,cmdline);
    isBg=parseline(buf,argv);
    if(argv[0]==NULL){
        return;
    }

    if(!builtin_cmd(argv)){
    //init mask
    sigemptyset(&mask);
    sigaddset(&mask,SIGCHLD);
    sigprocmask(SIG_BLOCK,&mask,NULL); //block SIGCHLD

        if((pid=fork())==0){
        sigprocmask(SIG_UNBLOCK,&mask,NULL); //unblock SIGCHLD
            if(execve(argv[0],argv,environ)<0){
                printf("%s:Command not found.\n",argv[0]);
                exit(0);
            }
        
        //set own pid as group pid
        setpgid(0,0);
        }

        if(!isBg){
            addjob(jobs,pid,FG,cmdline);
            waitfg(pid);

        }
    else{
            addjob(jobs,pid,BG,cmdline);
        printf("%d %s",pid,cmdline);
    }
        sigprocmask(SIG_UNBLOCK,&mask,NULL); //unblock SIGCHLD
    }
    

    return;
}

 

builtin_cmd

builtin_cmd做的事情比较简单,判断是否是内建命令,如果是,则直接执行并返回true,否则返回false。

/* 
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately.  
 */
int builtin_cmd(char **argv) 
{
    if(!strcmp(argv[0],"quit")||!strcmp(argv[0],"q")){
        exit(0);
    }
    if(!strcmp(argv[0],"jobs")){
        listjobs(jobs);
    return 1;
    }
    if(!strcmp(argv[0],"bg")||!strcmp(argv[0],"fg")){
    do_bgfg(argv);
    return 1;
    }
    return 0; 
}

dobgfg

这个方法还是比较复杂的,主要完成了bg和fg命令的操作,需要注意如下几点:

1.需要区分输入的是pid还是jid来调用不同函数。

2.通过发送SIGCONT来重启进程,发送对象需要为进程组。

3.不要忘记将后台进程改为前台进程后需要等待前台进程完成(调用waitfg)。

/* 
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv) 
{
    struct job_t *job;
    char *id=argv[1];
    pid_t pid;

    if(id==NULL){
        printf("%s command requireds pid or %%jobid argument\n",argv[0]);
        return;
    }

    //process by jobid
    if(id[0]=='%')
    {
        int jid = atoi(&id[1]);
        job=getjobjid(jobs,jid);
        if(job==NULL)
        {
            printf("%s:No such job\n",id);
            return;
        }
    }
    //process by pid
    else if(isdigit(id[0])){
    int pid = atoi(&id[1]);
    job = getjobpid(jobs,pid);
        if(job==NULL)
        {
            printf("%s:No such job\n",id);
            return;
        }    
    }
    else{
        printf("%s: argument must be a PID or %%jobid\n", argv[0]);
        return;
    }
    //send SIGCONT to restart
    kill(-(job->pid),SIGCONT);
    //set job status
    if(!strcmp(argv[0],"bg")){
        job->state = BG;
        printf("[%d] (%d) %s", job->jid, job->pid,job->cmdline);
    }else{
        job->state = FG;
        waitfg(job->pid);
    }


    return;
}

 

waitfg

没什么好说的,根据实验指导,这里直接使用忙等待来实现。

/* 
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
    while(pid == fgpid(jobs)){
        sleep(0);
    }
}

 

sigchld handler

回收僵尸进程的关键函数,需要注意如下几点:

1.理解waitpid的用法,这里使用WNOHANG|WUNTRACED的组合会更合适,表示立即返回,如果等待集合中没有进程被中止或停止返回0,否则返回进程的pid。

2.检验status的值来执行不同操作,status的含义有如下枚举:

  • WIFEXITED(status):
    如果进程是正常返回即为true,什么是正常返回呢?就是通过调用exit()或者return返回的
  • WIFSIGNALED(status):
    如果进程因为捕获一个信号而终止的,则返回true
  • WIFSTOPPED(status):
    如果返回的进程当前是被停止,则为true

  所以三种情况都是需要delete job的,当进程为停止状态同时需要设置job的status。

void sigchld_handler(int sig) 
{
    pid_t pid;
    int status;
    while((pid = waitpid(-1,&status,WNOHANG|WUNTRACED))>0){
        if(WIFEXITED(status)){
            deletejob(jobs,pid);
        }
        if(WIFSIGNALED(status)){
            deletejob(jobs,pid);
        }
        if(WIFSTOPPED(status)){
            struct job_t *job = getjobpid(jobs,pid);
            if(job !=NULL ){
        job->state = ST;
        }
        }
    }
    if(errno != ECHILD)
        unix_error("waitpid error");

    return;
}

 

sigint handler

ctrl-c的响应函数,直接调用kill函数给相关进程。

void sigint_handler(int sig) 
{
    pid_t pid = fgpid(jobs);
    if(pid!=0){
        kill(-pid,sig);
    }
    return;
}

 

sigtstp handler

ctrl-z的响应函数,直接调用kill函数给相关进程,需要注意kill前判断状态,不要重复发送信号。

/*
 * 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);

    if(pid!=0 ){
        struct job_t *job = getjobpid(jobs,pid);
        if(job->state == ST){
            return;
        }else{
            Kill(-pid,SIGTSTP);
        }
    }
    return;
}

 

posted @ 2018-10-07 14:48  Jarvis_Wu  阅读(3172)  评论(0编辑  收藏  举报