CSAPP - Shell Lab 详解
-
Shell Lab [Updated 7/28/03] (README, Writeup, Release Notes, Self-Study Handout)
Students implement their own simple Unix shell program with job control, including the ctrl-c and ctrl-z keystrokes, fg, bg, and jobs commands. This is the students’ first introduction to application level concurrency, and gives them a clear idea of Unix process control, signals, and signal handling.
前言
本篇博客将会详细介绍 CSAPP 之 ShellLab 的完成过程,实现一个简易(lou)的 shell。tsh 拥有以下功能:
- 可以执行外部程序
- 支持四个内建命令,名称和功能为:
quit
:退出终端jobs
:列出所有后台作业bg <job>
:继续在后台运行一个处于停止状态的后台作业,<job>
可以是 PID 或者 %JID 形式fg <job>
:将一个处于运行或者停止状态的后台作业转移到前台继续运行
- 按下 ctrl + c 终止前台作业
- 按下 ctrl + z 停止前台作业
实验材料中已经写好了一些函数,只要求我们实现下列核心函数:
eval
:解析并执行指令builtin_cmd
:识别并执行内建指令do_bgfg
:执行fg
和bg
指令waitfg
:阻塞终端直至前台任务完成sigchld_handler
:捕获SIGCHLD
信号sigint_handler
:捕获SIGINT
信号sigtstp_handler
:捕获SIGTSTP
信号
理论知识检验
Q1:wait是等待子进程终止,然后父进程去收割?
Q2:kill是父进程去杀死子进程?
以上两个问题若回答yes,则说明很有必要再把书本或者slides学习一遍,此外 man wait / man kill
也会给出非常棒的手册(都推荐)。
信号处理函数
sigint_handler 和 sigtstp_handler
这两个函数的主要任务,是在收到 shell 传来的信号时,将这个信号“转发”给在 shell 中运行的进程。这个过程很好办——先用 fgpid
获取前台进程(为啥只有前台进程嘞?因为 SIGTSTP 和 SIGINT 信号是只发给前台进程的)的 pid,之后走 kill 调用,向这个子进程组发对应的信号。
1 | /* |
Q3:为什么要 kill(-pid, sig)
?
如果 shell fork 出来的子进程,没有再 fork 它自己的子进程的话,填 “pid” 没有任何问题;但是,如果它 fork 了的话(shell 就有孙进程了),这时候子进程和孙进程的 pid 是不一样的。填正的 pid,只能保证子进程能被结束;但是孙进程么……就没那么幸运了——它会“丧父”(变成孤儿进程),直到操作系统“收养”它。
这里可以看出 kill 只是 send a signal to a process,并不一定是发送SIGKILL。
扩展:SIGKILL 无法被忽略或组织。
Q4:handler 中是否需要阻塞信号的接收?
有隐式阻塞机制,无须显式调用 sigprocmask
。
扩展:根据 G2 需要在handler入口和离开时暂存并恢复 errno
。
根据 G1,实际上不能应该是用Standard I/O函数,如printf
,但既然 sigquit_handler
中使用 printf
,那就默认我们也能用吧。
sigchld_handler
阅读代码注释,有2点要求需要注意:
- or stops because it received a SIGSTOP or SIGTSTP signal
- but doesn’t wait for any other currently running children to terminate.
可以 man waitpid
,里面有些好东西:
All of these system calls are used to wait for state changes in a child of the calling process, and obtain information about the child whose state has changed. A state change is considered to be: the child terminated; the child was stopped by a signal; or the child was resumed by a signal.
这里可以回答Q1:
wait for process to change state
,这里的改变状态不只是terminated
。
In the case of a terminated child, performing a wait allows the system to release the resources associated with the child; if a wait is not performed, then the terminated child remains in a “zombie” state (see NOTES below).
If a child has already changed state, then these calls return immediately. Otherwise, they block until either a child changes state or a signal handler interrupts the call (assuming that system calls are not automatically restarted using the SA_RESTART flag of sigaction(2)).
这里指明了对于一个terminated child 也可以调用 wait,即可以子进程先term,父进程后wait。
1 | /* |
选项含义:
WNOHANG
: return immediately if no child has exited.WUNTRACED
:also return if a child has stopped (but not traced via ptrace(2)). Status for traced children which have stopped is provided even if this option is not specified.
eval 和 waitfg
eval
Q5:由于shell不会终止,其fg子进程可以被正常reap,但是bg子进程怎么reap呢?
解决方法就是 detach
: setpgid(0, 0);
。这样做还能解决其他问题:
当我们按下 Ctrl + C,给子进程发终止消息的时候,如果 shell 和子进程的进程组号相同,那么它和子进程都会收到 转发的 SIGINT 信号,之后一起终止。只要我们在子进程里重新设下 gpid,就能解决这个问题了。
1 | /* |
这里还涉及一个利用 block / unblock 进行同步的问题(如果不加,则不能保证handler中
deletejob
会晚于父进程中的addjob
执行),建议阅读slide理解此问题。
其中 Fork
和 Execve
是CMU wrap过的函数:
1 | pid_t Fork() { |
waitfg
除了sigsuspend
, 其他方法不太行:
(上图中的 Program is correct, but very wasteful 指的是 while (!pid) ;
)
int sigsuspend(const sigset_t *mask)
的描述如下:
sigsuspend() temporarily replaces the signal mask of the calling thread with the mask given
by mask and then suspends the thread until delivery of a signal whose action is to invoke a
signal handler or to terminate a process.
If the signal terminates the process, then sigsuspend() does not return. If the signal is
caught, then sigsuspend() returns after the signal handler returns, and the signal mask is
restored to the state before the call to sigsuspend().
It is not possible to block SIGKILL or SIGSTOP; specifying these signals in mask, has no effect on the thread’s signal mask.
1 | /* |
builtin_cmd 和 do_bgfg
builtin_cmd
1 | /* |
do_bgfg
进程状态转化如下:
Q6:job是什么,怎么使用?
实验手册中这样说:The child processes created as a result of interpreting a single command line are known collectively as a job. In general, a job can consist of multiple child processes connected by Unix pipes.
Q7:fg %2
对 jid = 2
的进程 / 进程组有何影响?
对于 bg 命令,我们只是向目标进程发送 SIGCONT 信号,让它继续执行;对于 fg 命令呢,我们先判断目标进程是不是已经暂停了(如果是,就先启动它)—— 我们也可以对 bg / fg 目标job所在进程组都发一个CONT信号。之后调用 waitfg 等待进程结束。注意哦,这里的 kill 函数的第一个参数也是要填负值的。
当用户与命令行交互时,通常只有一个 foreground process(而非 foreground process group)在运行,只用等待这个进程结束。
1 | /* |
注意 kill(-job->pid, SIGCONT)
和 waitfg(job->pid)
。
参考资料
- 【【深入理解计算机系统 实验4 CSAPP】Shell Lab 实现 CMU 详细讲解 shelllab】 https://www.bilibili.com/video/BV1EF411h791/?share_source=copy_web&vd_source=1e8c177289cfed3be80e766714c3f11f (郭郭wg的讲解视频)
- csapp-shlab 详解 - 独小雪的文章 - 知乎 https://zhuanlan.zhihu.com/p/422490811 (通俗易懂)
- CSAPP 之 ShellLab 详解 - 之一Yo - 博客园 (cnblogs.com) (简洁明了)
- CSAPP 之 ShellLab 详解 - 之一Yo - 博客园 (cnblogs.com) (逐trace分析)