csappshlab实验分享

Posted 黑岩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了csappshlab实验分享相关的知识,希望对你有一定的参考价值。

csapp shell lab实验记录,主要涉及框架代码简述、难点分析、实现简述等内容

shlab

本次实验主要是运用课本第八章讲授的job control在框架代码的基础上实现一个简单的shell。正好最近上的OS课也讲了shell和job control,就简单地练练手。

1. 框架代码分析

本次实验的框架代码大多已经给出,要填空的部分为:

  • eval:解析执行命令行
  • builtin_cmd:识别并解释执行内部命令,如:quit,fg,bg,jobs
  • do_bgfg:在上述函数识别的基础上执行fg,bg
  • sigchld_handler:捕获处理SIGCHILD信号
  • sigint_handler:捕获处理SIGINT信号
  • sigstp_handler:捕获处理SIGSTP信号

整体来看,一次shell处理的控制流如下:

main函数不断获取输入,获取后传给eval执行

eval调用parse_line解析命令行,并根据命令类型调用相应函数处理。

由于这次lab不要求实现重定向、管道等复合命令,所以命令行不用解析为树型结构。(复合命令shell的一个简单实现可以参考xv6)

框架代码使用一个全局的job数组维护信息,并通过信号机制实际控制各job的状态

2. 实验难点

这次实验的难点主要在于信号处理,具体地有:

  • 竞争问题

    • 如果信号处理程序访问全局数据,那么需要避免信号处理程序和主程序之间、信号处理程序和信号处理程序之间发生数据竞争。
    • 具体地,在每一次访问全局数据时显示的利用sigprocmask阻塞可能发生数据竞争的信号处理
    sigprocmask(SIG_BLOCK, &mask_all, NULL); //暂时阻塞全部信号
    addjob(jobs, pid, FG, buf); //全局数据
    sigprocmask(SIG_SETMASK, &mask_all, NULL); //恢复
    
  • 同步问题

    • 创建子进程的正常步骤是

      fork()

      • 父进程更新job
      • 子进程execve

      这个流程能工作的一个前提是:父进程先更新job,子进程再execve。因为只有这样,子进程exit后父进程的信号处理程序才能在已经更新的job上删除这个进程信息。

      但如果子进程先execve到exit,此时job中还没有子进程的信息,信号处理程序不做任何事就返回,之后切换到父进程,它更新了job...再也不可能被删除

    • 为了解决这个同步问题,依然可以通过阻塞信号处理,我们在fork前显式阻塞child信号,父进程更新job后再解除即可同步

  • 如何实现进程的前后台执行?

    • 前台进程需要在shell中调用waitpid显式等待,后台程序则在创建后不等待,按原执行流进行
    • 所有子进程结束后都在信号处理程序中回收

    这其实也是官方手册的参考实现————将回收僵尸进程的工作集中到信号处理程序中进行

  • 信号转发

在实现Ctrl+CCtrl+Z时,我们需要了解终端和进程组的概念:

  • 简单来讲,终端是一类特殊的虚拟设备。我们对着黑框输入实际上是将数据传送给了终端,应用程序(如shell)能通过stdin从终端读取输入的数据(默认)。
  • 如上图所示,终端控制着一个session,当我们在终端按下ctrl+c时终端就会给session的前台进程发送SIGINT信号。在这个实验中,我们的shell运行在bash中作为bash的前台进程,所以所有的SIGINT都会发送给shell,所以为了在我们的shell运行其它进程时通过ctrl+c只终止该进程,我们首先需要将shell fork()出的进程放在另外的进程组里,然后再每次把SIGINT等信号转发给shell管理的前台进程组(框架代码里的job提供了这样的机制)

3. 实现综述

  • eval

    • 调用parse_line解析命令行,之后调用builtin_cmd尝试解析内部命令,成功则返回等待下一次输入;失败后将命令行作为其它进程用execve执行
    • 如果是前台执行,则在fork+正确更新jobs后显式调用waitfg()等待进程结束;如果是后台执行则在fork+更新后等待输入
void eval(char *cmdline) 

    int olderrno = errno; //save errno
    char *argv[MAXARGS] = NULL;
    char buf[MAXLINE];
    pid_t pid = 0;
    strcpy(buf, cmdline);
    int bg = parseline(buf, argv); 

    if (argv[0] == NULL) 
        return; //ignore empty line.
    

    sigset_t mask, prev, mask_all;
    sigfillset(&mask_all);
    sigemptyset(&mask);
    sigaddset(&mask, SIGCHLD);

    if (builtin_cmd(argv) == 0)
    
        sigprocmask(SIG_BLOCK, &mask, &prev);
        if ((pid = fork()) == 0) 
            sigprocmask(SIG_SETMASK, &prev, NULL);
            setpgid(0, 0); //create a new process group.
            if (execve(argv[0], argv, environ) < 0)
            
                //failed to execute, should exit the child process.
                printf("command not found.\\n");
                exit(0);
            
        
        if (!bg) 
            //front process
            sigprocmask(SIG_BLOCK, &mask_all, NULL);
            addjob(jobs, pid, FG, buf);
            sigprocmask(SIG_SETMASK, &prev, NULL);
            waitfg(pid);
         else 
            //back process
            sigprocmask(SIG_BLOCK, &mask_all, NULL);
            addjob(jobs, pid, BG, buf);
            sigprocmask(SIG_SETMASK, &prev, NULL);
            printf("[%d] (%d) %s", pid2jid(pid), pid, buf);
        
    
    errno = olderrno;
    return;

  • builtin_cmd/do_bgfg

    • 内部命令主要借助jobs提供的接口实现,都比较简单
    • 但需要注意鲁棒性,包括命令不合语法、jid/pid不存在等
int builtin_cmd(char **argv) 

    if (strcmp(argv[0], "quit") == 0) 
        exit(0);
     else if (strcmp(argv[0], "jobs") == 0) 
        listjobs(jobs);
        return 1;
     else if (strcmp(argv[0], "bg") == 0 || strcmp(argv[0], "fg") == 0) 
        do_bgfg(argv);
        return 1;
    
    return 0; /* not a builtin command */

void do_bgfg(char **argv) 

    char *argstr = argv[1];
    int is_jid = 0;
    struct job_t *job = NULL;
    pid_t pid;
    int jid;
    char *cmdline = NULL;
    sigset_t mask_all, prev;
    sigfillset(&mask_all);
    if (argstr == NULL)
    
        printf("%s command requires PID or %%jobid argument\\n", argv[0]);
        return;
    
    if (argv[1][0] == \'%\')
    
        argstr = argv[1] + 1;
        is_jid = 1;
    
    else
    
        argstr = argv[1];
    
    for (char *itr = argstr; *itr; itr++)
    
        if (!isdigit(*itr)) 
            printf("%s: argument must be a PID or %%jobid\\n", argv[0]);
            return;
        
    
    if (is_jid) 
        jid = atoi(argstr);
        sigprocmask(SIG_BLOCK, &mask_all, &prev);
        job = getjobjid(jobs, jid);
        if (job == NULL)
        
            printf("%%%d: No Such job\\n", jid);
            sigprocmask(SIG_SETMASK, &prev, NULL);
            return;
        
        pid = job->pid;
        cmdline = job->cmdline;
        sigprocmask(SIG_SETMASK, &prev, NULL);
    
    else
    
        pid = atoi(argstr);
        sigprocmask(SIG_BLOCK, &mask_all, &prev);
        job = getjobpid(jobs, pid);
        sigprocmask(SIG_SETMASK, &prev, NULL);
        if (job == NULL) 
            printf("(%d): No Such process\\n", pid);
            sigprocmask(SIG_SETMASK, &prev, NULL);
            return;
        
        jid = job->jid;
        cmdline = job->cmdline;
        sigprocmask(SIG_SETMASK, &prev, NULL);
    
    if (strcmp(argv[0], "bg") == 0)
    
        printf("[%d] (%d) %s", jid, pid, cmdline);
        sigprocmask(SIG_BLOCK, &mask_all, &prev);
        job->state = BG;
        sigprocmask(SIG_SETMASK, &prev, NULL);
        kill(-(pid), SIGCONT);
    
    else if (strcmp(argv[0], "fg") == 0)
    
        printf("%s", cmdline);
        sigprocmask(SIG_BLOCK, &mask_all, &prev);
        job->state = FG;
        sigprocmask(SIG_SETMASK, &prev, NULL);
        kill(-pid, SIGCONT);
        waitfg(pid);
    
    return;

  • waitfg
void waitfg(pid_t pid)

    sigset_t mask, prev;
    sigfillset(&mask);
    sigprocmask(SIG_BLOCK, &mask, &prev);
    struct job_t *job = getjobpid(jobs, pid);
    sigprocmask(SIG_SETMASK, &prev, NULL);
    while (job != NULL && job->state == FG)
    
        sigfillset(&mask);
        sigprocmask(SIG_BLOCK, &mask, &prev);
        job = getjobpid(jobs, pid);
        sigprocmask(SIG_SETMASK, &prev, NULL);
    
    return;

  • sigchld_handler

    • 接收到SIGCHILD信号后说明一定至少有一个子进程终止或暂停,可以通过waitpid(-1, &status, WNOHANG|WUNTRACED)获取这些进程号,然后正确更新jobs(i.e.回收僵尸进程)

    • 值得注意的是,这里的信号处理可能被其它信号处理程序中断,因此只能使用信号安全函数, 此外因为要访问全局数据jobs,需要在访问时阻塞同样访问该数据的其它信号(相当于上锁)

    • 同时,为了避免连续发生多个SIGINT信号时,由于阻塞的表现形式导致丢失(阻塞用一个二进制位实现,因此同一个信号连续超过2个就会被丢弃),最好如下:

      while (waitpid(-1, &status, WNOHANG|WUNTRACED) > 0) 
      	....
      
      
      • 但实验讲义中说只需调用一次waitpid... 我暂时还没想通只调用一次如何避免上述问题。由于测试用例比较弱,两种写法没出现问题
void sigchld_handler(int sig) 

    pid_t pid;
    int status;
    sigset_t mask_all, prev;
    sigfillset(&mask_all);
    sigprocmask(SIG_BLOCK, &mask_all, &prev);
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
    
        if (WIFSTOPPED(status)) 
            getjobpid(jobs, pid)->state = ST;
         else 
            deletejob(jobs, pid);
        
    
    sigprocmask(SIG_SETMASK, &prev, NULL);

  • sigint_handler|sigstp_handler

    • 主要就是完成第2节中所说的信号转发和全局数据更新.注意这里如果要向控制台打印信息,不能使用printf。所以我的实现相当冗长...
void sigint_handler(int sig) 

    pid_t fpid = fgpid(jobs);
    if (fpid == 0) 
        return;
    
    char buf[MAXLINE] = \'\\0\';
    strcpy(buf, "Job [");
    strcatNum(buf, pid2jid(fpid));
    strcat(buf, "] (");
    strcatNum(buf, fpid);
    strcat(buf, ") terminated by signal ");
    strcatNum(buf, sig);
    strcat(buf, "\\n");
    if (write(STDOUT_FILENO, buf, strlen(buf)) < 0) 
        exit(0);
    
    kill(-fpid, sig);
    return;

void sigtstp_handler(int sig) 

    pid_t fpid = fgpid(jobs);
    if (fpid == 0) 
        return;
    
    char buf[MAXLINE] = \'\\0\';
    strcpy(buf, "Job [");
    strcatNum(buf, pid2jid(fpid));
    strcat(buf, "] (");
    strcatNum(buf, fpid);
    strcat(buf, ") stopped by signal ");
    strcatNum(buf, sig);
    strcat(buf, "\\n");
    if (write(STDOUT_FILENO, buf, strlen(buf)) < 0) 
        exit(0);
    
    kill(-fpid, sig);
    return;

4. 总结

  • 本次实验的绝大部分内容都在课本和实验讲义中有所涉及,因此实验过程中多次有重温教材的感觉。这是一个比较合适的难度梯度!

  • 虽然在大一寒假的时候读过csapp,但由于基础不牢当时没能坚持下来,效果也不太理想。现在有了计算机系统、操作系统方面的基础,或许csapp已经不再是一本很的书,但它仍然是一本值得反复阅读的参考书。最近我也会结合操作系统课、网络课回味一遍这本神书...(if time)

以上是关于csappshlab实验分享的主要内容,如果未能解决你的问题,请参考以下文章

Cisco实验分享

关于互联网数据挖掘实验室技术分享的通知

新盟教育内部课程分享-浮动路由实验

Harbor 学习分享系列4 - Harbor常用功能实验.md

LUA分享——一起做实验!探索紫甘蓝的秘密

技术分享 | ROS小实验——“圆龟”