C/C++ 修改系統時間,導致sem_timedwait 一直阻塞的問題解決和分析


修改系統時間,導致sem_timedwait 一直阻塞的問題解決和分析


介紹

最近修復項目問題時,發現當系統時間往前修改后,會導致sem_timedwait函數一直阻塞。通過搜索了發現int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);傳入的第二個阻塞時間參數是絕對的時間戳,那么該函數是存在缺陷的。

sem_timedwait存在的缺陷的理由:

假設當前系統時間是1565000000(2019-08-05 18:13:20)sem_timedwait傳入的阻塞等待的時間戳是1565000100(2019-08-05 18:15:00),那么sem_timedwait就需要阻塞1分40秒(100秒),若在sem_timedwait阻塞過程中,中途將系統時間往前修改成1500000000(2017-07-14 10:40:00),那么sem_timedwait此時就會阻塞2年多! 這就是sem_timedwait存在的缺陷!!


sem_timedwait函數介紹

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
  • 如果信號量大於0,則對信號量進行遞減操作並立馬返回正常
  • 如果信號量小於0,則阻塞等待,當阻塞超時時返回失敗(errno 設置為 ETIMEDOUT)

第二個參數abs_timeout 參數指向一個指定絕對超時時刻的結構,這個結果由自 Epoch,1970-01-01 00:00:00 +0000(UTC) 秒數和納秒數構成。這個結構定義如下

struct timespec {
    time_t tv_sec;        /* 秒 */
    long   tv_nsec;       /* 納秒 */
};

解決方法

可以通過sem_trywait + usleep的方式來實現與sem_timedwait函數的類似功能,並且不會發生因系統時間往前改而出現一直阻塞的問題。

sem_trywait函數介紹

函數 sem_trywait()sem_wait()有一點不同,即如果信號量的當前值為0,則返回錯誤而不是阻塞調用。錯誤值errno設置為EAGAIN。sem_trywait()其實是sem_wait()的非阻塞版本。

int sem_trywait(sem_t *sem)

執行成功返回0,執行失敗返回 -1且信號量的值保持不變。

sem_trywait + usleep的方式實現

主要實現的思路:
sem_trywait函數不管信號量為0或不為0都會立刻返回,當函數正常返回的時候就不usleep;當函數不正常返回時就通過usleep來實現延時,具體是實現方式如下代碼中的bool Wait( size_t timeout )函數:

#include <string>
#include<iostream>

#include<semaphore.h>
#include <time.h>

sem_t g_sem;

// 獲取自系統啟動的調單遞增的時間
inline uint64_t GetTimeConvSeconds( timespec* curTime, uint32_t factor )
{
	// CLOCK_MONOTONIC:從系統啟動這一刻起開始計時,不受系統時間被用戶改變的影響
    clock_gettime( CLOCK_MONOTONIC, curTime );
    return static_cast<uint64_t>(curTime->tv_sec) * factor;
}

// 獲取自系統啟動的調單遞增的時間 -- 轉換單位為微秒
uint64_t GetMonnotonicTime()
{
    timespec curTime;
    uint64_t result = GetTimeConvSeconds( &curTime, 1000000 );
    result += static_cast<uint32_t>(curTime.tv_nsec) / 1000;
    return result;
}

// sem_trywait + usleep的方式實現
// 如果信號量大於0,則減少信號量並立馬返回true
// 如果信號量小於0,則阻塞等待,當阻塞超時時返回false
bool Wait( size_t timeout )
{
    const size_t timeoutUs = timeout * 1000; // 延時時間由毫米轉換為微秒
    const size_t maxTimeWait = 10000; // 最大的睡眠的時間為10000微秒,也就是10毫秒

    size_t timeWait = 1; // 睡眠時間,默認為1微秒
    size_t delayUs = 0; // 剩余需要延時睡眠時間

    const uint64_t startUs = GetMonnotonicTime(); // 循環前的開始時間,單位微秒
    uint64_t elapsedUs = 0; // 過期時間,單位微秒

    int ret = 0;

    do
    {
        // 如果信號量大於0,則減少信號量並立馬返回true
        if( sem_trywait( &g_sem ) == 0 )
        {
            return true;
        }

        // 系統信號則立馬返回false
        if( errno != EAGAIN )
        {
            return false;
        }

        // delayUs一定是大於等於0的,因為do-while的條件是elapsedUs <= timeoutUs.
        delayUs = timeoutUs - elapsedUs;

        // 睡眠時間取最小的值
        timeWait = std::min( delayUs, timeWait );

        // 進行睡眠 單位是微秒
        ret = usleep( timeWait );
        if( ret != 0 )
        {
            return false;
        }

        // 睡眠延時時間雙倍自增
        timeWait *= 2;

        // 睡眠延時時間不能超過最大值
        timeWait = std::min( timeWait, maxTimeWait );

        // 計算開始時間到現在的運行時間 單位是微秒
        elapsedUs = GetMonnotonicTime() - startUs;
    } while( elapsedUs <= timeoutUs ); // 如果當前循環的時間超過預設延時時間則退出循環

    // 超時退出,則返回false
    return false;
}

// 獲取需要延時等待時間的絕對時間戳
inline timespec* GetAbsTime( size_t milliseconds, timespec& absTime )
{
	// CLOCK_REALTIME:系統實時時間,隨系統實時時間改變而改變,即從UTC1970-1-1 0:0:0開始計時,
	//                 中間時刻如果系統時間被用戶改成其他,則對應的時間相應改變
    clock_gettime( CLOCK_REALTIME, &absTime );
    
	absTime.tv_sec += milliseconds / 1000;
    absTime.tv_nsec += (milliseconds % 1000) * 1000000;

    // 納秒進位秒
    if( absTime.tv_nsec >= 1000000000 )
    {
        absTime.tv_sec += 1;
        absTime.tv_nsec -= 1000000000;
    }

   return &absTime;
}

// sem_timedwait 實現的睡眠 -- 存在缺陷
// 如果信號量大於0,則減少信號量並立馬返回true
// 如果信號量小於0,則阻塞等待,當阻塞超時時返回false
bool SemTimedWait( size_t timeout )
{
    timespec absTime;
    // 獲取需要延時等待時間的絕對時間戳
    GetAbsTime( timeout, absTime );
    if( sem_timedwait( &g_sem, &absTime ) != 0 )
    {
        return false;
    }
    return true;
}

int main(void)
{
    bool signaled = false;
    uint64_t startUs = 0;
    uint64_t elapsedUs = 0;
    
    // 初始化信號量,數量為0
    sem_init( &g_sem, 0, 0 );
    
    ////////////////////// sem_trywait+usleep 實現的睡眠 ////////////////////
    // 獲取開始的時間,單位是微秒
    startUs = GetMonnotonicTime(); 
    // 延時等待
    signaled = Wait(1000);
    // 獲取超時等待的時間,單位是微秒
    elapsedUs = GetMonnotonicTime() - startUs;
    // 輸出 signaled:0     Wait time:1000ms
    std::cout << "signaled:" << signaled << "\t Wait time:" << elapsedUs/1000 << "ms" << std::endl;

    ////////////////////// sem_timedwait 實現的睡眠  ////////////////////
	///////////////////// 存在缺陷,原因當在sem_timedwait阻塞中時,修改了系統時間,則會導致sem_timedwait一直阻塞 //////////////////
    // 獲取開始的時間,單位是微秒
    startUs = GetMonnotonicTime();
    // 延時等待
    signaled = SemTimedWait(2000);
    // 獲取超時等待的時間,單位是微秒
    elapsedUs = GetMonnotonicTime() - startUs;
    // 輸出 signaled:0     SemTimedWait time:2000ms
    std::cout << "signaled:" << signaled << "\t SemTimedWait time:" << elapsedUs/1000 << "ms" << std::endl;

    return 0;
}

測試結果:

[root@lincoding sem]# ./sem_test 
signaled:0	 Wait time:1000ms
signaled:0	 SemTimedWait time:2000ms

總結

盡量不要使用sem_timedwait函數來實現延時等待的功能,若要使用該延時等待的功能,建議使用sem_trywait+usleep 實現的延時阻塞!



免責聲明!

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



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