這里我們說的多進程程序指的是一個進程使用 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.227, 62.210.92.35, 2001: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->log, 0,
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 子進程只有一個主線程:
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7fd42b17c740 (LWP 5247) "nginx" ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:804
(gdb)
nginx 父進程不處理客戶端請求,處理客戶端請求的邏輯在子進程中,在單個子進程客戶端請求數量達到一定數量時,父進程會重新 fork 一個新的子進程來處理新的客戶端請求,也就是說子進程數量可以有多個,你可以開多個 shell 窗口,使用 gdb attach 到各個子進程上去調試。
總結起來,我們可以使用這種方法添加各種斷點調試 nginx 的功能,慢慢我們就能熟悉 nginx 的各個內部邏輯了。
然而,方法一存在一個缺點,即程序已經啟動了,我們只能使用 gdb 觀察程序在這之后的行為,如果我們想調試程序從啟動到運行起來之間的執行流程,方法一可能不太適用。有些讀者可能會說,我用 gdb 附加到進程后,我加好斷點然后使用 run 命令重啟進程這樣不就可以調試程序從啟動到運行起來之間的執行流程了。問題是這種方法不是通用的,因為對於多進程服務模型,有些父子進程有一定的依賴關系,是不方便在運行過程中重啟的。這個時候就可以使用方法二來調試了。
方法二
gdb 調試器提供一個選項叫 follow-fork ,通過 set follow-fork mode 來設置是當一個進程 fork 出新的子進程時,gdb 是繼續調試父進程還是子進程(取值是 child),默認是父進程(取值是 parent)。
# fork之后gdb attach到子進程
set follow-fork child
# fork之后gdb attach到父進程,這是默認值
set follow-fork parent
我們可以使用 show follow-fork mode 查看當前值:
(gdb) show follow-fork mode
Debugger response to a program call of fork or vfork is "child".
我們還是以調試 nginx 為例,先進入 nginx 可執行文件所在的目錄,將方法一中的 nginx 服務停下來:
[root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin/
[root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -s stop
nginx 源碼中存在這樣的邏輯,這個邏輯會在程序 main 函數處被調用:
//src/os/unix/ngx_daemon.c:13行
ngx_int_t
ngx_daemon(ngx_log_t *log)
{
int fd;
switch (fork()) {
case -1:
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed");
return NGX_ERROR;
//fork出來的子進程走這個case
case 0:
break;
//父進程中fork返回值是子進程的PID,大於0,因此走這個case
//因此主進程會退出
default:
exit(0);
}
//...省略部分代碼...
}
如上述代碼中注釋所示,為了不讓主進程退出,我們在 nginx 的配置文件中增加一行:
daemon off;
這樣 nginx 就不會調用 ngx_daemon 函數了。
接下來,我們執行 gdb nginx
,然后通過設置參數將配置文件 nginx.conf 傳給待調試的 nginx 進程:
Quit anyway? (y or n) y
[root@iZbp14iz399acush5e8ok7Z sbin]# gdb nginx
...省略部分輸出...
Reading symbols from nginx...done.
(gdb) set args -c /usr/local/nginx/conf/nginx.conf
(gdb)
接着輸入 run 命令嘗試運行 nginx:
(gdb) run
Starting program: /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-72.el8_1.1.x86_64
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
warning: Loadable section ".note.gnu.property" outside of ELF segments
warning: Loadable section ".note.gnu.property" outside of ELF segments
warning: Loadable section ".note.gnu.property" outside of ELF segments
warning: Loadable section ".note.gnu.property" outside of ELF segments
[Detaching after fork from child process 7509]
如前文所述,gdb 遇到 fork 指令時默認會 attach 到父進程去,因此上述輸出中有一行提示 ”Detaching after fork from child process 7509“,我們按 Ctrl + C 將程序中斷下來,然后輸入 bt 命令查看當前調用堆棧,輸出的堆棧信息和我們在方法一中看到的父進程的調用堆棧一樣,說明 gdb 在程序 fork 之后確實 attach 了父進程:
^C
Program received signal SIGINT, Interrupt.
0x00007ffff6f73c5d in sigsuspend () from /lib64/libc.so.6
Missing separate debuginfos, use: yum debuginfo-install 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) bt
#0 0x00007ffff6f73c5d in sigsuspend () from /lib64/libc.so.6
#1 0x000000000044ae32 in ngx_master_process_cycle (cycle=0x71f720) at src/os/unix/ngx_process_cycle.c:164
#2 0x000000000040bc05 in main (argc=3, argv=0x7fffffffe4e8) at src/core/nginx.c:382
(gdb)
如果想在 fork 之后 gdb 去 attach 子進程,我們可以在程序運行之前在 gdb 中設置 set follow-fork child
,然后使用 run 命令重新運行程序。
(gdb) set follow-fork child
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[Attaching after Thread 0x7ffff7fe7740 (LWP 7664) fork to child process 7667]
[New inferior 2 (process 7667)]
[Detaching after fork from parent process 7664]
[Inferior 1 (process 7664) detached]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
^C
Thread 2.1 "nginx" received signal SIGINT, Interrupt.
[Switching to Thread 0x7ffff7fe7740 (LWP 7667)]
0x00007ffff703842b in epoll_wait () from /lib64/libc.so.6
(gdb) bt
#0 0x00007ffff703842b in epoll_wait () from /lib64/libc.so.6
#1 0x000000000044e546 in ngx_epoll_process_events (cycle=0x71f720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:800
#2 0x000000000043f317 in ngx_process_events_and_timers (cycle=0x71f720) at src/event/ngx_event.c:247
#3 0x000000000044c38f in ngx_worker_process_cycle (cycle=0x71f720, data=0x0) at src/os/unix/ngx_process_cycle.c:750
#4 0x000000000044926f in ngx_spawn_process (cycle=0x71f720, 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=0x71f720, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
#6 0x000000000044acf4 in ngx_master_process_cycle (cycle=0x71f720) at src/os/unix/ngx_process_cycle.c:131
#7 0x000000000040bc05 in main (argc=3, argv=0x7fffffffe4e8) at src/core/nginx.c:382
(gdb)
我們接着按 Ctrl +C 將程序中斷下來,然后使用 bt 命令查看當前線程調用堆棧確實是我們在方法一中子進程的主線程所在的調用堆棧,這說明 gdb 確實 attach 到子進程了。
我們可以利用方法二調試程序 fork 之前和之后的任何邏輯,是一種較為通用的多進程調試方法,建議讀者掌握。