C/C++ Modify System Time, Sem_time dwait Cause Blocking Problem Solution and Analysis

Solution and analysis of the problem that sem_timedwait has been blocked by modifying system time

introduce

When recently fixing a project problem, it was found that when the system time was modified forward, the sem_timedwait function would be blocked all the time. By searching, it is found that int sem_time dwait (sem_t*sem, const struct timespec*abs_timeout); the second blocking time parameter passed in is absolute timestamp, so the function is defective.

Reasons for defects in sem_timedwait:

Assuming that the current system time is 1565 000 000 (2019-08-05 18:13:20) and the time stamp of the blocking waiting of sem_timedwait is 1565 000 100 (2019-08-05 18:15:00), then sem_timedwait needs to block for 1 minute and 40 seconds (100 seconds). In the process of sem_timedwait blocking, the system time is modified to 1500000 (201 000) in the middle. 7-07-14 10:40:00), then sem_time dwait will be blocked for more than two years! This is the defect of sem_timedwait!!

Introduction of sem_timedwait function

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
  • If the semaphore is greater than 0, the semaphore is degraded and returned to normal immediately.
  • If the semaphore is less than 0, the blocking waits and returns to failure when the blocking timeout occurs (errno is set to ETIMEDOUT)

The second parameter, abs_timeout, points to a structure that specifies an absolute timeout, which is composed of seconds and nanoseconds from Epoch, 1970-01:00:00+0000 (UTC). This structure is defined as follows

struct timespec {
    time_t tv_sec;        /* second */
    long   tv_nsec;       /* nanosecond */
};

Solution

Similar to sem_time dwaim function can be achieved by sem_trywait + usleep, and there will be no blocking problem due to the change of system time forward.

Introduction of sem_trywait function

The function sem_trywait() differs from sem_wait() in that if the current value of the semaphore is 0, an error is returned instead of a blocking call. Error value errno is set to EAGAIN. Sem_trywait () is actually a non-blocking version of sem_wait ().

int sem_trywait(sem_t *sem)

The successful execution returns 0, the failed execution returns - 1, and the semaphore value remains unchanged.

Implementation of sem_trywait + usleep

The main ideas of realization are as follows:
The sem_trywait function returns immediately regardless of whether the semaphore is 0 or not, and it does not usleep when the function returns normally. When the function returns abnormally, it uses usleep to realize the delay, which is realized by bool Wait (size_t timeout) function in the following code:

#include <string>
#include<iostream>

#include<semaphore.h>
#include <time.h>

sem_t g_sem;

// Get the incremental time of the call from system startup
inline uint64_t GetTimeConvSeconds( timespec* curTime, uint32_t factor )
{
    // CLOCK_MONOTONIC: Timing from the moment the system starts, unaffected by user changes in system time
    clock_gettime( CLOCK_MONOTONIC, curTime );
    return static_cast<uint64_t>(curTime->tv_sec) * factor;
}

// Get the incremental time of the call from system startup -- converted to microseconds
uint64_t GetMonnotonicTime()
{
    timespec curTime;
    uint64_t result = GetTimeConvSeconds( &curTime, 1000000 );
    result += static_cast<uint32_t>(curTime.tv_nsec) / 1000;
    return result;
}

// Implementation of sem_trywait + usleep
// If the semaphore is greater than 0, reduce the semaphore and return to true immediately
// If the semaphore is less than 0, the block waits and returns false when the block timeout occurs
bool Wait( size_t timeout )
{
    const size_t timeoutUs = timeout * 1000; // Delay time from millimeter to microsecond
    const size_t maxTimeWait = 10000; // The maximum sleep time is 10,000 microseconds, or 10 milliseconds.

    size_t timeWait = 1; // Sleep time, default 1 microsecond
    size_t delayUs = 0; // Remaining Sleep Delays

    const uint64_t startUs = GetMonnotonicTime(); // The start time before the loop, in microseconds
    uint64_t elapsedUs = 0; // Expiration time, unit microsecond

    int ret = 0;

    do
    {
        // If the semaphore is greater than 0, reduce the semaphore and return to true immediately
        if( sem_trywait( &g_sem ) == 0 )
        {
            return true;
        }

        // The system signal immediately returns false.
        if( errno != EAGAIN )
        {
            return false;
        }

        // delayUs must be greater than or equal to zero, because the condition for do-while is elapsed Us <= timeout Us.
        delayUs = timeoutUs - elapsedUs;

        // Minimum Sleep Time
        timeWait = std::min( delayUs, timeWait );

        // The unit of sleep is microseconds.
        ret = usleep( timeWait );
        if( ret != 0 )
        {
            return false;
        }

        // Sleep delay doubles
        timeWait *= 2;

        // Sleep delay should not exceed maximum
        timeWait = std::min( timeWait, maxTimeWait );

        // Calculating the start time is now run in microseconds
        elapsedUs = GetMonnotonicTime() - startUs;
    } while( elapsedUs <= timeoutUs ); // Exit the loop if the current loop time exceeds the default delay time

    // Exit overtime, return false
    return false;
}

// Get the absolute timestamp that needs to delay the waiting time
inline timespec* GetAbsTime( size_t milliseconds, timespec& absTime )
{
    // CLOCK_REALTIME: The real-time time of the system changes with the change of the real-time time of the system, i.e. from UTC1970-1 to 10:0:0.
    //                 In the middle time, if the system time is changed by the user, the corresponding time will change accordingly.
    clock_gettime( CLOCK_REALTIME, &absTime );
    
    absTime.tv_sec += milliseconds / 1000;
    absTime.tv_nsec += (milliseconds % 1000) * 1000000;

    // Nanosecond carry second
    if( absTime.tv_nsec >= 1000000000 )
    {
        absTime.tv_sec += 1;
        absTime.tv_nsec -= 1000000000;
    }

   return &absTime;
}

// Sleep implemented by sem_timedwait -- defective
// If the semaphore is greater than 0, reduce the semaphore and return to true immediately
// If the semaphore is less than 0, the block waits and returns false when the block timeout occurs
bool SemTimedWait( size_t timeout )
{
    timespec absTime;
    // Get the absolute timestamp that needs to delay the waiting time
    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;
    
    // Initialize semaphores, number 0
    sem_init( &g_sem, 0, 0 );
    
    ////////////////////// Sleep implemented by sem_trywait+usleep////////////////////
    // Get the start time in microseconds
    startUs = GetMonnotonicTime(); 
    // Delayed waiting
    signaled = Wait(1000);
    // Gets the time to wait overtime in microseconds
    elapsedUs = GetMonnotonicTime() - startUs;
    // Output signaled: 0 Wait time: 1000ms
    std::cout << "signaled:" << signaled << "\t Wait time:" << elapsedUs/1000 << "ms" << std::endl;

    ////////////////////// Sleep implemented by sem_timedwait////////////////////
    ///////////////////// There is a flaw, because when the system time is modified in sem_timedwait blocking, sem_timedwait will always be blocked.//////////////////
    // Get the start time in microseconds
    startUs = GetMonnotonicTime();
    // Delayed waiting
    signaled = SemTimedWait(2000);
    // Gets the time to wait overtime in microseconds
    elapsedUs = GetMonnotonicTime() - startUs;
    // Output 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

summary

Try not to use sem_timedwait function to achieve the function of delay waiting. To use the function of delay waiting, it is recommended to use sem_trywait+usleep to achieve delay blocking!

Keywords: less

Added by neex1233 on Sat, 17 Aug 2019 15:17:22 +0300