Shlab调试记录和一键测试脚本

文章目录
  1. 1. 写在前面
  2. 2. 调试
    1. 2.1. trace16
    2. 2.2. trace13
    3. 2.3. 感想
  3. 3. 一键测试脚本
    1. 3.1. 代码
    2. 3.2. 用法简述
    3. 3.3. 注意事项

写在前面

用时 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.

就因为这一个负号, 我调试了两个半小时, 而且我感觉我一路下来的各种猜想也都很合理, 只能感叹系统级代码真难调试啊。

感想

也算是学到了几招吧:

  1. 在调用系统函数时一定要检查返回值, 不然会报一些很难调试的错误
  2. 在修改全局变量时一定要用 sigprocmask 拦截别的信号, 避免冲突
  3. 子进程只要状态发生变化就会发 SIGCHLD 信号给父进程, 终止、暂停、恢复都会发送这个信号
  4. 可以用 waitpid 来检查子进程的变化状态, 它会回收终止的子进程
  5. 只在信号处理程序中调用异步安全的函数
  6. 调试时大胆猜想, 小心求证

一键测试脚本

我让 AI 写了一份一键测试脚本, 比官方的形如 make test13 的测试方便不少。脚本的功能是在 traceA 到 traceB(要求 A < B)上分别运行 tsh 和 tshref, 并把输出结果放到两个文件中。比如说, 如果我们输入 ./test_traces.sh 1 5, 就能测试 trace01.txttrace05.txt, 并将结果分别保存到 _tshref_output.txt_tsh_output.txt。之后用各种编辑器自带的比较文件功能就能很方便的比较输出异同。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/bin/bash

# 脚本名称:test_traces.sh

# 显示用法
usage() {
echo "用法: $0 lower upper"
echo " lower: 开始的 trace 文件编号 (1-16)"
echo " upper: 结束的 trace 文件编号 (1-16)"
echo "示例: $0 1 5"
echo " 这将测试 trace01.txt 到 trace05.txt,并将输出分别保存到 _tshref_output.txt 和 _tsh_output.txt"
}

# 检查是否提供了两个参数
if [ "$#" -ne 2 ]; then
echo "错误: 请提供 lower 和 upper 两个参数"
usage
exit 1
fi

lower=$1
upper=$2
ref_output_file="_tshref_output.txt"
tsh_output_file="_tsh_output.txt"

# 清空输出文件(如果已存在)
> "$ref_output_file"
> "$tsh_output_file"

# 获取当前工作目录(模仿 make 的 Entering directory 格式)
directory=$(pwd)

# 循环运行指定范围的 trace 文件
for ((i=lower; i<=upper; i++)); do
# 补齐两位数格式(例如 01, 02, ..., 16)
trace_num=$(printf "%02d" $i)

# 输出 make 样式的进入目录信息(仅第一次循环时)
if [ $i -eq $lower ]; then
echo "make[1]: Entering directory \`$directory'" >> "$ref_output_file"
echo "make[1]: Entering directory \`$directory'" >> "$tsh_output_file"
fi

# 为 tshref 输出添加分隔符和内容
echo "===== Trace $trace_num =====" >> "$ref_output_file"
echo "./sdriver.pl -t trace$trace_num.txt -s ./tshref -a \"-p\"" >> "$ref_output_file"
head -n 1 "trace$trace_num.txt" >> "$ref_output_file"
./sdriver.pl -t "trace$trace_num.txt" -s ./tshref -a "-p" >> "$ref_output_file" 2>&1
echo "" >> "$ref_output_file" # 添加空行作为间隔

# 为 tsh 输出添加分隔符和内容
echo "===== Trace $trace_num =====" >> "$tsh_output_file"
echo "./sdriver.pl -t trace$trace_num.txt -s ./tsh -a \"-p\"" >> "$tsh_output_file"
head -n 1 "trace$trace_num.txt" >> "$tsh_output_file"
./sdriver.pl -t "trace$trace_num.txt" -s ./tsh -a "-p" >> "$tsh_output_file" 2>&1
echo "" >> "$tsh_output_file" # 添加空行作为间隔
done

echo "测试完成。参考实现结果已保存到 $ref_output_file,学生实现结果已保存到 $tsh_output_file"

用法简述

  1. 保存脚本:
    • 将上述代码保存到一个文件,例如 test_traces.sh
  2. 赋予执行权限:
    • 在终端中运行以下命令,为脚本添加执行权限:
      1
      chmod +x test_traces.sh
    • 这一步是必须的,因为在 Linux/Unix 系统中,脚本默认没有执行权限,需要手动赋予。
  3. 运行脚本:
    • 使用以下格式运行脚本:
      1
      ./test_traces.sh lower upper
      • lower:起始的 trace 文件编号(1-16)。
      • upper:结束的 trace 文件编号(1-16)。
    • 示例:
      1
      ./test_traces.sh 1 5
      • 这将测试 trace01.txttrace05.txt,并将结果分别保存到 _tshref_output.txt_tsh_output.txt
  4. 检查输出:
    • 测试完成后,比较 _tshref_output.txt(参考实现结果)和 _tsh_output.txt(学生实现结果)。
    • 用各种编辑器自带的比较文件功能就能很方便的比较输出异同。

注意事项

  • 环境要求:确保当前目录下有 sdriver.pltshreftsh 以及对应的 traceXX.txt 文件,否则脚本会报错。
  • 覆盖输出:每次运行脚本时,输出文件(_tshref_output.txt_tsh_output.txt)会被清空并重新生成。