본문 바로가기

프로그래밍(TA, AA)/C C++

[시스템프로그래밍] 시그널

시그널은 소프트웨어 인터럽트(software interrupt)로, 프로세스에 뭔가 발생했음을 알리는 간단한 메시지를 비동기적으로 보내는 것입니다. 시그널은 0으로 나누기처럼 프로그램에서 예외적인 상황이 일어나는 경우나, 프로세스가 시그널을 보낼 수 있는 함수를 사용해 다른 프로세스에 시그널을 보내는 경우에 발생합니다. 보통 시그널로 전달되는 메시지는 무엇이 발생했는지를 표시하는 미리 정의된 상수를 사용합니다. 시그널을 받은 프로세스는 시그널에 따른 기본 동작을 수행하거나, 시그널을 무시하거나, 시그널 처리를 위해 특별히 지정된 함수를 수행하도록 할 수 있습니다.


프로그램에서 시그널을 보내려면 아래 함수들을 사용하며, 이중 kill 함수를 가장 많이 사용합니다.


 기능

 함수원형

 시그널 보내기

 int kill(pid_t pid, int sig);

 int raise(int sig);

 void abort(void);

 int sigsend(idtype_t idtype, id_t id, int sig);


프로세스가 시그널을 받을 때 기본 처리 방법은 대부분 프로세스를 종료하는 것입니다. 그러나 프로세스가 종료하기 전에 처리할 작업이 남아 있거나 특정 시그널에 대해서는 종료하고 싶지 않을 경우에는 시그널을 처리하는 함수를 지정할 수 있습니다. 이런 함수를 시그널 핸들러(signal handler)라고 합니다. 


 기능

 함수원형

 시그널 핸들러 지정

 void (*signal(int  sig, void (*disp)(int)))(int);

 void (*sigset(int sig, void (*disp)(int)))(int);

 int sigignore(int sig); 


유닉스 관련 표준 중 하나인 POSIX에서는 시그널을 개별적으로 처리하지 않고 복수의 시그널을 처리할 수 있게 합니다. POSIX에서 복수의 시그널을 처리하기 위해 도입한 개념을 시그널 집합이라고 합니다. 시그널 집합을 사용하면 여러 시그널을 지정해 처리할 수 있습니다. 시그널 집합 관련 함수는 아래와 같습니다.


 기능

 함수원형

 시그널 집합

 int sigemptyset(sigset_t *set);

 int sigfillset(sigset_t *set);

 int sigaddset(sigset_t *set, int signo);

 int sigdelset(sigset_t *set, int signo);

 int sigsmember(sigset_t *set, int signo);


signal 함수나 sigset 함수가 단순히 시그널 핸들러만 지정할 수 있는데 비해 sigaction 함수는 시그널을 받아 이를 처리할 시그널 핸들러를 지정할 뿐만 아니라 플래그를 설정해 시그널 처리과정을 제어할 수도 있습니다. 또한 시그널 핸들러가 수행되는 동안 다른 시그널을 블록할 수도 있습니다.


 기능

 함수원형

 시그널 제어

 int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);


알람 시그널은 일정한 시간이 지난 후에 자동으로 시그널이 발생하도록 합니다. 일정한 시간 후에 한번 발생시킬 수도 있고, 일정 시간 간격을 두고 주기적으로 알람 시그널을 발생시킬 수도 있습니다. 다음은 알람 시그널 관련 함수입니다.

 기능

 함수원형

 알람 시그널

 unsigned int alarm(unsigned int sec);

 인터벌 타이머

 int getitimer(int which, struct itimerval *value);

 int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);


이외에 시그널 처리와 관련해 시그널의 정보를 출력하고, 시그널을 블록하거나 해제하는 함수와 시그널을 받을 때까지 기다리는 함수를 살펴보겠습니다. 아래는 시그널 관련 기타 함수를 정리해둔 표입니다.


 기능

 함수원형

 시그널 정보 출력

 void psignal(int sig, const char *s);

 char *strsignal(int sig);

 시그널 블록과 해제

 int sighold(int sig);

 int sigrelse(int sig);

 int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

 시그널 대기

 int sigpause(int sig);

 int sigsuspend(const sigset_t *set);

 int pause(void);



시그널의 개념


시그널은 프로세스에 뭔가 발생했음을 알리는 간단한 메시지를 비동기적으로 보내는 것입니다. 이 메시지는 무엇이 발생했는지를 표시하는 미리 정의된 상수를 사용합니다. 시그널을 받은 프로세스는 시그널의 종류에 따라 적절한 처리 방법을 지정할 수 있습니다. 이 절에서는 시그널의 기본 개념과 종류를 알아보겠습니다.



시그널의 발생


시그널은 소프트웨어 인터럽트(software interrupt)입니다. 시그널은 비동기적으로 발생하며, 유닉스 운영체제가 프로세스에 전달합니다. 시그널은 다음과 같은 3가지 경우에 발생합니다.


  • 0으로 나누기처럼 프로그램에서 예외적인 상황이 일어나는 경우
  • 프로세스가 kill 함수와 같이 시그널을 보낼 수 있는 함수를 사용해 다른 프로세스에 시그널을 보내는 경우
  • 사용자가 [Ctrl] + [C] 같은 인터럽트 키를 입력한 경우


시그널 처리 방법


시그널을 받은 프로세스가 이를 처리하는 방법은 다음과 같이 4가지로 정리할 수 있습니다.


  • 프로세스가 받은 시그널에 따라 기본 동작을 수행한다. 각 시그널에는 기본 동작이 지정되어 있다. 대부분 시그널의 기본 동작은 프로세스를 종료하는 것이다. 이외에 시그널을 무시하거나 프로세스의 수행 일시 중지/재시작 등을 기본 동작으로 수행한다.
  • 프로세스가 받은 시그널을 무시한다. 프로세스가 시그널을 무시하기로 지정하면 유닉스는 프로세스에 시그널을 전달하지 않는다.
  • 프로세스는 시그널의 처리를 위해 미리 함수를 지정해놓고 시그널을 받으면 해당 함수를 호출해 처리한다. 시그널 처리를 위해 지정하는 함수를 시그널 핸들러라고 한다. 시그널을 받으면 기존 처리 작업을 중지한 후 시그널 핸들러를 호출하며, 시그널 핸들러의 동작이 완료되면 기존 처리 작업을 계속 수행한다.
  • 프로세스는 특정 부분이 실행되는 동안 시그널이 발생하지 않도록 블록할 수 있다. 블록된 시그널은 큐에 쌓여 있다가 시그널 블록이 해제되면 전달된다.


시그널의 종류


시그널은 <signal.h> 파일에 정의되어 있습니다. 현재 정의되어 있는 시그널은 아래와 같습니다. 기본 처리 중 종료는 프로세스가 그냥 종료되는 것이고, 코어 덤프는 코어 파일을 만들고 종료하는 것입니다.


[root@dev-web ch07]# 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

16) SIGSTKFLT   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      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3

38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8

43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13

48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12

53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7

58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2

63) SIGRTMAX-1  64) SIGRTMAX



시그널 보내기


프로그램에서 시그널을 보내려면 kill, raise, abort 함수를 사용하면 됩니다. 이중 가장 많이 사용하는 것이 kill 함수입니다. kill은 함수 외에 명령도 있는데, 프로세스를 종료시킬 때 주로 사용합니다. 예를 들어, PID가 3255인 프로세스를 강제로 종료하려면 다음과 같이 사용합니다.


# kill -9 3255


사실 kill 명령은 인자로 지정한 프로세스에 시그널을 보내는 명령입니다. 위의 예는 PID가 3255인 프로세스에 9번 시그널을 보내라는 의미이며, 9번 시그널은 SIGKIL이고, 프로세스를 강제로 종료합니다. 



시그널 보내기 함수


kill 함수와 raise, abort 함수의 사용 방법을 알아보겠습니다.


시그널 보내기: kill(2)

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

kill 함수는 pid에 대응하는 프로세스에 sig로 지정한 시그널을 보냅니다. pid는 특정 프로세스 또는 프로세스 그룹을 의미합니다. sig에 0(널 시그널)을 지정하면 실제로 시그널을 보내지 않고 오류를 확인합니다. 예를 들면, pid가 정상인지 검샇바니다.


pid에 지정한 값에 따라 시그널을 어떻게 보낼 것인지를 결정합니다.


  • pid가 0보다 큰 수: pid로 지정한 프로세스에게 시그널을 보낸다.
  • pid가 -1이 아닌 음수: 프로세스 그룹 ID가 pid의 절대값인 프로세스 그룹에 속하고 시그널을 보낼 권한을 가지고 있는 모든 프로세스에게 시그널을 보낸다.
  • pid가 0: 특별한 프로세스(스케줄러 등)를 제외하고 프로세스 그룹ID가 시그널을 보내는 프로세스의 프로세스 그룹 ID와 같은 모든 프로세스에게 시그널을 보냅니다.


시그널 보내기: raise(3)

#include <signal.h>

int raise(int sig);

raise 함수는 이를 호출한 프로세스에 인자로 지정한 시그널을 보냅니다. 만약 시그널 핸들러가 호출되면 시그널 핸들러의 수행이 끝날 때까지 raise 함수는 리턴하지 않습니다. raise 함수는 수행을 성공하면 0을, 실패하면 -1을 리턴합니다.



시그널 보내기: abort(3)

#include <stdlib.h>

void abort(void);

abort 함수는 이를 호출한 프로세스에 SIGABRT 시그널을 보냅니다. SIGABRT 시그널은 프로세스를 비정상적으로 종료시키고 코어 덤프 파일을 생성합니다. SIGABRT 시그널은 최소한 해당 프로세스가 연 파일은 모두 닫습니다. abort 함수는 raise(SIGABRT)와 같은 동작을 수행하지만 프로세스를 종료시키므로 리턴하지 않습니다.



시그널 기본 처리


프로세스가 시그널을 받을 때 수행하는 기본적인 처리는 대부분 프로세스를 종료하는 것입니다. 만일 프로세스를 종료하기 전에 처리할 작업이 남아있거나 특정 시그널에 대해 종료하고 싶지 않으면, 시그널을 받을 때 수행할 함수(시그널 핸들러:signal handler)를 지정하면 됩니다. 이렇게 시그널을 확인해 처리하는 일을 시그널 붙잡기(catching a signal)이라고 합니다.



시그널 핸들러 함수


시그널 핸들러를 지정할 때는 signal과 sigset 함수를 사용합니다.



시그널 핸들러 지정: signal(3)

#include <stdlib.h>

void (*signal(int sig, void (*disp)(int)))(int);

signal 함수를 사용하면 시그널을 받을 때 해당 시그널을 처리할 함수나 상수를 지정할 수 있습니다. 첫번째 인자인 sig에는 SIGKILL과 SIGSTOP 시그널을 제외한 모든 시그널을 지정할 수 있습니다. 두번째 인자인 disp에는 sig로 지정한 시그널을 받았을 때 처리할 방법을 지정합니다. disp 인자에는 다음 세 가지 중 하나를 설정해야 합니다.


  • 시그널 핸들러의 주소
  • SIG_IGN: 시그널을 무시하도록 지정
  • SIG_DFL: 시그널의 기본 처리 방법을 수행하도록 지정


signal 함수는 시그널 핸들러의 주소를 리턴합니다. signal 함수가 실패하면 SIG_ERR를 리턴합니다. 만약 두 번째 인자인 disp가 함수 주소고, sig가 SIGKILL, SIGTRAP, SIGPWR이 아니면 signal 함수는 시그널을 처리한 후 시그널 처리 방법을 기본 처리 방법(SIG_DFL)으로 재설정합니다. 따라서 시그널 처리를 계속하려면 signal 함수를 호출해 시그널을 처리한 후 다시 signal 함수를 설정해야 합니다.



시그널 핸들러 지정: sigset(3)

#include <signal.h>

void (*sigset(int tsig, void (*disp)(int)))(int);

sigset 함수의 인자 구조는 signal 함수와 동일합니다. sigset 함수도 첫번째 인자인 sig에 SIGKILL과 SIGSTOP 시그널을 제외한 어떤 시그널도 지정할 수 있습니다. 두 번째 인자인 disp에도 signal 함수처럼 시그널 핸들러 함수의 주소나 SIG_IGN, SIG_DFL 중 하나를 지정해야 합니다. 리턴값은 시그널 핸들러 함수의 주소입니다. sigset 함수가 실패하면 SIG_ERR를 리턴합니다.


sigset 함수가 signal 함수와 다른 점은 시그널 핸들러가 한 번 호출된 후에 기본 동작으로 재설정하지 않고 시그널 핸들러를 자동으로 재지정한다는 것입니다. 



시그널 집합


시그널은 보통 개별적으로 처리됩니다. 하지만 POSIX 표준에서 시그널과 관련해 정의한 함수 중 상당수는 시그널을 개별적으로 처리하지 않고 복수의 시그널을 처리할 수 있게 합니다. POSIX에서 복수의 시그널을 처리하기 위해 도입한 개념을 시그널 집합이라고 합니다. 이 절에서는 시그널 집합의 개념과 이를 활용하기 위한 함수를 살펴볼 것입니다.



시그널 집합의 개념


시그널 집합은 시그널을 비트 마스크로 표현합니다. 시그널 하나가 비트 하나를 가리킵니다. 각 비트가 특정 시그널과 1:1로 연결되어 있습니다. 만일 비트값이 1이면 해당 시그널이 설정된 것이고, 0이면 시그널이 설정되지 않은 것입니다.


유닉스에서는 시그널 집합의 처리를 위해 sigset_t라는 구조체를 제공합니다. sigset_t 구조체는 <sys/sygnal.h>에 정의되어 있으며, sigset_t 구조체는 크기가 4인 unsigned int 배열을 사용합니다.

typedef struct {
	unsigned int __sigbits[4];
} sigset_t;


시그널 집합 처리 함수


시그널 집합을 처리할 때 사용하는 sigemptyset, sigfillset, sigaddset, sigdelset, sigismember 함수에 관해 살펴보겠습니다.



시그널 집합 비우기: sigemptyset(3)

#include <signal.h>

int sigemptyset(sigset_t *set);

sigemptyset 함수는 시스템에서 정의한 모든 시그널을 배제해 인자로 지정한 시그널 집합을 빈 집합(empty set)으로 만듭니다. 즉, 시그널 집합의 모든 비트를 0으로 설정합니다. sigemptyset 함수는 수행을 성공하면 0을, 실패하면 -1을 리턴합니다.


시그널 집합에 모든 시그널 설정: sigfillset(3)

#include <signal.h>

int sigfillset(sigset_t *set);

sigfillset 함수는 인자로 받은 시그널 집합을 시스템에서 정의한 모든 시그널을 포함하는 집합으로 만듭니다. 즉, 시그널 집합의 모든 비트를 1로 설정합니다. sigfillset 함수는 수행을 성공하면 0을, 실패하면 -1을 리턴합니다.


시그널 집합에 시그널 설정 추가: sigaddset(3)

#include <signal.h>

int sigaddset(sigset_t *set, int signo);

sigaddset 함수는 signo로 지정한 시그널 set으로 지정한 시그널 집합에 추가합니다. sigaddset 함수는 수행을 성공하면 0을, 실패하면 -1을 리턴합니다.


시그널 집합에서 시그널 설정 삭제: sigdelset(3)

#include <signal.h>

int sigdelset(sigset_t *set, int signo);

sigdelset 함수는 signo로 지정한 시그널을 set으로 지정한 시그널 집합에서 제거합니다. sigdelset 함수는 수행을 성공하면 0을, 실패하면 -1을 리턴합니다.


시그널 집합에 설정된 시그널 확인: sigismember(3)

#include <signal.h>

int sigismember(sigset_t *set, int signo);

sigismember 함수는 signo로 지정한 시그널이 set으로 지정한 시그널 집합에 포함되어 있으면 1을, 포함되어 있지 않으면 0을 리턴합니다.



sigaction 함수의 활용


sigaction 함수는 signal이나 sigset 함수처럼 시그널을 받았을 때 이를 처리할 수 있는 함수를 지정할 수 있게 합니다. signal이나 sigset 함수는 단순히 시그널 핸들러만 지정할 수 있는 데 비해 sigaction 함수는 훨씬 다양하게 시그널을 제어할 수 있습니다. 지금부터 sigaction 함수의 활용법을 살펴보겠습니다.


sigaction 구조체


sigaction 함수의 인자로 정의된 sigaction 구조체는 다음과 같은 구조입니다. sigaction 구조체는 <sys/signal.h>에 정의되어 있으며, 구조체의 멤버는 시그널 처리를 위한 시그널 핸들러 주소, 시그널 핸들러가 수행되는 동안 블록될 시그널, 추가적인 기능을 설정할 수 있는 플래그로 구성되어 있습니다.

struct sigaction {
	int sa_flags;
	union {
		void (*sa_handler)();
		void (*sa_sigaction)(int, siginfo_t *, void *);
	} _funcptr;
	sigset_t sa_mask;
}

sa_flags

sa_flags에는 시그널 전달 방법을 수정할 플래그를 지정합니다. sa_flags에 지정합니다. sa_flags에 지정할 수 있는 값은 <sys/signal.h> 파일에 정의되어 있습니다. sa_flags는 다음 값들을 논리 OR 연산으로 연결해 지정합니다.


  플래그

 설명

 SA_ONSTACK

 이 값을 설정하고 시그널을 받으면 시그널을 처리할 프로세스에 sigaltstack 시스템 호출로 생성한 대체 시그널 스택이 있는 경우에만 대체 스택에서 시그널을 처리합니다. 그렇지 않으면 시그널은 일반 스택에서 처리됩니다.

 SA_RESETHAND

 이 값을 설정하고 시그널을 받으면 시그널의 기본 처리 방법은 SIG_DFL로 재설정되고 시그널이 처리되는 동안 시그널을 블록하지 않습니다.

 SA_NODEFER

 이 값을 설정하고 시그널을 받으면 시그널이 처리되는 동안 유닉스 커널에서 해당 시그널을 자동으로 블록하지 못합니다.

 SA_RESTART

 이 값을 설정하고 시그널을 받으면 시스템은 시그널 핸들러에 의해 중지된 기능을 재시작하게 합니다.

 SA_SIGINFO

 이 값이 설정되지 않은 상태에서 시그널을 받으면 시그널 번호만 시그널 핸들러로 전달됩니다. 만약 이 값을 설정하고 시그널을 받으면 시그널 번호 외에 추가 인자 두 개가 시그널 핸들러로 전달됩니다. 두 번째 인자가 NULL이 아니면 시그널이 발생한 이유가 저장된 siginfo_t 구조체를 가리킵니다. 세 번째 인자는 시그널이 전달될 때 시그널을 받는 프로세스의 상태를 나타내는 ucontext_t 구조체를 가리킵니다.

 SA_NOCLDWAIT

 이 값이 설정되어 있고 시그널이 SIGCHLD면 시스템은 자식 프로세스가 종료될 때 좀비 프로세스를 만들지 않습니다.

 SA_NOCLDSTOP

 이 값이 설정되어 있고 시그널이 SIGCHLD면 자식 프로세스가 중지 또는 재시작할 때 부모 프로세스에 SIGCHLD 시그널을 전달하지 않습니다.


sa_handler와 sa_sigaction

sa_handler와 sa_sigaction은 메모리가 중첩됩니다. 만일 sa_flags에 SA_SIGNFO가 설정되어 있지 않으면 sa_handler에는 시그널을 처리할 동작을 지정합니다. sa_flags에 SA_SIGINFO가 설정되어 있으면 sa_sigaction 멤버를 사용합니다.


sa_mask

sa_mask에는 시그널 핸들러가 동작 중일 때 블록할 시그널을 시그널 집합으로 지정합니다. 시그널 핸들러가 시작되어 시그널을 전달할 때 이미 블록된 시그널 집합에 sa_mask로 지정한 시그널 집합을 추가합니다. sa_flags에 SA_NODEFER를 설정하지 않으면 시그널 핸들러를 호출하게 한 시그널도 블록됩니다.



sigaction 함수


sigaction(2)는 시그널을 받아 이를 처리할 시그널 핸들러를 지정할 뿐만 아니라 플래그를 설정해 시그널을 처리하는 과정을 제어할 수도 있습니다. 또한 시그널 핸들러가 수행되는 동안 다른 시그널을 블록할 수도 있습니다. sigaction 함수의 원형은 다음과 같습니다.

#include <signal.h>

int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);
  • sig: 처리할 시그널
  • act: 시그널을 처리할 방법을 지정한 구조체 주소
  • oact: 기존에 시그널을 처리하던 방법을 저장할 구조체 주소

sigaction 함수는 sig에 지정한 시그널을 받았을 때 처리할 방법을 sigaction 구조체인 act로 받습니다. 첫번째 인자로 SIGKILL과 SIGSTOP 시그널을 제외한 어떤 시그널도 사용할 수 있습니다. 두 번째와 세 번째 인자는 sigaction 구조체를 사용해 지정합니다. 두 번째 인자로는 NULL 또는 sig에 지정된 시그널을 받았을 때 처리할 방법을 지정한 구조체 주소를 지정합니다. 세번째 인자는 NULL 또는 기존 처리 방법이 저장됩니다. sigaction 함수는 수행을 성공하면 0을, 실패하면 -1이 리턴됩니다.


다음은 sigaction 함수로 SIGINT 시그널을 처리하는 시그널 핸들러를 지정한 예제입니다. 특정 플래그를 지정하지 않고, SIGQUIT 시그널만 블록했습니다.



시그널 발생 원인 검색


sa_flags에 SA_SIGINFO 플래그를 지정하면 시그널이 발생한 원인을 알 수 있습니다. SA_SIGINFO를 지정하면 sigaction 구조체에서 시그널 핸들러를 지정할때 sa_handler 대신 sa_sigaction을 사용합니다. SA_SIGINFO 플래그를 설정하면 시그널 핸들러는 다음과 같이 인자 세 개를 받는 형태로 정의됩니다.

void handler(int sig, siginfo_t *sip, ucontext_t *ucp);

ucontext_t는 프로세스의 내부 상태를 나타내는 구조체입니다. siginfo_t 구조체만 살펴보겠습니다.


siginfo_t 구조체

siginfo_t 구조체는 <sys/siginfo.h> 파일에 정의되어 있으며, 다음과 같은 항목으로 구성되어 있습니다.

typdedef struct {
	int		si_signo;
	int		si_errno;
	int		si_code;
	union sigval	si_value;
	union {
		...
	}__data;
} siginfo_t;
  • si_signo: 시스템에서 제공하는 시그널 번호를 저장한다.
  • si_errno: 0 또는 시그널과 관련된 오류 번호를 저장한다.
  • si_code: 시그널 발생 원인을 정의하는 코드를 저장한다. si_code 값이 SI_NOINFO면 si_signo만 의미가 있고, siginfo_t 구조체의 나머지 멤버는 사용되지 않는다.
  • __data: 시그널의 종류에 따라 값이 저장된다. union의 세부 내용은 <sys/siginfo.h> 파일을 참조하면 됩니다.


사용자 프로세스에 의한 시그널 발생 원인 코드

si_code의 값이 0과 같거나 작다면 사용자 프로세스가 kill, raise, abort 등의 함수로 시그널을 생성한 것입니다. 사용자 프로세스에서 시그널을 생성하면 si_code에는 아래 표에 정의된 값들이 저장됩니다. 이들 코드는 <sys/siginfo.h>에 정의되어 있으며, man -s 3HEAD siginfo.h 명령으로 확인할 수 있습니다.


 코드

 값

 의미

 SI_USER

 0

 kill(2), sigsend(2), raise(3), abort(3) 함수가 시그널을 보냄

 SI_LWP

 -1

 _lwp_kill(2) 함수가 시그널을 보냄

 SI_QUEUE

 -2

 sigqueue(3) 함수가 시그널을 보냄

 SI_TIMER

 -3

 timer_settime(3) 함수가 생성한 타이머가 만료되어 시그널을 보냄

 SI_ASYNCIO

 -4

 비동기 입출력 요청이 완료되어 시그널을 보냄

 SI_MESGQ

 -5

 빈 메시지 큐에 메시지가 도착했음을 알리는 시그널을 보냄



시스템 프로세스에 의한 시그널 발생 원인 코드

si_code의 값이 양수면 시스템에서 시그널을 생성한 것입니다. 시스템에서 시그널을 생성하는 이유는 여러 가지입니다. man -s 3HEAD siginfo.h 명령으로 시그널별 발생 원인을 나타낸 코드를 볼 수 있습니다. 이는 <sys/machsig.h>에 정의되어 있습니다.


시그널 발생 원인 출력: psiginfo(3)

#include <siginfo.h>

void psiginfo(siginfo_t *pinfo, char *s);

psiginfo 함수는 siginfo_t 구조체 포인터를 인자로 받아 시그널이 발생한 원인을 표준 오류로 출력합니다. 첫 번째 인자인 pinfo에는 시그널 핸들러의 두번째 인자로 받은 siginfo_t 구조체의 주소를 지정합니다. 함수를 실행하면 두 번째 인자인 s에 지정한 문자열이 먼저 출력되고 시그널 정보가 출력됩니다.




알람 시그널


일정한 시간이 지난 후에 자동으로 시그널이 발생하게 하려면 알람 시그널을 사용합니다. 알람 시그널은 일정 시간 후에 한 번 발생시킬 수도 있고, 일정 간격을 두고 주기적으로 발생시킬 수도 있습니다. 이 절에서는 알람 시그널의 개념을 이해하고, 알람 시그널과 타이머를 생성하고 관리하는 함수를 살펴보겠습니다.



알람 시그널 함수


정해진 시간에 한 번 시그널을 보내는 알람 시그널 함수에 대해 살펴보겠습니다.


알람 시그널 생성: alarm(2)

#include <unistd.h>

unsigned int alarm(unsigned int sec);

alarm 함수는 인자로 초 단위 시간을 받습니다. 인자로 지정한 시간이 지나면 SIGALRM 시그널이 생성되어 프로세스에 전달됩니다. 프로세스별로 알람시계는 하나밖에 없으므로 알람은 하나만 설정할 수 있습니다. 따라서 알람 시ㅏ그널을 생성하기 전에 다시 ALARM 함수를 호출하면 이전 설정은 없어지고 재설정됩니다. 인자로 0을 지정하면 이전에 설정한 알람 요청은 모두 취소됩니다. alarm 함수는 이전에 호출한 alarm 함수의 시간이 남아 있으면 해당 시간을, 그렇지 않으면 0을 리턴합니다.


다음은 alarm 함수를 사용해 프로그램을 실행한 후 2초 후에 알람 시그널이 발생하게 하는 예제입니다.



인터벌 타이머


알람 시그널은 정해진 시간에 한 번 시그널을 보냅니다. 이를 좀 더 개선해 일정 시간 간격을 두고 타이머 역할을 하도록 할 수 있는데, 타이머 기능을 제공하기 위해 getitimer와 setitimer 함수를 사용합니다.


타이머의 종류

유닉스 시스템은 프로세스별로 4개의 타이머를 제공합니다. 타이머에 관한 정보는 <sys/time.h> 파일에 정의되어 있습니다. 각 타이머가 사용하는 시간의 유형에 따라 각기 다른 시그널이 생성됩니다.


  • ITIMER_REAL: 실제 시간을 사용한다. 이 타이머가 만료되면 SIGALRM 시그널이 생성된다.
  • ITMER_VIRTUAL: 프로세스의 가상 시간을 사용한다. 이 시간은 프로세스가 동작 중일 때만 작동한다. 이 시간이 만료되면 SIGVTALRM 시그널이 생성된다.
  • ITIMER_PROF: 시스템이 프로세스를 위해 실행 중인 시간과 프로세스의 가상 시간을 모두 사용한다. 이 타이머가 만료되면 SIGPROF 시그널이 생성된다.
  • ITIMER_REALPROF: 실제 시간을 사용한다. 이는 멀티스레드 프로그램의 실제 실행 시간을 측정할 때 사용한다. 이와 관련된 시그널은 SIGPROF다.


타이머 정보 검색: getitimer(2)

#include <sys/time.h>

int getitimer(int which, struct itimerval *value)


getitimer 함수는 타이머 정보를 검색하는 함수로, which에는 검색할 타이머의 종류를 지정하고, value에는 타이머의 현재 시간과 타이머 간격 정보를 저장할 itimerval 구조체 포인터를 지정합니다. itimerval 구조체는 <sys/time.h> 파일에 다음과 같이 정의되어 있습니다.

struct itimerval {
	struct timeval it_interval;
	struct timeval it_value;
};


itimerval 구조체의 첫번째 멤버인 it_interval에는 타이머의 간격 정보가 저장되고, it_value에는 타이머가 만료될 때까지 남은 시간이 저장됩니다. it_value의 값이 0이면 타이머 기능이 멈춥니다. it_interval의 값이 0이면 다음 번에 타이머가 만료될 때 타이머 기능이 멈춥니다. 이 시간 정보들은 timeval 구조체에 정의됩니다.

struct timeval {
	time_t		tv_sec;
	suseconds_t	tv_usec;
};


timeval 구조체에는 초와 마이크로초 단위로 시간을 저장합니다. 즉, 타이머에 1/1,000,000초단위 정확도를 부여할 수 있습니다.



타이머 설정: setitimer(2)

#include <sys/time.h>

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

settitimer는 타이머를 설정하는 함수로, which에는 설정할 타이머의 종류를 지어하고, value에는 설정할 타이ㅏ머 정보를 저장해 인터벌 타이머를 설정합니다. ovalue는 NULL 또는 이전 설정값을 저장합니다. value와 ovalue는 itimerval 구조체를 가리키는 포인터입니다.




기타 시그널 처리 함수


유닉스에는 시그널 정보를 출력하거나, 시그널이 올 때까지 기다리거나, 시그널을 보내는 등 시그널 처리와 관련 있는 기타 함수를 제공합니다. 이 절에서는 이들 함수를 자세히 살펴보겠습니다.



시그널 정보 출력


시그널 정보를 출력하려면 앞서 다룬 예제에서 사용한 psignal이나 strsignal 함수를 사용해야 합니다.



시그널 정보 출력: psignal(3)

#include <siginfo.h.h>

void psignal(int sig, const char *s);

psignal 함수는 두 번째 인자인 s로 지정한 문자열을 출력한 후 첫번째 인자인 sig로 지정한 시그널을 가리키는 이름을 붙여 표준 오류로 출력합니다.



시그널 정보 출력: strsignal(3)

#include <string.h>

char *strsignal(int sig);

strsignal 함수는 인자로 받은 시그널을 가리키는 이름을 문자열로 리턴합니다. 인자로 받은 시그널이 없으면 NULL을 리턴합니다.



시그널 블록킹과 해제


시그널 블록킹 함수를 사용하면 프로세스가 동작하는 동안에 특정 시그널들을 받지 않도록 블록하거나 해제할 수 있습니다.


시그널 블록과 해제: sighold(3), sigrelse(3)

#include <signal.h>

int sighold(int sig);
int sigrelse(int sig);

sighold 함수는 인자로 받은 시그널을 프로세스의 시그널 마스크에 추가합니다. 시그널 마스크에 추가된 시그널은 블록되어 해당 시그널을 받지 않습니다. sigrelse 함수는 프로세스의 시그널 마스크에서 시그널을 해제합니다.


시그널 집합 블록과 해제: sigprocmask(2)

sighold, sigrelse 함수와 기능이 유사하지만 시그널 집합을 사용해 한 번에 여러 시그널을 블록할 수 있습니다.

#include <signal.h>

int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

sigprocmask 함수는 set에 지정한 시그널 집합을 블록할 것인지, 해제할 것인지를 how에 지정해 호출합니다. 첫번째 인자인 how에 올 수 있는 값은 다음과 같습니다.


  • SIG_BLOCK: set에 지정한 시그널 집합을 시그널 마스크에 추가한다.
  • SIG_UNBLOCK: set에 지정한 시그널 집합을 시그널 마스크에서 제거한다.
  • SIG_SETMASK: set에 지정한 시그널 집합으로 현재 시그널 마스크를 대체한다.


두번째 인자인 set은 블록하거나 해제할 시그널 집합을 가리키고, 세 번째 인자인 oset은 NULL이 아니면 이전 설정값이 저장됩니다.



시그널 기다리기


pause 함수 외에 시그널이 도착하기를 기다릴 때 사용할 수 있는 함수들을 살펴봅시다.



시그널 대기: sigpause(3)

#include <signal.h>

int sigpause(int sig);

sigpause 함수는 인자로 지정한 시그널을 프로세스의 시그널 마스크에서 제거하고 프로세스가 시그널을 받을 때까지 기다립니다. pause 함수를 sigpause(SIGINT) 함수로 바꾸면 SIGINT 시그널의 블록이 해제되고 시그널을 기다립니다. [Ctrl] + [C]를 입력해보면 시그널 핸들러가 동작하는 것을 알 수 있습니다.



시그널 기다리기 : sigsuspend(2) 

#include <signal.h>

int sigsuspend(const sigset_t *set);

sigsuspend 함수는 인자로 지정한 시그널 집합에 설정된 시그널들로 프로세스의 시그널 마스크를 교체하고, 블록되지 않은 시그널이 도착할 때까지 프로세스의 수행을 멈추고 기다립니다. 시그널이 도착하면 프로세스의 시그널 마스크는 이전 설정으로 되돌아 갑니다.


다음은 sigsuspend 함수를 사용해 SIGALRM을 제외한 모든 시그널을 블록한 후 시그널을 기다리게 하는 예제입니다.



기타 시그널 관련 함수


앞서 설명한 koll, raise, abort 함수 외에 시그널을 보낼 때 사용할 수 있는 다른 함수로 sigsend가 있습니다. 또한 시그널의 처리를 무시하도록 설정하는 sigignore 함수도 있습니다. 이들 함수의 사용법을 살펴보겠습니다.


시그널 보내기: sigsend(2)

#include <signal.h>

int sigsend(idtype_t idtype, id_t id, int sig);

sigsend 함수는 sig로 지정한 시그널을 id에 지정한 프로세스나 프로세스 그룹에 보냅니다. idtype은 id에 지정한 값의 의미를 알려줍니다. idtype에는 아래 나타난 값을 사용할 수 있습니다. sigsend 함수가 kill 함수와 다른 점은 특정 프로세스뿐만 아니라 프로세스 그룹 등 시그널을 받을 대상을 다양하게 지정할 수 있다는 것입니다.


 값

 의미

 P_PID

 프로세스 ID가 id인 프로세스에 시그널을 보낸다.

 P_PGID

 프로세스 그룹 ID가 id인 모든 프로세스에 시그널을 보낸다.

 P_SID

 세션 ID가 id인 모든 프로세스에 시그널을 보낸다.

 P_TASKID

 태스크 ID가 id인 모든 프로세스에 시그널을 보낸다.

 P_UID

 유효 사용자 ID(EUID)가 id인 모든 프로세스에 시그널을 보낸다.

 P_GID

 유효 그룹 ID(EGID)가 id인 모든 프로세스에 시그널을 보낸다.

 P_PROJID

 프로젝트 ID가 id인 모든 프로세스에 시그널을 보낸다.

 P_CID

 스케줄러 클래스 ID가 id인 모든 프로세스에 시그널을 보낸다.

 P_CTID

 프로세스 콘트랙트 ID가 id인 모든 프로세스에 시그널을 보낸다.

 P_ALL

 id를 무시하고 모든 프로세스에 시그널을 보낸다.

 P_MYID

 함수를 호출하는 자신에게 시그널을 보낸다.


PID가 0인 프로세스는 시그널을 보내는 대상에서 항상 제외됩니다. P_PID로 직접 지정할 때를 제외하고는 PID가 1인 프로세스에 시그널을 보내는 것도 제외됩니다.



시그널 무시 처리: sigignore(3)

#include <signal.h>

int sigignore(int sig);

sigignore 함수는 인자로 지정한 시그널의 처리 방법을 SIG_IGN으로 설정합니다.