缺陷的背後---互斥鎖申請後未釋放異常退出

序言算法

       某日,開發哥哥一如往常的在線上發佈版本,kill掉應用程序後啓動新程序,程序啓動後,應用程序就一直阻塞在某處,因而版本回退,重啓舊版本,應用程序依舊阻塞在某處。pstack查看進程棧後發現,原來是第一次被kill掉的程序是運行在臨界區時被kill的,而代碼又有bug,在申請鎖的時,未對這種狀況「佔着資源的死去」進行處理,致使後續程序再申請鎖時只拋異常,而不釋放資源。sql

      那這個bug測試應該怎麼模擬呢?通常程序經常使用哪些鎖呢?針對進程/線程鎖什麼場景下須要鎖呢?加鎖的影響是什麼呢?鎖的粒度應該如何控制呢?怎麼測試「鎖」呢?本文爲說明這類問題,分如下結構進行總結:安全


     一:測試的「經典缺陷」
     二:鎖的經常使用基礎知識
        1、Linux多進程同步方式對比
        2、常見的鎖類別   
        3、互斥鎖和Mysql鎖的使用場景
        4、加鎖的影響和鎖的粒度
     三:鎖的測試方法和策略

 

1、測試的「經典缺陷」多線程

      缺陷定義 :  以上描述場景,其實是鎖的一個經典的使用場景。程序在獲取了臨界資源後異常退出,臨界資源一直處於加鎖狀態,其餘進程/線程申請鎖程序未正常處理就會致使阻塞,甚至死鎖等待併發

      測試模擬 :  測試的時候,必須清楚鎖的使用各個場景和異常場景,若是隻是經過無計劃的大數據壓測,重現該場景的可能性很低,由於該類異常必須在臨界區kill進程,所以測試必須使用GDB打斷點,運行到臨界區的斷點後,kill進程才能模擬。而後再次啓動程序,pstack查看進程棧是否阻塞app

      缺陷修復 :  這類缺陷主要產生的問題就是,後續進程在申請鎖時,若是出現了「鎖被已死進程」佔有後,應該怎麼讓系統回收鎖的問題。本質上這個問題就是進程間互斥鎖回收問題。函數

     下面是致使產生bug的代碼片斷:            高併發

void ProcessMutex::lock()  throw(CException){  
    if( pthread_mutex_lock(m_pMutex) != 0){
            throw CException(ERR_LOCK_CREATE, "Failed to lock mutex!", __FILE__, __LINE__);}}  

       修復方法:性能

       第一步:設置強健屬性爲 PTHREAD_MUTEX_ROBUST_NP。只要在互斥鎖初始化時調用pthread_mutexattr_setrobust_np設置支持回收機制。測試

ProcessMutex::ProcessMutex(const char* path, int id) throw(CException): m_ShMem(path, id, sizeof(pthread_mutex_t), true)
{
    m_pMutex = (pthread_mutex_t *)m_ShMem.address();

    // 設置互斥量進程間可共享
    if(m_ShMem.isCreator())
    {
        pthread_mutexattr_t mutex_attr;
        pthread_mutexattr_init(&mutex_attr);  
        
        pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED);  
       pthread_mutexattr_setrobust_np(&mutex_attr, PTHREAD_MUTEX_ROBUST_NP);
        pthread_mutex_init(m_pMutex, &mutex_attr);
        
        pthread_mutexattr_destroy(&mutex_attr);
    }
}

  第二步:捕獲獲取鎖返回異常EOWNERDEAD,調用pthread_mutex_consistent_np完成鎖owner的切換工做便可。

void ProcessMutex::lock()  throw(CException)
{  
    int iErrno = pthread_mutex_lock(m_pMutex);
    if( iErrno != 0)
    {
        if ( iErrno == EOWNERDEAD ) { pthread_mutex_consistent_np(m_pMutex); }
        else
        {
            throw CException(ERR_LOCK_CREATE, "Failed to lock mutex!", __FILE__, __LINE__);
        }
    }
}  

二 :鎖的經常使用基礎知識

       下面的介紹,主要是基於測試過程當中遇到的各類鎖的一些概括小結,有些其餘更高級的鎖暫未接觸到,後面有接觸到再更新本文章吧~~~

   一、Linux多進程同步方式對比

       在進行多進程開發的時候,常常會遇到各類進程間同步的場景,Linux多進程同步機制的性能和功能均有較大差別 ,通常使用如下4種方式: 

  • GCC內建原子操做
  • 基於共享內存的mutex(pthread mutex)
  • POSIX信號量
  • fcntl記錄鎖

        從功能上分析:原子操做< mutex < 信號量 < 記錄鎖。原子操做只支持有限的幾種整數運算;mutex只支持加鎖和解鎖兩種狀態;信號量則支持計數;記錄鎖功能最爲豐富,能支持讀寫鎖、區間鎖、屢次加鎖一次釋放、進程退出自動釋放等功能。

        那性能呢?簡單的測試方法:程序分別啓動1~5個子進程,在共享內存中存放一個int整數,每一個子進程對其自增1M次,總計時間,程序運行5次取均值。(時間單位爲毫秒),結果性能排名是:原子操做 > mutex > 信號量 > 記錄鎖。記錄鎖甚至在單進程的狀況下性能都低於mutex在5個進程下的表現,到多進程的時候性能比其它同步操做低了一個數量級以上。結果以下:

                                                        

    二、常見的鎖類別   

     第一類:unix內核級別鎖。這類鎖常用,針對於多進程或者多線程的程序在運行的過程當中,有時會出現公共資源搶佔使用的狀況就會使用到這類鎖,這類鎖經常使用的分如下4類:

  • 互斥鎖:mutex;獲取鎖失敗後會休眠,釋放cpu。
  • 自旋鎖:spinlock;遇到鎖時,佔用cpu空等。
  • 讀寫鎖:rwlock;同一時刻只有一個線程能夠得到寫鎖,可 以有多線程得到讀鎖。
  • 順序鎖:seqlock; 本質上是一個自旋鎖+一個計數器。

       互斥鎖其實是一種變量,在使用互斥鎖時,其實是對這個變量進行置0置1操做並進行判斷使得線程可以得到鎖或釋放鎖。 提供兩種得到鎖方法,經常使用的是pthread_mutex_lock: 

       pthread_mutex_lock:若是此時已經有另外一個線程已經得到了鎖,那麼當前線程調用該函數後就會被掛起等待,直到有另外一個線程釋放了鎖,該線程會被喚醒。

       pthread_mutex_trylock:若是此時有另外一個賢臣已經得到了鎖,那麼當前線程調用該函數後會當即返回並返回設置出錯碼爲EBUSY,即它不會使當前線程掛起等待

       而互斥鎖的底層實現,通常使用swap或exchange指令,這個指令的含義是將寄存器和內存單元中的數據進行交換,這條指令保證了操做lock和unlock的原子性。

       第二類:文件鎖:FileLock;防止多進程併發;是一種文件讀寫機制,在任何特定的時間只容許一個進程訪問一個文件。

       第三類:Mysql鎖。根據鎖類型:共享鎖(讀鎖),排他鎖(寫鎖);根據鎖策略:表鎖,行鎖,間隙鎖 ;根據鎖方法:悲觀鎖,樂觀鎖

     三、互斥鎖和Mysql鎖的使用場景

          針對互斥鎖,主要在如下三個常見場景常用:

  • 數據共享:主寫,子讀 主線程定時加載DB/文件/隊列內的數據,子線程讀取數據。
  • DB句柄:主讀,子讀 DB句柄在主線程/全局變量內定義,子線程須要更句柄來更新數據

  • 非線程安全的API使用: SHA256簽名,Rsa256 Localtime ->localtime_r

           針對Mysql鎖,主要分事務內和事務外:

  • 事務中,使用排他鎖 select...for update 只有指定主鍵,MySQL 纔會執行Row lock
  • 非事務,樂觀鎖 where 前置條件

     四、加鎖的影響和鎖的粒度

  • 沒必要要的加鎖,影響性能:以前接入銀行接口時,接口協議使用了SHA256算法,這個算法因爲開發哥哥的不正當使用,在加密時作了加鎖的操做,直接致使性能從800TPS降低到400TPS。

        錯誤的使用:

 unsigned char* digest = SHA256((unsigned char *)strUnSign.c_str(), strUnSign.size(), NULL);

       正確的使用,這樣後面使用該算法時,openssl庫的鎖能保證併發的可靠性

unsigned char digest[SHA256_DIGEST_LENGTH] = {0};
SHA256((unsigned char *)strUnSign.c_str(), strUnSign.size(), digest);
  • 高併發不加鎖訪問臨界資源,直接致使程序運行結果與實際不符合。

三:鎖的測試方法和策略

     測試方法:

  • 多進程/多線程調試:線程調試經常使用命令:break <linenum> thread <threadno>, info thread,thread <threadnum>, set scheduler-locking on,thread apply all bt。
  • 編譯特殊版本:一、進程/線程獲取鎖後,打印日誌並sleep N 秒,其餘線程獲取時,都會阻塞。 二、pstack,strace查看。
  • 代碼走讀:一、資源使用環境確認,判斷是否須要加鎖(多讀或者多寫)
  • 壓測:大數據壓測

         其中第一個和第二個方法主要是針對已知加鎖的地方進行測試;第三個和第四個方法用於肯定是否須要鎖

     測試點:

  • 鎖有效性測試:獲取到鎖後,其餘線程/進程獲取鎖是否在正常等待,等待多久?
  • 獲取鎖後程序異常退出測試:獲取到鎖時任務若是掛掉了,鎖還未被釋放,後續再請求分配鎖時是否會死鎖?
相關文章
相關標籤/搜索