一、网络摘文

参考目录: https://zhuanlan.zhihu.com/p/83581365

1. 背景介绍及原因分析

最近修复项目问题时,发现当系统时间往前修改后,会导致sem_timedwait函数一直阻塞。通过搜索发现

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

传入的第二个阻塞时间参数是绝对的时间戳,那么该函数是存在缺陷的。

(1)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存在的缺陷!!

(2)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;       /* 纳秒 */
};

2. 解决方法

可以通过 sem_trywait+ usleep 的方式来实现与 sem_timedwait 函数的类似功能,并且不会发生因系统时间往前改而出现一直阻塞的问题,下面来介绍一下sem_trywait函数。

(1)sem_trywait函数介绍

函数 sem_trywait() 和 sem_wait() 有一点不同,即如果信号量的当前值为0,则返回错误而不是阻塞调用。错误值errno设置为EAGAIN。sem_trywait()其实是sem_wait()的非阻塞版本

int sem_trywait(sem_t *sem)

执行成功返回0,执行失败返回 -1且信号量的值保持不变。

(2)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;

    // 释放信号量资源
    sem_destroy( &g_sem );
    
    return 0;
}

测试结果:

[root@lincoding sem]# ./sem_test 
signaled:0	 Wait time:1000ms
signaled:0	 SemTimedWait time:2000ms

3. 总结

尽量不要使用 sem_timedwait 函数来实现延时等待的功能,若要使用该延时等待的功能,建议使用 sem_trywait+uslee 实现的延时阻塞!

二、附录

我在项目中遇到的问题是:使用tplayerdemo播放视频时,通过命令 date -s 设置时间,会导致视频播放停止。

问题原因同上,在我的程序中,SemTimedWait() 实现如下:

int SemTimedWait(sem_t* sem, int64_t time_ms)
{
    int err;

    if(time_ms == -1)
    {
        err = sem_wait(sem);
    }
    else
    {
        struct timespec ts;
        clock_gettime(CLOCK_REALTIME, &ts);
        ts.tv_nsec += time_ms % 1000 * 1000 * 1000;
        ts.tv_sec += time_ms / 1000 + ts.tv_nsec / (1000 * 1000 * 1000);
        ts.tv_nsec = ts.tv_nsec % (1000*1000*1000);

        err = sem_timedwait(sem, &ts);
    }

    return err;
}

修改后的程序为:

static int64_t CdcMonoTimeUs(void)
{
    struct timespec ts;

    if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
        abort();

    return (1000000LL * ts.tv_sec) + (ts.tv_nsec / 1000);
}

int SemTimedWait(sem_t* sem, int64_t time_ms)
{
    int err;

    if(time_ms == -1)
    {
        err = sem_wait(sem);
    }
    else
    {
        #if 0
        struct timespec ts;
        clock_gettime(CLOCK_REALTIME, &ts);
        ts.tv_nsec += time_ms % 1000 * 1000 * 1000;
        ts.tv_sec += time_ms / 1000 + ts.tv_nsec / (1000 * 1000 * 1000);
        ts.tv_nsec = ts.tv_nsec % (1000*1000*1000);

        err = sem_timedwait(sem, &ts);
        #else
        // sem_trywait + usleep的方式实现
        // 如果信号量大于0,则减少信号量并立马返回true
        // 如果信号量小于0,则阻塞等待,当阻塞超时时返回false
        const int64_t timeoutUs = time_ms * 1000; // 延时时间由毫米转换为微秒
        const int64_t maxTimeWait = 100000; // 最大的睡眠的时间为100000微秒,也就是100毫秒

        int64_t timeWait = 10; // 睡眠时间,默认为10微秒
        int64_t delayUs = 0; // 剩余需要延时睡眠时间

        const int64_t startUs = CdcMonoTimeUs(); // 循环前的开始时间,单位微秒
        int64_t elapsedUs = 0; // 过期时间,单位微秒

        int ret = 0;

        do
        {
            // 如果信号量大于0,则减少信号量并立马返回true
            if(sem_trywait(sem) == 0)
            {
                return 0;
            }

            // 系统信号则立马返回false
            if(errno != EAGAIN)
            {
                return -1;
            }

            // delayUs一定是大于等于0的,因为do-while的条件是elapsedUs <= timeoutUs.
            delayUs = timeoutUs - elapsedUs;

            // 睡眠时间取最小的值
            timeWait = min(delayUs, timeWait);

            // 进行睡眠 单位是微秒
            ret = usleep(timeWait);
            if( ret != 0 )
            {
                return -1;
            }

            // 睡眠延时时间双倍自增
            timeWait *= 2;

            // 睡眠延时时间不能超过最大值
            timeWait = min(timeWait, maxTimeWait);

            // 计算开始时间到现在的运行时间 单位是微秒
            elapsedUs = CdcMonoTimeUs() - startUs;
        } while( elapsedUs <= timeoutUs ); // 如果当前循环的时间超过预设延时时间则退出循环

        return -1;
        #endif
    }

    return err;

补充知识:

函数 "clock_gettime" 是基于Linux C语言的时间函数,他可以用于计算精度和纳秒。

#include<time.h>
int clock_gettime(clockid_t clk_id,struct timespec *tp);

参数:

  • clk_id:检索和设置的clk_id指定的时钟时间。
  • CLOCK_REALTIME:系统实时时间,随系统实时时间改变而改变,即从UTC1970-1-1 0:0:0开始计时,中间时刻如果系统时间被用户改成其他,则对应的时间相应改变
  • CLOCK_MONOTONIC:从系统启动这一刻起开始计时,不受系统时间被用户改变的影响
  • CLOCK_PROCESS_CPUTIME_ID:本进程到当前代码系统CPU花费的时间
  • CLOCK_THREAD_CPUTIME_ID:本线程到当前代码系统CPU花费的时间
struct timespec
{
    time_t tv_sec; /* 秒*/
    long tv_nsec; /* 纳秒*/
};
Logo

一站式 AI 云服务平台

更多推荐