缺陷的背后(二)---互斥鎖申請后未釋放異常退出


序言

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

      那這個bug測試應該怎么模擬呢?一般程序常用哪些鎖呢?針對進程/線程鎖什么場景下需要鎖呢?加鎖的影響是什么呢?鎖的粒度應該如何控制呢?怎么測試“鎖”呢?本文為說明這類問題,分以下結構進行總結:


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

 

一、測試的“經典缺陷”

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

      測試模擬 :  測試的時候,必須清楚鎖的使用各個場景和異常場景,如果只是通過無計划的大數據壓測,重現該場景的可能性很低,因為該類異常必須在臨界區kill進程,因此測試必須使用GDB打斷點,運行到臨界區的斷點后,kill進程才能模擬。然后再次啟動程序,pstack查看進程棧是否阻塞

      缺陷修復 :  這類缺陷主要產生的問題就是,后續進程在申請鎖時,如果出現了“鎖被已死進程”占有后,應該怎么讓系統回收鎖的問題。本質上這個問題就是進程間互斥鎖回收問題。

     下面是導致產生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__);
        }
    }
}  

二 :鎖的常用基礎知識

       下面的介紹,主要是基於測試過程中遇到的各種鎖的一些歸納小結,有些其他更高級的鎖暫未接觸到,后面有接觸到再更新本文章吧~~~

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

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

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

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

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

                                                        

    2、常見的鎖類別   

     第一類: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鎖。根據鎖類型:共享鎖(讀鎖),排他鎖(寫鎖);根據鎖策略:表鎖,行鎖,間隙鎖 ;根據鎖方法:悲觀鎖,樂觀鎖

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

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

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

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

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

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

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

  • 不必要的加鎖,影響性能:之前接入銀行接口時,接口協議使用了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。
  • 編譯特殊版本:1、進程/線程獲取鎖后,打印日志並sleep N 秒,其他線程獲取時,都會阻塞。 2、pstack,strace查看。
  • 代碼走讀:1、資源使用環境確認,判斷是否需要加鎖(多讀或者多寫)
  • 壓測:大數據壓測

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

     測試點:

  • 鎖有效性測試:獲取到鎖后,其他線程/進程獲取鎖是否在正常等待,等待多久?
  • 獲取鎖后程序異常退出測試:獲取到鎖時任務如果掛掉了,鎖還未被釋放,后續再請求分配鎖時是否會死鎖?

        

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM