说实话,shell lab虽然不是一个很难的lab,但的确是一个很麻烦的lab,有许多细节需要注意。尤其是进程间的竞赛问题很难处理,一不小心就会掉进坑里。本人在做的时候,debug也花了很长时间,那么这篇文章就来记录一下,我在做lab的时候犯下的一些错误吧。
第一个坑:工作列表的处理问题。我们在参考CSAPP中的示例代码时,发现deleteJob这件事是在sigchild handler中完成的。由于我们在接收到SIGINT与SIGTSTP后,仍然需要修改工作列表,那么一个自然的想法是直接在对应的handler中修改工作列表。
但这样会带来很多问题。首先,在多进程的程序中,修改全局变量是一件很危险的事情,一不小心就会造成各种各样奇奇怪怪的竞赛现象。我们现在把对工作列表的处理分散了,那风险自然就增加了很多。举个例子。在CSAPP中的示例代码中,我们在fork子进程之前,只block掉了SIGCHLD,但如果我们用上述实现方式,我们还要blockSIGTSTP和SIGINT,否则立刻会出现竞赛现象。这对于不了解竞赛现象的人来说(比如刚开始做lab的我),想要注意到这一点并不容易,因而会带来一些逆天的bug。
其次,你在对应的handler中处理了这些信号,那SIGCONT怎么办?难道你还要写一个SIGCONT handler?或者直接在bg/fg指令中处理SIGCONT?这不仅仅是风格上的不一致问题,而且会带来实实在在的语义问题。比如,谁规定的SIGCONT信号只能由bg/fg指令产生?如果我们真的这样实现,那trace31肯定就过不了。因为trace31的SIGCONT信号就不是由bg/fg产生。
所以,我们最好在sigchild handler中统一修改对应的工作列表,而不把对工作列表的修改分散到各个地方。说起sigchild handler的修改,那应该是我要说的第二个坑。
第二个坑:关于sigchild handler。CSAPP中的示例代码呈现的sigchild handler是非常简单的,显然不能满足我们的要求。我们不可以真的只用sigchild handler去回收子进程,而应该同时处理子进程结束,暂停,恢复这三种情况。为了实现这一点,我们就需要 WCONTINUED | WUNTRACED 这些选项,并充分利用status关键字来区分这三种情况。
另外一点需要注意的是,我们必须要加入WNOHANG这个选项,因为当后台子进程未结束的时候,我们的shell不应该停下,应该继续处理其他进程,所以需要通过WNOHANG的指定,来退出循环。
第三个坑:进程与进程组。的确,我们shell每fork一次子进程,都会新建立进程组,但子进程仍然可能继续fork其他的子进程,那么在执行kill指令,以及处理SIGINT和SIGTSTP时,我们必须格外的注意区分进程与进程组。
在writeup里,指定了对于kill指令,进程与进程组明确的格式区分,但十分容易忽略的是,在处理SIGINT和SIGTSTP是,我们需要向整个前台进程组,都发送对应的信号。这就要求我们在调用kill函数的时候,应该传入的是-pid,以把信号传递到整个进程组,只是前台的单个进程。
第四个坑:烦人的SIGCONT。对于SIGCONT,我们需要对付两种可能的情况:通过bg/fg恢复任务的运行,或者通过其他方式,比如子进程自己向自己发送信号使其恢复。我们之前提到了,在sigchild handler中可以统一处理这两种情况。
但这里的一个小问题是,我们的sigchild handler不知道SIGCONT命令是用什么方式发送的,而sigchild handler必须决定,是将任务恢复到前台还是后台。为了解决这个问题,我加入了一个全局变量,用以记录是经由fg命令恢复的子进程,还是通过其他方式:这样,我们就可以通过调用该全局变量,决定任务应处于前台还是后台了!
第五个坑,是我遇到的一个竞赛情况。我在这一点里要说的竞赛,在别的实现里不会遇到。这个竞赛出现在bg指令。我将SIGCONT恢复后台指令后打印的提示信息也放进了sigchild handler中,但可能出现一种情况,就是在bg指令所在处理循环结束后,sigchild handler才被调用,此时tsh >:的提示和后台指令恢复时打印的信息位置将会发生错乱。
要解决这个问题,可以采取两种方式。一种是,干脆利落的,把提示信息的打印直接从handler里拿出来!实际上我们确实应该这样做,但当时我处理的时候,是在bg指令后,利用sigsuspend指令显式等待sigchild handler的返回。这样处理其实多少有点奇怪,但做lab的时候CPU烧了,恐怕来不及想这么多了(笑)。
好了,我想说的其实就这么多。在实现的过程中,还遇到了很多乱七八糟的其他bug,但这些多少是因为个人的不小心。这也没什么办法,毕竟那也是几百行代码,结构也挺复杂的。
顺带一提,由于竞赛这件事并不是一定会被trace的检查发现,有概率我们想要的进程刚好跑赢了,所以即使拿到了满分,也不一定说明代码就写对了。。。我最开始的代码有好几处问题,最后也可以拿到满分,所以我也不确定我的最终代码到底有没有问题。而且,由于时间原因,我也没有心情细心修整代码风格,注释也写得非常草率,望大家见谅了~
代码传送门:
Ubuntu Pastebin