메뉴

문서정보

작업을 하는중에 예외 상황이 발생하다.

회사에서 우리는 A 라는 작업을 한다. 가장 이상적인 환경은 A 라는 작업을 시작해서 쉼없이 고민을 해서 A 라는 작업을 일사천리로 끝내는 것이지만 이것은 어디까지나 이상적인 환경일 따름으로, 각종의 인터럽트(중단) 이 생기게 된다.

전화가 온다거나, 상사가 이러좀 와보라고 한다거나, 목이 마르다거나, 도움이 필요해서 전화를 걸어야될일등 수많은 사건이 발생하며 이러한 사건은 대게가 임의의 시간에 발생하게 된다(언제 발생할지를 알수 없다).

이러한 갑작스런 사건이 발생되면 우리는 이에 대한 적절한 조취를 취해야 한다. 전화를 받거나, 상사를 찾아간다거나, 화장실을 간다거나 해야 한다. 혹은 별로 중요하지 않은 사건이라면 무시할수도 있을 것이다. (상사의 호출을 무시하지는 말자)

프로세스도 마찬가지이다

우리의 프로세스도 마찬가지이다. 어떤일을 하는 프로세스가 있다면 일을 마치기 까지 각종 예외 상황이 발생하며, 우리는 이에 대해서 적절한 처리를 해주어야만한다. 바로 이러한 예외 상황을 발생하는 어떤 신호를(이를 테면 전화벨 소리같은) 것을 우리는 시그널이라고 한다. 이러한 상황들은 언제 발생할지 모르는 비동기적인 사건들이다.

이를테면 사용자가 Ctrl+C 키를 눌렀다던가, 누군가 현재 process 에 kill 신호를 보냈다거나 하는것들이 그것으로 대부분의 복잡한 프로그램들은 이러한 비동기적인 사건에 대한 처리를 해주어야 만하며(현실 세계에서와 마찬가지로 무시할수도 있다), 이러한 사건은 언제 일어날지 알수 없다.

시그널에도 여러종류가 있다

사회생활의 작업중 발생하는 상황이 여러가지가 있듯이, 컴퓨터 시스템 상에서 발생할수 있는 상황도 여러가지가 있다. 이는 OS에 따라 다르게 설정된다. OS에서 어떠한 시그널을 지원하는 지는 kill 명령을 이용해서 알아볼수 있다. 아래는 필자의 리눅스 박스로써 kernel 2.4.3 에서 지원하는 시그널을 캡쳐한 화면이다.
[root@localhost root]# kill -l  
1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL
5) SIGTRAP      6) SIGABRT      7) SIGBUS       8) SIGFPE
9) SIGKILL     10) SIGUSR1     11) SIGSEGV     12) SIGUSR2
13) SIGPIPE     14) SIGALRM     15) SIGTERM     17) SIGCHLD
18) SIGCONT     19) SIGSTOP     20) SIGTSTP     21) SIGTTIN
22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO
30) SIGPWR      31) SIGSYS      32) SIGRTMIN    33) SIGRTMIN+1
34) SIGRTMIN+2  35) SIGRTMIN+3  36) SIGRTMIN+4  37) SIGRTMIN+5
38) SIGRTMIN+6  39) SIGRTMIN+7  40) SIGRTMIN+8  41) SIGRTMIN+9
42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13
46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14
50) SIGRTMAX-13 51) SIGRTMAX-12 52) SIGRTMAX-11 53) SIGRTMAX-10
54) SIGRTMAX-9  55) SIGRTMAX-8  56) SIGRTMAX-7  57) SIGRTMAX-6
58) SIGRTMAX-5  59) SIGRTMAX-4  60) SIGRTMAX-3  61) SIGRTMAX-2
62) SIGRTMAX-1  63) SIGRTMAX
보통 32번까지의 시그널은 대부분의 UNIX 계열에서 비슷한 값을 보여주며, 33번부터 62번까지는 아마도 OS 마다 약간식의 차이를 보여줄것이다. Linux 상에서 33 번 부터 62 번까지는 리얼타임 시그널을 위해서 예약된 시그널들이다. 리얼타임 시그널은 설명할께 꽤 많음으로, 따로 기사를 만들어 다루도록 하겠다.

각각의 시그널은 int 형의 숫자로 표시되는데, 시그널 번호에 따라서 다른 상황을 나타낸다. 모든 시그널에 대해서 설명하기엔 양이 좀 많으므로 그중 중요한 시그널만을 정리하도록 하겠다.
시그널 이름 설명
SIGHUP 터미널을 읽어버렸을때 발생한다.
SIGABRT 프로그램의 비정상종료시 발생한다.
SIGINT Control-C 나 DELETE 키를 입력했을때 발생한다.
SIGIO 비동기적인 입출력이 발생했을때
SIGKILL 프로세스를 죽이기 위해서
SIGPIPE 단절된 파이프에 write 할경우 발생
SIGSEGV 잘못된 메모리 참조(주로 포인터를 잘못 썼을때)
SIGSTOP 프로세스의 일시중단 (Ctrl+z)
SIGSUSR1 사용자를 위해 정의된 시그널
특정한 프로세스에 시그널을 보내기 위해서 우리는 kill 명령을 사용한다. 예를 들어 특정 프로세스를 죽이고 싶다면,
[root@localhost root]$ kill -SIGKILL 1234
혹은
[root@localhost root]$ kill -9 1234
와 같이 하면 된다. 그러면 해당 pid 를 가지는 프로세스로 시그널이 전달되게 된다.

그렇다면 시그널을 전달받은 프로세스는 어떻게 되는가

위와 같이 kill 을 통해서 시그널을 전달 받은 프로세스는 어떻게 되는가 ? 위에서 설명했듯이 시그널을 전달받은 프로세스는 시그널을 무시하든지, 아니면 시그널에 대해서 적당한 함수를 호출해서 처리를 해주도록 하는등 어떤 행동을 취하도록 해주어야 한다. 만약 해당 시그널에 대해서 어떠한 행동도 명시하지 않았다면, 프로세스는 해당 시그널에 대해서 시그널에 따른 기본행동을 하게 된다.

대부분의 경우 시그널에 대한 기본행동은 프로세스 종료이며, SIGSEGV 같은 경우에는 core 덤프를 남기고 프로세스가 종료되기도 한다. 시그널에 따른 기본행동은 다른 문서를 참고하도록 한다. 기본행동은 그리 중요하다고 생각되지 않음으로 그냥 넘어가겠다.

그러나 SIGKILL,SIGSTOP 과 같은 시그널에 대해서는 프로세스가 절대 무시할수 없도록 되어 있다. 이유는 슈퍼유저등이 특정 프로세스를 무조건 죽여야 할경우가 생기기 때문이다.

예제를 통해 알아보는 프로세스의 시그널 처리

C는 Unix 에서의 시그널을 다루기 위해서 signal(2) 함수를 제공한다. 다음은 signal 을 처리하는 가장 기본적인 예이다.
예제: sigint.c
#include <signal.h> 
#include <unistd.h> 

void sig_handler(int signo); // 비프음 발생 함수

int main()
{
    int i = 0;
    signal(SIGINT, (void *)sig_handler);

    while(1)
    {
        printf("%d\n", i);
        i++;
        sleep(1);
    }
    return 1;
}


void sig_handler(int signo)
{
    printf("\a");
}

우리는 SIGINT 는 CTRL+c 를 입력할때 발생되는 시그널이라는걸 알고 있다. 프로세스가 SIGINT 를 받았을때 프로세스에서 특별히 시그널에 대한 행동을 지정하지 않았다면 해당 프로세스는 기본 행동을 한다고 앞에서 얘기를 했었다. SIGINT 에 대한 기본 행동은 "프로세스 종료" 이다.

그러나 특별한 경우에 CTRL+c 를 사용자가 입력하더라도, 프로세스를 종료 시키면 안되는 경우가 있을것이다. 예를 들어 vi 와 같은 에디터의 경우 CTRL+c 를 입력한다고 해서 프로세스가 종료되면 안될것이다. 대게의 경우 CTRL+c 를 무시하도록 SIGINT 에 대해서 행동을 정하는데, vi 같은 경우 CTRL+c 를 입력하면 beep 음을 발생하도록 되어 있다(물론 이것은 vi버젼에 따라 다를수 있다).

위의 예제에서 우리는 signal 을 이용해서 SIGINT 시그널을 받을경우 sig_handler 이라는 함수를 호출하도록 SIGINT 시그널에 대한 프로세스 행동을 지정했다. 예제를 컴파일후 실행시키고 도중에 CTRL+c 를 누르면 beep 음이 발생함을 알수 있을것이다. sgnal 함수의 원형은 다음과 같다.
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
signum 은 시그널 이름 혹은 시그널 번호이며, handler 는 signum 에 대한 행동을 취하는 함수이다. 인자로는 발생된 시그널의 번호가 들어간다. 위의 코드는 SIGINT 가 발생했을때 beep 음을 발생시키는 것인데, 그냥 해당 시그널에 대해서 무시하고 싶을 때가 있을것이다. 그럴때는 SIG_IGN 을 사용하면 된다.
// SIGINT 를 무시한다.  
signal(SIGINT, SIG_IGN);
해당 시그널에 대해서 기본 행동을 취하도록 하려면 SIG_DFL 을 사용하면 된다.

시그널이 발생하면 프로세스는 발생지점에서 일시중단한다.

시그널이 발생하면 프로세스는 그 지점에서 일시 중지 한다음 시그널에 대한 행동을 한다. 위의 프로그램은 화면에 증가되는 숫자를 계속해서 뿌려주게 되는데, 만약 11 이 출력되었을때 시그널을 발생시킨다면 (CTRL+c 를 클릭한다면) 프로세스는 해당 지점에서 중지 되고, handler 로 지정된 함수를 호출하게 된다. 그리고 작업이 끝난후 다시 중지된 지점으로 되돌아 오고 프로세스는 계속 진행된다. 그러므로 beep 음이 발생한후 12 가 출력되게 될것이다.

SIGHUP 를 구현해보자

마지막으로 SIGHUP 를 구현함으로써, 이번 기사를 끝내도록 하겠다.
보통 SIGHUP 를 해당 pid 에 주면 해당 pid 로 프로세스가 다시 시작되는 것을 볼수 있을것이다. 보통 데몬프로세스의 설정을 마친다음에 설정내용을 재 적용 시켜주기 위해서 자주 사용한다.

이러한 프로그램의 아이디어는 간단하다. SIGHUP 시그널을 받으면, 프로세스를 해당 지점에서 종료하고 execl계열 시스템콜 함수를 이용해서 프로세스를 다시 실행시키는 것이다. 물론 좀 복잡한 프로그램이라면 SIGHUP가 발생했을때 진행중이던 작업을 종료시키는 처리를 하는 루틴이 추가되어야 할것이다.

execl 계열 프로세스에 대한 정보는 프로세스 관계를 참고하기 바란다.
예제: sig_hup.c
#include <signal.h> 
#include <unistd.h> 

void sig_handler(int signo); // 비프음 발생 함수

int main()
{
    int i = 0;
    printf("Program start\n");
    if (signal(SIGHUP, (void *)sig_handler) == SIG_ERR)
    {
        perror("signal set error : ");
        exit(0);
    }

    while(1)
    {
        printf("%d\n", i);
        i++;
        sleep(1);
    }
    return 1;
}


void sig_handler(int signo)
{
    execl("./sig_hup", 0);
}

위의 프로그램을 컴파일 시킨후 실행을 시키고 kill 명령어를 이용해서 SIGHUP 를 발생시켜보자
[root@localhost root]# ps -aux | grep sig_hup
root      4209  0.0  0.1  1348  348 pts/9    S    02:17   0:00 ./sig_hup
[root@localhost root]# kill -HUP 4209      
[root@localhost root]# kill -HUP 4209      
처음 kill 을 시키면 ./sig_hup 가 다시 실행됨을 볼수 있을것이다. 그런데.. 다시 한번 kill 을 발생시키면 sig_hup 가 다시 실행되지 않음을(신호가 전달되지 않음) 알수 있다.

위의 코드는 그럴듯 하긴 하지만 문제점을 가지고 있기 때문이다. execl 계열의 함수를 이용해서 새로운 프로세스를 실행시킬경우 기존 프로세스의 자원중 몇가지가 새로운 프로세스로 전달되기 때문이다. signal 의 경우는 새로운 프로세스로 현재 상태가 전달되므로 시그널이 블럭상태로 넘어가게 된다. 이 문제를 해결하기 위해서는 signal 의 특성을 제어하는 몇가지 함수들을 사용해야 한다. 이에 대한 내용은 다음 기사에서 자세히 다루도록 하겠다.