- C++服務器開發(fā)精髓
- 張遠龍
- 1709字
- 2021-07-23 18:22:25
2.7 使用gdb調(diào)試多進程程序——以調(diào)試Nginx為例
這里說的多進程程序指的是一個進程使用 Linux 系統(tǒng)調(diào)用 fork 函數(shù)產(chǎn)生的子進程,沒有相互關(guān)聯(lián)的進程調(diào)試指的是gdb調(diào)試單個進程,前面已經(jīng)詳細講解過了。
在實際應用中,有一類應用會通過 Linux 函數(shù) fork 出新的子進程。以 Nginx 為例,Nginx對客戶端的連接采用了多進程模型,在接受客戶端的連接后,會創(chuàng)建一個新的進程來處理該連接上的信息來往,新產(chǎn)生的進程與原進程互為父子關(guān)系。那么如何用gdb調(diào)試這樣的父子進程呢?一般有兩種方法,下面詳細講解。
1.方法一
先在一個Shell窗口中用gdb調(diào)試父進程,等子進程被fork出來后,再新開一個Shell窗口使用gdb attach命令將gdb attach到子進程上。
這里以調(diào)試Nginx服務為例。從Nginx官網(wǎng)下載最新的Nginx源碼(本書采用的版本是1.18.0),然后編譯和安裝:

注意:使用make命令編譯時,我們?yōu)榱俗屔傻腘ginx帶有調(diào)試符號信息同時關(guān)閉編譯器優(yōu)化,設置了“-g-O0”選項。
啟動Nginx:

如上所示,Nginx默認開啟兩個進程,在筆者的機器上以root用戶運行的Nginx進程是父進程,進程號是5246,以nobody用戶運行的進程是子進程,進程號是5247。我們在當前窗口中使用gdb attach 5246命令將gdb attach到Nginx主進程上:

此時就可以調(diào)試Nginx父進程了,例如使用bt命令查看當前調(diào)用堆棧:



使用f 1命令切換到當前調(diào)用堆棧#1,可以發(fā)現(xiàn)Nginx父進程的主線程掛起在src/core/nginx.c:382處。
此時可以使用c命令讓程序繼續(xù)運行,也可以添加斷點或者進行其他調(diào)試操作。
再開一個Shell窗口,使用gdb attach 5247命令將gdb attach到nginx子進程上:

使用bt命令查看子進程的主線程的當前調(diào)用堆棧:


可以發(fā)現(xiàn),子進程掛起在src/event/modules/ngx_epoll_module.c:800的epoll_wait函數(shù)處。我們在epoll_wait函數(shù)返回后(src/event/modules/ngx_epoll_module.c:804)加一個斷點,然后使用c命令讓Nginx子進程繼續(xù)運行:

接著在瀏覽器中訪問Nginx網(wǎng)站,這里的 IP地址是筆者的云主機地址,讀者在實際調(diào)試時將其改成自己的 Nginx 服務器所在的地址即可,如果是本機,那么地址就是127.0.0.1,由于默認端口是80,所以不用指定端口號:

此時回到nginx子進程的調(diào)試界面,發(fā)現(xiàn)斷點被觸發(fā):

使用bt命令可以獲得此時的調(diào)用堆棧:


使用info threads命令可以查看子進程的所有線程信息,發(fā)現(xiàn)Nginx子進程只有一個主線程:

Nginx父進程不處理客戶端的請求,處理客戶端請求的邏輯在子進程中,當單個子進程的客戶端請求數(shù)達到一定數(shù)量時,父進程會重新 fork 一個新的子進程來處理新的客戶端請求,也就是說子進程數(shù)量可以有多個,我們可以開多個 Shell 窗口,使用 gdb attach到各個子進程上調(diào)試。
總之,我們可以使用這種方法添加各種斷點調(diào)試Nginx的功能,慢慢地就能熟悉Nginx的各個內(nèi)部邏輯了。
然而,該方法存在一個缺點,即程序已經(jīng)啟動了,我們只能使用gdb觀察程序在這之后的行為,如果想調(diào)試程序從啟動到運行的執(zhí)行流程,則可能不太適用。有些讀者可能會說:用gdb attach到進程后,加好斷點,然后使用run命令重啟進程,這樣就可以調(diào)試程序從啟動到運行的執(zhí)行流程了。問題是這種方法并不通用,因為對于多進程服務模型,有些父子進程有一定的依賴關(guān)系,是不方便在運行過程中重啟的。這時方法二就比較合適了。
2.方法二
gdb調(diào)試器提供了一個follow-fork選項,通過set follow-fork mode設置一個進程fork出新的子進程時,gdb是繼續(xù)調(diào)試父進程(取值是parent)還是繼續(xù)調(diào)試子進程(取值是child),默認繼續(xù)調(diào)試父進程(取值是parent):

可以使用show follow-fork mode查看當前值:


還是以調(diào)試nginx為例,先進入 nginx 可執(zhí)行文件所在的目錄,將方法一中的 Nginx服務停下來:

在Nginx源碼中存在這樣的邏輯,這個邏輯會在程序main函數(shù)處被調(diào)用:

如以上代碼中的注釋所示,為了不讓主進程退出,我們在Nginx的配置文件中增加一行,這樣Nginx就不會調(diào)用ngx_daemon函數(shù)了:

接下來執(zhí)行g(shù)db nginx,通過設置參數(shù)將配置文件nginx.conf傳給待調(diào)試的Nginx進程:

接著輸入run命令嘗試運行Nginx:

如前文所述,gdb遇到fork指令時默認會attach到父進程,因此在以上輸出中有一行提示“Detaching after fork from child process 7509”,我們按Ctrl+C組合鍵將程序中斷,然后輸入 bt 命令查看當前調(diào)用堆棧,輸出的堆棧信息和我們在方法一中看到的父進程的調(diào)用堆棧一樣,說明gdb在程序fork之后確實attach到父進程了:


如果想讓gdb在fork之后attach到子進程,則可以在程序運行之前設置set follow-fork child,然后使用run命令重新運行程序:

接著按Ctrl+C組合鍵將程序中斷,然后使用bt命令查看當前線程的調(diào)用堆棧,結(jié)果顯示它確實是方法一中子進程的主線程所在的調(diào)用堆棧,這說明gdb確實attach到子進程了。
我們可以利用方法二調(diào)試程序 fork之前和之后的任何邏輯,這是一種較為通用的多進程調(diào)試方法,建議掌握。
- 高手是如何做產(chǎn)品設計的(全2冊)
- Spring技術(shù)內(nèi)幕:深入解析Spring架構(gòu)與設計
- Vue.js入門與商城開發(fā)實戰(zhàn)
- JavaScript前端開發(fā)與實例教程(微課視頻版)
- Mastering Scientific Computing with R
- SSM輕量級框架應用實戰(zhàn)
- PLC編程與調(diào)試技術(shù)(松下系列)
- Ext JS 4 Web Application Development Cookbook
- 移動界面(Web/App)Photoshop UI設計十全大補
- Arduino家居安全系統(tǒng)構(gòu)建實戰(zhàn)
- PLC應用技術(shù)(三菱FX2N系列)
- 微服務從小白到專家:Spring Cloud和Kubernetes實戰(zhàn)
- Java圖像處理:基于OpenCV與JVM
- SQL Server 2016 從入門到實戰(zhàn)(視頻教學版)
- C語言程序設計實訓教程與水平考試指導