-
[시스템프로그래밍] Timer시스템프로그래밍 2023. 12. 16. 23:44
https://scwcart.tistory.com/entry/curses
curses
이번 글에서는 화면 공간을 프로그래밍하는 curses library에 대해서 알아볼 예정입니다. 초창기 컴퓨터 시절, 우주를 탐험하는 컨셉의 비디오 게임이 하나 나왔습니다. 이 게임을 구현하기 위해서
scwcart.tistory.com
지난 글에서 비디오 게임을 만들기 위해
1) Space programming
2) Time Handling
3) Signal Handling
4) IPC
4가지 주제가 필요하다고 했습니다.
그럼 이번 글에서는 두 번째 주제인 Time Handling에 대해 알아보겠습니다.
Time Handling: sleep
그럼 이제 두 번째, Time Handling에 대해 알아보겠습니다.
비디오 게임에서 어떤 요소들은 바로 표시되는 것이 아니라 특정 타이밍에 등장해야 하는 것들이 있습니다.
이렇게 하려면 명령을 받고 실행하기까지 사이의 시간이 잠깐 지연되도록 하면 되겠죠?
그래서 시간을 다룰 때는 sleep()이라는 지연 함수를 사용합니다.
그럼 아까 작성했던 hello2.c에서 Hello, world가 한꺼번에 출력되는게 아니라
1초마다 한 줄씩 출력되도록 코드를 바꿔 보겠습니다.
#include <unistd.h> #include <curses.h> int main() { int i; initscr(); //turn on curses clear(); for(i = 0; i<LINES; i++) { move(i, i+1); if (i%2 == 1) //홀수 번째에 standout(); //standout mode turn on addstr("Hello, world"); //문자열을 출력한 다음 if (i%2 == 1) standend(); //standout mode turn off sleep(1); refresh(); } endwin(); return 0; }
코드를 보면 변경정보를 업데이트하는 refresh()를 호출하기 전에 sleep(1)을 호출해 지연시간 1초를 주는 것을 볼 수 있습니다.
gif가 아니라서 사진으로 움직이는 것을 보여드리지는 못하지만,
실행해 보면 Hello, world가 1초에 하나씩 출력되는 것을 볼 수 있습니다.
그럼 이번에는 Hello, world가 1초에 한번씩 아래쪽 대각선 방향으로 움직이도록 해볼까요?
#include <unistd.h> #include <curses.h> int main() { int i; initscr(); //turn on curses clear(); for(i = 0; i<LINES; i++) { move(i, i+1); if (i%2 == 1) //홀수 번째에 standout(); //standout mode turn on addstr("Hello, world"); //문자열을 출력한 다음 if (i%2 == 1) standend(); //standout mode turn off refresh(); //hello world 추가한 것 업데이트 sleep(1); //지연 1초 move(i, i+1); //커서 위치 옮김 addstr(" "); //erase line } endwin(); return 0; }
코드를 보면 Hello, world를 화면에 업데이트하고 나서 1초 후에 커서를 다시 앞으로 옮겨 빈 문자열을 넣어서 내용을 지우고,
다시 반복문을 돌아 Hello, world를 출력한 뒤 지우고를 반복하여 문자열 위치를 옮기는 것을 볼 수 있습니다.
한 번 실행해 볼까요?
이것도 실행해 보면 Hello, world가 아래쪽 대각선 방향으로 1초마다 옮겨지는 것을 볼 수 있습니다.
#include <unistd.h> #include <curses.h> #define LEFTEDGE 10 #define RIGHTEDGE 30 #define ROW 10 int main() { char message[] = "Hello"; char blank[] = " "; int dir = +1; int pos = LEFTEDGE; initscr(); clear(); while(1) { move(ROW, pos); addstr(message); //draw string move(LINES-1, COLS-1); //커서 위치 구석으로 refresh(); //show string sleep(1); move(ROW, pos); //erase string addstr(blank); pos += dir; if (pos >= RIGHTEDGE) dir = -1; if (pos <= LEFTEDGE) dir = +1; } return 0; }
이 코드도 전에 작성했던 코드와 마찬가지로 문자열 추가 후 커서 위치를 옮겨 문자열을 지움으로써
문자열의 움직임을 구현한 코드입니다.
조금 달라진 점은, 움직일 수 있는 가로 범위를 제한하여 경계에 다다르면 방향을 바꿔 반대 방향으로 이동하도록
LEFT/RIGHT EDGE, direction변수를 추가했습니다.
실행하면 다음과 같습니다.
Time handling: Alarms
사실 프로그램에서 시간 간격을 다루는 방법은 sleep 말고 타이머를 이용하는 방법도 있습니다.
sleep()이 일정 시간동안 프로세스(혹은 스레드)가 실행을 일시정지시키는 API라면,
타이머는 일정 시간이 경과하면 알림이 발생하도록 예약하는 API입니다.
타이머를 구현하는 인터페이스로는 alarm()이 있습니다.
alarm()은 특정 시간 간격마다 계속 알림을 발생시키는 것이 아니라, 알림을 한 번 발생시킨 후 만료되는 일회성 인터페이스입니다.
alarm(5)와 같이 매개변수 자리에 알림을 발생시킬 시간 간격을 초 단위로 설정해주면,
해당 시간만큼 경과했을 때 SIGARLM signal이 프로세스에게 전달됩니다.
프로그래머는 알람이 울렸을 때 특정 동작을 수행하도록 interrupt handler 함수를 작성해 signal 함수에 설정해 주면 됩니다.
sleep()은 위에서 봤던 것처럼 매개변수에 지정된 시간이 다 경과할 때까지, 혹은 신호(SIGINT)가 잡혀 호출을 중단할 때까지
프로세스나 스레드의 실행을 일시정지시킵니다. 설정한 시간이 지나 sleep이 완료되면 0을 반환합니다.
설정한 시간이 다 지나지 않았는데 신호에 의해 호출이 중단된 경우 남은 시간을 반환합니다.
그럼 alarm을 한번 사용해 볼까요?
#include <stdio.h> #include <unistd.h> #include <signal.h> int main() { void wakeup(int); printf("about to sleep for 4 seconds\n"); signal(SIGALRM, wakeup); //signal catch, interrupt handle alarm(4); pause(); printf("Morning so soon?\n"); return 0; } void wakeup(int signum) { #ifndef SHHHH printf("Alarm received from kernel\n"); #endif }
4초 뒤에 알람이 울리고, 알람이 울리면 wakeup함수로 이동해 문자열을 출력하는 코드입니다.
실행해 보면,
'about to sleep for 4 seconds'가 출력된 후 4초 후에
'Alarm received ~' 문자열이 출력되는 모습을 볼 수 있습니다.
sleep()도 alarm()과 동일하게 설정한 시간이 지나면 커널이 프로세스에 SIGALRM을 전송하고,
일시 정지에서 signal handler로 이동하도록 합니다.
sleep()은 초 단위로 지연 시간을 설정하는데, 좀 더 지연 시간을 세밀하게 설정하고 싶다면
지연 시간을 마이크로초 단위로 받는 usleep()을 사용하면 됩니다. 참고로 마이크로초는 (1, 1,000,000)초, 즉 백만분의 1초입니다.
Time Interval Handling: setitimer()
alarm()이나 sleep()은 알람을 한 번 울리면 바로 만료됩니다.
그럼 특정 시간마다 계속 알람이 울리게 하고 싶으면 어떻게 할까요?
이 때 사용하는 system call이 바로 setitimer()입니다.
setitimer()은 미래 특정 시점에 최초 만료되고, 그 이후로는 선택적으로 일정한 간격으로 계속 재설정 및 만료되는 타이머입니다.
시스템 콜 함수는 다음과 같이 정의됩니다.
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old value);
첫 번째 인자인 which에는 어떤 타이머가 들어갈지 알려줍니다.
각각의 프로세스는 1) real, 2) virtual, 3) profile 이렇게 3가지의 타이머를 가지고 있고,
각 타이머들은 1) 첫 알람이 울리기까지의 시간 간격, 2) 알람을 반복할 시간 간격 이렇게 2가지의 설정을 가지고 있습니다.
먼저 3가지 타이머 타입을 알아볼까요?
1) ITIMER_REAL
실제 시간으로 카운트다운하는 타이머를 생성합니다.
타이머가 만료되면, SIGALRM signal이 생성되어 프로세스에게 전달됩니다.
2) ITIMER_VIRTUAL
프로세스의 가상 시간(예: user-mode CPU time)으로 카운트다운하는 타이머를 생성합니다.
타이머가 만료되면, SIGALRM signal이 생성되어 프로세스에게 전달됩니다.
3) ITIMER_PROF
프로세스 시간으로 카운트다운하는 profiling timer를 생성합니다. (예: the sum of both user-mode, kernel-mode CPU time)
타이머가 만료되면, SIGALRM signal이 생성되어 프로세스에게 전달됩니다.
실행이 끝난 후 30초 후에 종료되는 프로그램이 있다고 가정해 보겠습니다.
user code에서 10초를 사용했고, kernel code에서 5초를 사용했고, 남은 15초는 sleep했습니다.
그럼 ITIMER_REAL로는 30초가 경과했고,
ITIMER_VIRTUAL로는 10초가 경과했으며,
ITIMER_PROF로는 15초가 경과했음을 알 수 있습니다.
두 번째, 세 번째 인자로는 itimerval 구조체를 전달합니다.
struct itimerval { struct timerval it_interval; //반복 간격 struct timeval it_value; //초기 간격 }
it_interval, it_value 모두 timeval이라는 구조체로 선언되어 있습니다.
timeval 구조체 내부를 한번 볼까요?
struct timeval { time_t tv_sec; //seconds suseconds_t tv_usec; //Microseconds(long int) }
그럼 이제 각 타이머들을 설정하는 방법을 보겠습니다.
위에서 각 타이머들엔 1) 초기 시간 간격, 2) 반복 시간 간격 이렇게 2가지의 설정 사항이 필요하다고 했습니다.
두 가지의 시간 간격을 결정해서 구조체 itimerval의 it_value 필드에 초기 간격을, it_interval 필드에 반복 간격을 설정하고
setitimer을 호출하면 itimerval 구조체가 타이머에 전달됩니다.
현재 타이머의 현재 상태가 궁금하다면 getitimer()로 타이머 현재 상태를 검색하여 읽어올 수 있습니다.
getitimer()는 구조체가 가리키는 버퍼에서 지정된 타이머의 현재 상태, 즉 다음 만료까지 남은 시간을 알려줍니다.
그림으로 보면 이렇습니다. 각 타이머별로 초기 간격, 반복 간격이 정해져 있고,
setitimer과 getitimer로 구조체를 주고받고 있습니다.
그런데 모든 프로세스가 다 타이머를 3개씩 갖고 있다면,
시스템이 100개의 프로세스를 동시에 돌린다면 타이머는 300개를 돌려야 합니다. 이렇게까지 타이머가 많이 필요할까요?
사실 시스템이 필요로 하는 타이머는 시스템 시계를 ticking하기 위한 타이머 하나만 필요합니다.
따라서 시스템은 이 ticking용 시계 하나만을 가지고, 프로세스들은 실행될 때마다 본인의 자체 알람 시계, private timer를 가지고 옵니다. 프로세스 수행 중에는 이 private timer(보통 real time timer)만 일정하게 카운트다운하고, 다른 두 타이머는 프로세스가 특정한 상태에 있을 때만 카운트다운을 합니다.
그림으로 볼까요? 시스템은 전체 프로세스 타이머를 업데이트하기 위한 시스템 시계 하나만을 갖습니다.
그리고 각 프로세스들은 본인들의 자체 private timer를 갖고 있습니다. 하나는 alarm(5), 하나는 alarm(12)로 설정되어 있네요.
예를 들어 시스템 시계가 초당 100비트로 뛴다고 설정되어 있으면,
커널은 alarm(5)를 가지고 있는 프로세스에 대해 카운터를 500으로 설정해 500번 클럭을 누르면 SIGALRM을 보냅니다.
마찬가지로 alarm(12)를 가지고 있는 프로세스는 카운터를 1200으로 설정하고, 1200번 클럭을 누르면 SIGALRM을 보냅니다.
다음 글에서는 세 번째 주제인 Signal Handling을 알아보겠습니다.
끝!
'시스템프로그래밍' 카테고리의 다른 글
[시스템프로그래밍] Inter-process communication (1) 2023.12.18 [시스템프로그래밍] Signal Handling (1) 2023.12.17 [시스템프로그래밍] Curses (1) 2023.12.16 [시스템프로그래밍] Wait (0) 2023.12.14 [시스템프로그래밍] Process Management (0) 2023.12.13