歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> Linux技術

Linux應用程序錯誤使用pthread_mutex_lock互斥鎖觸發SIG_ABRT信號的原因分析

本文分析在Linux應用程序中錯誤使用pthread_mutex鎖時會概率性觸發SIG_ABRT信號而導致程序崩潰(庫打印輸出 :Assertion `mutex->__data.__owner == 0' failed)的原因。

程序環境如下:(1)Glibc-2.15 (2)Linux-4.1.12 (3)樹莓派1b

首先給出出錯的示例程序:

#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退出。

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

加鎖函數源碼:

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_usercnt()函數中也可以看到,只有在原子加鎖期間才會改變這__owner值(該值也可認為是臨界資源的一部分而被保護起來了),因此是不可能出現加鎖已經加鎖的線程的,所以也不會調用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鎖用於保護臨界資源,嚴格按照“加鎖-->寫入/讀取臨界資源-->解鎖”的流程執行,對於線程間同步的需求使用條件變量或信號量實現。

Copyright © Linux教程網 All Rights Reserved