歡迎光臨
每天分享高質量文章

驚心動魄,Linux被死鎖陣痛後的破門實錄

線上某服務時不時報出如下異常(大約一天二十多次):“Deadlock found when trying to get lock;”。

Oh, My God! 是死鎖問題。儘管報錯不多,對效能目前看來也無太大影響,但還是需要解決,保不齊哪天成為效能瓶頸。

為了防止死鎖再出現,我總結了下死鎖原因以及常見的一些方法,希望對大家有幫助。

在計算機系統中有很多一次只能由一個行程使用的資源,如印表機,磁帶機,一個檔案的I節點等。在多道程式設計環境中,若干行程往往要共享這類資源,而且一個行程所需要的資源不止一個。這樣,就會出現若干行程競爭有限資源,又推進順序不當,從而構成無限期迴圈等待的局面。這種狀態就是死鎖。

系統發生死鎖現象不僅浪費大量的系統資源,甚至導致整個系統崩潰,帶來災難性後果。所以,對於死鎖問題在理論上和技術上都必須給予高度重視。


1、死鎖的原理

死鎖 (deadlocks): 是指兩個或兩個以上的行程(執行緒)在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的行程(執行緒)稱為死鎖行程(執行緒)。 由於資源佔用是互斥的,當某個行程提出申請資源後,使得有關行程(執行緒)在無外力協助下,永遠分配不到必需的資源而無法繼續執行,這就產生了一種特殊現象死鎖。

一種交叉持鎖死鎖的情形,此時執行程式中兩個或多個執行緒發生永久堵塞(等待),每個執行緒都在等待被其它執行緒佔用並堵塞了的資源。例如,如果執行緒 1 鎖住了記錄 A 並等待記錄 B,而執行緒 2 鎖住了記錄 B 並等待記錄 A,這樣兩個執行緒就發生了死鎖現象。在計算機系統中 , 如果系統的資源分配策略不當,更常見的可能是程式員寫的程式有錯誤等,則會導致行程因競爭資源不當而產生死鎖的現象。

產生死鎖的四個必要條件:

(1) 互斥條件:一個資源每次只能被一個行程(執行緒)使用。
(2) 請求與保持條件:一個行程(執行緒)因請求資源而阻塞時,對已獲得的資源保持不放。
(3) 不剝奪條件 : 此行程(執行緒)已獲得的資源,在末使用完之前,不能強行剝奪。
(4) 迴圈等待條件 : 多個行程(執行緒)之間形成一種頭尾相接的迴圈等待資源關係。

圖 1. 交叉持鎖的死鎖示意圖:

註釋:在執行 func2 和 func4 之後,子執行緒 1 獲得了鎖 A,正試圖獲得鎖 B,但是子執行緒 2 此時獲得了鎖 B,正試圖獲得鎖 A,所以子執行緒 1 和子執行緒 2 將沒有辦法得到鎖 A 和鎖 B,因為它們各自被對方佔有,永遠不會釋放,所以發生了死鎖的現象。


2、使用 pstack 和 gdb 工具對死鎖程式進行分析

2.1 pstack 在 Linux 平臺上的簡單介紹

pstack 是 Linux(比如 Red Hat Linux 系統、Ubuntu Linux 系統等)下一個很有用的工具,它的功能是列印輸出此行程的堆疊資訊。可以輸出所有執行緒的呼叫關係棧。

2.2 gdb 在 Linux 平臺上的簡單介紹

GDB 是 GNU 開源組織釋出的一個強大的 UNIX 下的程式除錯工具。Linux 系統中包含了 GNU 除錯程式 gdb,它是一個用來除錯 C 和 C++ 程式的除錯器。可以使程式開發者在程式執行時觀察程式的內部結構和記憶體的使用情況 .

gdb 所提供的一些主要功能如下所示:

1 執行程式,設定能影響程式執行的引數和環境 ;

2 控製程式在指定的條件下停止執行;

3 當程式停止時,可以檢查程式的狀態;

4 當程式 crash 時,可以檢查 core 檔案;

5 可以修改程式的錯誤,並重新執行程式;

6 可以動態監視程式中變數的值;

7 可以單步執行程式碼,觀察程式的執行狀態。

gdb 程式除錯的物件是可執行檔案或者行程,而不是程式的原始碼檔案。然而,並不是所有的可執行檔案都可以用 gdb 除錯。如果要讓產生的可執行檔案可以用來除錯,需在執行 g++(gcc)指令編譯程式時,加上 -g 引數,指定程式在編譯時包含除錯資訊。除錯資訊包含程式裡的每個變數的型別和在可執行檔案裡的地址對映以及原始碼的行號。gdb 利用這些資訊使原始碼和機器碼相關聯。gdb 的基本命令較多,不做詳細介紹,大家如果需要進一步瞭解,請參見 gdb 手冊。

清單 1. 測試程式

#include  

#include  

#include  

 

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t mutex3 = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t mutex4 = PTHREAD_MUTEX_INITIALIZER;

 

static int sequence1 = 0;

static int sequence2 = 0;

 

int func1()

{

    pthread_mutex_lock(&mutex1;);

    ++sequence1;

    sleep(1);

    pthread_mutex_lock(&mutex2;);

    ++sequence2;

    pthread_mutex_unlock(&mutex2;);

    pthread_mutex_unlock(&mutex1;);

 

    return sequence1;

}

 

int func2()

{

    pthread_mutex_lock(&mutex2;);

    ++sequence2;

    sleep(1);

    pthread_mutex_lock(&mutex1;);

    ++sequence1;

    pthread_mutex_unlock(&mutex1;);

    pthread_mutex_unlock(&mutex2;);

 

    return sequence2;

}

 

void* thread1(void* arg)

{

    while (1)

    {

        int iRetValue = func1();

 

        if (iRetValue == 100000)

        {

            pthread_exit(NULL);

        }

    }

}

 

void* thread2(void* arg)

{

    while (1)

    {

        int iRetValue = func2();

 

        if (iRetValue == 100000)

        {

            pthread_exit(NULL);

        }

    }

}

 

void* thread3(void* arg)

{

    while (1)

    {

        sleep(1);

        char szBuf[128];

        memset(szBuf, 0, sizeof(szBuf));

        strcpy(szBuf, “thread3”);

    }

}

 

void* thread4(void* arg)

{

    while (1)

    {

        sleep(1);

        char szBuf[128];

        memset(szBuf, 0, sizeof(szBuf));

        strcpy(szBuf, “thread3”);

    }

}

 

int main()

{

    pthread_t tid[4];

    if (pthread_create(&tid;[0], NULL, &thread1;, NULL) != 0)

    {

        _exit(1);

    }

    if (pthread_create(&tid;[1], NULL, &thread2;, NULL) != 0)

    {

        _exit(1);

    }

    if (pthread_create(&tid;[2], NULL, &thread3;, NULL) != 0)

    {

        _exit(1);

    }

    if (pthread_create(&tid;[3], NULL, &thread4;, NULL) != 0)

    {

        _exit(1);

    }

 

    sleep(5);

    //pthread_cancel(tid[0]);

 

    pthread_join(tid[0], NULL);

    pthread_join(tid[1], NULL);

    pthread_join(tid[2], NULL);

    pthread_join(tid[3], NULL);

 

    pthread_mutex_destroy(&mutex1;);

    pthread_mutex_destroy(&mutex2;);

    pthread_mutex_destroy(&mutex3;);

    pthread_mutex_destroy(&mutex4;);

 

    return 0;

}

清單 2. 編譯測試程式

[dyu@xilinuxbldsrv purify]$ g++ -g lock.cpp -o lock -lpthread

清單 3. 查詢測試程式的行程號

[dyu@xilinuxbldsrv purify]$ ps -ef|grep lock

dyu       6721  5751  0 15:21 pts/3    00:00:00 ./lock

清單 4. 對死鎖行程第一次執行 pstack(pstack –行程號)的輸出結果

[dyu@xilinuxbldsrv purify]$ pstack 6721

Thread 5 (Thread 0x41e37940 (LWP 6722)):

#0  0x0000003d1a80d4c4 in __lll_lock_wait () from /lib64/libpthread.so.0

#1  0x0000003d1a808e1a in _L_lock_1034 () from /lib64/libpthread.so.0

#2  0x0000003d1a808cdc in pthread_mutex_lock () from /lib64/libpthread.so.0

#3  0x0000000000400a9b in func1() ()

#4  0x0000000000400ad7 in thread1(void*) ()

#5  0x0000003d1a80673d in start_thread () from /lib64/libpthread.so.0

#6  0x0000003d19cd40cd in clone () from /lib64/libc.so.6

Thread 4 (Thread 0x42838940 (LWP 6723)):

#0  0x0000003d1a80d4c4 in __lll_lock_wait () from /lib64/libpthread.so.0

#1  0x0000003d1a808e1a in _L_lock_1034 () from /lib64/libpthread.so.0

#2  0x0000003d1a808cdc in pthread_mutex_lock () from /lib64/libpthread.so.0

#3  0x0000000000400a17 in func2() ()

#4  0x0000000000400a53 in thread2(void*) ()

#5  0x0000003d1a80673d in start_thread () from /lib64/libpthread.so.0

#6  0x0000003d19cd40cd in clone () from /lib64/libc.so.6

Thread 3 (Thread 0x43239940 (LWP 6724)):

#0  0x0000003d19c9a541 in nanosleep () from /lib64/libc.so.6

#1  0x0000003d19c9a364 in sleep () from /lib64/libc.so.6

#2  0x00000000004009bc in thread3(void*) ()

#3  0x0000003d1a80673d in start_thread () from /lib64/libpthread.so.0

#4  0x0000003d19cd40cd in clone () from /lib64/libc.so.6

Thread 2 (Thread 0x43c3a940 (LWP 6725)):

#0  0x0000003d19c9a541 in nanosleep () from /lib64/libc.so.6

#1  0x0000003d19c9a364 in sleep () from /lib64/libc.so.6

#2  0x0000000000400976 in thread4(void*) ()

#3  0x0000003d1a80673d in start_thread () from /lib64/libpthread.so.0

#4  0x0000003d19cd40cd in clone () from /lib64/libc.so.6

Thread 1 (Thread 0x2b984ecabd90 (LWP 6721)):

#0  0x0000003d1a807b35 in pthread_join () from /lib64/libpthread.so.0

#1  0x0000000000400900 in main ()

清單 5. 對死鎖行程第二次執行 pstack(pstack –行程號)的輸出結果

[dyu@xilinuxbldsrv purify]$ pstack 6721

Thread 5 (Thread 0x40bd6940 (LWP 6722)):

#0  0x0000003d1a80d4c4 in __lll_lock_wait () from /lib64/libpthread.so.0

#1  0x0000003d1a808e1a in _L_lock_1034 () from /lib64/libpthread.so.0

#2  0x0000003d1a808cdc in pthread_mutex_lock () from /lib64/libpthread.so.0

#3  0x0000000000400a87 in func1() ()

#4  0x0000000000400ac3 in thread1(void*) ()

#5  0x0000003d1a80673d in start_thread () from /lib64/libpthread.so.0

#6  0x0000003d19cd40cd in clone () from /lib64/libc.so.6

Thread 4 (Thread 0x415d7940 (LWP 6723)):

#0  0x0000003d1a80d4c4 in __lll_lock_wait () from /lib64/libpthread.so.0

#1  0x0000003d1a808e1a in _L_lock_1034 () from /lib64/libpthread.so.0

#2  0x0000003d1a808cdc in pthread_mutex_lock () from /lib64/libpthread.so.0

#3  0x0000000000400a03 in func2() ()

#4  0x0000000000400a3f in thread2(void*) ()

#5  0x0000003d1a80673d in start_thread () from /lib64/libpthread.so.0

#6  0x0000003d19cd40cd in clone () from /lib64/libc.so.6

Thread 3 (Thread 0x41fd8940 (LWP 6724)):

#0  0x0000003d19c7aec2 in memset () from /lib64/libc.so.6

#1  0x00000000004009be in thread3(void*) ()

#2  0x0000003d1a80673d in start_thread () from /lib64/libpthread.so.0

#3  0x0000003d19cd40cd in clone () from /lib64/libc.so.6

Thread 2 (Thread 0x429d9940 (LWP 6725)):

#0  0x0000003d19c7ae0d in memset () from /lib64/libc.so.6

#1  0x0000000000400982 in thread4(void*) ()

#2  0x0000003d1a80673d in start_thread () from /lib64/libpthread.so.0

#3  0x0000003d19cd40cd in clone () from /lib64/libc.so.6

Thread 1 (Thread 0x2af906fd9d90 (LWP 6721)):

#0  0x0000003d1a807b35 in pthread_join () from /lib64/libpthread.so.0

#1  0x0000000000400900 in main ()

連續多次檢視這個行程的函式呼叫關係堆疊進行分析:當行程吊死時,多次使用 pstack 檢視行程的函式呼叫堆疊,死鎖執行緒將一直處於等鎖的狀態,對比多次的函式呼叫堆疊輸出結果,確定哪兩個執行緒(或者幾個執行緒)一直沒有變化且一直處於等鎖的狀態(可能存在兩個執行緒 一直沒有變化)。

輸出分析:

根據上面的輸出對比可以發現,執行緒 1 和執行緒 2 由第一次 pstack 輸出的處在 sleep 函式變化為第二次 pstack 輸出的處在 memset 函式。但是執行緒 4 和執行緒 5 一直處在等鎖狀態(pthread_mutex_lock),在連續兩次的 pstack 資訊輸出中沒有變化,所以我們可以推測執行緒 4 和執行緒 5 發生了死鎖。

Gdb into thread輸出:

清單 6. 然後透過 gdb attach 到死鎖行程

(gdb) info thread

  5 Thread 0x41e37940 (LWP 6722)  0x0000003d1a80d4c4 in __lll_lock_wait ()

  from /lib64/libpthread.so.0

  4 Thread 0x42838940 (LWP 6723)  0x0000003d1a80d4c4 in __lll_lock_wait ()

  from /lib64/libpthread.so.0

  3 Thread 0x43239940 (LWP 6724)  0x0000003d19c9a541 in nanosleep ()

from /lib64/libc.so.6

  2 Thread 0x43c3a940 (LWP 6725)  0x0000003d19c9a541 in nanosleep ()

from /lib64/libc.so.6

* 1 Thread 0x2b984ecabd90 (LWP 6721)  0x0000003d1a807b35 in pthread_join ()

from /lib64/libpthread.so.0

清單 7. 切換到執行緒 5 的輸出

(gdb) thread 5

[Switching to thread 5 (Thread 0x41e37940 (LWP 6722))]#0  0x0000003d1a80d4c4 in

__lll_lock_wait () from /lib64/libpthread.so.0

(gdb) where

#0  0x0000003d1a80d4c4 in __lll_lock_wait () from /lib64/libpthread.so.0

#1  0x0000003d1a808e1a in _L_lock_1034 () from /lib64/libpthread.so.0

#2  0x0000003d1a808cdc in pthread_mutex_lock () from /lib64/libpthread.so.0

#3  0x0000000000400a9b in func1 () at lock.cpp:18

#4  0x0000000000400ad7 in thread1 (arg=0x0) at lock.cpp:43

#5  0x0000003d1a80673d in start_thread () from /lib64/libpthread.so.0

#6  0x0000003d19cd40cd in clone () from /lib64/libc.so.6

清單 8. 執行緒 4 和執行緒 5 的輸出

(gdb) f 3

#3  0x0000000000400a9b in func1 () at lock.cpp:18

18          pthread_mutex_lock(&mutex2;);

(gdb) thread 4

[Switching to thread 4 (Thread 0x42838940 (LWP 6723))]#0  0x0000003d1a80d4c4 in

__lll_lock_wait () from /lib64/libpthread.so.0

(gdb) f 3

#3  0x0000000000400a17 in func2 () at lock.cpp:31

31          pthread_mutex_lock(&mutex1;);

(gdb) p mutex1

$1 = {__data = {__lock = 2, __count = 0, __owner = 6722, __nusers = 1, __kind = 0,

__spins = 0, __list = {__prev = 0x0, __next = 0x0}},

  __size = “0200000000000000B32000001”, ’00’

, __align = 2}

(gdb) p mutex3

$2 = {__data = {__lock = 0, __count = 0, __owner = 0, __nusers = 0,

__kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}},

__size = ’00’ , __align = 0}

(gdb) p mutex2

$3 = {__data = {__lock = 2, __count = 0, __owner = 6723, __nusers = 1,

__kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}},

  __size = “0200000000000000C32000001”, ’00’

, __align = 2}

(gdb)

從上面可以發現,執行緒 4 正試圖獲得鎖 mutex1,但是鎖 mutex1 已經被 LWP 為 6722 的執行緒得到(__owner = 6722),執行緒 5 正試圖獲得鎖 mutex2,但是鎖 mutex2 已經被 LWP 為 6723 的 得到(__owner = 6723),從 pstack 的輸出可以發現,LWP 6722 與執行緒 5 是對應的,LWP 6723 與執行緒 4 是對應的。所以我們可以得出, 執行緒 4 和執行緒 5 發生了交叉持鎖的死鎖現象。檢視執行緒的原始碼發現,執行緒 4 和執行緒 5 同時使用 mutex1 和 mutex2,且申請順序不合理。


3、總結

本文簡單介紹了一種在 Linux 平臺下分析死鎖問題的方法,對一些死鎖問題的分析有一定作用。希望對大家有幫助。理解了死鎖的原因,尤其是產生死鎖的四個必要條件,就可以最大可能地避免、預防和解除死鎖。所以,在系統設計、行程排程等方面註意如何不讓這四個必要條件成立,如何確定資源的合理分配演演算法,避免行程永久佔據系統資源。

此外,也要防止行程在處於等待狀態的情況下佔用資源 , 在系統執行過程中,對行程發出的每一個系統能夠滿足的資源申請進行動態檢查,並根據檢查結果決定是否分配資源,若分配後系統可能發生死鎖,則不予分配,否則予以分配。因此,對資源的分配要給予合理的規劃,使用有序資源分配法和銀行家演演算法等是避免死鎖的有效方法。


4 如何盡可能避免死鎖

1)以固定的順序訪問表和行。比如對第2節兩個job批次更新的情形,簡單方法是對id串列先排序,後執行,這樣就避免了交叉等待鎖的情形;又比如對於3.1節的情形,將兩個事務的sql順序調整為一致,也能避免死鎖。

2)大事務拆小。大事務更傾向於死鎖,如果業務允許,將大事務拆小。

3)在同一個事務中,盡可能做到一次鎖定所需要的所有資源,減少死鎖機率。

4)降低隔離級別。如果業務允許,將隔離級別調低也是較好的選擇,比如將隔離級別從RR調整為RC,可以避免掉很多因為gap鎖造成的死鎖。

5)為表新增合理的索引。可以看到如果不走索引將會為表的每一行記錄新增上鎖,死鎖的機率大大增大。

本文作者:IBM

轉載連結:http://www.ibm.com/developerworks/cn/linux/l-cn-deadlock/

《Linux雲端計算及運維架構師高薪實戰班》2018年09月16日即將開課中,120天衝擊Linux運維年薪30萬,改變速約~~~~

    *宣告:推送內容及圖片來源於網路,部分內容會有所改動,版權歸原作者所有,如來源資訊有誤或侵犯權益,請聯絡我們刪除或授權事宜。

    – END –


    更多Linux好文請點選【閱讀原文】

    ↓↓↓

    贊(0)

    分享創造快樂