
如何通过调试学习 nginx ?
我是张小方,公众号「高性能服务器开发」,前携程旅行网技术专家组专家,擅长高性能服务器的开发!
在实际的应用中,有一类应用会通过 Linux 函数 fork 出新的子进程。以 nginx 为例,nginx 对客户端的连接是采用多进程模型,当 nginx 接受客户端连接后,创建一个新的进程来处理该连接上的信息来往。新产生的进程与原进程互为父子关系。那么如何用 gdb 调试这样父子进程呢?一般有两种方法:
方法一
用 gdb 先调试父进程,等子进程被 fork 出来后,使用 gdb attach 到子进程上去。当然,您需要重新开启一个 Shell 窗口用于调试,gdb attach 的用法在前面已经介绍过了。
我们这里以调试 nginx 服务为例。
从 nginx 官网 http://nginx.org/en/download.html 下载最新的 nginx 源码,然后编译安装(笔者写作此书时,nginx 最新稳定版本是 1.18.0)。
注意:使用 make 命令编译时我们为了让生成的 nginx 带有调试符号信息同时关闭编译器优化,我们设置了 "-g -O0" 选项。
启动 nginx:
如上所示,nginx 默认会开启两个进程,在我的机器上以 root 用户运行的 nginx 进程是父进程,进程号 5246,以 nobody 用户运行的进程是子进程,进程号 5247。我们在当前窗口使用 gdb attach 5246 命令将 gdb 附加到 nginx 主进程上去。
此时我们就可以调试 nginx 父进程了,例如使用 bt 命令查看当前调用堆栈:
使用 f 1 命令切换到当前调用堆栈 #1,我们可以发现 nginx 父进程的主线程挂起在 src/core/nginx.c:382 处。
此时你可以使用 c 命令让程序继续运行起来,也可以添加断点或者做一些其他的调试操作。
再开一个 shell 窗口,使用 gdb attach 5247 将 gdb 附加到 nginx 子进程:
我们使用 bt 命令查看一下子进程的主线程当前调用堆栈:
可以发现子进程挂起在 src/event/modules/ngx_epoll_module.c:800 的 epollwait 函数处。我们在 epollwait 函数返回后(src/event/modules/ngx_epoll_module.c:804)加一个断点,然后使用 c 命令让 nginx 子进程继续运行。
接着我们在浏览器里面访问 nginx 的站点,我这里的 ip 地址是我的云主机地址,读者实际调试时改成自己的 nginx 服务器所在的地址,如果是本机就是 127.0.0.1,由于默认端口是 80,所以不用指定端口号。
此时我们回到 nginx 子进程的调试界面发现断点被触发:
使用 bt 命令可以获得此时的调用堆栈:
使用 info threads 命令可以查看子进程所有线程信息,我们发现 nginx 子进程只有一个主线程:
nginx 父进程不处理客户端请求,处理客户端请求的逻辑在子进程中,在单个子进程客户端请求数量达到一定数量时,父进程会重新 fork 一个新的子进程来处理新的客户端请求,也就是说子进程数量可以有多个,你可以开多个 shell 窗口,使用 gdb attach 到各个子进程上去调试。
总结起来,我们可以使用这种方法添加各种断点调试 nginx 的功能,慢慢我们就能熟悉 nginx 的各个内部逻辑了。
然而,方法一存在一个缺点,即程序已经启动了,我们只能使用 gdb 观察程序在这之后的行为,如果我们想调试程序从启动到运行起来之间的执行流程,方法一可能不太适用。有些读者可能会说,我用 gdb 附加到进程后,我加好断点然后使用 run 命令重启进程这样不就可以调试程序从启动到运行起来之间的执行流程了。问题是这种方法不是通用的,因为对于多进程服务模型,有些父子进程有一定的依赖关系,是不方便在运行过程中重启的。这个时候就可以使用方法二来调试了。
方法二
gdb 调试器提供一个选项叫 follow-fork ,通过 set follow-fork mode 来设置是当一个进程 fork 出新的子进程时,gdb 是继续调试父进程(取值是 parent)还是子进程(取值是 child),默认是父进程(取值是 parent)。
我们可以使用 show follow-fork mode 查看当前值:
我们还是以调试 nginx 为例,先进入 nginx 可执行文件所在的目录,将方法一中的 nginx 服务停下来:
nginx 源码中存在这样的逻辑,这个逻辑会在程序 main 函数处被调用:
如上述代码中注释所示,为了不让主进程退出,我们在 nginx 的配置文件中增加一行:
这样 nginx 就不会调用 ngx_daemon 函数了。
接下来,我们执行 gdb nginx,然后通过设置参数将配置文件 nginx.conf 传给待调试的 nginx 进程:
接着输入 run 命令尝试运行 nginx:
如前文所述,gdb 遇到 fork 指令时默认会 attach 到父进程去,因此上述输出中有一行提示 ”Detaching after fork from child process 7509“,我们按 Ctrl + C 将程序中断下来,然后输入 bt 命令查看当前调用堆栈,输出的堆栈信息和我们在方法一中看到的父进程的调用堆栈一样,说明 gdb 在程序 fork 之后确实 attach 了父进程:
如果想在 fork 之后 gdb 去 attach 子进程,我们可以在程序运行之前在 gdb 中设置 set follow-fork child,然后使用 run 命令重新运行程序。
我们接着按 Ctrl +C 将程序中断下来,然后使用 bt 命令查看当前线程调用堆栈确实是我们在方法一中子进程的主线程所在的调用堆栈,这说明 gdb 确实 attach 到子进程了。
我们可以利用方法二调试程序 fork 之前和之后的任何逻辑,是一种较为通用的多进程调试方法,建议读者掌握。
如果你不熟悉 gdb,这里是一份 gdb 教程。
