ZYB ARTICLES REPOS

一个内核调试脚本的修改

脚本

在调试内核的时候,每次启动调试免不了要开qemu要开gdb。这至少要开两个terminal窗口才能做到。因此就想能不能用一个脚本搞定。于是写出了第一版的调试脚本。如下:

#!/bin/bash

qemu-system-i386 \
    -boot d \
    -drive file=disk.img,format=raw,index=0,media=disk \
    -drive file=kernel.iso,index=1,media=cdrom \
    -s -S \
    &

pid=$!
echo "pid is ${pid}"

i386-elf-gdb KERNEL.ELF -x gdbscript

kill -9 $pid

echo "kill pid ${pid}"

这个脚本在运行了一段时间后发现了一个问题,就是在gdb里调试的时候如果按 CTRL+C 想中断当前运行的 qemu 继续输入调试命令的时候发现 qemu 已经退出了。 提示

qemu-system-i386: terminating on signal 2 from pid 34216 (<unknown process>)
Remote communication error.  Target disconnected.: Broken pipe.

这个问题百思不得其解,不知道为什么在gdb里按 CTRL+C 会导致qemu收到SIGINT(2)信号。后来在两个终端分别执行qemugdb命令发现在gdb里按 CRTL+Cqemu 没有退出,也可以在 gdb 里继续输入命令调试了。这才意思到可能是bash脚本出问题了。

于是改出第二版本测试脚本

exec i386-elf-gdb KERNEL.ELF -x gdbscript

经过测试发现,所有症状没有变。

继续改出第三版本

#!/bin/bash

set -m

qemu-system-i386 \
    -boot d \
    -drive file=disk.img,format=raw,index=0,media=disk \
    -drive file=kernel.iso,index=1,media=cdrom \
    -s -S \
    &

pid=$!
echo "pid is ${pid}"

set +m

exec i386-elf-gdb KERNEL.ELF -x gdbscript

kill -9 $pid

echo "kill pid ${pid}"

这一版本的代码再在gdb里按 CTRL+C 后的确不会再发SIGINT信号给qemu了,但是当输入q退出gdb后发现,后面的killecho指令并没有执行。

因此继续改出第四版本

#!/bin/bash

set -m

qemu-system-i386 \
    -boot d \
    -drive file=disk.img,format=raw,index=0,media=disk \
    -drive file=kernel.iso,index=1,media=cdrom \
    -s -S \
    &

pid=$!
echo "pid is ${pid}"

set +m

(
    trap '' SIGINT
    i386-elf-gdb KERNEL.ELF -x gdbscript
)

kill -9 $pid

echo "kill pid ${pid}"

这一版本的测试脚本完全满足了要求。在gdb里按 CTRL+C 不再发 SIGINT 信号给 qemu 强制 qemu 退出了,也能在 gdb 里继续输入调试命令调试了。 在退出 gdb 后,后面的指令也会继续执行。

当然这个版本程序还可以继续进化,因为 gdb 本身支持 SIGINT 就可以直接写成下面这样。

#!/bin/bash

set -m

qemu-system-i386 \
    -boot d \
    -drive file=disk.img,format=raw,index=0,media=disk \
    -drive file=kernel.iso,index=1,media=cdrom \
    -s -S \
    &

pid=$!
echo "pid is ${pid}"

set +m

i386-elf-gdb KERNEL.ELF -x gdbscript

kill -9 $pid

echo "kill pid ${pid}"

测试

注:以下测试在macOS上进行

如果说在脚本执行期间按下 CRTL+C 会给脚本进程组都发一条 SIGINT,那么编写如下脚本

#!/bin/bash

sleep 10 &

echo "sleep 13"
sleep 13

运行watch -n1 "ps aux |grep sleep"监视sleep进程,当执行到sleep 13时,按下 CRTL+C,此时发现 sleep 10 进程还存活,并且会睡够10秒才退出。这是怎么回事呢?

继续编写测试代码sigint.c

#include 
#include 
#include 
#include 

void handle_sigint(int sig) {
    printf("\ncaught signal %d\n", sig);
    exit(0);
}

int main() {
    signal(SIGINT, handle_sigint);
    while (1) {
        printf("running...\n");
        sleep(1);
    }
    return 0;
}
gcc -o sigint sigint.c
#!/bin/bash
./sigint &

echo "sleep 13"
/bin/sleep 13

当执行到sleep 13时,按下 CRTL+C,此时发现 sigint 程序收到 SIGINT 信号退出了。

sleep 13
running...
running...
^C
caught signal 2

这个结果就与前面那个sleep 10的脚本看起来矛盾了。这是为什么呢?

继续把修改脚本

#!/bin/bash

./sigint &
sleep 10 &

echo "sleep 13"
sleep 13

监视命令:watch -n1 "ps aux | grep -e sigint -e sleep"

当执行到sleep 13时,按下 CRTL+C 可以看到 sigint 程序收到 SIGINT 信号退出了。但是sleep进程还是顽强地生存了10s。

猜测sleep进程在后台运行做了特殊处理。但根据ps命令的结果来看,没有改变进程组。那就可能是sleep在后台运行的时候忽略了SIGINT信号。

继续测试:

执行该脚本,按下 CRTL+C,此时发现 sigint 程序收到 SIGINT 信号退出了,sleep 10进程还存活。

经过测试发现,如果此时尝试执行killall -2 sleep是杀不死sleep的,如果killall -9 sleep是能杀死的。

进一步验证,如果在前台手动执行sleep 10,再用killall -2 sleep发现是能杀死sleep

因此可以得出结论,sleep在后台运行是会忽略掉SIGINT信号。

这个也可以写个脚本验证(这个只能在Linux系统上执行

#!/bin/bash

sleep 10 &
pid=$!

# -e选项来过滤感兴趣的系统调用。在这种情况下,关心的是rt_sigaction系统调用
strace -p $pid -e rt_sigaction &

echo "sleep 13"
sleep 13

启动监视sleep窗口后,执行本脚本,当执行到sleep 13时,这次不再按 CRTL+C 了,而是用killall -2 sleep来发送一个信号给所有的sleep进程。发送后可以看到sleep 13立即退出了,sleep 10还在运行,同时strace输出

strace: Process 358458 attached
--- SIGINT {si_signo=SIGINT, si_code=SI_USER, si_pid=358488, si_uid=0} ---

这也可以证明我们的猜测。

总结

这个脚本用到了几个知识点

  1. 一个脚本启动的所有后台和前台进程都是一个进程组
  2. 如果在脚本执行期间按 CRTL+C 会向进程组的所有进程发送 SIGINT
  3. 如果想让进程忽略掉来自进程组的信号的一个方法是可以将其设置为新的进程组

设定新的进程组的方法是使用bash的作业控制:使用set -m命令启用作业控制;使用set +m命令禁用作业控制,它们是 Bash shell 中用于控制作业控制(job control)的选项。作业控制是一种允许用户在同一个 shell 会话中并行执行多个任务的机制。

在使用 set -m(启用作业控制)的情况下,Bash 会为每个后台进程创建一个新的进程组。这样,每个后台进程都有自己的进程组,与其他后台进程和脚本的主进程分开。

如果使用set +m:禁用作业控制。禁用作业控制后,将无法在后台启动进程,也无法使用fg、bg、jobs等命令。

另外在bash脚本中使用圆括号的情况,圆括号 () 用于在子 shell 中执行一组命令。子 shell 是一个独立的 shell 进程,它从父进程继承了环境变量和当前工作目录,但对这些值所做的任何更改都不会影响父进程。当bash执行到圆括号内的代码时,它是会等到这个圆括号内的所有命令执行完才会继续执行吗?