天天看点

CS:APP3e 深入理解计算机系统_3e ShellLab(tsh)实验

详细的题目要求和资源可以到 http://csapp.cs.cmu.edu/3e/labs.html 或者 http://www.cs.cmu.edu/~./213/schedule.html 获取。

Signal (IPC)

signal(2) - Linux manual page - man7.org

fork(2) - Linux manual page - man7.org

wait(2) - Linux manual page - man7.org

sigprocmask(2) - Linux manual page - man7.org

access(2) - Linux manual page - man7.org

sigemptyset(3): POSIX signal set operations

How to Use C's Volatile Keyword - Barr Group

Atomic Types

tsh的提示符为“tsh> ”

用户的输入分为第一个的<code>name</code>和后面的参数,之间以一个或多个空格隔开。如果<code>name</code>是一个tsh内置的命令,那么tsh应该马上处理这个命令然后等待下一个输入。否则,tsh应该假设<code>name</code>是一个路径上的可执行文件,并在一个子进程中运行这个文件(这也称为一个工作、job)

tsh不需要支持管道和重定向

如果用户输入<code>ctrl-c</code> (<code>ctrl-z</code>),那么<code>SIGINT</code> (<code>SIGTSTP</code>)信号应该被送给每一个在前台进程组中的进程,如果没有进程,那么这两个信号应该不起作用。

如果一个命令以“&amp;”结尾,那么tsh应该将它们放在后台运行,否则就放在前台运行(并等待它的结束)

每一个工作(job)都有一个正整数PID或者job ID(JID)。JID通过"%"前缀标识符表示,例如,“%5”表示JID为5的工作,而“5”代笔PID为5的进程。

tsh应该有如下内置命令:

tsh应该回收(reap)所有僵尸孩子,如果一个工作是因为收到了一个它没有捕获的(没有按照信号处理函数)而终止的,那么tsh应该输出这个工作的PID和这个信号的相关描述。

利用测试文件逐步构建tsh,例如先从trace01.txt开始。

<code>setpgid</code>中的WUNTRACED and WNOHANG选项有用(参看前期准备)

当解析命令并产生子进程的时候(<code>fork</code> )的时候,必须先调用<code>sigprocmask</code> block <code>SIGCHLD</code>信号,调用<code>addjob</code>将刚刚创建的工作加入到工作列表里,然后unblock该信号(课件里有讲这个竞争产生的问题)。另外,由于子进程会继承block的特性,所以子进程要记得unblock。

一些具有终端环境的进程会尝试从父进程读写数据,例如/bin/sh,还有一些程序例如<code>more</code> <code>less</code> <code>vi</code> <code>emacs</code> 会对终端做一些“奇怪的设置”。本次实验用<code>/bin/ls</code> <code>/bin/echo</code>这样的文字模式的程序测试即可。

当我们在真正的shell(例如bash)中执行tsh时,tsh本身也是被放在前台进程组中的,它的子进程也会在前台进程组中,例如下图所示:

所以当我们在终端输入<code>ctrl-c</code> (<code>ctrl-z</code>)的时候,<code>SIGINT</code> (<code>SIGTSTP</code>)信号应该被送给每一个在前台进程组中的所有进程,包括我们在tsh中认为是后台进程的程序。一个决绝的方法就是在<code>fork</code>之后<code>execve</code>之前,子进程应该调用<code>setpgid(0, 0)</code>使得它进入一个新的进程组(其pgid等于该进程的pid)。tsh接收到<code>SIGINT</code> <code>SIGTSTP</code>信号后应该将它们发送给tsh眼中正确的“前台进程组”(包括其中的所有进程)。

我首先将书上(8.5.5节)说的6个关于信号处理函数安全性的要求列出(详细的解释请参考书),在编程的时候要注意:

尽量保持信号处理函数的简单性,例如只改变一个flag

在信号处理函数内部只调用<code>async-signal-safe</code>的函数(<code>man 7 signal</code>里面有完全的列出)

在进入和退出信号处理函数的时候保存和还原<code>errno</code>变量(参考:Thread-local storage )

当试图访问全局结构变量的时候暂时block所有的信号,然后还原

全局变量的声明为<code>volatile</code>

将flag(标志)声明为<code>sig_atomic_t</code>

下面我就实验要求完成的7个函数说几个注意的地方,代码中的注释也解释了一些:

在调用<code>parseline</code>解析输出后,我们首先判断这是一个内置命令(shell实现)还是一个程序(本地文件)。如果是内置命令,进入<code>builtin_cmd(argv, cmdline)</code> ,否则创建子进程并在job列表里完成添加。这里要注意在<code>fork</code>前用<code>access</code>判断是否存在这个文件,不然fork以后无法回收,另外要注意一个线程并行竞争(race)的问题:<code>fork</code>以后会在job列表里添加job,信号处理函数<code>sigchld_handler</code>回收进程后会在job列表中删除,如果信号来的比较早,那么就可能会发生先删除后添加的情况。这样这个job永远不会在列表中消失了(内存泄露),所以我们要先block<code>SIGCHLD</code> ,添加以后再还原。

更新:<code>fork</code>子进程后发生错误退出子进程应该使用<code>_exit</code>而非<code>exit</code> (<code>unix_error</code>里面也是用的<code>exit</code> ) 参考:What is the difference between using _exit() &amp; exit() in a conventional Linux fork-exec?

这个函数分情况判断是哪一个内置命令,要注意如果用户仅仅按下回车键,那么在解析后<code>argv</code>的第一个变量将是一个空指针。如果用这个空指针去调用<code>strcmp</code>函数会引发segment fault。

这个函数单独处理了<code>bg</code>和<code>fg</code>这两个内置命令。要注意<code>fg</code>有两个对应的情况:1.后台程序是stopped的状态,这时我们需要设置相关变量,然后发送继续的信号。2.如果这个进程本身就在运行,我们就只需要改变job的状态,设置相关变量,然后进入<code>waitfg</code>等待这个新的前台进程执行完毕。

写这个也出现了一个让我debug 几个小时的兼容性问题:

在<code>man 7 signal</code>中,<code>SIGCHLD</code>描述如下:

也就是说,子进程终止或者停止的时候会向父进程发送这个信号,然后父进程进入<code>sigchld_handler</code>信号处理函数进行回收或者提示。但是在我的机器上却发现在子进程从stopped变到running(收到<code>SIGCONT</code> )的时候也会向父进程发送这个信号。这样就会出现一个问题:我们要使后台一个stopped的进程重新运行,但是它会向父进程(shell)发送一个<code>SIGCHLD</code> ,这样父进程就会进入信号处理函数<code>sigchld_handler</code>试图回收它(不是stop),而它有没有结束,所以信号处理函数会一直等待它执行完毕,在shell中显示的情况就是卡住了。

经过长时间调试确认后发现在POSIX某个标准中<code>SIGCHLD</code>信号的定义如下:

SIGCHLD The SIGCHLD signal is sent to a process when a child process terminates, is interrupted, or resumes after being interrupted. One common usage of the signal is to instruct the operating system to clean up the resources used by a child process after its termination without an explicit call to the <code>wait</code> system call.

<code>or resumes after being interrupted.</code> ,看到这句的时候我就要吐血了。。。

为了进一步证实我的想法,我在FreeBSD11.1上面查了一下手册:

CS:APP3e 深入理解计算机系统_3e ShellLab(tsh)实验

他说的是“changed”,看来我的机器是按照POSIX的某个标准实现的。

我的解决方案是设置一个<code>pid_t</code>的全局变量stopped_resume_child记录我们要fg的stopped进程,在进入信号处理函数后首先检查这个变量是否大于零,如果是就直接退出不做处理。(这里其实有一个和其他进程竞争的问题,时间有限就不去做更改了)

我之前声明了一个<code>volatile sig_atomic_t</code>的全局变量<code>fg_pid_reap</code> ,只要信号处理函数回收了前台进程,它就会将<code>fg_pid_reap</code> 置1,这样我们的<code>waitfg</code>函数就会退出,接着读取用户的下一个输入。使用busy<code>sleep</code>会有一些延迟,实验报告上要求这么实现我也没办法; )

注意保存<code>errno</code> 。

注意到这里不能使用while来回收进程,因为我们的后台还可能有正在运行的进程,这样做的话会使得<code>waitpid</code>一直等待这个进程结束。当然使用if只回收一次也可能会导致信号累加的问题,例如多个后台程序同时结束,实验报告上要求这么实现我也没办法 ; )

注意如果程序是被stop的话<code>SIGTSTP ctrl-z</code> ,我们不用回收、删除job列表中的节点。

注意是群发,即<code>killpg</code>,不能只发一个。

不解释。

为了方便检查结果,我写了一个bash脚本,用来比较我的<code>tsh</code>和实验给的正确参考程序<code>tshref</code>的输出结果(测试用例为trace01.txt~trace16.txt):

全部打印出来太长,这里列出最后几个:

可以发现除了PID不同以外其余都相同,说明<code>tsh</code>实现正确。

[完整项目代码](https://files.cnblogs.com/files/liqiuhao/tsh.7z)

这次实验给我最大的教训就是不要完全相信文档,自己去实现和求证也很重要。另外,并行产生的竞争问题也有了一些了解。

另外,有意思的是,我在做实验之前看到实验指导里说:

– In waitfg, use a busy loop around the sleep function. – In sigchld handler, use exactly one call to waitpid.

当时我还想说用<code>sleep</code> 和在<code>waitpid</code>里面只用一个回收是不是不安全或者太傻了,结果我上github一看不仅都是这样,而且他们的代码非常不安全(上面提到的六个安全注意点完全不遵守,各种调用也没有检查返回值和异常),于是觉得自己写的肯定比他们好多了

结果。。。如果注意这些安全问题会有很多麻烦,时间也有限,我就把几个容易实现的实现了,还有两个“访问全局结构变量前block”和“在信号处理函数中仅使用<code>async-signal-safe</code>没有实现。

最后,改编一下Mutt E-Mail Client作者的一句话总结一下这次实验:

All code about this ShellLab on github sucks. This one just sucks less 😉