共计 4132 个字符,预计需要花费 11 分钟才能阅读完成。
修改系统时间,导致 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
实现的延时阻塞!