zoukankan      html  css  js  c++  java
  • 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 << "	 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 << "	 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 实现的延时阻塞!


  • 相关阅读:
    学习Java的Day02
    学习Java的Day01
    多线程的了解
    几个MQ的区别
    HTML5——存储(cookie、localStorage、sessionStorage)的区别
    dubbo mock配置
    Springboot分布式,excel导出,运用POI导出,前端用的jsp
    oracle 添加字段和添加注释
    可以重复的Map:IdentityHashMap
    数组转list问题
  • 原文地址:https://www.cnblogs.com/xiaolincoding/p/11369715.html
Copyright © 2011-2022 走看看