Linux系统调用nginx多进程gdbattach的用法

网络收集 7个月前 (07-06) Linux知识 228 0
TAGS:
国内服务器大促销查看详情

这里我们说的多进程程序指的是一个进程使用 Linux 系统调用 fork() 函数产生的子进程,没有相互关联的进程就是普通的 gdb 调试,不必刻意讨论。

在实际的应用中,如有这样一类程序,如 nginx,对于客户端的连接是采用多进程模型,当 nginx 接受客户端连接后,创建一个新的进程来处理这一路连接上的信息来往。新产生的进程与原进程互为父子关系。那么如何用 gdb 调试这样父子进程呢?一般有两种方法:

方法一

用 gdb 先调试父进程,等子进程fork出来后,使用 gdb attach 到子进程上去。当然,您需要重新开启一个 Shell 窗口用于调试,gdb attach 的用法在前面已经介绍过了。


我们这里以调试 nginx 服务为例。


从 nginx 官网 http://nginx.org/en/download.html 下载最新的 nginx 源码,然后编译安装(笔者写作此文时,nginx 最新稳定版本是 1.18.0)。

## 下载 nginx 源码
[root@iZbp14iz399acush5e8ok7Z zhangyl]# wget http://nginx.org/download/nginx-1.18.0.tar.gz
--2020-07-05 17:22:10--  http://nginx.org/download/nginx-1.18.0.tar.gz
Resolving nginx.org (nginx.org)... 95.211.80.22762.210.92.352001:1af8:4060:a004:21::e3
Connecting to nginx.org (nginx.org)|95.211.80.227|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1039530 (1015K) [application/octet-stream]
Saving to: ‘nginx-1.18.0.tar.gz’

nginx-1.18.0.tar.gz                            100%[===================================================================================================>]   1015K   666KB/s    in 1.5s    

2020-07-05 17:22:13 (666 KB/s) - ‘nginx-1.18.0.tar.gz’ saved [1039530/1039530]

## 解压nginx
[root@iZbp14iz399acush5e8ok7Z zhangyl]# tar zxvf nginx-1.18.0.tar.gz

## 编译nginx
[root@iZbp14iz399acush5e8ok7Z zhangyl]# cd nginx-1.18.0
[root@iZbp14iz399acush5e8ok7Z nginx-1.18.0]# ./configure --prefix=/usr/local/nginx
[root@iZbp14iz399acush5e8ok7Z nginx-1.18.0]make CFLAGS="-g -O0"

## 安装,这样nginx就被安装到/usr/local/nginx/目录下
[root@iZbp14iz399acush5e8ok7Z nginx-1.18.0]make install

注意:使用 make 命令编译时我们为了让生成的 nginx 带有调试符号信息同时关闭编译器优化,我们设置了 "-g -O0" 选项。

启动 nginx:

[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
[root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)

如上所示,nginx 默认会开启两个进程,在我的机器上以 root 用户运行的 nginx 进程是父进程,进程号 5246,以 nobody 用户运行的进程是子进程,进程号 5247。我们在当前窗口使用 gdb attach 5246 命令将 gdb 附加到 nginx 主进程上去。

[root@iZbp14iz399acush5e8ok7Z sbin]# gdb attach 5246
...省略部分输出信息...
0x00007fd42a103c5d in sigsuspend () from /lib64/libc.so.6
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-72.el8_1.1.x86_64 libxcrypt-4.1.1-4.el8.x86_64 pcre-8.42-4.el8.x86_64 sssd-client-2.2.0-19.el8.x86_64 zlib-1.2.11-10.el8.x86_64
(gdb)

此时我们就可以调试 nginx 父进程了,例如使用 bt 命令查看当前调用堆栈:

(gdb) bt
#0  0x00007fd42a103c5d in sigsuspend () from /lib64/libc.so.6
#1  0x000000000044ae32 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:164
#2  0x000000000040bc05 in main (argc=3, argv=0x7ffe49109d68) at src/core/nginx.c:382
(gdb) f 1
#1  0x000000000044ae32 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:164
164             sigsuspend(&set);
(gdb) l
159                 }
160             }
161
162             ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "sigsuspend");
163
164             sigsuspend(&set);
165
166             ngx_time_update();
167
168             ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
(gdb)

使用 f 1 命令切换到当前调用堆栈 #1,我们可以发现 nginx 父进程的主线程挂起在 src/core/nginx.c:382 处。

此时你可以使用 c 命令让程序继续运行起来,也可以添加断点或者做一些其他的调试操作。

再开一个 shell 窗口,使用 gdb attach 5247 将 gdb 附加到 nginx 子进程:

[root@iZbp14iz399acush5e8ok7Z sbin]# gdb attach 5247
...部署输出省略...
0x00007fd42a1c842b in epoll_wait () from /lib64/libc.so.6
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-72.el8_1.1.x86_64 libblkid-2.32.1-17.el8.x86_64 libcap-2.26-1.el8.x86_64 libgcc-8.3.1-4.5.el8.x86_64 libmount-2.32.1-17.el8.x86_64 libselinux-2.9-2.1.el8.x86_64 libuuid-2.32.1-17.el8.x86_64 libxcrypt-4.1.1-4.el8.x86_64 pcre-8.42-4.el8.x86_64 pcre2-10.32-1.el8.x86_64 sssd-client-2.2.0-19.el8.x86_64 systemd-libs-239-18.el8_1.2.x86_64 zlib-1.2.11-10.el8.x86_64
(gdb)

我们使用 bt 命令查看一下子进程的主线程当前调用堆栈:

(gdb) bt
#0  0x00007fd42a1c842b in epoll_wait () from /lib64/libc.so.6
#1  0x000000000044e546 in ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:800
#2  0x000000000043f317 in ngx_process_events_and_timers (cycle=0x1703720) at src/event/ngx_event.c:247
#3  0x000000000044c38f in ngx_worker_process_cycle (cycle=0x1703720, data=0x0) at src/os/unix/ngx_process_cycle.c:750
#4  0x000000000044926f in ngx_spawn_process (cycle=0x1703720, proc=0x44c2e1 <ngx_worker_process_cycle>, data=0x0, name=0x4cfd70 "worker process", respawn=-3)
    at src/os/unix/ngx_process.c:199
#5  0x000000000044b5a4 in ngx_start_worker_processes (cycle=0x1703720, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
#6  0x000000000044acf4 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:131
#7  0x000000000040bc05 in main (argc=3, argv=0x7ffe49109d68) at src/core/nginx.c:382
(gdb) f 1
#1  0x000000000044e546 in ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:800
800         events = epoll_wait(ep, event_list, (int) nevents, timer);
(gdb)

可以发现子进程挂起在 src/event/modules/ngx_epoll_module.c:800 的 epoll_wait 函数处。我们在 epoll_wait 函数返回后(src/event/modules/ngx_epoll_module.c:804)加一个断点,然后使用 c 命令让 nginx 子进程继续运行。

800         events = epoll_wait(ep, event_list, (int) nevents, timer);
(gdb) list
795         /* NGX_TIMER_INFINITE == INFTIM */
796
797         ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log0,
798                        "epoll timer: %M"timer);
799
800         events = epoll_wait(ep, event_list, (int) nevents, timer);
801
802         err = (events == -1) ? ngx_errno : 0;
803
804         if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
(gdb) b 804
Breakpoint 1 at 0x44e560: file src/event/modules/ngx_epoll_module.c, line 804.
(gdb) c
Continuing.

接着我们在浏览器里面访问 nginx 的站点,我这里的 ip 地址是我的云主机地址,读者实际调试时改成自己的 nginx 服务器所在的地址,如果是本机就是 127.0.0.1,由于默认端口是 80,所以不用指定端口号。

http://101.37.25.166:80
等价于
http://101.37.25.166

此时我们回到 nginx 子进程的调试界面发现断点被触发:

Breakpoint 1, ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:804
804         if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
(gdb) 

使用 bt 命令可以获得此时的调用堆栈:

(gdb) bt
#0  ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:804
#1  0x000000000043f317 in ngx_process_events_and_timers (cycle=0x1703720) at src/event/ngx_event.c:247
#2  0x000000000044c38f in ngx_worker_process_cycle (cycle=0x1703720, data=0x0) at src/os/unix/ngx_process_cycle.c:750
#3  0x000000000044926f in ngx_spawn_process (cycle=0x1703720, proc=0x44c2e1 <ngx_worker_process_cycle>, data=0x0, name=0x4cfd70 "worker process", respawn=-3)
    at src/os/unix/ngx_process.c:199
#4  0x000000000044b5a4 in ngx_start_worker_processes (cycle=0x1703720, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
#5  0x000000000044acf4 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:131
#6  0x000000000040bc05 in main (argc=3, argv=0x7ffe49109d68) at src/core/nginx.c:382
(gdb) 

使用 info threads 命令可以查看子进程所有线程信息,我们发现 nginx 子进程只有一个主线程:

[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
[root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
0

nginx 父进程不处理客户端请求,处理客户端请求的逻辑在子进程中,在单个子进程客户端请求数量达到一定数量时,父进程会重新 fork 一个新的子进程来处理新的客户端请求,也就是说子进程数量可以有多个,你可以开多个 shell 窗口,使用 gdb attach 到各个子进程上去调试。

总结起来,我们可以使用这种方法添加各种断点调试 nginx 的功能,慢慢我们就能熟悉 nginx 的各个内部逻辑了。

然而,方法一存在一个缺点,即程序已经启动了,我们只能使用 gdb 观察程序在这之后的行为,如果我们想调试程序从启动到运行起来之间的执行流程,方法一可能不太适用。有些读者可能会说,我用 gdb 附加到进程后,我加好断点然后使用 run 命令重启进程这样不就可以调试程序从启动到运行起来之间的执行流程了。问题是这种方法不是通用的,因为对于多进程服务模型,有些父子进程有一定的依赖关系,是不方便在运行过程中重启的。这个时候就可以使用方法二来调试了。

方法二

gdb 调试器提供一个选项叫 follow-fork ,通过 set follow-fork mode 来设置是当一个进程 fork 出新的子进程时,gdb 是继续调试父进程还是子进程(取值是 child),默认是父进程(取值是 parent)。

[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
[root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
1

我们可以使用 show follow-fork mode 查看当前值:

[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
[root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
2

我们还是以调试 nginx 为例,先进入 nginx 可执行文件所在的目录,将方法一中的 nginx 服务停下来:

[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
[root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
3

nginx 源码中存在这样的逻辑,这个逻辑会在程序 main 函数处被调用:

[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
[root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
4

如上述代码中注释所示,为了不让主进程退出,我们在 nginx 的配置文件中增加一行:

[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
[root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
5

这样 nginx 就不会调用 ngx_daemon 函数了。

接下来,我们执行 gdb nginx,然后通过设置参数将配置文件 nginx.conf 传给待调试的 nginx 进程:

[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
[root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
6

接着输入 run 命令尝试运行 nginx:

[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
[root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
7

如前文所述,gdb 遇到 fork 指令时默认会 attach 到父进程去,因此上述输出中有一行提示 ”Detaching after fork from child process 7509“,我们按 Ctrl + C 将程序中断下来,然后输入 bt 命令查看当前调用堆栈,输出的堆栈信息和我们在方法一中看到的父进程的调用堆栈一样,说明 gdb 在程序 fork 之后确实 attach 了父进程:

[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
[root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
8

如果想在 fork 之后 gdb 去 attach 子进程,我们可以在程序运行之前在 gdb 中设置 set follow-fork child,然后使用 run 命令重新运行程序。

[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
[root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
9

我们接着按 Ctrl +C 将程序中断下来,然后使用 bt 命令查看当前线程调用堆栈确实是我们在方法一中子进程的主线程所在的调用堆栈,这说明 gdb 确实 attach 到子进程了。

我们可以利用方法二调试程序 fork 之前和之后的任何逻辑,是一种较为通用的多进程调试方法,建议读者掌握。


文章来自:程序员小方(微信公众号)

版权声明:部分文章内容、图片来源于互联网获取,如有侵权请联系删除,发送邮件:server889#qq.com 请将#改为@,我们将第一时间审核处理!

相关推荐

网友评论

  • (*)

最新评论

相关推荐

php源码论坛源码香港vps数据中心西丽机房江阴数据西安机房win7腾讯管家门户整站PHP系统小程序香港服务器服务器租用电影网站SEO迅雷看看装修网站装饰整站系统NAS和服务器香港主机海外服务器香港机柜江阴市云计算数据中心高防服务器deituiCMS得推分类香港服务器cn2香港机房下载网站游戏服务器国外服务器端口描述香港 GIA-VPS宜兴国际数据中心网站服务器linux系统服务器屏蔽网站外贸公司韩国服务器独立服务器韩国高配低价服务器西安高新电信机房大型商城超市PHP专用服务器建设网站采集站网站站群广渠门IDC机房香港CN2CDNDDoS防火墙香港固定IP专线新之洲数据韩国机房CDN加速自动采集免费源码高清壁纸香港美国VPSBGP多线国内服务器黑客攻击访问速度高防云主机本溪市南地IDC机房服务器机房区块链小说网站维护网站西安联通机房服务器托管棋牌游戏棋牌服务器服务器配置企业服务器香港将军澳CN2 VPS北京大兴大族机房BGP服务器硬件网站建设云服务器大连黑石礁机房香港数据中心数据库服务器数据库系统香港云服务器襄阳联通枢纽楼IDC机房广告联盟联盟服务器WSTShop个人网店人才网独立云服务器企业云服务器服务器访问速度中国移动(广东湛江)数据中心交友网站婚恋网站主机网站优化香港日本韩国中国移动安徽(淮南)数据中心小程序服务器衡阳服务器租用