修改系統(tǒng)時(shí)間,導(dǎo)致sem_timedwait 一直阻塞的問題解決和分析
介紹
最近修復(fù)項(xiàng)目問題時(shí),發(fā)現(xiàn)當(dāng)系統(tǒng)時(shí)間往前修改后,會(huì)導(dǎo)致sem_timedwait函數(shù)一直阻塞。通過搜索了發(fā)現(xiàn)int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);傳入的第二個(gè)阻塞時(shí)間參數(shù)是絕對(duì)的時(shí)間戳,那么該函數(shù)是存在缺陷的。
sem_timedwait存在的缺陷的理由:
假設(shè)當(dāng)前系統(tǒng)時(shí)間是1565000000(2019-08-05 18:13:20),sem_timedwait傳入的阻塞等待的時(shí)間戳是1565000100(2019-08-05 18:15:00),那么sem_timedwait就需要阻塞1分40秒(100秒),若在sem_timedwait阻塞過程中,中途將系統(tǒng)時(shí)間往前修改成1500000000(2017-07-14 10:40:00),那么sem_timedwait此時(shí)就會(huì)阻塞2年多! 這就是sem_timedwait存在的缺陷!!
sem_timedwait函數(shù)介紹
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- 如果信號(hào)量大于0,則對(duì)信號(hào)量進(jìn)行遞減操作并立馬返回正常
- 如果信號(hào)量小于0,則阻塞等待,當(dāng)阻塞超時(shí)時(shí)返回失敗(
errno設(shè)置為ETIMEDOUT)
第二個(gè)參數(shù)abs_timeout 參數(shù)指向一個(gè)指定絕對(duì)超時(shí)時(shí)刻的結(jié)構(gòu),這個(gè)結(jié)果由自 Epoch,1970-01-01 00:00:00 +0000(UTC) 秒數(shù)和納秒數(shù)構(gòu)成。這個(gè)結(jié)構(gòu)定義如下
struct timespec {
time_t tv_sec; /* 秒 */
long tv_nsec; /* 納秒 */
};
解決方法
可以通過sem_trywait + usleep的方式來(lái)實(shí)現(xiàn)與sem_timedwait函數(shù)的類似功能,并且不會(huì)發(fā)生因系統(tǒng)時(shí)間往前改而出現(xiàn)一直阻塞的問題。
sem_trywait函數(shù)介紹
函數(shù) sem_trywait()和sem_wait()有一點(diǎn)不同,即如果信號(hào)量的當(dāng)前值為0,則返回錯(cuò)誤而不是阻塞調(diào)用。錯(cuò)誤值errno設(shè)置為EAGAIN。sem_trywait()其實(shí)是sem_wait()的非阻塞版本。
int sem_trywait(sem_t *sem)
執(zhí)行成功返回0,執(zhí)行失敗返回 -1且信號(hào)量的值保持不變。
sem_trywait + usleep的方式實(shí)現(xiàn)
主要實(shí)現(xiàn)的思路:
sem_trywait函數(shù)不管信號(hào)量為0或不為0都會(huì)立刻返回,當(dāng)函數(shù)正常返回的時(shí)候就不usleep;當(dāng)函數(shù)不正常返回時(shí)就通過usleep來(lái)實(shí)現(xiàn)延時(shí),具體是實(shí)現(xiàn)方式如下代碼中的bool Wait( size_t timeout )函數(shù):
#include <string>
#include<iostream>
#include<semaphore.h>
#include <time.h>
sem_t g_sem;
// 獲取自系統(tǒng)啟動(dòng)的調(diào)單遞增的時(shí)間
inline uint64_t GetTimeConvSeconds( timespec* curTime, uint32_t factor )
{
// CLOCK_MONOTONIC:從系統(tǒng)啟動(dòng)這一刻起開始計(jì)時(shí),不受系統(tǒng)時(shí)間被用戶改變的影響
clock_gettime( CLOCK_MONOTONIC, curTime );
return static_cast<uint64_t>(curTime->tv_sec) * factor;
}
// 獲取自系統(tǒng)啟動(dòng)的調(diào)單遞增的時(shí)間 -- 轉(zhuǎn)換單位為微秒
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的方式實(shí)現(xiàn)
// 如果信號(hào)量大于0,則減少信號(hào)量并立馬返回true
// 如果信號(hào)量小于0,則阻塞等待,當(dāng)阻塞超時(shí)時(shí)返回false
bool Wait( size_t timeout )
{
const size_t timeoutUs = timeout * 1000; // 延時(shí)時(shí)間由毫米轉(zhuǎn)換為微秒
const size_t maxTimeWait = 10000; // 最大的睡眠的時(shí)間為10000微秒,也就是10毫秒
size_t timeWait = 1; // 睡眠時(shí)間,默認(rèn)為1微秒
size_t delayUs = 0; // 剩余需要延時(shí)睡眠時(shí)間
const uint64_t startUs = GetMonnotonicTime(); // 循環(huán)前的開始時(shí)間,單位微秒
uint64_t elapsedUs = 0; // 過期時(shí)間,單位微秒
int ret = 0;
do
{
// 如果信號(hào)量大于0,則減少信號(hào)量并立馬返回true
if( sem_trywait( &g_sem ) == 0 )
{
return true;
}
// 系統(tǒng)信號(hào)則立馬返回false
if( errno != EAGAIN )
{
return false;
}
// delayUs一定是大于等于0的,因?yàn)閐o-while的條件是elapsedUs <= timeoutUs.
delayUs = timeoutUs - elapsedUs;
// 睡眠時(shí)間取最小的值
timeWait = std::min( delayUs, timeWait );
// 進(jìn)行睡眠 單位是微秒
ret = usleep( timeWait );
if( ret != 0 )
{
return false;
}
// 睡眠延時(shí)時(shí)間雙倍自增
timeWait *= 2;
// 睡眠延時(shí)時(shí)間不能超過最大值
timeWait = std::min( timeWait, maxTimeWait );
// 計(jì)算開始時(shí)間到現(xiàn)在的運(yùn)行時(shí)間 單位是微秒
elapsedUs = GetMonnotonicTime() - startUs;
} while( elapsedUs <= timeoutUs ); // 如果當(dāng)前循環(huán)的時(shí)間超過預(yù)設(shè)延時(shí)時(shí)間則退出循環(huán)
// 超時(shí)退出,則返回false
return false;
}
// 獲取需要延時(shí)等待時(shí)間的絕對(duì)時(shí)間戳
inline timespec* GetAbsTime( size_t milliseconds, timespec& absTime )
{
// CLOCK_REALTIME:系統(tǒng)實(shí)時(shí)時(shí)間,隨系統(tǒng)實(shí)時(shí)時(shí)間改變而改變,即從UTC1970-1-1 0:0:0開始計(jì)時(shí),
// 中間時(shí)刻如果系統(tǒng)時(shí)間被用戶改成其他,則對(duì)應(yīng)的時(shí)間相應(yīng)改變
clock_gettime( CLOCK_REALTIME, &absTime );
absTime.tv_sec += milliseconds / 1000;
absTime.tv_nsec += (milliseconds % 1000) * 1000000;
// 納秒進(jìn)位秒
if( absTime.tv_nsec >= 1000000000 )
{
absTime.tv_sec += 1;
absTime.tv_nsec -= 1000000000;
}
return &absTime;
}
// sem_timedwait 實(shí)現(xiàn)的睡眠 -- 存在缺陷
// 如果信號(hào)量大于0,則減少信號(hào)量并立馬返回true
// 如果信號(hào)量小于0,則阻塞等待,當(dāng)阻塞超時(shí)時(shí)返回false
bool SemTimedWait( size_t timeout )
{
timespec absTime;
// 獲取需要延時(shí)等待時(shí)間的絕對(duì)時(shí)間戳
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;
// 初始化信號(hào)量,數(shù)量為0
sem_init( &g_sem, 0, 0 );
////////////////////// sem_trywait+usleep 實(shí)現(xiàn)的睡眠 ////////////////////
// 獲取開始的時(shí)間,單位是微秒
startUs = GetMonnotonicTime();
// 延時(shí)等待
signaled = Wait(1000);
// 獲取超時(shí)等待的時(shí)間,單位是微秒
elapsedUs = GetMonnotonicTime() - startUs;
// 輸出 signaled:0 Wait time:1000ms
std::cout << "signaled:" << signaled << "\t Wait time:" << elapsedUs/1000 << "ms" << std::endl;
////////////////////// sem_timedwait 實(shí)現(xiàn)的睡眠 ////////////////////
///////////////////// 存在缺陷,原因當(dāng)在sem_timedwait阻塞中時(shí),修改了系統(tǒng)時(shí)間,則會(huì)導(dǎo)致sem_timedwait一直阻塞 //////////////////
// 獲取開始的時(shí)間,單位是微秒
startUs = GetMonnotonicTime();
// 延時(shí)等待
signaled = SemTimedWait(2000);
// 獲取超時(shí)等待的時(shí)間,單位是微秒
elapsedUs = GetMonnotonicTime() - startUs;
// 輸出 signaled:0 SemTimedWait time:2000ms
std::cout << "signaled:" << signaled << "\t SemTimedWait time:" << elapsedUs/1000 << "ms" << std::endl;
return 0;
}
測(cè)試結(jié)果:
[root@lincoding sem]# ./sem_test
signaled:0 Wait time:1000ms
signaled:0 SemTimedWait time:2000ms
總結(jié)
盡量不要使用sem_timedwait函數(shù)來(lái)實(shí)現(xiàn)延時(shí)等待的功能,若要使用該延時(shí)等待的功能,建議使用sem_trywait+usleep 實(shí)現(xiàn)的延時(shí)阻塞!