시스템 시간 수정으로 인한 sem_timedwait 함수의 무한 대기 문제 해결 및 분석
개요
최근 프로젝트 이슈를 해결하다가 시스템 시간을 과거로 수정한 후 sem_timedwait 함수가 무한 대기하게 되는 현상을 발견했습니다. 해당 함수의 두 번째 인자인 절대 시간戳이 영향을 미치는 문제를 분석한 결과, 이 함수에 결함이 존재합니다.
sem_timedwait 함수의 결함 이유:
시스템 시간이 1565000000 (2019-08-05 18:13:20)일 때, sem_timedwait 함수에 전달되는 절대 시간戳가 1565000100 (2019-08-05 18:15:00)이라면 함수는 100초 (1분 40초) 동안 대기해야 합니다. 만약 이 기간 중 시스템 시간이 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);
- 인자 sem의 값이 0보다 크면 즉시 감소시키고 정상적으로 반환
- 인자 sem의 값이 0보다 작으면 지정된 시간까지 대기하며, 시간 초과시
-1을 반환 (errno는ETIMEDOUT으로 설정됨)
두 번째 인자 abs_timeout은 에폭(Epoch, 1970-01-01 00:00:00 UTC)부터의 초와 나노초로 구성된 절대 시간戳를 가리킵니다. 이 구조체는 다음과 같이 정의됩니다.
struct timespec {
time_t tv_sec; /* 초 */
long tv_nsec; /* 나노초 */
};
해결 방법
sem_trywait 함수와 usleep을 조합하여 sem_timedwait 함수와 유사한 기능을 구현할 수 있습니다. 이 방법은 시스템 시간이 과거로 변경되는 상황에서도 무한 대기 문제가 발생하지 않습니다.
sem_trywait 함수 소개
sem_trywait() 함수는 sem_wait() 함수와의 차이점은, 인자 sem의 값이 0일 경우 즉시 에러를 반환한다는 점입니다. 에러 코드 errno는 EAGAIN으로 설정됩니다. sem_trywait()은 sem_wait()의 비동기 모드 버전이라고 할 수 있습니다.
int sem_trywait(sem_t *sem)
성공 시 0을 반환하며, 실패 시 -1을 반환하고 인자 sem의 값은 그대로 유지됩니다.
sem_trywait + usleep 방법으로 구현
주요 구현 아이디어:
sem_trywait 함수는 인자 sem의 값이 0이든 아니든 즉시 반환합니다. 함수가 정상적으로 반환되는 경우 usleep를 실행하지 않고 즉시 반환합니다. 함수가 정상적으로 반환되지 않는 경우 usleep를 통해 지연을 실현합니다. 구체적인 구현 방법은 아래 bool Wait( size_t timeout ) 함수를 참조하세요:
#include <string>
#include<iostream>
#include<semaphore.h>
#include <time.h>
sem_t sem;
// CLOCK_MONOTONIC 시간을 초 단위로 변환
inline uint64_t GetTimeConvSeconds( timespec* curTime, uint32_t factor )
{
clock_gettime( CLOCK_MONOTONIC, curTime );
return static_cast<uint64_t>(curTime->tv_sec) * factor;
}
// CLOCK_MONOTONIC 시간을 마이크로초 단위로 반환
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을 통해 대기 구현
bool Wait( size_t timeout )
{
const size_t timeoutUs = timeout * 1000; // 밀리초를 마이크로초로 변환
const size_t maxTimeWait = 10000; // 최대 대기 시간 10毫秒
size_t timeWait = 1; // 최초 대기 시간 1 마이크로초
size_t delayUs = 0; // 남은 대기 시간
const uint64_t startUs = GetMonnotonicTime(); // 시작 시간
uint64_t elapsedUs = 0; // 경과 시간
int ret = 0;
do
{
// sem의 값이 0이면 즉시 반환
if( sem_trywait( &sem ) == 0 )
{
return true;
}
// 에러 코드가 EAGAIN가 아니면 즉시 반환
if( errno != EAGAIN )
{
return false;
}
// 남은 대기 시간 계산
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 ); // 예상 대기 시간보다 경과 시간이 많으면 중단
// 시간 초과로 중단
return false;
}
// 절대 시간戳 계산
inline timespec* GetAbsTime( size_t milliseconds, timespec& absTime )
{
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을 통해 대기 구현 - 결함 존재
bool SemTimedWait( size_t timeout )
{
timespec absTime;
GetAbsTime( timeout, absTime );
if( sem_timedwait( &sem, &absTime ) != 0 )
{
return false;
}
return true;
}
int main(void)
{
bool signaled = false;
uint64_t startUs = 0;
uint64_t elapsedUs = 0;
//_semaphore초기화, 값 0으로 설정
sem_init( &sem, 0, 0 );
////////////////////// sem_trywait+usleep 대기 구현 ////////////////////
startUs = GetMonnotonicTime();
signaled = Wait(1000);
elapsedUs = GetMonnotonicTime() - startUs;
std::cout << "signaled:" << signaled << "\t 대기 시간:" << elapsedUs/1000 << "ms" << std::endl;
////////////////////// sem_timedwait을 통해 대기 구현 ////////////////////
////////////////////// 결함이 있는 방법 ///////////////////////
startUs = GetMonnotonicTime();
signaled = SemTimedWait(2000);
elapsedUs = GetMonnotonicTime() - startUs;
std::cout << "signaled:" << signaled << "\t SemTimedWait 대기 시간:" << elapsedUs/1000 << "ms" << std::endl;
return 0;
}
테스트 결과
[root@lincoding sem]# ./sem_test
signaled:0 대기 시간:1000ms
signaled:0 SemTimedWait 대기 시간:2000ms
결론
sem_timedwait 함수를 통해 지연 대기 기능을 구현하는 것은 권장되지 않습니다. 만약 이 함수를 사용하려면 sem_trywait + usleep을 통해 대기를 구현하는 것이 좋습니다!