csapp shlab
CSAPP shell Lab
Lab Assignment L5: **Writing Your Own Unix Shell! **
Introduction
The purpose of this assignment is to become more familiar with the concepts of process control and signalling. You’ll do this by writing a simple Unix shell program that supports job control.
Looking at the tsh.c (tiny shell) file, you will see that it contains a functional skeleton of a simple Unix shell. To help you get started, we have already implemented the less interesting functions. Your assignment is to complete the remaining empty functions listed below. As a sanity check for you, we’ve listed the approximate number of lines of code for each of these functions in our reference solution (which includes lots of comments).
- eval: Main routine that parses and interprets the command line. [70 lines] 解析命令行
- builtin cmd: Recognizes and interprets the built-in commands: quit, fg, bg, and jobs. [25 lines] 检测是否为内置命令
quit
、fg
、bg
、jobs
- do bgfg: Implements the bg and fg built-in commands. [50 lines] 实现内置命令
fg
、bg
- waitfg: Waits for a foreground job to complete. [20 lines] 等待前台作业执行完成
- sigchld handler: Catches SIGCHILD signals. 80 lines] 处理
SIGCHILD
信号,即子进程停止或者终止 - sigint handler: Catches SIGINT (ctrl-c) signals. [15 lines] 处理
SIGINT
信号,即键盘中断ctrl-c
- sigtstp handler: Catches SIGTSTP (ctrl-z) signals. [15 lines]处理
SIGSTP
信号,即来自终端的停止信号
The tsh
Specification
Your tsh shell should have the following features:
The prompt should be the string “tsh> ”.
The command line typed by the user should consist of a name and zero or more arguments, all separated by one or more spaces. If
name
is a built-in command, then tsh should handle it immediately and wait for the next command line. Otherwise, tsh should assume thatname
is the path of an executable file, which it loads and runs in the context of an initial child process (In this context, the term job refers to this initial child process).tsh need not support pipes (|) or I/O redirection (< and >).
Typing ctrl-c (ctrl-z) should cause a SIGINT (SIGTSTP) signal to be sent to the current foreground job, as well as any descendents of that job (e.g., any child processes that it forked). If there is no foreground job, then the signal should have no effect.
If the command line ends with an ampersand &, then tsh should run the job in the background. Otherwise, it should run the job in the foreground.
Each job can be identified by either a process ID (PID) or a job ID (JID), which is a positive integer assigned by tsh. JIDs should be denoted on the command line by the prefix ’%’. For example, “%5” denotes JID 5, and “5” denotes PID 5. (We have provided you with all of the routines you need for manipulating the job list.)
tsh should support the following built-in commands:
– The quit command terminates the shell.
– The jobs command lists all background jobs.
– The bg command restarts by sending it a SIGCONT signal, and then runs it in the background. The argument can be either a PID or a JID.
– The fg command restarts by sending it a SIGCONT signal, and then runs it in the foreground. The argument can be either a PID or a JID.
tsh should reap all of its zombie children. If any job terminates because it receives a signal that it didn’t catch, then tsh should recognize this event and print a message with the job’s PID and a description of the offending signal.
开始冻手
Revisit: 回收子进程
一个终止了但还未被回收的进程称为僵死进程。对于一个长时间运行的程序(比如 Shell)来说,内核不会安排init
进程去回收僵死进程,而它虽不运行却仍然消耗系统资源,因此实验要求我们回收所有的僵死进程。
waitpid
是一个非常重要的函数,一个进程可以调用waitpid
函数来等待它的子进程终止或停止,从而回收子进程,在本实验大量用到,我们必须学习它的用法:
- 子进程终止
- 子进程收到信号停止
- 子进程收到信号重新执行
如果一个子进程在调用之前就已经终止了,那么函数就会立即返回,否则,就会阻塞,直到一个子进程改变状态。
等待集合以及监测那些状态都是用函数的参数确定的,函数定义如下:
1 |
|
各参数含义及使用
- pid:判定等待集合成员
- pid > 0 : 等待集合为 pid 对应的单独子进程
- pid = -1: 等待集合为所有的子进程
- pid < -1: 等待集合为一个进程组,ID 为 pid 的绝对值
- pid = 0 : 等待集合为一个进程组,ID 为调用进程的 pid
- options:修改默认行为
- WNOHANG:集合中任何子进程都未终止,立即返回 0
- WUNTRACED:阻塞,直到一个进程终止或停止,返回 PID
- WCONTINUED:阻塞,直到一个停止的进程收到 SIGCONT 信号重新开始执行
- 也可以用或运算把 options 的选项组合起来。例如 WNOHANG | WUNTRACED 表示:立即返回,如果等待集合中的子进程都没有被停职或终止,则返回值为 0;如果有一个停止或终止,则返回值为该子进程的 PID
- statusp:检查已回收子进程的退出状态
- waitpid 会在 status 中放上关于导致返回的子进程的状态信息
Revisit: 并发编程原则
- 保存和恢复errno。很多函数会在出错时设置errno,在处理程序中调用这样的函数可能会告饶主程序中其他依赖于errno的部分,解决办法是在进入处理函数时用局部变量保存它,运行完成后再将其恢复
- 访问全局数据时,阻塞所有信号。
- 不可以用信号来对其它进程中发生的事情计数。未处理的信号是不排队的,即每种类型的信号最多只能有一个待处理信号。举例:如果父进程将要接受三个相同的信号,当处理程序还在处理一个信号时,第二个信号就会加入待处理信号集合,如果此时第三个信号到达,那么它就会被简单地丢弃,从而出现问题
- 注意考虑同步错误:竞争。
Revisit: 竞争
这是一个 Unix Shell 的框架,父进程在一个全局列表中记录子进程,并设置了一个 SIGCHLD 处理程序来回收子进程,乍一看没问题,但是考虑如下可能的事件序列:
- 第 29 行,创建子进程运行
- 假设子进程在父进程运行到 32 行,即运行
addjob
函数之前就结束了,并发送一个 SIGCHLD 信号 - 父进程接收到信号,运行信号处理程序,调用
deletejob
函数,而这个job
本来就没有添加入列表 - 返回父进程,调用
addjob
函数,而这个子进程已经终止并回收了,job
早就不存在了
也就是说,在这里,deletejob
函数的调用发生在了addjob
之前,导致错误。我们称addjob
和deletejob
存在竞争。
解决方法即在父进程folk
之前就阻塞SIGCHLD信号
错误处理包装函数
1 |
|
eval
解析命令行,判断其是内置命令,还是程序路径,分别执行,如果是前台作业,要等待其完成,如果是后台作业,则要输出其相应信息
1 |
|
builtin_cmd
判断是否为内置命令
1 |
|
do_bgfg
实现内置命令bg和fg,这两个命令的功能如下:
bg <job>
:通过向<job>
对应的作业发送SIGCONT
信号来使它重启并放在后台运行fg <job>
:通过向<job>
对应的作业发送SIGCONT
信号来使它重启并放在前台运行- 输入时后面的参数有
%
则代表jid
,没有则代表pid
1 |
|
waitfg
该函数从要求实现阻塞父进程,直到当前的前台进程不再是前台进程了。这里显然要显式的等待信号
解决方法是用sleep函数或sigsuspend函数,该函数相当于
1 |
|
在调用sigsuspend
之前阻塞 SIGCHLD 信号,调用时又通过sigprocmask
函数,在执行pause
函数之前解除对信号的阻塞,从而正常休眠。有同学可能会问了:这里并没有消除竞争啊?如果在第 1 行和第 2 行之间子进程终止不还是会发生永久休眠吗?
这就是sigsuspend
与上述代码的不同之处了,它相当于上述代码的原子版本,即第 1 行和第 2 行总是一起发生的,不会被中断!
1 |
|
信号处理函数
sigchld_handler
回收所有僵死进程
1 |
|
sigint_handler
实现一个SIGINT信号处理函数,将信号传给前台程序
1 |
|
sigstp_handler
实现一个SIGSTOP信号处理函数,将信号传给前台程序
1 |
|
结果
左边参考 右边自己实现
总结
- 从未想过每天都在使用的shell需要考虑这么多:回收进程、竞争避免等
- 第一次接触并行并发的概念,希望能再厉害一点点