Linux System Programming (Robert Love) chapter 11 시간
커널은 다음 세가지 방법으로 경과 시간을 측정
실제 시간 : 현실에서 사용하는 진짜 시간과 날짜(wall_time), 사용자 상호작용/이벤티 타임스탬프에 사용
프로세스 시간 : 프로세스를 실행하는데 소비한 시간, 프로세스는 프로파일링과 통계 산출 목적으로 특정 알고리즘을 처리하는데 걸린 시간을 측정
모노토닉 시간 : 반드시 일정하게 증가, 운영체제는 시스템 가동시간(uptime 사용), 두 샘플링 사이의 시간 차이를 계산할 때 유용
시간 측정 방법은 다음 두 형식중 하나로 표현
상대시간 : 현재 시각과 같이 특정 기준점에 대한 상대값으로 표현, 모노토닉 시간은 상대 시간을 계산하는데 유용
절대시간 : 특정 기준점 없이 절대값으로 표현(2000년 3월 25일 정오같은 표현, 절대 시간을 계산하는데 적합)
유닉스 시스템은 UTC로 정의된 기원 부터 경과된 절대 시간을 초로 표현(epoch time)(UTC = GMT, ZULU)
운영체제는 커널이 관리하는 소프트웨어 시계를 사용해서 시간 경과를 추적
Jiffie counter : 커널은 타이머를 이용해서 타이머 동작주기마다 틱이나 지피라는 한단위 만큼 늘림
HZ : 시스템 타이머의 빈도(아키텍쳐마다 다름)(100을 사용하다 최근 250을 사용, 4ms마타 시스템 타이머 동작)
CLOCK_PER_SEC : POSIX 인터페이스에서 고정 주파수에 맞춰 시간 측정값을 제공하는데 이 때의 고정 주파수
sysconf(_SC_CLK_TCK)로 HZ값을 알 수 있음
커널은 시작하면서 하드웨어 시계에서 현재 시간을 받아 운영체제 시각을 초기화, 시스템 종료시 현재 시각을 하드웨어 시계에 다시 기록
hwclock 명령으로 하드웨어 시각과 운영체제 시각을 동기화 가능
시간에 대한 자료 구조
유닉스 시스템이 발전함에 따라 시간 관리를 위한 독자적인 인터페이스를 구현했으며 단순한 시간 개념을 여러 자료 구조로 표현
전통적인 표현볍
가장 단순한 자료구조는 time_t이며 <time.h>에 정의 (time_t 내부 구조를 숨기기 위함)
대부분의 유닉스 시스템에서 long임 (typedef long time_t)
마이크로 초 정밀도
1
2
3
4
5
6
#include <sys/time.h>
struct timeval {
time_t tv_sec ; // 초
suseconds_t tv_usec // 마이크로초
}
timeval 구조체는 time_t를 확장하여 마이크로초 정밀도를 추가
나노 초 정밀도
1
2
3
4
5
6
#include <sys/time.h>
struct timespec {
time_t tv_sec ; // 초
long tv_nsec ; // 나노 초
}
timespec 구조체가 나노초까지 해상도를 제공
timespec 구조체 등장 이후 대다수 시간 관련 인터페이스는 timespec을 사용하도록 변경(그라나 중요함수는 여전히 timeval 사용)
시간 세분하기
C표준은 좀 더 사람에게 가까운 형태로 세분화된 시간을 표현하는 tm 구조체를 제공
구조체에 대한 설명은 책에 있음
프로세스 시간을 위한 타입
clock_t는 tick을 표현, int로 표현, 인터페이스에 따라 clock_t 틱은 시스템의 실제 타이머 주파수(HZ) 나 CLOCK_PER_SEC를 나타냄
POSIX 시계
POSIX 시계는 5가지 타입이 있음(clockid_t 타입)
CLOCK_REALTIME : 시스템 전역에서 사용하는 실제시간 시계, 설정하려면 특수 권한 필요
CLOCK_MONOTONIC : 시스템 시동과 같이 불특정 시작 시점 부터 단조롭게 증가하는 시계, 어떤 프로세스도 설정하지 못함
CLOCK_MONOTONIC_RAW : CLOCK_MONOTONIC과 유사하나 시간이 뒤틀렸을 때 조정되지 않음(리눅스 전용)
CLOCK_PROCESS_CPUTIME_ID : 프로세서 수준에서 지원되는 각 프로세스에서 사용 가능한 고해상되 시계(x86의 경우 TSC 레지스터 사용)
CLOCK_THREAD_CPUTIME_ID : CLOCK_PROCESS_CPUTIME_ID와 유사하나 스레드 마다 유일한 시계
시계해상도
1
2
3
#include <time.h>
int clock_getres ( clock_t clock_id , struct timespec * res );
clock_getres() 함수 호출이 성공하면 res가 NULL이 아닐 경우 clock_id로 지정한 시계의 해상도를 res에 저장
성공시 0반환, 실패하면 -1을 반환하고 errno를 EFAULT, EINVAL 중 하나로 설정
현재시간 얻기
1
2
3
#include <time.h>
time_t time ( time_t * t );
time() 함수를 호출하면 epoch time 이후 경과한 현재 시각을 초 단위로 표현하여 반환
에러 발생시 -1을 반환, errno를 설정(유일한 값은 EFAULT로 t가 유효한 포인터가 아님)
더 나은 인터페이스
1
2
3
#include <sys/time.h>
int gettimeofday ( struct timeval * tv , struct timezone * tz );
gettimeofday()호출이 성공시 tv에 현재시각을 기록하고 0을 반환,
tz는 사용하지 않으므로 NULL로 설정(커널이 시간대를 관리하지 않음, glibc도 timezone 구조체의 tz_dsttime 필드를 미사용)
호출 실패 시 -1 반환, errno 설정(EFAULT : tv,tz 가 유효하지 않은 포인터)
고급 인터페이스
1
2
3
#include <time.h>
int clock_gettime ( clockid_t clock_id , struct timespec * ts );
clock_gettime()은 지정한 시계의 시간을 얻기 위한 함수, 나노 정밀도를 허용
호출이 성공하면 0을 반환, clock_id로 지정한 시계의 현재 시간을 ts에 저장
실패 시 -1을 반환, errno를 설정(EFAULT, EINVAL)
프로세스 시간 얻기
1
2
3
4
5
6
7
8
9
10
#include <sys/time.h>
struct tms {
clock_t tms_utime ; /* 소비한 사용자 시간 */
clock_t tms_stime ; /* 소비한 시스템 시간 */
clock_t tms_cutime ; /* 자식 프로세스가 소비한 사용자 시간 */
clock_t tms_cstime ; /* 자식 프로세스가 소비한 시스템 시간 */
}
clock_t times ( struct tms * buf );
times() 시스템 콜은 실행 중인 프로세스와 자식 프로세스의 프로세스 시간을 틱 단위로 가져옴(호출 성공 시 buf 채움)
사용자 시간 : 사용자 영역에서 코드를 수행한 시간
시스템 시간 : 커널 영역에서 코드를 수행한 시간
자식 프로세스가 끝나고 부모가 wait() 또는 관련 함수를 호출하고 나서야 각 자식 프로세스에 대한 보고 시간이 구조체의 해당 필드에 포함
호출 실패 시 -1 반환, errno를 설정(EFAULT : buf가 유효하지 않은 포인터)
현재 날짜와 시각 설정하기
1
2
3
4
#define _SVID_SOURCE
#include <time.h>
int stime ( time_t * t );
stime() 호출 성공 시 시스템 시간을 t 값으로 설정하고 0을 반환 (time()함수 대응)
CAP_SYS_TIME 기능이 필요(일반적으로 root사용자)
호출이 실패 시 -1을 반환, errno를 설정(EFAULT, EPERM)
정확하게 시각 설정하기
1
2
3
#include <sys/time.h>
int settimeofday ( const struct timeval * tv , const struct timezone * tz );
settimeofday() 호출이 성공시 시스템 시간을 tz값으로 설정 후 0반환, tz에는 NULL이 권장 됨(gettimeofday() 대응)
호출이 실패 시 -1을 반환, errno를 설정(EFAULT. EINVAL, EPERM)
시각 설정을 위한 고급 인터페이스
1
2
3
#include <time.h>
int clock_settime ( clockid_t clock_id , const struct timespec * ts );
clokc_settime() 호출 성공 시 clock_id로 지정한 시계를 ts 시간으로 설정, 성공시 0반환
호출이 실패 시 -1 반환, errno를 설정(EFAULT, EINVAL, EPERM)
대부분의 시스템에서 유일하게 설정가능한 시계는 CLOCK_REALTIME, 나노 초 해상도를 제공한다는 장점이 있음
시간 다루기
1
2
3
4
#include <time.h>
char * asctime ( const struct tm * tm );
char * asctime_r ( const struct tm * tm , char * buf );
asctime()함수는 tm 구조체를 ASCII 문자열로 변환, 정적으로 할당된 문자열을 가리키는 포인터 반환
asctime()은 스레드 세이프 하지 않으므로 asctime_r을 사용(문자열을 buf에 저장, 최소 26글자)
에러 발생의 경우 NULL을 반환
1
2
3
#include <time.h>
time_t mktime ( struct tm * tm );
mktime()은 tm 구조체를 time_t로 변환, tm에 따라 tzset()을 호출해 시간대를 설정
에러가 발생하면 -1을 반환
1
2
3
4
#include <time.h>
char * ctime ( const time * timep );
char * ctime_r ( const time_t * timep , char * buf );
ctime()은 time_t를 ASCII 표현으로 변환, 실패하면 NULL을 반환
줄바꿈 문자를 반환 문자에 붙임
ctime()은 스레드 세이프 하지 않으므로 ctime_r을 사용(문자열을 buf에 저장, 최소 26글자)
1
2
3
4
#include <time.h>
struct tm * gmtime ( const time_t * timep );
struct tm * gmtime_r ( const time_t * timep , struct tm * result );
gmtime()은 time_t를 tm구조체로 변환하여 UTC 시간대로 표현, 실패하면 NULL을 반환
gmtime()은 스레드 세이프 하지 않으므로 gmtime_r을 사용
1
2
3
4
#include <time.h>
struct tm * localtime ( const time_t * timep );
struct tm * localtime_r ( const time_t * timep , struct tm * result );
localtime(), localtime_r은 gmtime(), gmtime_r()과 비슷하게 동작, 그러나 time_t를 사용자 시간대에 맞춰 표현
localtime()도 tzset()을 호출하여 시간대를 초기화 함(localtime_r()의 동작은 표준에 명세되어 있지 않음)
1
2
3
#include <time.h>
double difftime ( time_t time1 , time_t time0 );
difftime()은 두 값사이에 경과한 초를 double타입으로 변환하여 반환
오버플로우가 감지를 고려하지 않은 점만 제외하고 (double)(time1 - time0)와 같다
리눅스에서 time_t는 정수타입이므로 double 타입으로 변환할 필요는 없지만 이식성을 고려하여 difftime() 사용을 권장
시스템 시계 조율
실제 시간을 갑작스럽게 조정하면 동작 과정에서 절대 시각에 의존하는 어플리케이션은 혼란에 빠짐(ex. Makefile 증분 빌드)
이를 위해 유닉스는 adjtime() 함수를 통해 제공된 시간 차이에 따라 현재 시각을 점진적으로 조정(adjtime()을 사용하여 주기적으로 시간 보정)
1
2
3
4
#define _BSD_SOURCE
#include <sys/time.h>
int adjtime ( const struct timeval * delta , struct timeval * olddelta );
adjtime() 함수 호출이 성공하면 커널이 delta로 지정한 시간에 맞추어 천천히 조정 작업을 시작하도록 하고 0을 반환
delta가 양수면 시간을 빠르게 가게, 음수면 느리게 가도록(갑작스런 시간변화는 안생김)
delta가 NULL이 아니라면 이전에 등록된 조정값을 처리하는 작업은 멈춤, olddelta가 NULL이 아닐 시 이전에 등록되었으나 아직 적용되지 않은 갑이 가져옴
delta가 NULL이고 olddelta가 NULL이 아니면 현재 조정중인 값을 가져올 수 있음
adjtime()에 적용할 조정값은 크기가 작아야 함(NTP가 이상적 활용 사례)
에러 발생 시 -1을 반환, errno를 설정(EFAULT, EINVAL, EPERM)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/timex.h>
struct timex {
int mode ; /* 모드 셀렉터 */
long offset ; /* 시간 오프셋(usec) */
long freq ; /* 주파수 오프셋 (스케일된 ppm) */
long maxerror ; /* 최대 에러 (usec) */
long esterror ; /* 예상 에러(usec) */
int status ; /* 시계 상태 */
long constant ; /* PPL 시간 상수 */
long precision ; /* 시계 정밀도 (usec) */
long tolerance ; /* 시계 주파수 허용 오차 (ppm) */
struct timeval time ; /* 현재 시각 */
long tick ; /* 틱 경과 시각(usec) */
};
int adjtimex ( struct timex * adj );
adjtimex()를 호출하면 커널 시간관련 파라미터를 읽어 adj가 가리키는 timex 구조체에 기록(adjtime()보다 훨씬 강력하고 복잡, 리눅스 전용)
timex 구조체의 mode 필드에 따라 선택적으로 특정 파라메터를 추가적으로 설정, mode 필드는 플래그 비트단위 OR(책에 설명 있음)
mode가 0이면 아무런 값도 설정하지 않음(설정값을 가져옴), CAP_SYS_TIME 기능이 있는 사용자(root)만 0이 아닌 값을 설정 가능
호출이 성공 시 현재 시계 상태 반환(TIME_OK/INS/DEL 등), 실패시 -1 반한 및 errno설정(EFAULT, EINVAL, EPERM) (책에 설명)
자세한 설명은 RFC130을 참조
잠들기와 대기
1
2
3
#include <unistd.h>
unsigned int sleep ( usigned int seconds )
sleep()은 seconds로 지정한 초 동안 프로세스를 잠재움
sleep()은 호출이 성공하면 0을 반환, 그렇지 않다면 잠들지 않은 초를 반환(시그널이 잠들기를 방해한 상황)
마이크로 초 해상도로 잠들기
1
2
3
4
5
6
7
8
9
10
/* BSD 버전 */
#include <unistd.h>
void usleep ( unsigned long usec );
/* SUSv2 버전 */
#define _XOPEN_SOURCE 500
#include <unistd.h>
int usleep ( usecond_t usec );
usleep() 호출이 성공하면 프로세스는 usec 동안 잠든다
BSD는 반환 값이 없으며, SUS버전은 성공시 0을 실패 시 -1 반환 및 errno설정
나노 초 해상도로 잠들기
1
2
3
4
#define _POSIX_C_SOURCE 199309
#include <time.h>
int nanosleep ( const struct timespec * req , struct timespec * rem );
nanosleep() 함수는 호출이 성공하면 req로 명시한 시간 동안 프로세스가 잠들며 0을 반환
에러가 발생하면 -1을 반환 및 errno를 적절한 값으로 설정 (EFAULT, EINVAL, EINTR)
시그널이 잠들기를 방해하면 -1을 반환하고 errno를 EINTR로 설정, rem이 NULL이 아니라면 남은 시간을 rem에 저장(두번째 인자를 이용하면 인터럽트가 걸리더라도 계속 잠들게 프로그래밍 가능, 책에 예제)
nanosleep()은 sleep()/usleep()보다 장점, 왠만하면 이 함수를 사용하자
높은 해상도 제공, POSIX.1b에서 표준화, 시그널로 구현되어 있지 않음
고급 잠들기 기법
1
2
3
#include <time.h>
int clock_nanosleep ( clockid_t clock_id , int flag , const struct timespec * req , struct timespec * rem );
clock_nanosleep()은 nanosleep()과 유사하게 동작하는 고급인터페이스
clock_id는 측정 시계를 명시, CLOCK_PROCESS_CPUTIME_ID를 제외하고 유효(잠들면 프로세스 시간도 잠듬)
어떤 시간까지 잠들어야 한다면 CLOCK_REALTIME, 상대 시간 동안 잠들어야 한다면 CLOCK_MONOTONIC이 제격
flag는 TIMER_ABSTIME이면 req로 지정된 값은 절대값으로 취급(잠재적 경쟁상태 해결, 책에 자세한 설명), 0이면 상대값
이식성을 고려한 잠들기
2장에서 설명했던 select는 이식성이 높은 마이크로 초 단위로 잠들기 방법을 제공(책에 잘 설명되어 있다.)
시간 초과
스케줄러 동작(커널이 정시에 깨웠지만 스케줄러가 다른 작업 선택 수행)이나 타이머 초과(타이머 정밀도가 요청 받은 시간 간격보다 떨어지는 경우) 때문에 요청보다 초과해서 잠들 수 있음
POSIX에서 제공하는 고해상도 시계와 더 높은 HZ 값을 사용하여 시간 초과 가능성을 최소로 줄임
잠들기 대안
되도록이면 잠들기를 피하자, 1초 미만으로 잠든다면 크게 문제되지 않음
이벤트를 기다리며 busy wait를 위해 잠들기로 도배한 코드는 나쁘다. 파일 디스크립터를 븍록해서 커널이 잠들기를 처리하고 프로세스를 깨우도록 하자
타이머
간단한 알람
1
2
3
#include <uinstd.h>
unsigned int alarm ( unsigned int seconds );
alarm() 함수를 호출하면 실제 시간에서 seconds 초가 경과한 후에 호출한 프로세스에 SIGALRM 시그널을 전송하도록 예약
앞서 걸어둔 시그널이 대기 중인 상황이라면 호출은 그 알람을 취소하고 새로 요청한 알람으로 대체 후 이전 알람에서 남아 있는 초 숫자 반환
second가 0이라면 이전 알람은 취소되나 새로운 알람은 안걸어 둠
SIGALRM 시그널 처리를 위한 시그널 핸들러 등록 필요
인터벌 타이머
1
2
3
4
5
6
7
8
9
#include <sys/time.h>
struct itiemerval {
struct timeval it_interval ; /* 다음 값 */
struct timeval it_value ; /* 현재 값 */
}
int gettimer ( int which , struct itimerval * value );
int settimer ( int which , const struct itimerval * value , struct itimervla * ovalue );
인터벌타이머는 alarm()처럼 동작하나 옵션으로 자동 재장전 기능 제공하며 명확하게 구분된 세가지 모드 중 하나로 동작
ITIMER_REAL : 실제 시간을 측정, 지정된 실제 시간이 경과하면 프로세스에 시그널(SIGALRM) 전송
ITIMER_VIRTUAL : 프로세스 사용자 영역 코드가 수행되는 동안에만 타이머가 흐름, SIGVTALRM 시그널 전송
ITIMER_PROF : 프로세스가 실행 중이거나 커널이(시스템 콜 완료 등) 프로세스를 대신해서 실행 중인 경우 타이머 흘러감(SIGPROF 시그널 전송)
itimerval 구조체는 타이머 만료 시간과 만료 이후에 타이머를 다시 설정하기 위한 경과 시간을 사용자가 명시하도록 함
settimer()는 which가 가리키는 타이머에 it_value로 지정한 만료시간을 설정 지정 시간이 경과하면 it_interval로 지정한 시간으로 타이머 재설정
ovalue가 NULL이 아니면 which가 가리키는 인터벌 타이머에 설정된 이전 값 반환
성공 하면 0을 반환, 실패 시 -1반환 및 errno를 설정(EFAULT, EINVAL)
고급 타이머
가장 강력한 타이머 인터페이스는 POSIX 시계에서 시작
타이머 생성하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <signal.h>
#include <time.h>
struct sigevent {
union sigval sigev_value ;
int sigev_signo ;
int sigev_notify ;
void ( * sigev_notify_function )( union sigval );
pthread_attr_t * sigev_notify_attributes ;
}
union sigval {
int sival_int ;
void * sival_ptr ;
}
int timer_create ( clockid_t clockid , struct sigevent * evp , timer_t * tiemrid );
timer_create() 호출이 성공하면 clockid로 지정한 POSIX 시계와 관련된 새로운 타이머를 생헝하며 timerid에 새롭고 유일한 타이머 식별자를 저장한 다음 0을 반환
호출이 실패하면 -1을 반환 및 errno 설정(EAGAIN, EINVAL, ENOTSUP)
evp 인자가 NULL이 아니라면 타이머가 만료될 때 발생하는 비동기식 통지를 지정, 프로세스는 sigev_notify로 타이머 만료시 동작을 명세
SIGEV_NONE : NULL 통지, 타이머 만료 시 아무일도 일어나지 않음
SIGEV_SIGNAL : 타이머가 만료될 때 커널은 프로세스에 sigev_signo로 지정한 시그널을 보냄
SIGEV_THREAD : 타이머가 만료될 때 커널은 새로운 스레드를 띄워서 sigev_notify_function 함수에 sigev_value를 인자로 넘겨서 실행, sigev_notify_attributes가 스레드 속성
evp 가 NULL이면 SIGEV_SIGNAL, SIGALRM, 타이머 id로 설정되서 타이머 만료를 알림
타이머 설정하기
1
2
3
#include <time.h>
int timer_settime ( tiemr_t timerid , int flag , const struct itimerspec * value , struct itiemrspec * ovalue );
timer_settime() 호출 시 만료시간이 value이고 timerid로 지정한 타이머가 설정
flag는 TIMER_ABSTIME이면 req로 지정된 값은 절대값으로 취급(고급 잠들기 기법 참조)
타이머 만료 정보 얻기
1
2
3
#include <time.h>
int timer_gettime ( timer_t timerid , struct itimerspec * value );
timer_gettime() 호출이 성공하면 timerid로 지정한 타이머의 만료 시간을 value에 저장
호출 성공 시 0반환, 실패 시 -1 반환 및 errno설정(EFAULT, EINVAL)
타이머 초과 횟수 얻기
1
2
3
#include <time.h>
int timer_getoverrun ( timer_t timerid );
호출이 성공하면 타이머의 초기 만료 시점과 타이머 만료 상태를 프로세스에 통지한 시점 사이에 일어난 추가적인 타이머 만료 횟수를 반환
POSIX는 초과횟수가 DELAYTIMER_MAX 이상일 시 DELAYTIMER_MAX 반환(리눅스는 0부터 다시 시작)
실패하면 -1 반환 및 errno를 설정(EINVAL : timerid가 유효하지 않음)
타이머 삭제하기
1
2
3
#include <time.h>
int timer_delete ( timer_t timerid );
timer_delete() 호출이 성공하면 timerid 관련 타이머를 삭제하고 0을 반환
실패하면 -1 반환 및 errno를 설정(EINVAL : timerid가 유효하지 않음)
Licensed under CC BY-NC-SA 4.0