pthread_mutex 鎖問題

shiyuan310發表於2024-03-21

在產品測試過程中,出現了pthread_mutex 在新增鎖pthread_mutex_lock( a ) 的時候失敗,出現瞭如下的core:庫列印輸出 :Assertion `mutex->__data.__owner == 0' failed)

然後再用gdb進去看的時候,發現那個鎖被一個未知執行緒ID佔用。 按理說,如果是正常被未知執行緒佔用,當前執行緒去加鎖時,就會一直等著,不會出現如上core問題。

並且產品中出現這個問題,這個鎖被該執行緒獨佔,不會被其他執行緒佔用。

基於上面出現的奇怪現象,在網上查詢,發現了很多類似的現象,下面一這篇帖子中描述進行記錄:

https://blog.csdn.net/tianyexing2008/article/details/131854188 出問題的程式

https://blog.csdn.net/wangkai6666/article/details/118277225 介紹gdb除錯上面出問題的程式,分析的是同一個問題

https://blog.csdn.net/tianyexing2008/article/details/131854188 一個使用鎖的正常例子。

使用連線中的程式,在arm執行,確實出現同樣的錯誤,下面給出例項:

#include <stdio.h>
#include <unistd.h>
#include "pthread.h"

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void * process(void * arg)
{
fprintf(stderr, "Starting process %s\n", (char *) arg);

while (1) {
/* 加鎖等待某些資源 */
pthread_mutex_lock(&lock);
fprintf(stderr, "Process %s lock mutex\n", (char *) arg);
/* 加鎖成功表示資源就緒 */
usleep(1000);
/* do something */
}

return NULL;
}

int main(void)
{
pthread_t th_a, th_b;
int ret = 0;

ret = pthread_create(&th_a, NULL, process, "a");
if (ret != 0) fprintf(stderr, "create a failed %d\n", ret);

ret = pthread_create(&th_b, NULL, process, "b");
if (ret != 0) fprintf(stderr, "create b failed %d\n", ret);

while (1) {
/* 等待並檢測某些資源就緒 */
/* something */
/* 解鎖告知執行緒資源就緒 */
pthread_mutex_unlock(&lock);
fprintf(stderr, "Main Process unlock mutex\n");
}

return 0;
}

本示例程式中,main函式首先建立兩個執行緒,然後主執行緒等待某些資源就緒(虛擬碼,程式中未體現),待就緒後解鎖mutex lock以告知子執行緒可以執行相應的處理(在解鎖後列印輸出解鎖成功),不斷迴圈;建立出的兩個執行緒均呼叫process函式,該函式會嘗試加鎖mutex lock,加鎖成功則表示資源就緒可以處理(列印輸出加鎖成功),否則在鎖上等待,亦往復迴圈。本程式中對mutex鎖的用法特殊,並不對臨界資源進行保護,而是作為執行緒間”生產---消費“同步功能的一個簡化示例,加鎖以等待資源就緒,解鎖以通知資源就緒,加鎖和解鎖的操作分別在不同的執行緒中執行。
執行該程式後不到10s時間程式就會出錯退出,並且觸發SIG_ABRT訊號,終端列印輸出如下:

Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Process b lock mutex
Process a lock mutex
Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Main Process unlock mutex
Process b lock mutex
pthread_test: pthread_mutex_lock.c:62: __pthread_mutex_lock: Assertion `mutex->__data.__owner == 0' failed.
Aborted
程式在Glibc庫中的pthread_mutex_lock.c的第62行__pthread_mutex_unlock()函式中出錯,程式ABRT退出。

當然每次執行出現assert的時間不一致,有的唱,有的短。


下面先來分析對應的原始碼,首先是加鎖流程:

加鎖函式原始碼:

int
__pthread_mutex_lock (mutex)
pthread_mutex_t *mutex;
{
assert (sizeof (mutex->__size) >= sizeof (mutex->__data));

unsigned int type = PTHREAD_MUTEX_TYPE (mutex);
if (__builtin_expect (type & ~PTHREAD_MUTEX_KIND_MASK_NP, 0))
return __pthread_mutex_lock_full (mutex);

pid_t id = THREAD_GETMEM (THREAD_SELF, tid);

if (__builtin_expect (type, PTHREAD_MUTEX_TIMED_NP)
== PTHREAD_MUTEX_TIMED_NP) //1---判斷鎖型別
{
simple:
/* Normal mutex. */
LLL_MUTEX_LOCK (mutex); //2---加鎖(原子操作)
assert (mutex->__data.__owner == 0); //3---Owner判斷
}

...

/* Record the ownership. */
mutex->__data.__owner = id; //4---Owner賦值
#ifndef NO_INCR
++mutex->__data.__nusers;
#endif

return 0;
}

加鎖函式的主要4步操作已經列出,首先會判斷鎖的型別,這裡僅對PTHREAD_MUTEX_TIMED_NP型別的鎖做出分析,該該型別的鎖為預設的鎖型別,當一個執行緒加鎖後其餘請求鎖的執行緒會排入一個等待佇列,並在鎖解鎖後按優先順序獲得鎖。然後程式呼叫LLT_MUTEX_LOCK()宏執行底層加鎖動作,這個加鎖流程是原子的且不同的架構實現並不相同,然後會判斷是否已經有執行緒獲取了該鎖(因為PTHREAD_MUTEX_TIMED_NP型別的鎖是不允許巢狀加鎖的),若已經有執行緒獲取了鎖則出錯退出(示例程式中就是在此出錯的),在函式的最後會把當前獲得鎖的執行緒號賦給__owner欄位(執行緒與鎖繫結)就結束了,此時當前執行緒進入臨界區,其他對鎖請求的執行緒將阻塞。

下面來看一下解鎖流程:

解鎖函式原始碼:

int
internal_function attribute_hidden
__pthread_mutex_unlock_usercnt (mutex, decr)
pthread_mutex_t *mutex;
int decr;
{
int type = PTHREAD_MUTEX_TYPE (mutex);
if (__builtin_expect (type & ~PTHREAD_MUTEX_KIND_MASK_NP, 0))
return __pthread_mutex_unlock_full (mutex, decr);

if (__builtin_expect (type, PTHREAD_MUTEX_TIMED_NP) //1---判斷鎖型別
== PTHREAD_MUTEX_TIMED_NP)
{
/* Always reset the owner field. */
normal:
mutex->__data.__owner = 0; //2---Owner解除
if (decr)
/* One less user. */
--mutex->__data.__nusers;

/* Unlock. */
lll_unlock (mutex->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex)); //3---原子解鎖
return 0;
}

...
}

解鎖函式的3步主要操作如上,首先依舊是判斷鎖型別,然後解除鎖和執行緒的繫結關係,最後就呼叫lll_unlock()函式原子的解鎖,此時若有加鎖執行緒需要獲取鎖,相應執行緒會從
LLT_MUTEX_LOCK()函式返回繼續執行。

以上就是呼叫pthread mutex函式加解鎖函式的主要流程,其中需要關注的一點就是這兩個函式的執行並不是原子的,是可能存在上下文切換動作的。在通常的用法中,我們加鎖操作一般都是為了保護臨界資源不被重入改寫,一半都是嚴格按照“加鎖-->寫入/讀取臨界資源-->解鎖”的流程執行(由加鎖的執行緒負責解鎖),而從前文中分析的__pthread_mutex_lock()和__pthread_mutex_unlock函式中也可以看到,只有在原子加鎖期間才會改變這__owner值(該值也可認為是臨界資源的一部分而被保護起來了),因此是不可能出現加鎖已經加鎖的執行緒的,所以也不會呼叫assert()函式而退出程式的。

但是我們專案中就遇到了這個問題,就是在一個執行緒加鎖解鎖,保護臨界資源的時候出現的問題。按理說加鎖時,發現鎖被其他執行緒佔用,就應該一直等在LLL_MUTEX_LOCK (mutex);
進入不到後面的assert才對。專案中遇到的和這個帖子中描述的還不一樣。

但是本程式中對鎖的用法顯然並不這麼“一般”,而是作為一種執行緒間的同步功能使用。其中主程序中不停的解鎖,即是執行緒A和B沒有加鎖也同樣如此,而執行緒A和B會競爭的每隔一定時間去加鎖,那麼就有可能出現如下圖中所示的一種情況:1、

該圖中主程序待資源就緒後正在解鎖一個未被加鎖的mutex_lock時發成了執行緒切換,執行緒A打斷解鎖流程完成了一整個加鎖的流程,隨後執行緒又且換回了主程序繼續執行真正的解鎖操作,這樣執行緒A所加的鎖就被莫名其妙的解掉了(關鍵的一點),此時若執行緒B在等待該鎖,則會進入到加鎖流程,從而在加鎖成功後崩潰在這個__owner判斷上。其實該程式出錯的主要原因即是解了並未加鎖的mutex_lock,如若主程序解得鎖是已經上了鎖的,則執行緒A是沒有機會加鎖的,主程序會原子的完成整個mutex_unlock動作。

另外,其實可以適當的調整程式再來看一下另外一種可能的情形(兩個執行流),同樣是“執行緒間同步”用法:2

這種情況就是在資源就緒較慢且資源處理較快的情況容易出現崩潰,同樣是機率性出現的。最後來看第三種可能的情況:3、

這種情況崩潰出現線上程A加鎖的過程中被主程序解鎖,然後執行緒A或其他執行緒又一次加鎖的時候。

其實不論上述哪一種同步的情況,其出錯的原因有兩點:

(1)解了未被上鎖的鎖;

(2)A執行緒加的鎖由其他執行緒去解,進一步分析就是沒有嚴格按照“加鎖-->解鎖”的流程使用mutex鎖。 正如前面說所,我們專案中嚴格按照加鎖解鎖流程使用,同樣出現了上述問題。

最後對於以上這種“執行緒間同步”的使用方法可以使用條件變數或者是訊號量實現而不要使用mutex鎖,mutex鎖一般被用在保護執行緒間臨界資源的情況下。


總結:

1、不要去解鎖一個未被加鎖的mutex鎖;

2、不要一個執行緒中加鎖而在另一個執行緒中解鎖;

3、使用mutex鎖用於保護臨界資源,嚴格按照“加鎖-->寫入/讀取臨界資源-->解鎖”的流程執行,對於執行緒間同步的需求使用條件變數或訊號量實現。

裡面有很好的gdb 分析core的命令,其中有一個進行反彙編:disas,就可以看到彙編指令。

相關文章