1、死鎖的調試
一個正在生產環境下運行的進程死鎖了,然后並沒有在調試器里面打開它,但發現沒有響應,日志輸出也停止了。那么我們會想到“我剛剛加上了新的鎖策略,不一定穩定,這可能是死鎖了”。
產生死鎖的四個必要條件
(1) 互斥條件:一個資源每次只能被一個進程(線程)使用。
(2) 請求與保持條件:一個進程(線程)因請求資源而阻塞時,對已獲得的資源保持不放。
(3) 不剝奪條件 : 此進程(線程)已獲得的資源,在末使用完之前,不能強行剝奪。
(4) 循環等待條件 : 多個進程(線程)之間形成一種頭尾相接的循環等待資源關系。
可以使用 pstack 和 gdb 工具對死鎖程序進行分析:
(1)pstack 是 Linux(比如 Red Hat Linux 系統、Ubuntu Linux 系統等)下一個很有用的工具,它的功能是打印輸出此進程的堆棧信息。可以輸出所有線程的調用關系棧。
(2)使用gdb處理死鎖問題的方式有以下兩種:
1.1 gdb附加進程調試
第一種方法是使用gdb附加進程調試;
使用gdb <program> pid附加到進行中進行調試。
調試過程其實比較簡單。程序卡死的情況下,多數為線程死鎖。
(1)使用info threads來查看各個線程狀態。
(2)使用thread id進行線程之間的切換。
(3)使用bt/backtrace進行線程堆棧信息查看,可以看到線程的運行情況。
1.2 kill -11產生core dump
第二種方法是kill -11 產生core dump;
Core的意思是內存, Dump的意思是扔出來, 堆出來.開發和使用Unix程序時, 有時程序莫名其妙的down了, 卻沒有任何的提示(有時候會提示core dumped). 這時候可以查看一下有沒有形如core.進程號的文件生成運行過程中發生異常, 程序異常退出時, 由操作系統把程序當前的內存狀況存儲在一個core文件中, 叫core dump.這個文件便是操作系統把程序down掉時的內存內容扔出來生成的, 它可以做為調試程序的參考.Core Dump又叫核心轉儲。
(1)使用ulimit -c unlimited命令允許操作系統在程序運行掛掉時拋出core文件;
(2)當程序無日志輸出或疑似死鎖時,使用kill -11 pid命令,讓進程產生一個段錯誤(Segmentation Fault),從而生成core文件;core 文件里面包含了死鎖時進程的內存鏡像;
(3)用 gdb 打開這個 core 文件,然后使用命令
thread apply all bt
打出所有線程的棧,如果你發現有那么幾個棧停在 pthread_wait 或者類似調用上,大致就可以得出結論;
舉一個簡單的例子(為了代碼盡量簡單,使用了C++11的thread library)。
| #include <iostream> #include <thread> #include <mutex> #include <chrono> using namespace std;
mutex m1,m2;
void func_2() { m2.lock(); cout<< "about to dead_lock"<<endl; m1.lock();
}
void func_1() { m1.lock();
chrono::milliseconds dura( 1000 );// delay to trigger dead_lock this_thread::sleep_for( dura );
m2.lock();
}
int main() { thread t1(func_1); thread t2(func_2); t1.join(); t2.join(); return 0; } |
編譯代碼
$> g++ -Wall -std=c++11 dead_lock_demo.cpp -o dead_lock_demo -g -pthread
運行程序,發現程序打印出“about to dead_lock” 就不動了,現在我們使用gdb來調試。注意gdb的版本要高於7.0,之前使用過gdb6.3調試多線程是不行的。
在這之前需要先產生core dump文件:
$> ps -aux | grep dead_lock_demo
找出 dead_lock_demo 線程號,然后:
$> kill -11 pid
此時會生成core dump 文件,在我的系統上名字就是 core
然后調試:
$> gdb dead_lock_demo core
$> thread apply all bt
| 說明: Kill命令的常用信號名稱: 9 - SIGKILL 無條件終止進程 11 - SIGSEGV 段錯誤 15 - SIGTERM 向進程發送終止信號 kill命令可以帶信號號碼選項,也可以不帶。如果沒有信號號碼,kill命令就會發出終止信號(15),這個信號可以被進程捕獲,使得進程在退出之前可以清理並釋放資源。 |
2、預處理功能:assert和NDEBUG
在C++中有時會用到類似於頭文件保護的技術,以便有選擇的執行調試代碼。基本思想是,程序可以包含一些用於調試的代碼,但是這些代碼只在開發程序的時候使用。當應用程序編寫完成准備發布時,要先屏蔽掉調試代碼。這種方法用到了兩種預處理功能:assert和NDEBUG。
2.1 assert預處理宏
assert是一種預處理宏。所謂預處理宏其實就是一個預處理變量,它的行為有點類似於內聯函數。assert宏使用一個表達式作為它的條件:
assert(expr);
首先對expr求值,如果表達式為假,assert輸出信息並終止程序的執行。如果表達式為真,assert什么也不做。
assert宏常用於檢查“不能發生”的條件。例如,一個對輸入文本進行操作的程序可能要求所有給定單詞的長度都大於某個閥值。此時,程序可以包含一條如下所示的語句:
assert(word.size()>threshold)
2.1 NDEBUG預處理變量
assert的行為依賴於一個名為NDEBUG的預處理變量的狀態。如果定義了NDEBUG,則assert什么也不做。默認狀態下沒有定義NDEBUG,此時assert將執行運行時檢查。
我們可以使用一個#define語句定義NDEBUG,從而關閉調試狀態。
定義NDEBUG能避免檢查各種條件所需的運行時開銷,當然此時根本就不會執行運行時檢查。因此,assert應該僅用於驗證那些確實不可能發生的事。我們可以把assert當成調試程序的一種輔助手段,但是不能用它代替真正的運行時邏輯檢查,也不能替代程序本身應該包含的錯誤檢查。
除了使用assert外,也可以使用NDEBUG編寫自己的條件調試代碼。如果NDEBUG未定義,將執行#ifndef和#endif之間的代碼;如果定義了NDEBUG,這些代碼將被忽略。
| void pring(const int ia[], size_t size) { #ifndef NDEBUG // __func__ 編譯器定義的一個局部靜態變量,用於存放函數的名字 cerr << __func__ << ":array size is " << size << endl; #endlf } |
C++編譯器除了定義了之外,預處理器還定義了另外4個對於調試很有用的名字:
| 名字 |
作用 |
| __func__ |
當前調試的函數名字 |
| __FILE__ |
存放文件名的字符串字面值 |
| __LINE__ |
存放當前行號的整型字面值 |
| __TIME__ |
存放文件編譯時間的字符串字面值 |
| __DATE__ |
存放文件編譯日期的字符串字面值 |
