CSAPP-ShellLab

Lab6 Shlab

Shell Lab要求我们实现一个简单的Shell程序,该程序支持任务控制(job control)。此实验旨在让我们熟悉进程控制信号处理

参考资料

  • 实验文档(细看):http://csapp.cs.cmu.edu/3e/malloclab.pdf
  • 实验代码:http://csapp.cs.cmu.edu/3e/shlab-handout.tar
  • CSAPP第八章:异常控制流。建议写代码前看一遍。

tsh.c已经搭好了骨架,需要实现下列函数。方括号内是参考解答的代码行数。

  • 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]
  • do_bgfg: 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]

Shell程序主要逻辑:在注册信号处理程序和初始化任务列表后,持续接收用户输入的命令cmdline,并通过eval(cmdline)执行命令。

建议边测试边实现,从trace1.txt到trace16.txt一步步推进代码。

1 实现

代码地址:https://github.com/Lyb-code/CSAPP-Labs/blob/master/ShellLab/shlab-handout/tsh.c

增加全局变量fg_completed,在wait_fg()和sigchld_handler()中使用。

volatile sig_atomic_t fg_completed = 0;//前台任务停止或执行结束时,置为1

eval

执行用户输入的命令。如果是内置命令(quit, jobs, bg or fg),则直接执行。否则fork出一个子进程,运行对应的可执行文件(即任务)。如果是前台任务,则需要等待其结束。代码如下。

注意

  • 在fork()创建子进程之前,需要阻塞SIGCHLD,原因:fork()创建子进程后,父进程会调用addjob,将子进程添加到任务列表。子进程执行完毕,会向父进程发送SIGCHILD,父进程接收该信号后运行sigchld_handler,该函数调用deletejob删除对应的任务。

    存在这样一种可能,子进程结束的太快,在父进程addjob前就触发了deletejob,导致该任务还没加入jobs就被删除了,这显然不对。虽然无法控制子父进程的执行顺序,但是我们可以控制sigchld_handler的运行时机,在父进程addjob执行结束前,可以阻塞SIGCHLD,保证addjob一定在deletejob前执行。

    fork()之后,execve 之前,子进程解除对SIGCHLD的阻塞。子进程默认继承父进程的block位,因为子进程也可能创建子进程,需要处理SIGCHILD,故恢复阻塞位,解除对SIGCHILD的阻塞。

  • 在 fork ()之后,execve 之前,子进程调用 setpgid(0, 0),将其进程组ID设置为其PID。原因:默认情况下,fork()出的子进程和父进程./tsh同属一个进程组。当键入 ctrl-c 时, 会向./tsh父进程所在的进程组发送SIGINT,这样每个子进程也能收到SIGINT,这样不对。需要保证子进程和父进程不属于一个进程组,当./tsh接收SIGINT后再将其转发到对应的前台作业(的进程组)。

  • 在访问全局变量(如jobs)时,需要保证访问操作不被中断,避免出现并发安全问题,故阻塞所有信号。

  • addjob后继续阻塞SIGCHLD,保证waitfg(会置fg_completed为0,并循环等待至fg_completed变为1)在sigchld_handler(会置fg_completed为1)之前执行。

  • 其他可参考注释

void eval(char *cmdline) 
{
    char* argv[MAXARGS];
    int bg;
    pid_t pid;
    sigset_t mask_all, mask_chld, mask_prev;
    
    sigfillset(&mask_all);
    sigemptyset(&mask_chld);
    sigaddset(&mask_chld, SIGCHLD);
    //解析命令,将参数存入argv
    bg = parseline(cmdline, argv);

    if (argv[0] == NULL) return;/*ignore empty lines*/

    if (!builtin_cmd(argv)) {//如果是内置命令(quit, jobs, bg or fg),builtin_cmd直接执行。否则执行下述代码
        //阻塞SIGCHLD:对于指定任务,保证addjob在deletejob之前执行
        sigprocmask(SIG_BLOCK, &mask_chld, &mask_prev);/*Block SIGCHLD*/
        if ((pid = fork()) == 0) { /* child process */
            setpgid(0, 0);//将子进程的进程组ID设置为子进程的pid
            //在子进程中解除对SIGCHLD的阻塞
            sigprocmask(SIG_SETMASK, &mask_prev, NULL);/*Unblock SIGCHLD*/
            if (execve(argv[0], argv, environ) < 0) {//执行任务
                printf("%s: command not found.\n", argv[0]);
                exit(1);
            }
            //记得退出,否则任务执行失败时,会继续执行父进程执行的代码,可能带来未知错误。
            exit(0);
        }
        /* parent process */
        //在访问全局变量时,不能被信号处理程序中断,故阻塞所有信号。
        sigprocmask(SIG_BLOCK, &mask_all, NULL);/*Block all signals*/
        addjob(jobs, pid, bg ? BG : FG, cmdline);
        //继续阻塞SIGCHLD:对于前台任务,保证waitfg(pid)在sigchld_handler之前执行
        sigprocmask(SIG_SETMASK, &mask_chld, NULL);/*still block SIGCHLD*/
        if (!bg) {
            //等待前台任务结束
            waitfg(pid);
        } else {
            sigprocmask(SIG_BLOCK, &mask_all, NULL);
            //后台任务,打印信息即可
            printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
            fflush(stdout);
        }
        //恢复进程的Block位,解除对SIGCHLD的阻塞
        sigprocmask(SIG_SETMASK, &mask_prev, NULL);/*Unblock SIGCHLD*/
    }
    
    return;
}

builtin_cmd

如果argv是内置命令,则直接执行并返回1。否则返回0,指明不是内置命令。

int builtin_cmd(char **argv) 
{
    sigset_t mask_all, mask_prev;
    sigfillset(&mask_all);

    if (!strcmp(argv[0], "quit"))
        exit(1);
    else if (!strcmp(argv[0], "jobs")) {
        //在访问全局变量时,不能被信号处理程序中断,故阻塞所有信号。
        sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
        listjobs(jobs);
        sigprocmask(SIG_SETMASK, &mask_prev, NULL);
        return 1;
    } else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
        do_bgfg(argv);
        return 1;
    }
    return 0;     /* not a builtin command */
}

do_bgfg

执行bg和fg命令。bg命令和fg命令的语义见注释。注意:

  • 如果在访问全局变量前阻塞了信号,那么在return前必须恢复block位、解除对信号的阻塞。不影响之后的命令执行。
  • 重启一个进程时,使用kill函数向子进程所在的进程组发SIGCONT,所以是**-pid**(子进程的进程组ID与PID相同),trace13.txt测试了这一点。
void do_bgfg(char **argv) 
{
    int jid;
    pid_t pid = 0;
    struct job_t *job;
    char *command = argv[0];
    char *param = argv[1];
    sigset_t mask_all, mask_prev;
    sigfillset(&mask_all);

    if (param != NULL) {
        if (*param == '%') {
            jid = atoi(param+1);
            if (jid == 0) {
                printf("%s: argument must be a PID or %%jobid\n", command);
                return;
            }
        } else {
            pid = atoi(argv[1]);
            if (pid == 0) {
                printf("%s: argument must be a PID or %%jobid\n", command);
                return;
            }
        }
    } else {
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }
    //在访问全局变量时,不能被信号处理程序中断,故阻塞所有信号。
    sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
    if (*param == '%') {
        job = getjobjid(jobs, jid);
        if (job == NULL) {
            printf("%s: No such job\n", param);
            //一定要记得解除信号阻塞后再return,不影响之后的命令执行
            sigprocmask(SIG_SETMASK, &mask_prev, NULL);
            return;
        }
        pid = job->pid;
    } else {
        job = getjobpid(jobs, pid);
        if (job == NULL) {
            printf("(%s): No such process\n", param);
            //一定要记得解除信号阻塞后再return,不影响之后的命令执行
            sigprocmask(SIG_SETMASK, &mask_prev, NULL);
            return;
        }
    }
    if (!strcmp(argv[0], "bg")) {// bg command
        // change a stopped background job to a running background job
        // 这里可以再全面一些,对job的当前状态(FG ST BG UNDEF)做分类处理
        printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
        job->state = BG;//ST -> BG
    } else {// fg command
        // change a stopped or running background job to a running background job
        // 这里可以再全面一些,对job的当前状态(FG ST BG UNDEF)做分类处理
        job->state = FG;//ST or BG -> FG
    }
    // send a SIGCONT signal
    //向子进程所在的进程组发SIGCONT,所以是-pid(子进程的进程组ID与PID相同)
    kill(-pid, SIGCONT);
    // waiting for foreground job
    if (!strcmp(argv[0], "fg")) {
        //等待前台任务结束
        waitfg(pid);
    }
    sigprocmask(SIG_SETMASK, &mask_prev, NULL);
    return;
}

wait_fg

wait_fg等待前台任务结束。下面代码是我能想到的最优雅的。

void waitfg(pid_t pid)//正确且优雅
{
    fg_completed = 0;
    sigset_t mask_empty;
    sigemptyset(&mask_empty);

    while (!fg_completed) {
        //1.将block位设置为mask_empty(使当前进程可以接收任何信号,如SIGCHLD)
        //2.pause()暂停当前进程,直至收到信号
        //3.恢复block位为第1步之前的数据
        sigsuspend(&mask_empty);
    }
    return;
}

等待逻辑:waitfg将全局变量fg_completed置为0,并循环等待至该值变为1;前台任务停止或执行结束时,会向父进程发送SIGCHILD,触发sigchld_handler将fg_completed置为1,waitfg即可跳出循环。

sigsuspend(const sigset_t *mask)等价于如下三行代码的原子版本,在waitfg中的作用见上面的注释。

sigprocmask(SIG_SETMASK, &mask, &prev);//将block位设置为参数mask
pause();//阻塞直至接收到信号
sigprocmask(SIG_SETMASK, &prev, NULL);//恢复block位

如果在循环中使用sleep()而不是sigsuspend(),就会导致每次循环都必须等待特定秒数,前台任务结束时不能立即响应,不够优雅。

sigchld_handler

SIGCHLD信号处理程序。当子任务执行结束(成为僵尸进程)、因接收到SIGSTOP或SIGTSTP信号而停止时,Kernel会向它的父进程(即我们的shell程序)发送SIGCHLD。sigchld_handler需要分情况处理,回收所有僵尸进程。

注意:

  • 进程不会使用队列记录信号,只会用pending中的1个二进制位记录SIGCHLD,无法得知有几个SIGCHLD到达,故用while而不是if

  • waitpid函数可参考:https://blog.csdn.net/yiyi__baby/article/details/45539993

    WNOHANG的作用:如果当前所有子进程都在运行状态,没有停止或结束,则waitpid立即返回0。

    WUNTRACED的作用:使waitpid能在子进程处于停止(stopped)状态时返回该子进程的pid。

void sigchld_handler(int sig) 
{
    int olderrno = errno;
    pid_t pid;
    struct job_t *cur_job;
    int status;
    sigset_t mask_all, mask_prev;
    
    sigfillset(&mask_all);
    //pending signals are not queued!用while而不是if
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        //在访问全局变量时,不能被信号处理程序中断,故阻塞所有信号。
        sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
        cur_job = getjobpid(jobs, pid);
        //如果是前台任务,修改fg_completed,让waitfg跳出循环
        if (cur_job->state == FG) fg_completed = 1;
        if (WIFEXITED(status)) {
            //子进程正常结束,删除对应任务
            deletejob(jobs, pid);
        } else if (WIFSIGNALED(status)) {
            //子进程因信号而结束,打印相关信息并删除对应任务
            printf("Job [%d] (%d) terminated by signal %d\n", cur_job->jid, cur_job->pid, WTERMSIG(status));
            deletejob(jobs, pid);
        } else if (WIFSTOPPED(status)) {
            //子进程处于暂停状态,打印相关信息并修改state
            printf("Job [%d] (%d) stopped by signal %d\n", cur_job->jid, cur_job->pid, WSTOPSIG(status));
            cur_job->state = ST;
        }
        sigprocmask(SIG_SETMASK, &mask_prev, NULL);
    }
    errno = olderrno;
    return;
}

sigint_handler

SIGINT信号处理程序。当用户键入ctrl-c时,Kernel会向标准Unix Shell的前台进程组发送SIGINT。我们的Shell程序是作为标准Unix Shell的子进程运行的,属于该前台进程组,应接收SIGINT并向自己的前台进程组发送SIGINT。

void sigint_handler(int sig) 
{
    int olderrno = errno;
    pid_t fgj_pid;
    sigset_t mask_all, mask_prev;

    sigfillset(&mask_all);
    //在访问全局变量时,不能被信号处理程序中断,故阻塞所有信号。
    sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
    fgj_pid = fgpid(jobs);//foreground job pid
    sigprocmask(SIG_SETMASK, &mask_prev, NULL);
    if (fgj_pid != 0) {
        //向前台进程组发送SIGINT,故是-fgj_pid(子进程的进程组ID与PID相同)
        kill(-fgj_pid, SIGINT);
    }
    errno = olderrno;
    return;
}

sigtstp_handler

SIGTSTP信号处理程序。当用户键入ctrl-z时,Kernel会向标准Unix Shell的前台进程组发送SIGTSTP。我们的Shell程序是作为标准Unix Shell的子进程运行的,属于该前台进程组,应接收SIGTSTP并向自己的前台进程组发送SIGTSTP。

void sigtstp_handler(int sig) 
{
    int olderrno = errno;
    pid_t fgj_pid;
    sigset_t mask_all, mask_prev;

    sigfillset(&mask_all);
    //在访问全局变量时,不能被信号处理程序中断,故阻塞所有信号。
    sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
    fgj_pid = fgpid(jobs);//foreground job pid
    sigprocmask(SIG_SETMASK, &mask_prev, NULL);
    if (fgj_pid != 0) {
        //向前台进程组发送SIGTSTP,故是-fgj_pid(子进程的进程组ID与PID相同)
        kill(-fgj_pid, SIGTSTP);
    }
    errno = olderrno;
    return;
}

测试

要评估我们Shell程序的正确性,就是要保证对于任何一个trace文件,它的表现是和参考程序tshref一致的。即make testxx和 make rtestxx的输出要相同(进程号不必相同)。以trace15.txt为例:

make text15的输出

在这里插入图片描述

make rtest15的输出

在这里插入图片描述

其余trace文件的测试同理。

2 总结

ShellLab让我了解Shell的任务控制,学习进程控制(创建新进程,回收僵尸进程)和信号处理(阻塞信号,信号处理程序)。

总体不难,但有些bug还是很难调,尤其是因为信号阻塞不当出现的问题,要仔细分析出函数的前后关系,才能正确阻塞。边测试边添加代码是一个好的习惯,一步步保证鲁棒性。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值