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);
}
}

最新文章

  1. ASP.NET MVC5+EF6+EasyUI 后台管理系统(2)-easyui构建前端页面框架[附源码]
  2. Cookie 用法 小记
  3. C++的一些小的知识点
  4. [转]理解RESTful架构
  5. 基于wke封装的duilib的webkit浏览器控件,可以c++与js互交,源码及demo下载地址
  6. C#语法中一个问号(?)和两个问号(??)的运算符是什么意思?
  7. 【转】html input radio取得被选中项的value
  8. C#一些小技巧(二)
  9. win8/win10/win2012r2 存储池 冗余分析
  10. Django: 之用户注册、缓存和静态网页
  11. ural1987 Nested Segments
  12. 【也许CTO并不是终点开篇】CTO也只不过是CTO罢了
  13. 三 Struts2 添加返回数据
  14. Docker 简单部署 ElasticSearch
  15. Spark流处理调优步骤
  16. redis的入门篇---五种数据类型及基本操作
  17. Python3基础 response.read 输出网页的源代码
  18. 《深入应用C++11:代码优化与工程级应用》开始发售
  19. Netty权威指南之AIO编程
  20. async与await

热门文章

  1. MySQL 数据库SQL语句——高阶版本2
  2. insert/delete/select/update 以及一些在select中常用的函数之类的
  3. Ubuntu18修改系统时间
  4. 虫师Selenium2+Python_3、Python基础
  5. Solution -「LOJ #150」挑战多项式 ||「模板」多项式全家桶
  6. SpringBoot是如何做到自动装配的
  7. 都 2022 了,还不抓紧学 typeScript ?
  8. 痞子衡嵌入式:揭秘i.MXRT1170上串行NOR Flash双程序可交替启动设计
  9. 软件性能测试分析与调优实践之路-Java应用程序的性能分析与调优-手稿节选
  10. Tensorflow 2.x入门教程