시그널은 사용자나 커널이 프로세스에 무언가를 통지하는 목적으로 사용한다.
매크로로 이름이 붙여 있지만 그 실체는 int 타입 정수이다.
시그널 처리 방법은 다음과 같다.
- 시그널을 무시한다. 예를 들어 자식 프로세스가 종료한 경우 생성되는
SIGCHLD
는 무시된다.- 프로세스를 종료한다.
Ctrl+C
를 입력하면 생성되는SIGINT
가 해당된다.- 프로세스의 core 덤프를 생성하고 비정상 종료시킨다.
SIGEGV
가 해당된다.
자주 사용되는 시그널은 다음과 같다.
시그널 명 | 포착 가능 | 디폴트 기능 | 생성 원인과 용도 |
---|---|---|---|
SIGINT | O | 종료 | 주로 Ctrl+c 로 생성되며 프로그램을 중지하고 싶을 때 사용 |
SIGHUP | O | 종료 | 사용자가 로그아웃할 때 생성. 데몬 프로세스에서는 설정 파일을 다시 읽어 들이는 경우에 많이 사용됨 |
SIGPIPE | O | 종료 | 끊어진 파이프에 데이터를 쓰려고 시도하면 생성 |
SIGTERM | O | 종료 | 프로세스를 종료시킬 때 사용. kill 명령어를 시그널 지정 없이 사용할 때 전달됨 |
SIGKILL | X | 종료 | 프로세스를 확실하게 종료시키기 위해서 사용 |
SIGCHLD | O | 무시 | 자식 프로세스가 정지 또는 종료되면 발생 |
SIGSEGV | O | 코어덤프 | 액세스 금지된 메모리 영역에 접근하거나, 초기화하지 않은 포인터를 참조하거나, 버퍼 오버플로우가 발생한 경우에 생성됨. 프로그램에 버그에 의해 발생 |
SIGBUS | O | 코어덤프 | 얼라인먼트 위반, 포인터 작업을 잘못한 경우 등 프로그램의 버그에 의해 발생 |
SIGFPE | O | 코어덤프 | 산술 연산 에러. 0으로 나누거나 부동 소수점 오버플로우 등의 상황에서 발생 |
포착 가능
항목이 O
인 시그널은 그 시그널이 전달될 때의 동작을 변경할 수 있다.#include <signal.h>
void (*signal(int sig, void (*fuunc)(int)))(int);
typedef를 사용해서 보기 쉽게 만들면 다음과 같다.
typedef void (*sighandler_t)(int);
sighandler_t signal(int sig, sighandler_t func);
sig의 시그널 번호를 받았을 때 func 함수를 호출하도록 동작을 변경한다.
즉, 함수 포인터를 받고 함수 포인터를 반환하는 함수이다.
func에는 사용자가 정의한 함수 외에도 다음과 같이 특별한 값을 사용할 수 있다.
상수 | 의미 |
---|---|
SIG_DFL | 커널의 디폴트 액션을 사용 |
SIG_IGN | 커널 레벨에서 시그널을 무시하도록 함 |
시그널은 프로세스의 상태를 고려하지 않고 실행 중에 함부로 끼어들기 때문에 다음의 문제가 발생한다.
- 핸들러 초기화
- 시스템 콜 수행 중에 시그널 발생
- 중복 호출해서는 안되는 함수를 중복 호출
- 시그널 블록
#include <signal.h>
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact)
struct sigaction{
// sa_handler나 sa_sigaction 중 하나만 사용
void (*sa_handler)(int);
void (*sa_sigaction)(int, sigifo_t*, void*);
sigset_t sa_mask;
int sa_flags;
};
sig에 해당하는 시그널에 대한 시그널 핸들러를 등록한다.
act에는 시그널 핸들러를 지정한다. 상수 SIG_IGN
, SIG_DFL
또는 임의의 함수 포인터 중 하나이다.
oldset에는 sigaction()을 호출할 때 설정되어 있던 시그널 핸들러가 들어간다. 불필요할 경우 NULL을 지정한다.
sa_sigaction은 sa_handle과 똑같이 시그널 핸들러를 지정하는 멤버이다. sa_sigaction은 시그널을 수신했을 때 시그널 번호 이외의 자세한 정보를 얻을 수 있다.
signal의 문제점을 sigaction으로 다음과 같이 개선되었다.
- 핸들러 재설정
- 시스템 콜의 재기동
SA_RESTART
플래그를 추가하면 시스템 콜을 재기동하고 그렇지 않은 경우에는 재기동하지 않는다.
- 시그널 블록
#include <signal.h>
typedef void(*sighandler_t)(int);
sighandler_t
trap_signal(int sig, sighandler_t handler){
struct sigaction act, old;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);
act.sa_flags=SA_RESTART;
if(sigaction(sig, &act, &old)<0)
return NULL;
return old.sa_handler;
}
SA_RESTART
를 설정한다.#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int sig);
int sigdelset(sigset_t *set, int sig);
int sigismember(const sigset_t *set, int sig);
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpending(sigset_t *set);
int siguspend(const sigset_t *mask);
sigaction()의 sa_mask와 반대로 블록한 시그널을 다시 받기 위한 API는 위와 같다.
sigprocmask()은 현재 프로세스의 시그널 마스크를 설정한다. how에 다음의 플래그를 지정하여 설정한다.
값 | 효과 |
---|---|
SIG_BLOCK | set에 포함되는 시그널을 시그널 마스크에 추가한다 |
SIG_UNBLOCK | set에 포함되는 시그널을 시그널 마스크에서 삭제한다 |
SIG_MASK | 시그널 마스크를 set으로 대체한다 |
sigpending()은 보류된 시그널을 set에 넣는다.
siguspend()는 시그널 마스크 mask를 설정하고 프로세스를 시그널 대기 상태로 만든다. 차단한 시그널을 해제하고 보류되어 있던 시그널을 처리할 때 사용한다.
siguspend()는 항상 시그널에 끼어들어 종료한다. 항상 -1을 반환한다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
typedef void (*sighandler_t)(int); // 시그널 핸들러
sighandler_t trap_signal(int sig, sighandler_t handler){
// sig: 핸들링하는 시그널, handler: 시그널을 처리할 함수
struct sigaction act, old;
act.sa_handler = handler; // 실행할 핸들러를 저장
sigemptyset(&act.sa_mask); // 핸들러가 실행하는 동안 블록할 시그널을 비움
act.sa_flags = SA_RESTART; // 시스템 콜 재기동
if (sigaction(sig, &act, &old) < 0){
// sig 시그널에 실행할 핸들러 act를 지정
// 핸들러 지정 전 설정되어 있던 핸들러 old를 가져옴
return NULL; // sigaction()이 에러가 발생해 -1을 반환하면 NULL을 반환
}
return old.sa_handler; // act 핸들러를 지정 성공하면 이전 핸들러를 반환
}
void print_exit(int sig){
printf("Got signal %d\n", sig);
exit(0);
}
int main(int argc, char *argv[]){
trap_signal(SIGINT, print_exit);
pause(); // trap_signal의 실행을 대기
exit(0);
}
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
Ctrl+C
가 시그널로 변환되어 대상이 되는 프로세스에 전달되기까지의 구조는 다음과 같다.
사용자가 키보드로 Ctrl+c
를 누르면 이것을 커널의 단말 드라이브가 파악한다.
셸을 사용하는 경우 cooked 모드로 되어 있어 특수한 기능을 수행하는 키가 존재한다.
어떤 키가 어떤 기능을 하는지는 stty -a
명령어를 실행하면 알 수 있다.
intr
는 interrupt를 말하며 ^D
는 Ctrl+C
를 의미한다. intr
로 설정된 Ctrl+C
가 눌렸기 때문에 이를 인터럽트라고 인식한다.
SIGINT
를 단말에서 동작 중인 프로세스에게 전송할 때 셸이 파이프로 연결된 프로세스 무리를 기동할 때 동작 중인 프로세스를 알 수 있다. 이를 위해 tcsetpgrp()라는 API가 사용된다.
인터럽트는 파이프에 연결된 프로세스 전체를 중지해야 하므로 프로세스 그룹 전체에 신호를 전송한다. 그래서 SIGINT
가 전송되면 파이프 전체 프로세스가 종료된다.
차례로 정리하면 다음과 같다.
- 셸이 파이프를 구성하는 프로세스를 fork()한다.
- 셸이 파이프의 PGID를 tcsetpgrp()로 단말에 통지한다.
- fork된 각 프로세스가 각각의 명령어를 exec한다.
- 사용자가
Ctrl+C
를 누른다.- 커널 내의 단말 드라이브가 그것을
SIGINT
로 변환하여 동작 중인 프로세스 그룹에 발송한다.- 프로세스 그룹이 시그널에 대한 기본 동작으로 종료된다.