一个内核调试脚本的修改
脚本
在调试内核的时候,每次启动调试免不了要开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)信号。后来在两个终端分别执行qemu
和gdb
命令发现在gdb
里按 CRTL+C
后 qemu
没有退出,也可以在 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
后发现,后面的kill
、echo
指令并没有执行。
因此继续改出第四版本
#!/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} ---
这也可以证明我们的猜测。
总结
这个脚本用到了几个知识点
- 一个脚本启动的所有后台和前台进程都是一个进程组
- 如果在脚本执行期间按
CRTL+C
会向进程组的所有进程发送SIGINT
- 如果想让进程忽略掉来自进程组的信号的一个方法是可以将其设置为新的进程组
设定新的进程组的方法是使用bash
的作业控制:使用set -m
命令启用作业控制;使用set +m
命令禁用作业控制,它们是 Bash shell 中用于控制作业控制(job control)的选项。作业控制是一种允许用户在同一个 shell 会话中并行执行多个任务的机制。
在使用 set -m
(启用作业控制)的情况下,Bash 会为每个后台进程创建一个新的进程组。这样,每个后台进程都有自己的进程组,与其他后台进程和脚本的主进程分开。
如果使用set +m
:禁用作业控制。禁用作业控制后,将无法在后台启动进程,也无法使用fg、bg、jobs等命令。
另外在bash
脚本中使用圆括号的情况,圆括号 () 用于在子 shell 中执行一组命令。子 shell 是一个独立的 shell 进程,它从父进程继承了环境变量和当前工作目录,但对这些值所做的任何更改都不会影响父进程。当bash执行到圆括号内的代码时,它是会等到这个圆括号内的所有命令执行完才会继续执行吗?