GDB调试器不只可以调试多线程程序,还可以调试多进程程序。
对于 C 和 C++ 程序而言,多进程的实现往往借助的是<unistd.h>头文件中的 fork() 函数或者 vfork() 函数。举个例子:
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
printf("this is child,pid = %d\n",getpid());
}
else
{
printf("this is parent,pid = %d\n",getpid());
}
return 0;
}
程序的存储路径为~/demo/myfork.c。可以看到,程序中包含 2 个进程,分别为父进程(又称主进程)和使用 fork() 函数分离出的子进程。
事实上在多数 Linux 发行版系统中,GDB 并没有对多进程程序提供友好的调试功能。无论程序中调用了多少次 fork() 函数(或者 vfork() 函数),从父进程中分离出多少个子进程,GDB 默认只调试父进程,而不调试子进程。
那么问题就出现了,如何使用 GDB 调试多进程程序中的子进程呢?
首先,无论父进程还是子进程,都可以借助 attach 命令启动 GDB 调试它。attach 命令用于调试正在运行的进程,要知道对于每个运行的进程,操作系统都会为其配备一个独一无二的 ID 号。在得知目标子进程 ID 号的前提下,就可以借助 attach 命令来启动 GDB 对其进行调试。
这里还需要解决一个问题,很多场景中子进程的执行时间都是一瞬而逝的,这意味着,我们可能还未查到它的进程 ID 号,该进程就已经执行完了,何谈借助 attach 命令对其调试呢?对于 C、C++ 多进程程序,解决该问题最简单直接的方法是,在目标进程所执行代码的开头位置,添加一段延时执行的代码。
例如,将上面程序中if(pid==0)判断语句整体做如下修改:
if(pid == 0)
{
int num =10;
while(num==10){
sleep(10);
}
printf("this is child,pid = %d\n",getpid());
}
可以看到,通过添加第 3~6 行代码,该进程执行时会直接进入死循环。这样做的好处有 2 个,其一是帮助 attach 命令成功捕捉到要调试的进程;其二是使用 GDB 调试该进程时,进程中真正的代码部分尚未得到执行,使得我们可以从头开始对进程中的代码进行调试。
有读者可能会问,进程都已经进行死循环了,后续代码还可以进行调试吗?当然可以,以上面示例中给出的死循环,我们只需用 print 命令临时修改 num 变量的值,即可使程序跳出循环,从而执行后续代码。
就以调试修改后的 myfork.c 程序(已将其编译为 myfork.exe 可执行文件)为例:
对于子进程 ID 号的获取,除了依靠 GDB 调试器打印出的信息,也可以使用 pidof 命令手动获取。有关 pidof 命令获取进程 ID 好的具体用法,我已经在《调用GDB的几种方式》一节中做了详细的讲解,这里不再重复赘述。
前面提到,GDB 调试多进程程序时默认只调试父进程。对于内核版本为 2.5.46 甚至更高的 Linux 发行版系统来说,可以通过修改 follow-fork-mode 或者 detach-on-fork 选项的值来调整这一默认设置。
确切地说,对于使用 fork() 或者 vfork() 函数构建的多进程程序,借助 follow-fork-mode 选项可以设定 GDB 调试父进程还是子进程。该选项的使用语法格式为:
参数 mode 的可选值有 2 个:
举个例子:
通过执行如下命令,我们可以轻松了解到当前调试环境中 follow-fork-mode 选项的值:
注意,借助 follow-fork-mode 选项,我们只能选择调试子进程还是父进程,且一经选定,调试过程中将无法改变。如果既想调试父进程,又想随时切换并调试某个子进程,就需要借助 detach-on-fork 选项。
detach-on-fork 选项的语法格式如下:
其中,mode 参数的可选值有 2 个:
和 detach-on-fork 搭配使用的,还有如表 1 所示的几个命令。
命令语法格式 | 功 能 |
---|---|
(gdb)show detach-on-fork | 查看当前调试环境中 detach-on-fork 选项的值。 |
(gdb) info inferiors | 查看当前调试环境中有多少个进程。其中,进程 id 号前带有 * 号的为当前正在调试的进程。 |
(gdb) inferiors id | 切换到指定 ID 编号的进程对其进行调试。 |
(gdb) detach inferior id | 断开 GDB 与指定 id 编号进程之间的联系,使该进程可以独立运行。不过,该进程仍存在 info inferiors 打印的列表中,其 Describution 列为 <null>,并且借助 run 仍可以重新启用。 |
(gdb) kill inferior id | 断开 GDB 与指定 id 编号进程之间的联系,并中断该进程的执行。不过,该进程仍存在 info inferiors 打印的列表中,其 Describution 列为 <null>,并且借助 run 仍可以重新启用。 |
remove-inferior id | 彻底删除指令 id 编号的进程(从 info inferiors 打印的列表中消除),不过在执行此操作之前,需先使用 detach inferior id 或者 kill inferior id 命令将该进程与 GDB 分离,同时确认其不是当前进程。 |
除表 1 罗列的这几个命令,GDB 调试其提供有其它的一些命令,由于不常用,这里不再罗列,读者可前往 GDB官网自行查看。
这里仍以调试 myfork.c 程序为例,不过为了让读者清楚地感受 detach-on-fork 选项的功能,这里需要对 else 语句块的代码进行如下修改:
else
{
int mnum=5;
while(mnum==5){
sleep(1);
}
printf("this is parent,pid = %d\n",getpid());
}
也就是说,myfork.c 程序中,父进程和子进程中各拥有一个死循环。
在此基础上,进行如下调试:
可以看到,通过设置 detach-on-fork 选项值为 off,再配合使用 info inferiors 等命令,即可随意切换到当前环境中的各个进程,并对它们进行调试。
感兴趣的读者,可自行以默认模式(即 detach-on-fork 值为 on)调试该程序,对比它们之间的区别。