这个实验通过实现一个支持作业控制的Unix Shell,让我们对进程控制和信号控制更加熟悉。课程Lab已经帮助我们搭建起了Shell的整体框架,并实现了与本次实验不太相关的代码,核心部分需要我们自己完成。

整体框架

Shell从标准输入(stdin)读取用户输入的命令,然后解析命令,Shell支持两种类型的命令:如果用户输入的是的内置命令(如quitjobs等),那么直接执行该命令;如果用户输入的是某个可执行文件的路径,那么通过fork一个子进程,在子进程中加载并执行命令。Shell把每次用户输入的命令抽象为一个job,一个job可以包含多个进程(例如管道)。每个job有两种运行方式,如果用户输入的命令以’&‘结尾,那么job将会在后台(background)运行,否则,job运行在前台(foreground)。在任意时刻,只允许存在01个前台job,但是可以有0或多个后台job运行。最后,为了支持用户能够向Shell发送信号,我们还需要实现3个信号处理程序,分别处理信号SIGCHLDSIGINTSIGTSTP

需要注意的地方

  • 默认的,一个子进程和它的父进程同属于一个进程组,而Unix系统提供的大量向进程发送信号的机制,都是基于进程组这个概念的。当我们输入Ctrl + C,内核会发送一个SIGINT信号到前台进程组的每个进程,类似的,输入Ctrl + Z会导致内核发送一个SIGTSTP信号给前台进程组中的每个进程。这儿的“前台进程组”指的是Shell进程所属的进程组。实验中,我们并不期望信号直接作用于Shell进程本身(否则Shell收到SIGINT信号就终止了),而是需要让Shell将信号转发给Shell前台作业中的子进程及其所属进程组中的所有进程。所以,我们不能让子进程和Shell进程同属一个进程组。具体做法是通过使用setpgid函数来改变子进程的进程组,当调用setpgid(0, 0)时,内核会创建一个新的进程组,其进程组ID是调用者进程的PID,并且会把调用者进程加入到这个进程组中。
  • Shell收到信号时,具体的工作需要信号处理函数来完成。例如收到SIGINT信号,那么信号处理函数会把该信号发往前台job中的进程及其所属进程组中的所有进程。实验中,我们是通过kill(pid_t pid, int sig)来发送信号,注意到我们并不仅仅是向PID = pid的进程发送信号,kill函数帮助我们实现了这一点:如果pid小于0kill发送信号sig给进程组|pid|pid的绝对值)中的每个进程。我们可以意识到,上一点需要注意的地方正是为这一点做铺垫的。
  • 父进程(Shellfork了一个子进程后,父进程需要将这个进程作为一个job添加到job队列中去(addjob),当子进程终止时,内核会发送一个SIGCHLD信号给父进程,然后在相应的信号处理程序中,把终止的子进程对应的jobjob队列中删除(deletejob)。考虑一种情况:当父进程fork了一个子进程之后,子进程先于父进程获得调度,并且在父进程执行addjob前,子进程就已经终止了,并发送了SIGCHLD信号给父进程。此时,在信号处理程序中deletejob不会做任何操作,因为此时父进程还没有把job加入到job队列中。出现这个问题的根本原因是在addjob之前调用了deletejob。解决这个问题的方法是:在父进程fork子进程之前,将SIGCHLD信号阻塞,当完成addjob之后,才解除对SIGCHLD信号的阻塞,这样就能保证在子进程被添加到job队列之后再回收该子进程。注意,子进程继承了它们父进程的被阻塞信号集合,所以我们必须在调用execve之前,解除子进程中阻塞的SIGCHLD信号。

代码

Shell Lab的代码在这里