Shell Lab 记录

by Gu Wei
2021年11月

IT SUCKS. MINE EVEN SUCKS MORE.

1. 介绍

本次实验要求实现一个简单的shell。

该实验在tsh.c文件中实现了大部分的框架,需要自己完成以下函数内容:

我们希望实现的shell具有以下功能:

通过make来得到我们shell的可执行目标文件,然后这里给出了一系列的验证文件,比如trace01.txt,其中包含了一些命令,我们可以通过make test01来得到我们shell的输出结果,可以和make rtest01输出的结果对比,或tshref.out比较,判断我们shell是否正确。

以上内容摘自知乎。其实就是handout的翻译。


 

2. 实现

2.0 总体思路

以上图为例说明。当我们./tsh后,就建立了图中shell进程。当输入内建命令,就直接在shell进程中运行,不用fork子进程。当然实际的shell运行内建命令并不一定是这么简单的。如果输入的不是内建命令的话,不妨假设是个前台工作。我们就fork一个进程(Foreground job),并且把这个子进程的pgid设置为pid(这里是20),最后再execve它。注意到,fork出的子进程默认pgid和父进程一致,所以之前pgid=10。于是该子进程之后fork出来的子进程child就会和foreground job有一致的pgid了。这样的设计为了给工作中所有进程发送信号变得可行。

我们通过维护jobs该全局变量来实现jobjg以及bg命令。对于前台工作,shell进程会挂起并等待它的完成,否则不会挂起。job命令只需访问jobs数组并输出即可;jg发送SIGCONT信号给一个后台工作来继续它的执行(之前可能被stop了),并修改其在jobs的state,同时把shell进程挂起来等待它完成;bg就发送SIGCONT信号即可。

而访问jobs这个全局变量,就会产生竞态。我们在shell进程中add job,并在sigchld_handlerdelete job,可能会产生子进程已经结束并发送sigchld信号给父进程了,父进程还没有add job的情况出现。这便需要我们在shell进程中阻塞信号来确保add在delete之后。此外,任何访问全局变量的critical section都要阻塞所有信号,这是因为父进程在读写全局变量的时候,如果突然被信号打断,而信号处理函数也要读写全局变量,这样就会导致奇怪的错误。

信号处理函数是个相当棘手的问题。那SIGCHLD信号来说吧。子进程的终止属于异步事件,父进程无法预知子进程何时终止。父进程可以调用不带WNOHANG标志的wait()或者waitpid()挂起父进程来回收子进程;也可以周期性调用带有WNOHANG标志的waitpid()来对已经终止的子进程轮询。而前者无法实现后台工作,后者浪费了CPU资源。于是我们采取了为SIGCHLD函数建立信号处理程序sigchld_handler。利用while(waitpid(-1,NULL,WNOHANG) > 0)来回收子进程。

大致想法如上。

2.1 eval & builtin_cmd

我将builtin_cmd函数放在了eval函数里面,可能是当初瞎写留下来的痕迹吧。

这里再强调一下如何保证add job在delete job之前执行吧。我们在fork前就阻塞了SIGCHLD信号,这样父进程在访问addjob前是接收不到SIGCHLD信号的。这样便不会执行SIGCHLD的处理程序,就能保证add在delete之前。

注意到我们在addjob(jobs, pid, BG, cmdline);前阻塞了所有信号,是为了全局变量的修改不被信号打断。

setpgid(0,0);把子进程的gpid设置为自己的pid。

此外,handout指出不对系统调用函数的返回值进行处理的话,要扣style分数5分。我几乎都没有处理,这里给个例子:

一般就是看返回值是不是-1,如果是的话,这些系统调用函数还会修改errno这个全局变量,我们可以通过strerror(errno)查看具体的错误类型。

2.2 do_bgfg

这个难度尚可,就是学习一下kill函数的用法。而之前我们把Foreground jobBackground jobpgidpid设置为一样,在这里便体现了其作用。

按理说这里读写job也应该阻塞信号,但是我没有写。。。

2.3 waitfg

waitfg函数网上看来看去就两种写法,我觉得很不对呀。忙等有效率差的问题,而用sigsuspend函数的则写错了,会产生竞态。我的版本测试下来应该没有问题。

我们的waitfg函数是没有回收子进程的,在handout有这句话:

While other solutions are possible, such as calling waitpid in both waitfg and sigchld handler, these can be very confusing.

便从了。

2.4 sigint_handler & sigtstp_handler

这两个代码应该一致的吧。

再强调几点:

2.5 sigchld_handler

这个就很有难度了。首先,由于信号不存在队列,所以一个未处理信号表明至少有一个信号到达,于是我们需要使用while循环,而不能用if。此外,对于接收到SIGSTP和SIGINT信号的我们需要分别处理。还需要注意访问全局变量时阻塞信号、WNOHANG|WUNTRACED的使用。等等不一而足。

最后强调一点,关于异步信号安全。我们知道信号处理程序和调用进程是并发执行的,不同于不同进程是有独立的地址空间。于是信号处理程序会和调用进程产生竞争。比如说printf函数,如果我们同时用在了信号处理程序和调用进程中,我们不能保证printf的输出是正确的。一个异步信号安全的函数要么是可重入的(reentrant,只用了局部变量),要么不会被信号处理程序所中断。下面是Linux保证的一些异步信号安全的函数:

acceptfchmodlseeksendtostat
accessfchownlstatsetgidsymlink
aio_errorfcntlmkdirsetpgidsysconf
aio_returnfdatasyncmkfifosetsidtcdrain
aio_suspendforkopensetsockopttcflow
alarmfpathconfpathconfsetuidtcflush
bindfstatpauseshutdowntcgetattr
cfgetispeedfsyncpipesigactiontcgetpgrp
cfgetospeedftruncatepollsigaddsettcsendbreak
cfsetispeedgetegidposix_trace_eventsigdelsettcsetattr
cfsetospeedgeteuidpselectsigemptysettcsetpgrp
chdirgetgidraisesigfillsettime
chmodgetgroupsreadsigismenbertimer_getoverrun
chowngetpeernamereadlinksignaltimer_gettime
clock_gettimegetpgrprecvsigpausetimer_settime
closegetpidrecvfromsigpendingtimes
connectgetppidrecvmsgsigprocmaskumask
creatgetsocknamerenamesigqueueuname
dupgetsockoptrmdirsigsetunlink
dup2getuidselectsigsuspendutime
execlekillsem_postsleepwait
execvelinksendsocketwaitpid
_Exit & _exitlistensendmsgsocketpairwrite

 

3. 写在后面

本人于2021年11月7日完成了shell lab,耗时三天。总体来说我觉得shell lab相当不错!你可以说它不难,代码量就充其量两百行,但是对于初学者的我们来说,中间的思维量还是相当大的。阻塞信号、信号处理程序、系统调用函数等等要点都在这个tiny shell中得到了体现。这也算是我第一次写了一些linux系统编程吧,感觉挺有趣的。

通过这个lab,几乎就是重读了书上的第八章(除了非本地跳转部分)。对整个异常控制流的理解早已超过当初第一遍读这一章的时候了。当然对于信号和进程的linux系统编程还有很多可以学习的地方。

在我做shell lab的时候参考了THE LINUX PROGRAMMING INTERFACE这本书的一些内容。个人觉得这本书应该相当不错的,除了看目录难以找到我想要的函数外。当然只查阅了几页,不能妄下结论。

可能接下来:

P.S. I've learnt ps though I don't have PS.