当前位置 : 主页 > 编程语言 > 其它开发 >

【csapp】shlab实验分享

来源:互联网 收集:自由互联 发布时间:2022-05-30
csapp shell lab实验记录,主要涉及框架代码简述、难点分析、实现简述等内容 目录 shlab 1. 框架代码分析 2. 实验难点 3. 实现综述 4. 总结 shlab 本次实验主要是运用课本第八章讲授的 job c
csapp shell lab实验记录,主要涉及框架代码简述、难点分析、实现简述等内容

目录
  • shlab
    • 1. 框架代码分析
    • 2. 实验难点
    • 3. 实现综述
    • 4. 总结

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)

上一篇:什么是数组迭代 数组迭代有哪些方法
下一篇:没有了
网友评论