Modify the system time, resulting in sem_timedwait has been blocked problem-solving and analysis
Introduction
A recent restoration project issues, found that when the system time forward revised, will lead sem_timedwait
function has been blocked. Found by searching for the int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
incoming second blocking time parameter is the absolute timestamp, the function is flawed.
The reason sem_timedwait defect exists:
Suppose the current system time 1565000000(2019-08-05 18:13:20)
, sem_timedwait
a time stamp is blocked waiting for incoming 1565000100(2019-08-05 18:15:00)
, then sem_timedwait
it needs to block 1分40秒(100秒)
, if the sem_timedwait
blocking process, the way forward will be to modify the system time 1500000000(2017-07-14 10:40:00)
, so sem_timedwait
this time will be blocked for over 2 years ! This is the sem_timedwait
flaw exists! !
sem_timedwait function introduction
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- If the signal is greater than 0, then the semaphore decrement operation immediately returns to normal and
- If the signal is less than 0, the block waiting, returns failure (obstruction when the timeout
errno
is setETIMEDOUT
)
The second parameter abs_timeout
parameter points to an absolute time specifies a time structure, since the result obtained by Epoch,1970-01-01 00:00:00 +0000(UTC)
the number of seconds and the number of nanoseconds configuration. This structure is defined as follows
struct timespec {
time_t tv_sec; /* 秒 */
long tv_nsec; /* 纳秒 */
};
Solution
By sem_trywait
+ usleep
to achieve with the way sem_timedwait
similar performance function, and the time to move forward because the system has been blocked and instead appear problem does not occur.
sem_trywait function introduction
Function sem_trywait()
and sem_wait()
a little different, that is, if the current semaphore value is 0, an error is returned instead of blocking calls. Error value errno set to EAGAIN. sem_trywait()
In fact, sem_wait()
non-blocking version.
int sem_trywait(sem_t *sem)
Successful execution returns 0, -1 on failure and return the semaphore value remains unchanged.
sem_trywait + usleep way to achieve
The main idea to achieve:
sem_trywait
a function regardless of the amount of signal is 0 or not 0 will return immediately, when the function does not return normally usleep
; when the function does not return to normal by the usleep
achieved delay, in particular a code implementations following bool Wait( size_t timeout )
function :
#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;
}
Test Results:
[root@lincoding sem]# ./sem_test
signaled:0 Wait time:1000ms
signaled:0 SemTimedWait time:2000ms
to sum up
Try not to use the sem_timedwait
function to achieve the delay waiting for the function to use the delay waiting for a function, it is recommended to use sem_trywait
+ usleep
delay blocking implementation!