TOC
Open TOC
Shell Lab
http://csapp.cs.cmu.edu/3e/shlab.pdf
Basic
- foreground and background
- job control
Specification
tsh → tiny shell
- prompt →
tsh>
- name + arguments
- built-in command → foreground
- assume that name is the path of an executable file, which it loads and runs in the context of an initial child process
- ctrl-c and ctrl-z
- Typing ctrl-c causes a SIGINT signal to be delivered to each process in the foreground job. The default action for SIGINT is to terminate the process.
- Typing ctrl-z causes a SIGTSTP signal to be delivered to each process in the foreground job. The default action for SIGTSTP is to place a process in the stopped state, where it remains until it is awakened by the receipt of a SIGCONT signal.
- end with &
- run the job in the background
- Each job can be identified by either a process ID (PID) or a job ID (JID)
- %5 denotes JID 5
- 5 denotes PID 5
- built-in commands
- quit
- 感觉在 Linux 中的等价物是 exit
- jobs
- The
bg <job>
command restarts<job>
by sending it a SIGCONT signal, and then runs it in the background. The<job>
argument can be either a PID or a JID - The
fg <job>
command restarts<job>
by sending it a SIGCONT signal, and then runs it in the foreground. The<job>
argument can be either a PID or a JID
- quit
- reap all of its zombie children
项目结构
Makefile # Compiles your shell program and runs the testsREADME # This filetsh.c # The shell program that you will write and hand intshref # The reference shell binary.
# The remaining files are used to test your shellsdriver.pl # The trace-driven shell drivertrace*.txt # The 15 trace files that control the shell drivertshref.out # Example output of the reference shell on all 15 traces
# Little C programs that are called by the trace filesmyspin.c # Takes argument <n> and spins for <n> secondsmysplit.c # Forks a child that spins for <n> secondsmystop.c # Spins for <n> seconds and sends SIGTSTP to itselfmyint.c # Spins for <n> seconds and sends SIGINT to itself
我们需要修改 tsh.c
有一个参考程序 tshref 和一个 shell 驱动程序 sdriver.pl
驱动程序的用法如下
Usage: ./sdriver.pl [-hv] -t <trace> -s <shellprog> -a <args>Options: -h Print this message -v Be more verbose -t <trace> Trace file -s <shell> Shell program to test -a <args> Shell arguments -g Generate output for autograder
键入如下命令进行 trace01 的测试
$ ./sdriver.pl -t trace01.txt -s ./tsh -a "-p"
Makefile 中提供了
$ make test01
还可以驱动参考程序
$ ./sdriver.pl -t trace01.txt -s ./tshref -a "-p"$ make rtest01
tshref.out 则给出了所有的参考程序的输出
trace01
直接就能过
trace02
处理一下内置命令 quit 即可
调用 parseline
trace03
需要在前台执行 /bin/echo
fork 出一个子进程并 execve
主进程还需要 waitpid
关键在于 waitpid 放在 sigchld_handler 中还是 waitfg 中
- In waitfg, use a busy loop around the sleep function.
- In sigchld handler, use exactly one call to waitpid.
注意 fork / addjob / deletejob 的周围需要进行信号的阻塞
可以开启 verbose 进一步观察行为是否一致
trace04
需要在后台执行 ./myspin
这个程序的行为是 sleep
trace05
需要实现 jobs 内置命令
直接调用 listjobs 即可
trace06
需要实现 sigint_handler
<C-c>
会发送一个 SIGINT 信号到前台进程组中的每个进程
这里需要理解 job 的分发
/bin/echo -e tsh> ./myspin 4./myspin 4
这两个 job 都是前台作业,/bin/echo 为 job [1],然后进入 sigchld_handler 被删除,所以 ./myspin 也为 job [1]
响应了 SIGINT 后,./myspin 被终止,随即进入 sigchld_handler 删除该 job,所以 waitfg 不能只用一个 pause 来实现
trace07
同时创建前台和后台作业,测试是否只对前台作业 SIGINT
正确实现 trace06 后,本阶段可以直接过
由于输出较长,考虑使用 diff 工具判断一致性
diff-so-fancy
尝试优化 Makefile,将 trace 索引作为参数传入
https://stackoverflow.com/questions/2826029/passing-additional-variables-from-command-line-to-make
.PHONY: cleanNO =FILENAME := $(join trace, $(NO))FILENAME := $(addprefix $(FILENAME), .txt)
test: $(FILES) $(DRIVER) -t $(FILENAME) -s $(TSH) -a $(TSHARGS)
rtest: $(FILES) $(DRIVER) -t $(FILENAME) -s $(TSHREF) -a $(TSHARGS)
output: $(FILES) mkdir -p tmp $(DRIVER) -t $(FILENAME) -s $(TSH) -a $(TSHARGS) > tmp/output $(DRIVER) -t $(FILENAME) -s $(TSHREF) -a $(TSHARGS) > tmp/outputref diff -u tmp/output tmp/outputref | diff-so-fancy
# clean upclean: rm -f $(FILES) *.o *~ rm -rf tmp
trace08
同时创建前台和后台作业,测试是否只对前台作业 SIGTSTP
发现前面写的有点问题:
- 进程组的概念
- execve 前需要
setpgid(0, 0);
- sigint_handler 和 sigtstp_handler 中的 kill 为
kill(-pid, sig);
- execve 前需要
- waitfg 中需要进行信号的阻塞,而且位置很关键,同时考虑 eval 中的信号阻塞位置
- 何时需要进行信号的阻塞 → 访问全局数据结构
- waitpid 需要设置 options 为
WNOHANG | WUNTRACED
- 使用错误处理包装函数
- 疑问:使用 printf 输出并非异步信号安全
trace09 & 10
实现内置命令 bg 和 fg
根据第二个参数来获取对应的 job,并发送 SIGCONT 信号
- 若为 bg,打印一条信息
- 若为 fg,waitfg
trace11 & 12
前台作业会创建子进程,它们共同构成了前台进程组
测试向前台进程组的每个进程发送 SIGINT 信号或 SIGTSTP 信号
可以直接过
trace13
尝试重启前台进程组的每个进程
可以直接过
trace14
对 do_bgfg 进行简单的错误处理
trace15
大杂烩
可以直接过
trace16
使用 mystop 和 myint 在其他进程中对 tsh 发出信号
通过 getpid 得到 tsh 的 pid,即调用者
最大的区别是 tsh 的 sigint_handler 和 sigtstp_handler 不会响应该信号(不是由 tsh 发出)
tsh 发现僵死进程后,直接进入 sigchld_handler
可以直接过
总结
最关键的地方在前半段对进程组的理解
tsh 利用进程组的概念,通过信号机制,实现了对进程的管理与控制,使进程可以呈现并发运行的假象(底层机制为上下文切换)
tsh 本质上是一个并发程序
为了做这个 lab,重新配置了 Vim,期间还尝试了 diff-so-fancy,优化了 Makefile 来更好的验证正确性
开启 verbose,行为与 tshref 完全一致
发现在 Vim 或 Makefile 中执行命令行,是没有 .bashrc 中的配置的
最后要仔细看书,程序中的所有细节均能从书中找到
代码
实际上不到 300 行
/* * eval - Evaluate the command line that the user has just typed in * * If the user has requested a built-in command (quit, jobs, bg or fg) * then execute it immediately. Otherwise, fork a child process and * run the job in the context of the child. If the job is running in * the foreground, wait for it to terminate and then return. Note: * each child process must have a unique process group ID so that our * background children don't receive SIGINT (SIGTSTP) from the kernel * when we type ctrl-c (ctrl-z) at the keyboard. */void eval(char *cmdline) { char *argv[MAXARGS]; char buf[MAXLINE]; int bg; pid_t pid;
sigset_t mask_all, prev_all; Sigfillset(&mask_all);
strcpy(buf, cmdline); bg = parseline(buf, argv); if (argv[0] == NULL) return;
if (!builtin_cmd(argv)) { Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
if ((pid = Fork()) == 0) { Setpgid(0, 0); Sigprocmask(SIG_SETMASK, &prev_all, NULL); if (execve(argv[0], argv, environ) < 0) { printf("%s: Command not found\n", argv[0]); exit(0); } }
if (!bg) { addjob(jobs, pid, FG, cmdline); Sigprocmask(SIG_SETMASK, &prev_all, NULL); waitfg(pid); } else { addjob(jobs, pid, BG, cmdline); printf("[%u] (%u) %s", pid2jid(pid), pid, cmdline); Sigprocmask(SIG_SETMASK, &prev_all, NULL); } } return;}
/* * builtin_cmd - If the user has typed a built-in command then execute * it immediately. */int builtin_cmd(char **argv) { if (!strcmp(argv[0], "quit")) exit(0); // exit or _exit? if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) { do_bgfg(argv); return 1; } if (!strcmp(argv[0], "jobs")) { listjobs(jobs); return 1; } return 0; /* not a builtin command */}
/* * do_bgfg - Execute the builtin bg and fg commands */void do_bgfg(char **argv) { int olderrno = errno;
if (argv[1] == NULL) { printf("%s command requires PID or %%jobid argument\n", argv[0]); errno = olderrno; return; }
sigset_t mask_all, prev_all; Sigfillset(&mask_all);
int isbg = (!strcmp("bg", argv[0])); int isjid = (argv[1][0] == '%');
const char *str = argv[1] + (isjid ? 1 : 0); char *end; unsigned long id = strtoul(str, &end, 10);
if (!strcmp(end, str)) { printf("%s: argument must be a PID or %%jobid\n", argv[0]); errno = olderrno; return; }
if (errno == ERANGE) { printf("%s: argument out of range\n", argv[0]); errno = olderrno; return; }
struct job_t *(*getjob)(struct job_t *, int) = (isjid) ? getjobjid : getjobpid;
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
struct job_t *job = getjob(jobs, id); if (job == NULL) { if (isjid) printf("%%%ld: No such job\n", id); else printf("(%ld): No such process\n", id); Sigprocmask(SIG_SETMASK, &prev_all, NULL); errno = olderrno; return; }
job->state = (isbg) ? BG : FG; kill(-(job->pid), SIGCONT);
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
if (!isbg) waitfg(job->pid); else printf("[%u] (%u) %s", job->jid, job->pid, job->cmdline);
errno = olderrno; return;}
/* * waitfg - Block until process pid is no longer the foreground process */void waitfg(pid_t pid) { sigset_t mask_all, prev_all; Sigfillset(&mask_all);
while (1) { Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); struct job_t *job = getjobpid(jobs, pid); Sigprocmask(SIG_SETMASK, &prev_all, NULL); if (job == NULL || job->state != FG) break; Sleep(1); }
if (verbose) printf("waitfg: Process (%u) no longer the fg process\n", pid);}
/***************** * Signal handlers *****************/
/* * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever * a child job terminates (becomes a zombie), or stops because it * received a SIGSTOP or SIGTSTP signal. The handler reaps all * available zombie children, but doesn't wait for any other * currently running children to terminate. */void sigchld_handler(int sig) { if (verbose) printf("sigchld_handler: entering\n"); int olderrno = errno;
sigset_t mask_all, prev_all; Sigfillset(&mask_all);
int status; pid_t pid = Waitpid(-1, &status, WNOHANG | WUNTRACED); if (pid == 0) { errno = olderrno; if (verbose) printf("sigchld_handler: exiting\n"); return; }
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
struct job_t *job = getjobpid(jobs, pid); assert(job != NULL);
int jid = job->jid;
if (WIFSTOPPED(status)) { job->state = ST; printf("Job [%u] (%u) stopped by signal %u\n", jid, pid, WSTOPSIG(status)); } else { deletejob(jobs, pid); if (verbose) printf("sigchld_handler: Job [%u] (%u) deleted\n", jid, pid);
if (WIFSIGNALED(status)) { printf("Job [%u] (%u) terminated by signal %u\n", jid, pid, WTERMSIG(status)); } else if (WIFEXITED(status)) { if (verbose) printf("sigchld_handler: Job [%u] (%u) terminates OK (status %u)\n", jid, pid, WEXITSTATUS(status)); } }
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
errno = olderrno; if (verbose) printf("sigchld_handler: exiting\n");
return;}
/* * sigint_handler - The kernel sends a SIGINT to the shell whenver the * user types ctrl-c at the keyboard. Catch it and send it along * to the foreground job. */void sigint_handler(int sig) { if (verbose) printf("sigint_handler: entering\n"); int olderrno = errno;
sigset_t mask_all, prev_all; Sigfillset(&mask_all); Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
pid_t pid = fgpid(jobs); if (pid == 0) { printf("sigint_handler: no current foreground job\n"); assert(0); } else { kill(-pid, sig); if (verbose) printf("sigint_handler: Job (%u) killed\n", pid); }
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
errno = olderrno; if (verbose) printf("sigint_handler: exiting\n"); return;}
/* * sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever * the user types ctrl-z at the keyboard. Catch it and suspend the * foreground job by sending it a SIGTSTP. */void sigtstp_handler(int sig) { if (verbose) printf("sigtstp_handler: entering\n"); int olderrno = errno;
sigset_t mask_all, prev_all; Sigfillset(&mask_all); Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
pid_t pid = fgpid(jobs); if (pid == 0) { printf("sigtstp_handler: no current foreground job\n"); assert(0); } else { kill(-pid, sig); if (verbose) printf("sigtstp_handler: Job [%u] (%u) stopped\n", pid2jid(pid), pid); }
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
errno = olderrno; if (verbose) printf("sigtstp_handler: exiting\n"); return;}