CSAPP shell Lab 详细解答
Shell Lab的任务为实现一个带有作业控制的简单Shell,需要对异常控制流特别是信号有比较好的理解才能完成。需要详细阅读CS:APP第八章异常控制流并理解所有例程。
Slides下载:https://www.cs.cmu.edu/afs/cs/academic/class/15213-f21/www/schedule.html
Lab主页:http://csapp.cs.cmu.edu/3e/labs.html
完整源码:https://github.com/zhangyi1357/CSAPP-Labs/blob/main/shlab-handout/tsh.c
示例程序分析
首先可以参考课本上给出的不带作业控制的Shell的代码。
/* $begin shellmain */
#include "csapp.h"
#define MAXARGS   128
/* Function prototypes */
void eval(char* cmdline);
int parseline(char* buf, char** argv); // implementation omitted
int builtin_command(char** argv);
int main()
{
    char cmdline[MAXLINE]; /* Command line */
    while (1) {
        /* Read */
        printf("> ");
        Fgets(cmdline, MAXLINE, stdin);
        if (feof(stdin))
            exit(0);
        /* Evaluate */
        eval(cmdline);
    }
}
/* $end shellmain */
/* $begin eval */
/* eval - Evaluate a command line */
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 */
    strcpy(buf, cmdline);
    bg = parseline(buf, argv);
    if (argv[0] == NULL)
        return;   /* Ignore empty lines */
    if (!builtin_command(argv)) {
        if ((pid = Fork()) == 0) {   /* Child runs user job */
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }
        /* Parent waits for foreground job to terminate */
        if (!bg) {
            int status;
            if (waitpid(pid, &status, 0) < 0)
                unix_error("waitfg: waitpid error");
        }
        else
            printf("%d %s", pid, cmdline);
    }
    return;
}
/* If first arg is a builtin command, run it and return true */
int builtin_command(char** argv)
{
    if (!strcmp(argv[0], "quit")) /* quit command */
        exit(0);
    if (!strcmp(argv[0], "&"))    /* Ignore singleton & */
        return 1;
    return 0;                     /* Not a builtin command */
}
/* $end eval */
main函数中负责读入cmdline发送给eval函数进行处理,如果发现读入EOF则退出程序。
eval函数的主要流程为使用parseline函数将cmdline解析为argv数组,然后发送到builtin_command函数进行处理,如果内置命令则在此函数内直接处理并返回1,反之则不处理返回0交还控制权到eval函数。
接下来eval函数运用fork-execve惯用法执行cmdline,父进程根据cmdline为前台或后台程序做不同处理,前台程序则等待其子进程执行完毕,后台程序则直接输出子进程PID和命令,而后返回控制权给main函数继续读入新的cmdline。

Shell示例程序流程简化图解
作业控制实现思路
作业控制实际上就是维护一个jobs数组,新建一个任务时将其加入到数组之中,任务执行完毕由父进程的中断处理程序将该任务删除。另外还需要在适当的时候将任务的状态进行调整,中断处理程序。
具体到本Lab,需要做的就是在eval函数中添加任务,然后在sigchld_handler处理程序中回收子进程并删除相应任务,还有sigint_handler和sigstop_handler中改变任务的状态。
值得注意的是,为了避免race,需要在fork之前阻塞SIGCHLD信号,然后完成fork,在父进程中添加该任务之后再解除SIGCHLD信号的阻塞,以免发生删除任务发生在添加任务之前的情况。另外,由于子进程会继承父进程的阻塞,所以在execve之前需要取消对SIGCHLD信号的阻塞。
本Lab对于jobs数组的各种操作的实现都已经提供,只需要调用相应api即可,无需自己实现。
Lab 实现
本Lab建议以trace[n].txt文件为指导,逐步实现其功能。
trace01 EOF
trace01要求在读取EOF信号时退出Shell,在初始代码中该功能已经实现。
        if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
            app_error("fgets error");
        if (feof(stdin)) { /* End of file (ctrl-d) */
            fflush(stdout);
            exit(0);
        }
trace02 quit
trace02则测试内置的quit命令,课本示例中也已经进行实现。
    // quit command
    if (!strcmp(argv[0], "quit"))
        exit(0);
trace03~04 前后台程序+作业控制
trace03为测试前台运行quit,trace04为测试后台运行myspin程序。
主要需要解析命令行末尾的&,并针对前后台运行进行不同的处理。其中parseline函数已经帮助解析了命令行末尾&,所以只需要对前后台程序进行不同处理即可。
如前所述,前台则需等待执行完毕,后台则只需要将其添加到jobs即可。
首先在eval函数中实现添加作业的代码以及前后台程序处理。特别注意这里对SIGCHLD信号在适当的地方进行了阻塞和解除阻塞。另外进行阻塞所使用的函数是包裹了错误处理的系统调用。具体实现参考源代码。
    Sigemptyset(&mask);
    Sigaddset(&mask, SIGCHLD);
    if (!builtin_cmd(argv)) {
        Sigprocmask(SIG_BLOCK, &mask, &prev);  // block SIGCHLD
        if ((pid = fork()) == 0) {   /* Child runs user job */
            Sigprocmask(SIG_UNBLOCK, &prev, NULL);  // unblock SIGCHLD
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }
        addjob(jobs, pid, bg ? BG : FG, cmdline);
        Sigprocmask(SIG_SETMASK, &prev, NULL);  // unblock SIGCHLD
对于后台程序按照给出的对照程序(tshref)输出其相应的任务号,PID以及命令行。
对于前台程序处理则依赖于sigchld_handler信号处理程序,接收到其终止信号时将其移出jobs数组。于是可以通过判断fgpid函数返回当前前台程序PID是否等于子进程的PID来判断是否运行完毕。
// code in evalvoid sigchld_handler(int sig)
{
    int old_errno = errno;
    pid_t pid;
    int status;
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        if (WIFEXITED(status)) {
            deletejob(jobs, pid);
        }
    }
    if (errno != ECHILD)
        unix_error("waitpid_error");
    errno = old_errno;
    return;
}
				/* Parent waits for foreground job to terminate */
        if (!bg)  // foreground
            waitfg(pid);
        else      // background
            printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
// waitfg function
void waitfg(pid_t pid)
{
    while (pid == fgpid(jobs))
        sleep(0);
    return;
}
具体到SIGCHLD的处理,需要在其中使用waitpid回收所有的终止的子进程。其中WNOHANG | WUNTRACED代表立即返回,如果有子进程停止或终止则返回其PID,用while循环包起来确保一次尽可能将所有已经终止或停止的子进程回收。
void sigchld_handler(int sig)
{
    int old_errno = errno;
    pid_t pid;
    int status;
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        if (WIFEXITED(status)) {
            deletejob(jobs, pid);
        }
    }
    errno = old_errno;
    return;
}
trace05 jobs
trace05为实现jobs功能,在完成了前面的基本的作业控制后非常简单,只需要在builtin_cmd中调用起始代码已经提供了的listjobs函数即可
    // jobs command
    if (!strcmp(argv[0], "jobs")) {
        listjobs(jobs);
        return 1;
    }
trace06~08 SIGINT和SIGSTOP
这三个trace是测试SIGINT和SIGSTOP能否被正确处理,值得注意的是,前台程序收到这两个信号都应该将其发送给其所在组的所有程序,而不是本身。
具体发送于是sigint和sigstop的任务非常简单,即收到信号后转手给所在的整个组发一下信号,给整个组发信号只需要给kill的pid为负数即可。
void sigint_handler(int sig)
{
    int olderrno = errno;
    // get the foreground job pid
    pid_t fg_pid;
    fg_pid = fgpid(jobs);
    // send the signal to the group in the foreground
    kill(-fg_pid, sig);
    errno = olderrno;
    return;
}
void sigtstp_handler(int sig)
{
    int olderrno = errno;
    // get the foreground job pid
    pid_t fg_pid;
    fg_pid = fgpid(jobs);
    // send the signal to the group in the foreground
    kill(-fg_pid, sig);
    errno = olderrno;
    return;
}
具体处理这两个的信号在sigchld_hanlder里,sigchld_handler里收到子进程终止或停止的消息后给出对应的输出然后改变其状态,对于终止的进程就在jobs里将其删除,对于停止的进程则设置其state为ST。值得注意的是在信号处理程序里不可以使用异步信号不安全的printf,我这里使用的是csapp.h里给出的Sio包。
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        if (WIFEXITED(status)) {
            deletejob(jobs, pid);
        }
        if (WIFSIGNALED(status)) { // terminated by ctrl-c
            Sio_puts("Job [");
            Sio_putl(pid2jid(pid));
            Sio_puts("] (");
            Sio_putl(pid);
            Sio_puts(") terminated by signal ");
            Sio_putl(WTERMSIG(status));
            Sio_puts("\n");
            deletejob(jobs, pid);
        }
        if (WIFSTOPPED(status)) { // stopped by ctrl-z
            Sio_puts("Job [");
            Sio_putl(pid2jid(pid));
            Sio_puts("] (");
            Sio_putl(pid);
            Sio_puts(") stopped by signal ");
            Sio_putl(WSTOPSIG(status));
            Sio_puts("\n");
            getjobpid(jobs, pid)->state = ST;
        }
    }
此外还有非常重要的一点就是,我们的shell程序本身是所有子进程的父进程,那么就会分配在同一个组里,终止子进程所在组会导致shell程序本身也被终止,这里的解决办法是给子进程设置一个单独的组,只需要添加在fork和exec之间。
        if ((pid = fork()) == 0) {   /* Child runs user job */
            setpgid(0, 0);
            Sigprocmask(SIG_UNBLOCK, &prev, NULL);  // unblock SIGCHLD
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }
trace09~10 bg 和 fg
trace09是关于内置命令bg和fg的,其使用方法为
$ fg/bg <job>
其中为响应任务的PID或JID,如果为JID则需%作为前缀。fg和bg都是发送SIGCONT信号来将相应任务重启。
首先在builtin_cmd函数中判断是否为bg或fg,如果是则执行相应的操作。
    // bg or fg command
    if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
        do_bgfg(argv);
        return 1;
    }
具体的do_bgfg函数首先根据有无%判断是PID还是JID,然后取得该job指针,然后给其所在进程组发送SIGCONT,最后根据其是fg还是bg来做出与eval中类似的行为。
void do_bgfg(char** argv)
{
    struct job_t* job;
    char* id = argv[1];
    if (id[0] == '%') { // jid
        job = getjobjid(jobs, atoi(id + 1));
    }
    else {              // pid
        job = getjobpid(jobs, atoi(id));
    }
    kill(-(job->pid), SIGCONT);
    if (!strcmp(argv[0], "fg")) {  // fg command
        job->state = FG;
        // wait for the job to terminate
        waitfg(job->pid);
    }
    else {                         // bg command
        job->state = BG;
        printf("[%d] (%d) %s", pid2jid(job->pid), job->pid, job->cmdline);
    }
    return;
}
trace11~13 Tests for SIGSTOP & SIGINT & fg/bg
trace11.txt - Forward SIGINT to every process in foreground process group
trace12.txt - Forward SIGTSTP to every process in foreground process group
trace13.txt - Restart every stopped process in process group
这三个traces主要测试前面是否正确实现了SIGSTOP和SIGINT的处理程序,以及fg/bg的实现,如果没有将进程组中的所有程序一并处理这里可能会出现错误,前面的实现中已经处理了这些情况,这里不再赘述。
trace14 Error handling
这个测试需要对fg和bg的输入参数进行一些错误处理,例如没有参数或参数非数值或所选任务或进程不存在等。在do_bgfg函数中进行相应处理即可。
void do_bgfg(char** argv)
{
    struct job_t* job;
    char* id = argv[1];
    // no argument for bg/fg
    if (id == NULL)
    {
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }
    if (id[0] == '%') { // jid
        if (!checkNum(id + 1)) {
            printf("%s: argument must be a PID or %%jobid\n", argv[0]);
            return;
        }
        int jid = atoi(id + 1);
        job = getjobjid(jobs, jid);
        if (job == NULL) {
            printf("%%%d: No such job\n", jid);
            return;
        }
    }
    else {              // pid
        if (!checkNum(id)) {
            printf("%s: argument must be a PID or %%jobid\n", argv[0]);
            return;
        }
        int pid = atoi(id);
        job = getjobpid(jobs, pid);
        if (job == NULL) {
            printf("(%d): No such process\n", pid);
            return;
        }
    }
    kill(-(job->pid), SIGCONT);
    if (!strcmp(argv[0], "fg")) {  // fg command
        job->state = FG;
        // wait for the job to terminate
        waitfg(job->pid);
    }
    else {                         // bg command
        job->state = BG;
        printf("[%d] (%d) %s", pid2jid(job->pid), job->pid, job->cmdline);
    }
    return;
}
trace15~16
trace15.txt - Putting it all together
trace16.txt - Tests whether the shell can handle SIGTSTP and SIGINT signals that come from other processes instead of the terminal.
对前面的程序进行的一些综合性测试,已经通过。
exit fix
参考exit与_exit的区别,可以知道在fork出的child中要用_exit来退出,否则exit会调用用atexit注册的函数并刷新父进程的缓冲区。一般来说在一个main函数中只调用一次exit或return。
        if ((pid = fork()) == 0) {   /* Child runs user job */
            setpgid(0, 0);
            Sigprocmask(SIG_UNBLOCK, &prev, NULL);  // unblock SIGCHLD
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                _exit(1);
            }
        }
												
											CSAPP shell Lab 详细解答的更多相关文章
- CSAPP2e:Shell lab 解答
		
期中之后的第一个lab 就是实现一个简单的Shell 程序,程序的大部分已经写好,只需要实现 eval 函数和处理信号的sigchld_handle, sigint_handle, sigtstp_h ...
 - 深入理解计算机系统项目之 Shell Lab
		
博客中的文章均为meelo原创,请务必以链接形式注明本文地址 Shell Lab是CMU计算机系统入门课程的一个实验.在这个实验里你需要实现一个shell,shell是用户与计算机的交互界面.普通意义 ...
 - CSAPP buffer lab记录——IA32版本
		
CSAPP buffer lab为深入理解计算机系统(原书第二版)的配套的缓冲区溢出实验,该实验要求利用缓冲区溢出的原理解决5个难度递增的问题,分别为smoke(level 0).fizz(level ...
 - office web apps安装部署,配置https,负载均衡(七)配置过程中遇到的问题详细解答
		
该篇文章,是这个系列文章的最后一篇文章,该篇文章将详细解答owa在安装过程中常见的问题. 如果您没有搭建好office web apps,您可以查看前面的一系列文章,查看具体步骤: office we ...
 - python编写shell脚本详细讲解
		
python编写shell脚本详细讲解 那,python可以做shell脚本吗? 首先介绍一个函数: os.system(command) 这个函数可以调用shell运行命令行command并且返回它 ...
 - CF468C Hack it! 超详细解答
		
CF468C Hack it! 超详细解答 构造+数学推导 原文极简体验 CF468C Hack it! 题目简化: 令\(f(x)\)表示\(x\)在十进制下各位数字之和 给定一整数\(a\)构造\ ...
 - 【CSAPP】Shell Lab 实验笔记
		
shlab这节是要求写个支持任务(job)功能的简易shell,主要考察了linux信号机制的相关内容.难度上如果熟读了<CSAPP>的"异常控制流"一章,应该是可以不 ...
 - CSAPP Bomb Lab记录
		
记录关于CSAPP 二进制炸弹实验过程 (CSAPP配套教学网站Bomb Lab自学版本,实验地址:http://csapp.cs.cmu.edu/2e/labs.html) (个人体验:对x86汇编 ...
 - Wscript.Shell 对象详细介绍
		
详细 WshShell 对象ProgID Wscript.Shell 文件名 WSHom.Ocx CLSID F935DC22-1CF0-11d0-ADB9-00C04FD58A0B IID F935 ...
 
随机推荐
- 【HTML】table表格拆分合并(colspan、rowspan)
			
代码演示 横向合并: <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http:// ...
 - linux 设置connect 超时
			
转载请注明来源:https://www.cnblogs.com/hookjc/ 将一个socket 设置成阻塞模式和非阻塞模式,使用fcntl方法,即: 设置成非阻塞模式: 先用fcntl的F_GET ...
 - 如何按规定的格式向mysql中导入数据
			
1.首先我们拿到数据,数据必须按照一定的格式书写的.如用|区分字段,换行区分row 12107 | 心情1 | 今天的心情很不好啊. 12108 | 天气 | 今天天气还行. 12109 | 臭美 | ...
 - 浅谈php web安全
			
首先,笔记不是web安全的专家,所以这不是web安全方面专家级文章,而是学习笔记.细心总结文章,里面有些是我们phper不易发现或者说不重视的东西.所以笔者写下来方便以后查阅.在大公司肯定有专门的we ...
 - 关于一些基础的dp——硬币的那些事(dp的基本引入)
			
1.最少硬币问题大体题意: 有n种硬币,面值分别是v1,v2......vn,数量无限,输入一个非负整数s,选用硬币使其和为s,要求输出最少的硬币组合. 我们可以这样分析: 定义一个名为Min[s]的 ...
 - Springboot原理
			
1. SpringBoot特点 一个starter导入所有 依赖管理 父项目做依赖管理:声明了所需依赖的版本号 依赖管理 <parent> <groupId>org.sprin ...
 - Solution -「JSOI2008」「洛谷 P4208」最小生成树计数
			
\(\mathcal{Description}\) link. 给定带权简单无向图,求其最小生成树个数. 顶点数 \(n\le10^2\),边数 \(m\le10^3\),相同边权的边数不 ...
 - python 2048游戏控制器
			
2048游戏控制器 1 evaluate 要用程序来处理就得对现实的问题进行量化,用数字来表示.在2048游戏中,我们的输入是一个棋局,让我们输出一个移动方向,这样我们需要对棋局进行量化,即我们要评估 ...
 - 私有化轻量级持续集成部署方案--05-持续部署服务-Drone(上)
			
提示:本系列笔记全部存在于 Github, 可以直接在 Github 查看全部笔记 持续部署概述 持续部署是能以自动化方式,频繁而且持续性的,将软件部署到生产环境.使软件产品能够快速迭代. 在之前部署 ...
 - Linux之history使用技巧
			
背景: 正常情况下,Linux系统中输入 history 只显示序号和历史命令如下图,但是当我们想要根据历史命令来排查一些故障问题时,无法精确获取该命令执行的详细信息,包括执行时间.执行的用户.是哪 ...