GDB調試多線程程序
GDB 調試器不僅僅支持調試單線程程序,還支持調試多線程程序。本質上講,使用 GDB 調試多線程程序的過程和調試單線程程序類似,不同之處在於,調試多線程程序需要監控多個線程的執行過程,進而找到導致程序出現問題的異常或 Bug,而調試單線程程序只需要監控 1 個線程。
表 1 羅列了 GDB 調試多線程程序時常用的命令以及它們各自的功能。
調試命令 | 功 能 |
---|---|
info threads | 查看當前調試環境中包含多少個線程,並打印出各個線程的相關信息,包括線程編號(ID)、線程名稱等。 |
thread id | 將線程編號為 id 的線程設置為當前線程。 |
thread apply id... command | id... 表示線程的編號;command 代指 GDB 命令,如 next、continue 等。整個命令的功能是將 command 命令作用於指定編號的線程。當然,如果想將 command 命令作用於所有線程,id... 可以用 all 代替。 |
break location thread id | 在 location 指定的位置建立普通斷點,並且該斷點僅用於暫停編號為 id 的線程。 |
set scheduler-locking off|on|step | 默認情況下,當程序中某一線程暫停執行時,所有執行的線程都會暫停;同樣,當執行 continue 命令時,默認所有暫停的程序都會繼續執行。該命令可以打破此默認設置,即只繼續執行當前線程,其它線程仍停止執行。 |
接下來,我將以調試如下 C 語言多線程程序為例,給大家詳細講解表 1 中這些命令的功能和用法,示例:
#include <stdio.h> #include <pthread.h> void* thread_job(void*name) { char * thread_name = (char*)name; printf("this is %s\n",thread_name); printf("http://c.biancheng.net\n"); } int main() { pthread_t tid1,tid2; pthread_create(&tid1, NULL, thread_job, "thread1_job"); pthread_create(&tid2, NULL, thread_job, "thread2_job"); pthread_join(tid1,NULL); pthread_join(tid2,NULL); printf("this is main\n"); return 0; }
可以看到,此程序中包含 3 個線程,分別為 main 主線程、tid1 子線程和 tid2 子線程。需要注意的是,將此程序編譯為可供 GDB 調試的可執行程序時,需執行如下命令:
gcc main.c -o main -g -lpthread
pthread 線程庫並不屬於 Linux 系統中的默認庫,所以編譯、鏈接時就需要為 gcc 命令附加 -lpthread 參數。
GDB 查看所有線程
info threads 命令的功能有 2 個,既可以查看當前調試環境下存在的線程數以及各線程的具體信息,也可以通過指定線程的編號查看某個線程的具體信息。info threads 命令的完整語法格式如下:
(gdb) info threads [id...]
其中,參數 id... 作為可選參數,表示要查看的線程編號,編號個數可以是多個。例:
其中 Id 列表示各個線程的編號(ID 號);Target Id 列表示各個線程的標識符;Frame 列打印各個線程執行的有關信息,例如線程名稱,線程暫停的具體位置等。
使用 GDB 調試多線程程序時,同一時刻我們調試的焦點都只能是某個線程,被稱為當前線程。整個調試過程中,GDB 調試器總是會從當前線程的角度為我們打印調試信息。如上所示,當執行 r 啟動程序后,GDB 編譯器自行選擇標識號為 LWP 54283(編號為 2)的線程作為當前線程,則隨后打印的暫停運行的信息就與該線程有關,而沒有打印出編號為 1 和 3 的暫停信息。
GDB 調試器為了方便用戶快速識別出當前線程,執行 info thread 命令后,Id 列前標有 * 號的線程即為當前線程。輸入的調試命令並不僅僅作用於當前線程,例如 continue、next 等,默認情況下它們作用於所有線程。
GDB調整當前線程
用 GDB 調試多線程程序的過程中,根據需要可以隨時對當前線程進行調整,這就需要用到 thead id 命令。thread id 命令用於將編號為 id 的線程設定為當前線程。舉個例子:
可以看到,改變當前線程的同時,GDB 調試器為我們打印出了該線程暫停執行的具體信息。再次執行 info threads 命令可以看到,編號為 3 的線程確實成為了新的當前線程。
GDB執行特定線程
如果想單獨控制某一線程進行指定的操作,可以借助 thread apply id... command 命令實現:
(gdb) thread apply id... command
參數 id... 表示要控制的目標線程的編號,編號個數可以是多個。如果想控制所有線程,可以用 all 代替書寫所有線程的編號;參數 command 表示要目標線程執行的操作,例如 next、continue 等。
如上所示,當我們調用 thread apply 2 next 命令對 2 號線程進行逐步調試時,3 號線程也會運行,這是為什么呢?這和 GDB 調試器的調試機制有關。
默認情況下,無論哪個線程暫停執行,其它線程都會隨即暫停;反之,一旦某個線程啟動(借助 next、step、continue 命令),其它線程也隨即啟動。GDB 調試默認的這種調試模式(稱為全停止模式),一定程序上可以幫助我們更好地監控程序中各個線程的執行。
GDB為特定線程設置斷點
當調試環境中擁有多個線程時,我們可以選擇為特定的線程設置斷點,該斷點僅對指定線程有效。使用如下命令:
(gdb) break location thread id (gdb) break location thread id if...
location 表示設置斷點的具體位置;id 表示斷點要作用的線程的編號;if... 參數作用指定斷點激活的條件,即只有條件符合時,斷點才會發揮作用。
默認情況下,當某個線程執行遇到斷點時,GDB 調試器會自動將該線程作為當前線程,並提示用戶 "[Switching to Thread n]",其中 n 即為新的當前線程。
示例:
可以看到,我們在第 7 行代碼處為 3 號線程單獨設置了一個普通斷點,該斷點僅對 3 號線程有效。
GDB設置線程鎖
使用 GDB 調試多線程程序時,默認的調試模式為:一個線程暫停運行,其它線程也隨即暫停;一個線程啟動運行,其它線程也隨即啟動。要知道,這種調試機制確實能幫我們更好地監控各個線程的“一舉一動”,但並非適用於所有場景。
一些場景中,我們可能只想讓某一特定線程運行,其它線程仍維持暫停狀態。要想達到這樣的效果,就需要借助 set scheduler-locking 命令。 此命令可以幫我們將其它線程都“鎖起來”,使后續執行的命令只對當前線程或者指定線程有效,而對其它線程無效。
set scheduler-locking 命令的語法格式如下:
(gdb) set scheduler-locking mode
其中,參數 mode 的值有 3 個,分別為 off、on 和 step,它們的含義分別是:
- off:不鎖定線程,任何線程都可以隨時執行;
- on:鎖定線程,只有當前線程或指定線程可以運行;
- step:當單步執行某一線程時,其它線程不會執行,同時保證在調試過程中當前線程不會發生改變。但如果該模式下執行 continue、until、finish 命令,則其它線程也會執行,並且如果某一線程執行過程遇到斷點,則 GDB 調試器會將該線程作為當前線程。
示例:
可以看到,通過執行 set scheduler-locking on 命令,接下來的 next 命令只對當前線程(2號線程)有效,其它線程仍保持原有的暫停狀態。
同時,我們可以通過執行 show scheduler-locking 命令,查看各個線程鎖定的狀態,例如: