解決進程間共享內存,由於某個進程異常退出導致死鎖問題

來源:點擊打開鏈接

發現問題

繼這篇Blog 解決Nginx和Fpm-Php等內部多進程之間共享數據問題 發完後,進程間共享內存又遇到了新的問題

昨天晚上QP同學上線後,早上看超時報表發現有一臺前端機器訪問QP超時,比其他前端機器高出了幾個數量級,前端的機器都是同構的

難道是這臺機器系統不正常?查看系統狀態也沒有任何異常,統計了一下超時日誌,發現超時都發生在早上QP服務重啓的過程中,正常情況下服務重啓時,ClusterMap 會保證流量的正常分配

難道是ClusterMap有問題?去ClusterMap Server端看了一下,一切正常

難道是訂閱者客戶端有問題嗎?隨便找了一臺正常的機器和有問題的這臺機器對比,查看下日誌也沒有發現問題,使用查詢工具檢查這兩臺機器訂閱者代理寫的共享內存,發現工具讀取共享內存返回的結果不一致,這就更奇怪了,都是相同的訂閱者,一臺機器有問題一臺沒問題

難道Server端給他們的消息不一致?去Server端把訂閱者的機器列表都打了出來,發現了有問題的機器根本不在訂閱者列表裏面,說明這臺機器沒有訂閱,貌似有點線索了,我下線了一臺它訂閱的QP機器驗證,發現共享內部數據沒有更新,pstack一下這個進程,發現內部的更新線程一直在等鎖,導致共享內存數據一直無法更新,gdb跟進去之後,_lock.data.nr_readers一直爲1,說明一直有一個讀進程佔着鎖導致寫進程無法進入,遍歷了所有fpm-php的讀進程發現都沒有佔着鎖,這說明在讀進程在獲得鎖後沒來得及釋放就掛掉了

測試

現在問題已經確認就是獲得讀鎖後進程異常退出導致的,我寫個測試程序復現這個問題


(! 2293)-> cat test/read_shared.cpp

#include

SharedUpdateData*   _sharedUpdateData = NULL;
cm_sub::CMMapFile*  _mmapFile = NULL;

int32_t initSharedMemRead(const std::string& mmap_file_path)
{
    _mmapFile = new (std::nothrow) cm_sub::CMMapFile();
    if (_mmapFile == NULL || !_mmapFile->open(mmap_file_path.c_str(), FILE_OPEN_WRITE) )
    {
        return -1;
    }
    _sharedUpdateData = (SharedUpdateData*)_mmapFile->offset2Addr(0);
    return 0;
}

int main(int argc, char** argv)
{
    if (initSharedMemRead(argv[1]) != 0) return -1;

    int cnt = 100;
    while (cnt > 0)
    {
        pthread_rwlock_rdlock( &(_sharedUpdateData->_lock));
        fprintf(stdout, "version = %ld, readers = %u\n",
            _sharedUpdateData->_version, _sharedUpdateData->_lock.__data.__nr_readers);
        if (cnt == 190)
        {  
            exit(0);
        }  
        sleep(1);
        pthread_rwlock_unlock( &(_sharedUpdateData->_lock));
        -- cnt;
        usleep(100*1000);
    }
    delete _mmapFile;
}

(! 2293)-> cat test/write_shared.cpp

#include

SharedUpdateData*   _sharedUpdateData = NULL;
cm_sub::CMMapFile*  _mmapFile = NULL;

int32_t initSharedMemWrite(const char* mmap_file_path)
{
    _mmapFile = new (std::nothrow) cm_sub::CMMapFile();
    if ( _mmapFile == NULL || !_mmapFile->open(mmap_file_path, FILE_OPEN_WRITE, 1024) )
    {
        return -1;
    }
    _sharedUpdateData = (SharedUpdateData *)_mmapFile->offset2Addr(0);
    madvise(_sharedUpdateData, 1024, MADV_SEQUENTIAL);

    pthread_rwlockattr_t attr;
    memset(&attr, 0x0, sizeof(pthread_rwlockattr_t));
    if (pthread_rwlockattr_init(&attr) != 0 || pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) != 0)
    {
        return -1;
    }
    pthread_rwlock_init( &(_sharedUpdateData->_lock), &attr);
    _sharedUpdateData->_updateTime = autil::TimeUtility::currentTime();
    _sharedUpdateData->_version = 0; 
    return 0;
}

int main()
{
    if (initSharedMemWrite("data.mmap") != 0) return -1;

    int cnt = 200;
    while (cnt > 0)
    {
        pthread_rwlock_wrlock( &(_sharedUpdateData->_lock));
        ++ _sharedUpdateData->_version;
        fprintf(stdout, "version = %ld, readers = %u\n",
                _sharedUpdateData->_version, _sharedUpdateData->_lock.__data.__nr_readers);
        sleep(1);
        pthread_rwlock_unlock( &(_sharedUpdateData->_lock));
        -- cnt;
        usleep(100*1000);
    }
    delete _mmapFile;
}

無論是讀進程還是寫進程,獲取鎖後來不及釋放就掛掉都會有這樣的問題

如何解決

問題已經復現,想想如何用一個好的辦法解決,在網上找了一遍,針對讀寫鎖沒有什麼好的解決辦法,只能在邏輯上自己解決,能想到的是使用超時機制,即寫進程內部增加一個超時時間,如果寫進程到了這個時間還是不能獲得鎖,就認爲死鎖,將讀進程的計數減1,這是一個暴力的解決辦法,不解釋了,如果誰有好的解決辦法指導我下

看下讀寫鎖的代碼,讀寫鎖和互斥鎖相比,更適合用在讀多寫少的場景,如果讀進程需要鎖住時間久,就更合適使用讀寫鎖了,我的應該場景是,讀多寫少,讀寫時間都非常短;暫時認爲互斥鎖和讀寫鎖性能差別應該不大,其實讀寫鎖內部同樣使用了互斥鎖,只不過是鎖的時間比較短,鎖住互斥區,進去看下是否有人正在寫,然後就釋放了,
需要注意的是,讀寫鎖默認是寫優先的,也就是說當正在寫,或者進入寫隊列準備寫時,讀鎖都是加不上的,需要等待

好,那我們看看互斥鎖能否解決我們的問題,互斥鎖內部有一個屬性叫Robust鎖

設置鎖爲Robust鎖: pthread_mutexattr_setrobust_np

        The robustness attribute defines the behavior when the owner
    of  a  mutex  dies.  The value of robustness could be either
    PTHREAD_MUTEX_ROBUST_NP or  PTHREAD_MUTEX_STALLED_NP,  which
    are  defined by the header <pthread.h>. The default value of
    the robustness attribute is PTHREAD_MUTEX_STALLED_NP.

        When the owner of a mutex with the  PTHREAD_MUTEX_STALLED_NP
    robustness    attribute    dies,   all   future   calls   to
    pthread_mutex_lock(3C) for this mutex will be  blocked  from
    progress in an unspecified manner.

修復非一致的Robust鎖: pthread_mutex_consistent_np

        A consistent mutex becomes inconsistent and is  unlocked  if
    its  owner dies while holding it, or if the process contain-
    ing the owner of the mutex unmaps the memory containing  the
    mutex or performs one of the exec(2) functions. A subsequent
    owner  of  the   mutex   will   acquire   the   mutex   with
    pthread_mutex_lock(3C),  which  will  return  EOWNERDEAD  to
    indicate that the acquired mutex is inconsistent.

        The pthread_mutex_consistent_np() function should be  called
    while  holding  the  mutex  acquired  by  a previous call to
    pthread_mutex_lock() that returned EOWNERDEAD.

        Since the critical section protected by the mutex could have
    been  left  in  an inconsistent state by the dead owner, the
    caller should make the mutex consistent only if it  is  able
    to  make  the  critical  section protected by the mutex con-
    sistent.

簡單來說就是當發現EOWNERDEAD時,pthread_mutex_consistent_np函數內部會判斷這個互斥鎖是不是Robust鎖,如果是,並且他OwnerDie了,那麼他會把鎖的owner設置成自己的進程ID,這樣這個鎖又可以恢復可用,很簡單吧

鎖釋放是可以解決了,但是通過共享內存在進程間共享數據時,還有一點是需要注意的,就是數據的正確性,即完整性,進程共享不同與線程,如果是一個進程中的多個線程,那麼進程異常退出了,其他線程也同時退出了,進程間共享都是獨立的,如果一個寫線程在寫共享數據的過程中,異常退出,導致寫入的數據不完整,讀進程讀取時就會有讀到不完整數據的問題,其實數據完整性非常好解決,只需要在共享內存中加一個完成標記就好了,鎖住共享區後,寫數據,寫好之後標記爲完成,就可以了,讀進程在讀取時判斷一下完成標記

測試代碼見:


(! 2295)-> cat test/read_shared_mutex.cpp

 #include 

 SharedUpdateData*   _sharedUpdateData = NULL;
 cm_sub::CMMapFile*  _mmapFile = NULL;

 int32_t initSharedMemRead(const std::string& mmap_file_path)
 {
    _mmapFile = new (std::nothrow) cm_sub::CMMapFile();
    if (_mmapFile == NULL || !_mmapFile->open(mmap_file_path.c_str(), FILE_OPEN_WRITE) )
    {
        return -1;
    }
    _sharedUpdateData = (SharedUpdateData*)_mmapFile->offset2Addr(0);
    return 0;
 }

 int main(int argc, char** argv)
 {
     if (argc != 2) return -1;
     if (initSharedMemRead(argv[1]) != 0) return -1;   

     int cnt = 10000;
     int ret = 0;
     while (cnt > 0)
     {
         ret = pthread_mutex_lock( &(_sharedUpdateData->_lock));
         if (ret == EOWNERDEAD)
         {
             fprintf(stdout, "%s: version = %ld, lock = %d, %u, %d\n",
                strerror(ret),
                _sharedUpdateData->_version, 
                _sharedUpdateData->_lock.__data.__lock,
                _sharedUpdateData->_lock.__data.__count,
                _sharedUpdateData->_lock.__data.__owner);
             ret = pthread_mutex_consistent_np( &(_sharedUpdateData->_lock));
             if (ret != 0)
             {
                 fprintf(stderr, "%s\n", strerror(ret));
                 pthread_mutex_unlock( &(_sharedUpdateData->_lock));
                 continue;
             }
         }
         fprintf(stdout, "version = %ld, lock = %d, %u, %d\n", 
            _sharedUpdateData->_version, 
            _sharedUpdateData->_lock.__data.__lock,
            _sharedUpdateData->_lock.__data.__count,
            _sharedUpdateData->_lock.__data.__owner);
         sleep(5);
         pthread_mutex_unlock( &(_sharedUpdateData->_lock));
         usleep(500*1000);
         -- cnt;
    }
    fprintf(stdout, "go on\n");
    delete _mmapFile;
 }

(! 2295)-> cat test/write_shared_mutex.cpp

#include 

SharedUpdateData*   _sharedUpdateData = NULL;
cm_sub::CMMapFile*  _mmapFile = NULL;


int32_t initSharedMemWrite(const char* mmap_file_path)
{
    _mmapFile = new (std::nothrow) cm_sub::CMMapFile();
    if ( _mmapFile == NULL || !_mmapFile->open(mmap_file_path, FILE_OPEN_WRITE, 1024) )
    {
        return -1;
    }
    _sharedUpdateData = (SharedUpdateData *)_mmapFile->offset2Addr(0);
    madvise(_sharedUpdateData, 1024, MADV_SEQUENTIAL);

    pthread_mutexattr_t attr;
    memset(&attr, 0x0, sizeof(pthread_mutexattr_t));
    if (pthread_mutexattr_init(&attr) != 0 || pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) != 0)
    {
        return -1;
    }
    if (pthread_mutexattr_setrobust_np(&attr, PTHREAD_MUTEX_ROBUST_NP) != 0)
    {
        return -1;
    }
    pthread_mutex_init( &(_sharedUpdateData->_lock), &attr);
    _sharedUpdateData->_version = 0;
    return 0;
}

int main()
{
    if (initSharedMemWrite("data.mmap") != 0) return -1;

    int cnt = 200;
    int ret = 0;
    while (cnt > 0)
    {
        ret = pthread_mutex_lock( &(_sharedUpdateData->_lock));
        if (ret == EOWNERDEAD)
        {
            fprintf(stdout, "%s: version = %ld, lock = %d, %u, %d\n",
                    strerror(ret),
                    _sharedUpdateData->_version,
                    _sharedUpdateData->_lock.__data.__lock,
                    _sharedUpdateData->_lock.__data.__count,
                                            _sharedUpdateData->_lock.__data.__owner);
            ret = pthread_mutex_consistent_np( &(_sharedUpdateData->_lock));
            if (ret != 0)
            {
                fprintf(stderr, "%s\n", strerror(ret));
                pthread_mutex_unlock( &(_sharedUpdateData->_lock));
                continue;
            }
        }
        ++ _sharedUpdateData->_version;
        fprintf(stdout, "version = %ld, lock = %d, %u, %d\n", _sharedUpdateData->_version,
                _sharedUpdateData->_lock.__data.__lock,
                _sharedUpdateData->_lock.__data.__count,
                _sharedUpdateData->_lock.__data.__owner);
        usleep(1000*1000);
        pthread_mutex_unlock( &(_sharedUpdateData->_lock));
        -- cnt;
        usleep(500*1000);
    }

    delete _mmapFile;
}

BTW:我們都知道加鎖是有開銷的,不僅僅是互斥導致的等待開銷,還有加鎖過程都是有系統調用到內核態的,這個過程開銷也很大,有一種互斥鎖叫Futex鎖(Fast User Mutex),Linux從2.5.7版本開始支持Futex,快速的用戶層面的互斥鎖,Fetux鎖有更好的性能,是用戶態和內核態混合使用的同步機制,如果沒有鎖競爭的時候,在用戶態就可以判斷返回,不需要系統調用,

當然任何鎖都是有開銷的,能不用盡量不用,使用雙Buffer,釋放鏈表,引用計數,都可以在一定程度上替代鎖的使用

發佈了22 篇原創文章 · 獲贊 5 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章