![C++服务器开发精髓](https://wfqqreader-1252317822.image.myqcloud.com/cover/623/39479623/b_39479623.jpg)
2.7 使用gdb调试多进程程序——以调试Nginx为例
这里说的多进程程序指的是一个进程使用 Linux 系统调用 fork 函数产生的子进程,没有相互关联的进程调试指的是gdb调试单个进程,前面已经详细讲解过了。
在实际应用中,有一类应用会通过 Linux 函数 fork 出新的子进程。以 Nginx 为例,Nginx对客户端的连接采用了多进程模型,在接受客户端的连接后,会创建一个新的进程来处理该连接上的信息来往,新产生的进程与原进程互为父子关系。那么如何用gdb调试这样的父子进程呢?一般有两种方法,下面详细讲解。
1.方法一
先在一个Shell窗口中用gdb调试父进程,等子进程被fork出来后,再新开一个Shell窗口使用gdb attach命令将gdb attach到子进程上。
这里以调试Nginx服务为例。从Nginx官网下载最新的Nginx源码(本书采用的版本是1.18.0),然后编译和安装:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_155_1.jpg?sign=1738940342-SeRXcKzC9wkwAZnnKVISkIzDpT61bciD-0-cab0c3970b7aca60bfbc1a0de8374802)
注意:使用make命令编译时,我们为了让生成的Nginx带有调试符号信息同时关闭编译器优化,设置了“-g-O0”选项。
启动Nginx:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_155_2.jpg?sign=1738940342-yS7zEFhe8L9cXA9gKkRsCQ9t43wnMroa-0-50e3898b23c8c47a1e1993b2ca1a9833)
如上所示,Nginx默认开启两个进程,在笔者的机器上以root用户运行的Nginx进程是父进程,进程号是5246,以nobody用户运行的进程是子进程,进程号是5247。我们在当前窗口中使用gdb attach 5246命令将gdb attach到Nginx主进程上:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_155_3.jpg?sign=1738940342-uRWhnEOcc9HpBm73Oh3Iu3RzxzGWRveo-0-8458fec02fd648315fbe067be8abc5ee)
此时就可以调试Nginx父进程了,例如使用bt命令查看当前调用堆栈:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_155_4.jpg?sign=1738940342-5NZEsacXVIJzprgYUDoKdNOv6iClJn49-0-17c9fc243b0b55a22395a66ea7076f46)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_155_5.jpg?sign=1738940342-bZHTx7XQ3wZQmFFTWV3jRzr96b2NOHwE-0-46b1dfab13c36b7031dc81b98d9c6291)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_156_1.jpg?sign=1738940342-U89AKaVBnpzvbefsQNiFYrQK9Iau0ETk-0-0caaa1288672dfc6dc82f5776be1b150)
使用f 1命令切换到当前调用堆栈#1,可以发现Nginx父进程的主线程挂起在src/core/nginx.c:382处。
此时可以使用c命令让程序继续运行,也可以添加断点或者进行其他调试操作。
再开一个Shell窗口,使用gdb attach 5247命令将gdb attach到nginx子进程上:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_156_2.jpg?sign=1738940342-oRduTNUPb1PHnf7Mz7PGlbY3VSdBIHqX-0-5092092374243962c5e477ee1d678cbf)
使用bt命令查看子进程的主线程的当前调用堆栈:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_156_3.jpg?sign=1738940342-MWGcY7RLtFYeJ6aMj9woffeMKBprsgNL-0-fbd476d555ea5765600704aa1b2ec7af)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_157_1.jpg?sign=1738940342-FNwwekzfYS0iqs9nVWV2R0y9ArcTAE2Z-0-8f87ef0310d38b3fdc553dd934d83d98)
可以发现,子进程挂起在src/event/modules/ngx_epoll_module.c:800的epoll_wait函数处。我们在epoll_wait函数返回后(src/event/modules/ngx_epoll_module.c:804)加一个断点,然后使用c命令让Nginx子进程继续运行:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_157_2.jpg?sign=1738940342-4JVH4Fn14pIhYh36pUjsG7g3hUoPyXKp-0-1c5469679f48e7c881aaa42cd53bbf78)
接着在浏览器中访问Nginx网站,这里的 IP地址是笔者的云主机地址,读者在实际调试时将其改成自己的 Nginx 服务器所在的地址即可,如果是本机,那么地址就是127.0.0.1,由于默认端口是80,所以不用指定端口号:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_157_3.jpg?sign=1738940342-iZtasJy7BGeLaoYOilk4aT79r0ItRPvL-0-4033c25c53738798ff69573a05147c76)
此时回到nginx子进程的调试界面,发现断点被触发:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_157_4.jpg?sign=1738940342-rxJusGjgnMdsny2Y9earL5tDuUKjUJPU-0-7f040a60d1d921b22735dcc10cbd49e2)
使用bt命令可以获得此时的调用堆栈:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_157_5.jpg?sign=1738940342-SrUfAiKgip58bt0rkvlyx7E9oTEbWYzd-0-a71d5ba0b7c465cf772f7a79420f38a9)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_158_1.jpg?sign=1738940342-VbF7edfwpCLED6vlhgF4hlijK4sTOC0O-0-6459fc10853817f05cdd48880ab38ac6)
使用info threads命令可以查看子进程的所有线程信息,发现Nginx子进程只有一个主线程:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_158_2.jpg?sign=1738940342-5zHLizp9nxjrwKB5qdl91YoE2JTwnxBE-0-2f075b3f745e2b845bdc29b19500ea73)
Nginx父进程不处理客户端的请求,处理客户端请求的逻辑在子进程中,当单个子进程的客户端请求数达到一定数量时,父进程会重新 fork 一个新的子进程来处理新的客户端请求,也就是说子进程数量可以有多个,我们可以开多个 Shell 窗口,使用 gdb attach到各个子进程上调试。
总之,我们可以使用这种方法添加各种断点调试Nginx的功能,慢慢地就能熟悉Nginx的各个内部逻辑了。
然而,该方法存在一个缺点,即程序已经启动了,我们只能使用gdb观察程序在这之后的行为,如果想调试程序从启动到运行的执行流程,则可能不太适用。有些读者可能会说:用gdb attach到进程后,加好断点,然后使用run命令重启进程,这样就可以调试程序从启动到运行的执行流程了。问题是这种方法并不通用,因为对于多进程服务模型,有些父子进程有一定的依赖关系,是不方便在运行过程中重启的。这时方法二就比较合适了。
2.方法二
gdb调试器提供了一个follow-fork选项,通过set follow-fork mode设置一个进程fork出新的子进程时,gdb是继续调试父进程(取值是parent)还是继续调试子进程(取值是child),默认继续调试父进程(取值是parent):
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_158_3.jpg?sign=1738940342-PBV7S2jyQqKQ84l3GpjectVq5RAy4Hz4-0-ab146f9cdfce8f93e7904ef1a6557b91)
可以使用show follow-fork mode查看当前值:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_158_4.jpg?sign=1738940342-kEducZJffsX0p8OsIzz8hP4CWLK2FPBh-0-44e58170bf6813b587b8f24ba042a452)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_158_5.jpg?sign=1738940342-xa8WtHwWK9M0r4HOpW55pGAXw224RZ1W-0-91e64bea4bba849ad542838dad0d358c)
还是以调试nginx为例,先进入 nginx 可执行文件所在的目录,将方法一中的 Nginx服务停下来:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_158_6.jpg?sign=1738940342-PqMWvaF0NvKHMEm7abO5zUwUBTl6yqyg-0-02722d9d889b797ac86b2a3dfabc4c27)
在Nginx源码中存在这样的逻辑,这个逻辑会在程序main函数处被调用:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_159_1.jpg?sign=1738940342-HJmzEmzmFLnWDHU7n9YueCCOVbNEv1fv-0-e9d312053f13b151e642674a582a8267)
如以上代码中的注释所示,为了不让主进程退出,我们在Nginx的配置文件中增加一行,这样Nginx就不会调用ngx_daemon函数了:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_159_2.jpg?sign=1738940342-9FyUg88k4jVJn6aEMWcToQGekEdXYazr-0-402fb2fb88fc883c5d5d241402f875d6)
接下来执行gdb nginx,通过设置参数将配置文件nginx.conf传给待调试的Nginx进程:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_159_3.jpg?sign=1738940342-NAMhUuyoP2fhpu9onreYWFq8x55Ok5J8-0-dc812c033a7f26b86de98941e6944de5)
接着输入run命令尝试运行Nginx:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_159_4.jpg?sign=1738940342-rgHyRJzLwQ5Yp4R4IZdm6NdjxFliiU1Z-0-b66ab29060d95a798ec5607cce49d86a)
如前文所述,gdb遇到fork指令时默认会attach到父进程,因此在以上输出中有一行提示“Detaching after fork from child process 7509”,我们按Ctrl+C组合键将程序中断,然后输入 bt 命令查看当前调用堆栈,输出的堆栈信息和我们在方法一中看到的父进程的调用堆栈一样,说明gdb在程序fork之后确实attach到父进程了:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_159_5.jpg?sign=1738940342-HTJ2gRCOoMNTdwL4oTuW5uCZuPQyz0qQ-0-4f598a9e88319a09a0678a294e88caed)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_160_1.jpg?sign=1738940342-Zjx3sc0o9RV6hRQV63rXiCr4AYjVuhe5-0-4d8ae7b7331abd071af2da45abf1403f)
如果想让gdb在fork之后attach到子进程,则可以在程序运行之前设置set follow-fork child,然后使用run命令重新运行程序:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_160_2.jpg?sign=1738940342-JM3epHWn07mPphELb0HUFZ8PAZHcAj2P-0-29755f7542e8e227a67a9140fe551b58)
接着按Ctrl+C组合键将程序中断,然后使用bt命令查看当前线程的调用堆栈,结果显示它确实是方法一中子进程的主线程所在的调用堆栈,这说明gdb确实attach到子进程了。
我们可以利用方法二调试程序 fork之前和之后的任何逻辑,这是一种较为通用的多进程调试方法,建议掌握。