POSIX Signal Handling
POSIX 신호 처리
- UNIX에서 Signal은 Process에 어떤 Event가 발생했음을 알리는 수단이다.
- Signal은 Kernel이 User-Process에게 전달할 수도 있고, 다른 User-Process가 전달할 수도 있다.
- Signal을 S/W Interrupt라 부르기도 한다.
- 대부분의 Signal은 Asynchronous하게 발생되기 때문에, Process는 Signal이 발생한 정확한 시점을 알 수 없다.
- Signal들은 발생했을 때의 대처방법인 Disposition(=Action)대로 수행된다.
* signal Linux Manual Page (URL)
Signal Dispositions (Signal Actions)
- 발생한 Event의 종류에 따른 대처방법이다.
Dispositions | Description |
Term | Signal이 발생된 프로세스는 종료된다. |
Ign | 전달받은 Signal을 무시한다. |
Core | 종료 시, Core의 Memory Image를 Dump하여 Kernel에 보관한 후 종료한다. 저장된 Memory Image는 Debugging등에 이용된다. |
Stop | Signal이 발생된 프로세스를 정지시킨다. |
Cont | Signal이 발생된 프로세스를 재개한다. (Continue) |
1) 특정 Signal이 발생할 때 마다 Signal Handler 호출한다.
- Signal이 발생되면, Kernel에 의해 Signal Handler가 수행된다.
- 특정 Signal이 발생하여 해당 Signal에 대한 Signal Handler를 호출하는 것을 "Catching Signal"이라 표현한다.
- Signal Handler는 신호들을 구분짓는 int형 변수(\(\texttt{signo}\))를 Argument를 갖는다.
- Signal Handler가 수행될 동안, 해당 Signal은 Blocked되어 프로세스에 전달되지 못한다.
(또한, \(\texttt{sa_mask}\)에 추가적으로 지정된 Signal도 Blocked된다.
- 만약, Blocked Signal이 Blocking 중에도 한 번 이상 발생되면, Blocked 상태에서 해제된 이후에 프로세스에게 한 번만 전달된다.
(어찌됐든, Blocked 상태에서는 프로세스에게 전달되지 않으며, 몇 번 발생됐건 한 번만 전달된다. 즉, 기본적으로 UNIX Signal들은 Queue에 쌓이지 않는다.)
- 대부분의 Signal들은 \(\texttt{sigaction()}\)을 호출하여 Signal이 발생될 때 호출할 함수를 지정받는다.
- Signal들 중, \(\texttt{SIGKILL}\)과 \(\texttt{SIGSTOP}\)은 Catching될 수 없다.
2) \(\texttt{SIG_IGN}\)을 설정하여 Signal의 발생을 무시한다.
- 단, \(\texttt{SIGKILL}\)과 \(\texttt{SIGSTOP}\)은 무시할 수 없다.
3) \(\texttt{SIG_DFL}\)를 설정하여 Default Disposition으로 설정한다.
- Default Disposition이란, Signal이 발생되면, 해당 프로세스를 종료시키는 것이다.
- 어떤 Signal에 대해서는 프로세스의 Core Image를 현재 Working Directory에 생성하게 한다.
- \(\texttt{SIGCHLD}\)와 \(\texttt{SIGURG}\)의 Default Disposition은 IGN(Ignoring)이다.
※ Slow System Call을 수행중이던 프로세스에게 Signal 발생 Event가 통보되면,
해당 Signal에 대한 Signal Handler가 수행된 후, Error를 리턴하고 즉시 종료된다.
- Signal Handler에 의한 Context 변경을 우려하여 프로세스를 종료시키는 것이다.
- Signal Handler는 Asynchronous하게 실행되고, 다른 스레드가 실행하는 것이기 때문에
프로그램 실행에 영향을 주는 특정 데이터값을 변경할 가능성이 다분하다고 판단하고 지레 종료시키는 것이다.
ex) Simple Echo 프로그램에서 \(\texttt{SIGCHLD}\)가 발생되면, 미리 지정해놓았던 Signal Handler가 수행되고,
\(\texttt{accept()}\)에서 Blocked되어 있던 Parent는 종료된다.
(다시 \(\texttt{accept()}\)를 수행하거나 다시 Blocked 상태로 들어가지 않는다.)
Why Signals Sent (Signal이 발생하는 이유)
1) \(\texttt{kill()}\) System Call
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
2) \(\texttt{kill}\) Command
- 대부분의 Command들은 그에 대응되는 System Call이 있으며, 해당 Command를 실행하면 System Call이 호출된다.
3) Terminal Characters
a) SIGINT Signal
- 키보드로부터 Ctrl+C가 입력되면 전송되는 Signal이다.
b) SIGTSTP Signal
- 키보드로부터 Ctrl+Z가 입력되는 전송되는 Signal이다.
c) Etc
4) H/W Condition
a) CPU Trap
b) SIGFPE Signal
c) SIGSEGV Signal
- Signal for Segment Violation
- 프로세스에 할당된 Segment에 대한 정보 불일치가 발생한 경우 전송되는 Signal이다.
5) S/W Condition
a) SIGURG
- 네트워크 상에서 Urgent Data를 전송할 때 사용하는 Signal이다.
\(\texttt{signal()}\) Function (User-Defined)
#include <signal.h>
// sigaction() Function
// sigemptyset() Function
// sigaction Structure
typedef void Sigfunc(int);
// void Sigfunc(int) 함수 포인터 타입 이름을 Sigfunc로 치환한다.
// 즉, void func(int)가 하나의 Type이 되는것이다.
Sigfunc *signal(int signo, Sigfunc *func)
// void (*signal(int signo, void (*func) (int))) (int); typedef Alias를 생략할 경우
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
// Signal Set "act.sa_mask"에 포함된 모든 Signal들을 비운다.
// sa_mask는 Signal Handler가 호출될 때, Blocked될 Signal들이 저장되는 변수이다.
// 즉, sa_mask가 비어있으면, Signal Handler가 수행되는 동안, 어떠한 Signal도 Blocked되지 않음을 의미한다.
// Blocked Signal은 프로세스에 전달될 수 없다.
act.sa_flags = 0;
if(signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
// SA_RESTART Flag의 Complement이다.
// Catching한 Signal이 SIGALRM이고, SA_INTERRUPT가 정의되어 있다면, SA_INTERRUPT Flag를 지정한다.
#endif
} else {
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART; /* SVR4, 4.4BSD */
// SA_RESTART
// OPtional Flag
// SA_RESTART가 설정되어 있으면, 이 Signal에 의해 일시중지된 System Call은 자동으로 Kernel에 의해 다시 시작된다.
// Catching한 Signal이 SIGALRM이 아니고, SA_RESTART가 정의되어 있다면, SA_RESTART Flag를 지정한다.
// SIGALRM은 일반적으로 IO 시간만료가 발생했을 때 생성되는데, 이 경우, Signal이 Blocked된 System Call을 일시중지시킨다.
#endif
}
if (sigaction(signo, &act, &oact) < 0)
// signo Signal에 대한 Disposition을 리턴한다.
return (SIG_ERR);
return (oact.sa_handler);
}
Sigfunc *Signal(int signo, Sigfunc *func)
{
Sigfunc *sigfunc;
if ( (sigfunc = signal(signo, func)) == SIG_ERR) {
printf("signal error");
exit(1);
}
return(sigfunc);
}
- Signal() 함수는 signo Signal의 Handler를 func으로 설정하고, sa_mask를 비운다.
Arguments:
1) int signo
- Signal의 이름 역할을 하는 정수값이다.
2) Sigfunc *func
- Function Pointer 이거나, 상수 SIG_IGN과 SIG_DFL 중 하나이다.
* \(\texttt{sigaction}\) Structure
// in <signal.h>
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
* sigemptyset() Function
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
※ sigprocmask() 함수를 이용하여 선택적으로 Signal들을 Block하거나 Unblock할 수 있다.
- 이를 이용하여 Critical Region을 실행하는 동안, Signal이 Catching되지 않게 하여 보호할 수 있다.
SIGCHLD Signal (= SIGCLD Signal)
- Parent가 fork()를 통해 Child를 생성할 때, Child가 Zombie가 되는 것을 방지하기 위해 wait하는데,
SIGCHLD를 Catching하는 Signal Handler에서 wait을 수행한다.
- SIGCHLD Signal의 Default Disposition은 IGN(Ignore)이다.
(SIGCHLD에 대한 Signal Handler가 지정되어 있지 않은 경우, 무시된다.)
* Zombie State (Z State)
- 좀비 상태는 Parent가 추후에 Child에 대한 정보를 Fetch하는 경우를 위해 존재하는 상태이다.
(Fetch하는 정보 : Child의 PID, Termination Status, Child의 Resource Utilization 등)
- 좀비 상태인 프로세스의 PPID는 1(init Process)이며, init 프로세스는 좀비 프로세스를 제거한다.
- 좀비 프로세스는 Kernel 공간을 차지하기 때문에, 적절히 처리해야 한다.
* Slow System Call
- 영원히 Block될 가능성이 있는 System Call을 의미한다.
- accept()의 경우, 클라이언트가 연결을 영원히 요청하지 않을 경우, 영원히 Block 상태에 머물게 된다.
- read()의 경우, 입력되는 문자열이 영원히 도착하지 않을 경우, 영원히 Block 상태에 머물게 된다.
* sig_chld() Function (User-Defined)
#include <sys/wait.h>
// wait() Function
#include <sys/types.h>
// pid_t Type
void sig_chld(int signo)
{
pid_t pid;
int stat;
while ( pid = waitpid(-1, &stat, WNOHANG) ) > 0 )
printf("child %d terminated\n", pid);
return;
}
wait() System Call
* wait() Linux Manual Page (URL)
#include <sys/wait.h>
pid_t wait(int *statloc);
- wait()는 호출한 프로세스의 한 Child가 Terminate될 때 까지 Block된다.
- Child가 Z state (Zombie State)로 되는 것을 방지한다.
- Shell Program에서 명령어(Child)가 실행되는 동안, Parent를 Block시키기 위해 개발되었다.
Return:
종료된 Child의 PID를 직접적으로 리턴하고,
Child의 Termination Status(상수)를 statloc에 간접적으로 리턴한다.
* Termination Status를 통해 Child가 정상적으로 끝났는지, Signal에 의해 끝났는지, Job Control로 인해 끝났는지를 검사할 수 있다.
- 이를 구분짓는데 C의 Macro가 사용되었다.
Arguments:
1) int *statloc
- Child의 Termination Status(상수)가 statloc에 저장된다.
waitpid() System Call
* waitpid() Linux Manual Page (URL)
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
- wait()에서 확장된 형태로, 어느 프로세스를 기다릴 것인지, Block 여부를 조절할 수 있다.
- 한 호스트에서 여러 클라이언트를 생성하여 서버와 통신하다가, 서버의 여러 Child를 거의 동시에 Terminate시켰을 때,
하나의 Child를 제외하곤 나머지 모두 Zombie가 된다.
(Blocked 상태에서 발생한 Signal들은 Unblocked되고 딱 한 번만 재전송되기 때문이다.)
waitpid()를 통해 PID를 지정하여 wait시킴으로써 이를 방지할 수 있다.
Return:
종료된 Child의 PID를 직접적으로 리턴하고,
Child의 Termination Status(상수)를 statloc에 간접적으로 리턴한다.
에러가 발생했을 경우, -1을 리턴한다.
Arguments:
1) pid_t pid
- 기다릴 프로세스의 PID이다.
- pid를 -1로 지정할 경우, 가장 먼저 Terminate되는 Child를 지정하게 된다.
2) int *statloc
- Child의 Termination Status(상수)가 statloc에 저장된다.
3) int options
WNOHANG
- With No Hang의 약자이다.
- Kernel에 Terminate된 Child가 없으면 Block시키지 않는다.
SIGPIPE Signal
- write Operation을 수행하다 RST를 수신한 프로세스가 해당 소켓에 다시금 데이터를 입력했을 때 발생하는 Signal이다.
- SIGPIPE에 대한 대처방식의 Default는 프로세스를 Terminate하는 것이다.
(즉, 원치 않는 종료를 막기 위해서는 SIGPIPE를 Catching해야 한다.)
- SIGPIPE를 Catching하여 Signal Handler가 리턴하거나,
SIGPIPE를 무시할 경우, write Operation은 EPIPE를 리턴한다.
SIGTERM Signal and SIGKILL Signal
- UNIX 시스템 종료 시, init 프로세스가 모든 User 프로세스에게 전달하는 Signal이다.
- SIGTERM Signal 전송 후, 일정시간(5~20초) 동안 대기한 다음에 실행 중인 모든 프로세스에 SIGKILL Signal을 보낸다.
- SIGKILL Signal은 모든 실행 중인 프로세스들에게 종료를 위한 짧은 시간을 준다.
- 즉, 서버가 SIGTERM을 Catching하지 못하면, SIGKILL에 의해 종료된다.
- 클라이언트 측에서 능동적으로 서버 측의 종료 여부를 알기 위해서는 select() 함수 또는 poll() 함수를 이용해야 한다
※ SIGTERM Signal은 Catching이 가능하고, SIGKILL Signal은 Catching이 불가능하다.
Reference: UNIX Network Programming Volume 1, 3E: The Sockets Networking API (W. Richard Stevens, Bill Fenner, Andrew M. Rudoff 저, Addison-Wesley, 2004)