CSAPP Lab5 解题分析

Author Avatar
Xin Qiu Jul 12, 2016
  • Read this article on other device

这个实验是要实现一个简易的shell程序,主要考虑的是异常和信号的处理。

前期工作

解压缩shlab-handout.tar,执行make clean | make,就可以得到编译好的文件。

文件夹中的tshref是正确的结果,可以用来验证。与其用

./sdriver.pl -t trace01.txt -s ./tsh -a "-p"

来测试,我觉得实验手册里提到的另一种方法更好用,

make test01

这是对自己写的tsh进行测试,要想对tshref进行测试,只需要在后面的test前面添加一个r即可,也就是make rtest01

需要实现的函数

  • void eval(char *cmdline) 执行用户输入的命令
  • int builtin_cmd(char **argv) 执行内部命令
  • void do_bgfg(char **argv) 执行bg和fg命令
  • void waitfg(pid_t pid) block进程直到它不再是前台进程
  • void sigchld_handler(int sig) SIGCHLD信号处理
  • void sigint_handler(int sig) SIGINT信号处理
  • void sigtstp_handler(int sig) SIGTSTP信号处理

builtin_cmd

这是比较好写的一个函数,我选择第一个把他完成。按照实验文档说的,当输入quit时,退出tsh。输入jobs就列出存在的job。输入fg/bg是前台后台执行命令。

/* 
 * 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")) // quit
        exit(0);
    if (!strcmp(argv[0], "jobs")) // jobs
    {
        listjobs(jobs);
        return 1;
    }

    if (!strcmp(argv[0], "echo")) // echo
    {
        printf("%s\n", argv[1]);
        return 1;
    }

    if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) // bg or fg
    {  
        do_bgfg(argv);
        return 1;
    }
    return 0;     /* not a builtin command */
}

大概闲着无聊,添加了个最简易最简易的echo233.

eval

执行函数算是基础,参考书中图8-23中的eval,可以大概知道eval函数应该是什么样子的。但那只是简易并有缺陷的shell,它并不能回收后台子进程,所以之后就要用到信号的相关知识。关于阻塞信号,可以用到以下三个函数:

  • int sigemptyset(sigset_t *set);
  • int sigaddset(sigset_t *set, int signum);
  • int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

这类似线程锁,SIG_BLOCK和SIG_UNBLOCK用来加锁和解除锁。是用sigprocmask来同步进程,可以保证父进程在相应的deletejob之前执行了addjob。

另外eval还要处理job的添加,这分前台执行和后台执行。

/* 
 * 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 bg;
    pid_t pid;
    sigset_t mask;

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

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


        // Child process ,set new process group, run command 
        if((pid = fork()) == 0) {
            if(setpgid(0, 0) < 0) {
                unix_error("setpgid error");
            }

            if (execve(argv[0], argv, environ) < 0)
            {
                printf("%s: Command not found\n", argv[0]);
                exit(0);
            }
        } 
        else {
            if(!bg){
                addjob(jobs, pid, FG, cmdline); // add job to job list as fg
            }
            else {
                addjob(jobs, pid, BG, cmdline); // add job to job list as bg
            }
            sigprocmask(SIG_UNBLOCK, &mask, NULL); // Parent unblocks SIGCHLD


            if (!bg)
                waitfg(pid);
            else 
                printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
        }
    }
}

waitfg

waitfg就是要等待前台进程结束,所以只需要用循环即可

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

do_bgfg

这是前台和后台的切换,一些条件细节就不多说,其中主要是JID和PID的判断。bg job 给后台 job 发送 SIGCONT 信号来继续执行该任务,fg job 给前台 job 发送 SIGCONT 信号来继续执行该任务。首先要判断进程是否继续执行。关于前后台的执行,主要做的就是修改job的state状态。

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

    // If id does not exist
    if(id == NULL) {
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }

    // For a JID
    if(id[0] == '%') {  
        jid= atoi(&id[1]);  
        if(!(job= getjobjid(jobs, jid))) {  
            printf("%s: No such job\n", id);  
            return;  
        }  
    } 
    // For a PID
    else if(isdigit(id[0])) { 
        pid_t pid= atoi(id);  

        if(!(job= 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(-(job->pid), SIGCONT) < 0) {
        if(errno != ESRCH){
            unix_error("kill error");
        }
    }

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

接下来就是比较麻烦的三个信号handler的处理。

sigint_handler

sigint_handler是主要的处理,也是比较简单的。它用来捕获键盘的Ctrl-C。当捕获到终止信号,它会查询当前前台执行的job的pid,使用kill来终止程序。

/* 
 * 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) 
    {
        if (kill(-pid, SIGKILL) < 0) // kill pid
        {
            unix_error("error when kill in sigint_handler");
            return;
        }
        printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, sig);
        deletejob(jobs, pid);
    }
    return;
}

sigtstp_handler

同样,sigtstp_handler是捕获Ctrl-Z的挂起信号,通过获取当前前台job的pid,修改其state为ST,接着使用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) 
{
    int pid = fgpid(jobs);  
    int jid = pid2jid(pid); 

    if (pid != 0) { 

        printf("Job [%d] (%d) Stopped by signal %d\n", jid, pid, sig);   
        getjobpid(jobs, pid)->state = ST;  
        kill(-pid, SIGTSTP);  
    } 
    return;
}

sigchld_handler

sigchld_handler是用来捕获 SIGCHLD 信号的,比起其他两个信号处理函数,这个是用来回收进程的,可能会稍微有点麻烦。参考书中8.4.3相关内容,用到了WIFSTOPPED, WIFSIGNALED,WIFEXITED,分别代表判断进程被停止, 被未捕获的信号停止,exit的停止。其实就是回收过程中的僵尸进程的回收,Ctrl-Z、Ctrl-C信号下进程的回收。

/* 
 * 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(fgpid(jobs), &status, WNOHANG|WUNTRACED)) > 0) {  
        if (WIFSTOPPED(status)){  
            getjobpid(jobs, pid)->state = ST;
            printf("[%d] Stopped %s\n", pid2jid(pid), jobs->cmdline);
        }  
        else if (WIFSIGNALED(status)){  
            printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status)); 
            deletejob(jobs,pid);
        }  
        else if (WIFEXITED(status)){  
            deletejob(jobs, pid);  
        }  
    }  

    if (errno != ECHILD) {  
        unix_error("waitpid error");   
    }  

    return; 
}

其他

虽然job事务处理没有让我们写,但实验给出的源代码写的很好,很是值得读一读。