csapp shlab实验有哪些心得分享?

2026-05-17 03:211阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计2569个文字,预计阅读时间需要11分钟。

csapp shlab实验有哪些心得分享?

《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执行

csapp shlab实验有哪些心得分享?

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)

本文共计2569个文字,预计阅读时间需要11分钟。

csapp shlab实验有哪些心得分享?

《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执行

csapp shlab实验有哪些心得分享?

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)