写在前面
用时 10h30min, 通过所有测试。这个 lab 还是比较直接的, 按着 trace 的顺序逐步实现即可, 难点主要在调试和不熟悉系统调用语法上。我在写代码的过程中遇到了两个调了蛮久的 bug, 分别在 trace13 和 trace16 上, 下面简要复盘一下。
调试
trace16
先说 trace16 的 bug, 这个调了半小时。trace16 测试的是 shell 能否处理子进程被外部信号中断或停止的情况。我遇到的问题是 trace16 在 ./mystop
那里卡住了, 于是猜测原因是 jobs 没能正确更新子进程的暂停状态。后面加了点调试语句发现果然如此。那么修复思路就是在子进程暂停后把它对应的 job 状态进行更新。但怎么知道子进程什么时候暂停呢?答案是 sigchld_handler.
我一开始以为 sigchld_handler 只会在某个子进程终止后被调用, 所以只在 sigchld_handler 里写了回收已经终止的子进程的逻辑。后来发现子进程只要状态发生变化就会发 SIGCHLD 信号给父进程, 子进程终止只是一种情况, 暂停、恢复也会发送这个信号。所以让 sigchld_handler 检查已经停止的子进程, 并更新它们对应的 jobs 状态就修复了这个 bug.
trace13
然后是 trace13 的 bug, 这个调了两小时半。这里的问题是在 fg %1
卡住。第一个猜测自然是进程没能被正确移到前台, 但加了调试语句后发现它确实被移到前台了。之后的猜测就是 waitfg 的实现有误, 这个猜测看起来很合理, 毕竟 handout 里写 waitfg 大概需要 20 行, 而我只用了 4 行。但测了半天发现 waitfg 也没问题。
进一步加入各种调试语句, 发现在 fg %1 之后, 进程确实被顺利移到了前台, jobs 也正确更新了, waitfg 只是因为它没结束, 所以一直循环着等着它。好吧, 那我猜是 sigchld_handler 没能正确回收它。我检查了一下, 发现终止的进程也都被正确回收了。那还能是什么原因呢?难道它被暂停了, 但没有被正确启动, 导致我们的 shell 误以为这个暂停着的进程是前台进程, 从而卡住了?这听起来就像是一个并行导致的问题, 于是我又调试了好久, 最后发现它确实被正确启动了, 这个进程只是单纯地还没有终止而已。
那这怎么可能呢?凭什么它在 tshref 里几秒钟就执行完了, 在我的实现里五分钟了都没执行完?原来是我的“启动”写错了。我使用的是 kill(job->pid, SIGCONT)
, 这个代码只把启动信号发给了这一个子进程, 而不是发给它所在的进程组, 从而让这个子进程所在组的别的进程没有被正确启动。而看看 mysplit.c
的代码, 会发现它等待它的子进程执行完才会终止, 所以我们把代码改成 kill(-job->pid, SIGCONT)
就解决了这个 bug.
就因为这一个负号, 我调试了两个半小时, 而且我感觉我一路下来的各种猜想也都很合理, 只能感叹系统级代码真难调试啊。
感想
也算是学到了几招吧:
- 在调用系统函数时一定要检查返回值, 不然会报一些很难调试的错误
- 在修改全局变量时一定要用 sigprocmask 拦截别的信号, 避免冲突
- 子进程只要状态发生变化就会发 SIGCHLD 信号给父进程, 终止、暂停、恢复都会发送这个信号
- 可以用 waitpid 来检查子进程的变化状态, 它会回收终止的子进程
- 只在信号处理程序中调用异步安全的函数
- 调试时大胆猜想, 小心求证
一键测试脚本
我让 AI 写了一份一键测试脚本, 比官方的形如 make test13
的测试方便不少。脚本的功能是在 traceA 到 traceB(要求 A < B)上分别运行 tsh 和 tshref, 并把输出结果放到两个文件中。比如说, 如果我们输入 ./test_traces.sh 1 5
, 就能测试 trace01.txt
到 trace05.txt
, 并将结果分别保存到 _tshref_output.txt
和 _tsh_output.txt
。之后用各种编辑器自带的比较文件功能就能很方便的比较输出异同。
代码
1 |
|
用法简述
- 保存脚本:
- 将上述代码保存到一个文件,例如
test_traces.sh
。
- 将上述代码保存到一个文件,例如
- 赋予执行权限:
- 在终端中运行以下命令,为脚本添加执行权限:
1
chmod +x test_traces.sh
- 这一步是必须的,因为在 Linux/Unix 系统中,脚本默认没有执行权限,需要手动赋予。
- 在终端中运行以下命令,为脚本添加执行权限:
- 运行脚本:
- 使用以下格式运行脚本:
1
./test_traces.sh lower upper
lower
:起始的 trace 文件编号(1-16)。upper
:结束的 trace 文件编号(1-16)。
- 示例:
1
./test_traces.sh 1 5
- 这将测试
trace01.txt
到trace05.txt
,并将结果分别保存到_tshref_output.txt
和_tsh_output.txt
。
- 这将测试
- 使用以下格式运行脚本:
- 检查输出:
- 测试完成后,比较
_tshref_output.txt
(参考实现结果)和_tsh_output.txt
(学生实现结果)。 - 用各种编辑器自带的比较文件功能就能很方便的比较输出异同。
- 测试完成后,比较
注意事项
- 环境要求:确保当前目录下有
sdriver.pl
、tshref
、tsh
以及对应的traceXX.txt
文件,否则脚本会报错。 - 覆盖输出:每次运行脚本时,输出文件(
_tshref_output.txt
和_tsh_output.txt
)会被清空并重新生成。