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
實現的延時阻塞!