Signals Table
프로그램 실행 중에는 하드웨어나 소프트웨어에서 여러 경로를 통해 예외나 오류가 발생할 수 있는데 이것을 OS 가 시그널이라는 일종의 abstraction layer 를 만들어서 일관된 인터페이스를 제공하는 것입니다.
시그널은 두 종류로 분류할 수 있는데 synchronous ( exceptions ) 와 asynchronous ( interrupts ) 시그널 입니다. synchronous 시그널은 스레드가 실행 중에 자신으로부터 발생하는 오류로 아래 테이블에서 보면 SIGFPE, SIGILL, SIGSEGV, SIGBUS 같은 시그널이 여기에 해당합니다. Go 언어 에서는 synchronous 시그널이 발생하면 run-time panic 으로 처리됩니다.
asynchronous 시그널은 프로세스 외부에서 비동기적으로 발생하는 시그널로 터미널에서 사용자가
Ctrl-c 를 누르거나, kill
명령으로 신호를 보내거나, 커널로부터 전달되는 시그널입니다.
하나의 프로세스에는 여러 개의 스레드가 실행 중일 수 있는데 이때는 해당 시그널이 mask
되어있지 않은 스레드 중에 하나가 선택되어 처리됩니다.
따라서 동일한 시그널이 여러 번 발생한다면 각각 다른 스레드에서 처리될 수 있습니다.
만약에 모든 스레드가 해당 시그널을 mask 했을 경우는 pending 상태가 되고
나중에 처음으로 unmask 하는 스레드에 의해 처리됩니다.
기본적으로 프로세스 내 모든 스레드들은 설정한 signal handler 를 공유하고, 각 스레드 별로 signal mask 를 설정할 수 있습니다. 따라서 특정 스레드에서 시그널 처리를 전담하게 하려면 해당 스레드에서만 시그널을 unmask 시키고 나머지 스레드에서는 모두 mask 시키면 됩니다. ( signal mask 는 parent thread 로 부터 상속됩니다.)
Program Error Signals
Signal | Num | DefaultAction | Description |
---|---|---|---|
SIGFPE | n/a | Terminate (core dump) |
산술연산 에러를 나타냅니다. 이름은 floating-point exception 에서 왔지만 실질적으로 모든 산술연산 에러를 포함합니다. ( Floating/Integer division by zero, modulo operation by zero, Floating/Integer overflow ... ) |
SIGILL | n/a | Terminate (core dump) |
프로세스가 illegal, malformed, unknown, or privileged instruction 을 실행하려 하면 전달됩니다. |
SIGSEGV | n/a | Terminate (core dump) |
프로세스에 할당된 메모리 주소 범위를 벗어나 참조하게 되면 segmentation violation 이 발생하고 신호가 프로세스에 전달됩니다. ( 또는 read-only memory 에 쓸경우 ) |
SIGBUS | n/a | Terminate (core dump) |
정렬되지 않은 메모리 주소를 사용하거나 존재하지 않는 물리적 주소를 사용하게 되면 프로세스에 전달됩니다. |
SIGABRT | 6 | Terminate (core dump) |
프로그램 코드에서 비정상적인 종료를 할 경우를 나타내며 abort 함수를 이용해 종료할때 발생합니다. |
SIGTRAP | n/a | Terminate (core dump) |
디버거에서 사용하는 신호로 특정함수 실행, 변수값 변경 같은 상황이 발생하면 전달됩니다. |
SIGEMT | n/a | Terminate (core dump) |
software 로 emulation 되지 않은 instruction 이 실행되거나 OS 에서 emulation 실패가 발생하면 프로세스에 전달됩니다. |
SIGSYS | n/a | Terminate (core dump) |
system call 을 할때 인수전달에 오류가 있으면 발생합니다. system call 을 하기위해 주로 libc 를 이용하므로 실질적으로 거의 발생하지 않습니다. |
Termination Signals
Signal | Num | DefaultAction | Description |
---|---|---|---|
SIGTERM | 15 | Terminate | 프로세스에 종료를 요청할때 사용합니다. SIGKILL 과 달리 trap 하거나 ignore 할 수 있습니다. signal handler 를 이용하여 종료하기 전에 필요한 뒤처리 작업을 할 수 있습니다. |
SIGINT | 2 | Terminate | 실행중인 프로세스를 interrupt 할때 사용되는 신호로 터미널에서 Ctrl-c 키를 입력하면 프로세스에 전달되고 실행중인 프로세스는 종료됩니다. |
SIGQUIT | 3 | Terminate (core dump) |
터미널에서 Ctrl-\ 키를 입력하면 발생하며 프로세스가 종료할때 추가로 core dump 합니다. 사용자에 의한 에러 발견으로 볼 수 있습니다. |
SIGKILL | 9 | Terminate | 이 신호를 받으면 프로세스는 즉시 종료합니다. SIGTERM, SIGINT 와 달리 trap 하거나 ignore 할 수 없으므로 필요한 뒤처리 작업을 할 수 없습니다. |
SIGHUP | 1 | Terminate | 터미널과 network, 모뎀 연결이 끊기거나 또는 터미널 프로그램이 종료될경우 모든 자손 프로세스들에 전달됩니다. 이 신호는 용도가 하나 더 있는데 daemon 프로세스에서 사용하는 설정파일을 수정할 경우 restart 할 필요 없이 이 신호를 보내면 설정파일을 reload 합니다. |
Alarm Signals
Signal | Num | DefaultAction | Description |
---|---|---|---|
SIGALRM | 14 | Terminate | alarm 또는 setitimer function 에서 설정한 값이 초과되면 발생합니다. 측정에는 real 또는 clock 시간이 사용됩니다. |
SIGVTALRM | n/a | Terminate | 현재 프로세스에서 사용한 CPU 시간이 alarm 또는 setitimer function 에서 설정한 값을 초과했을 경우 발생합니다. ( Virtual Time ALARM ) |
SIGPROF | n/a | Terminate | 프로세스 에서 사용한 cpu 시간과 프로세스를 대신해 system call 을 하는데 소비된 cpu 시간이 alarm 또는 setitimer function 에서 설정한 값을 초과했을 경우 전달됩니다. code profiling 하는 프로그램에서 사용됩니다. |
Asynchronous I/O Signals
Signal | Num | DefaultAction | Description |
---|---|---|---|
SIGIO | n/a | Ignore | socket 이나 터미널 같은 FD (file descriptor) 에서 input 또는 output 할 준비가 되면 발생합니다. kernel 이 caller 를 대신해 FD 를 조사해 신호를 전달하므로 비동기 I/O requests 만드는데 사용할 수 있습니다. |
SIGURG | n/a | Ignore | socket 에서 urgent or out-of-band 데이터가 도착하면 전달됩니다. |
SIGPOLL | n/a | Ignore | System V 신호이름으로 SIGIO 와 같습니다. (호환성 유지를 위해 정의됨) |
Job Control Signals
Signal | Num | DefaultAction | Description |
---|---|---|---|
SIGCHLD | n/a | Ignore | child 프로세스가 terminate, stop, continue 상태 변경이 되면 parent 프로세스에 전달됩니다. |
SIGCONT | n/a | Continue | SIGSTOP, SIGTSTP 신호로 중단된 프로세스를 다시 시작합니다. ( 실행중인 프로세스에서는 무시됩니다. ) |
SIGSTOP | n/a | Stop | sendable stop signal not from tty. 이 신호를 받으면 프로세스는 즉시 정지합니다. trap 하거나 ignore 할 수 없습니다. |
SIGTSTP | n/a | Stop | stop signal from tty. 터미널에서 Ctrl-z 키를 입력할때 발생하며 SIGSTOP 과는 달리 trap 하거나 ignore 할 수 있습니다. |
SIGTTIN | n/a | Stop | background process 가 tty 로부터 읽기를 시도하면 전달됩니다. |
SIGTTOU | n/a | Stop | background process 가 tty 로 쓰기를 시도하면 전달됩니다. 터미널 옵션 tostop 가 enable 되어있어야 합니다. ( stty -a 로 값을 볼수있음 ) |
Operation Error Signals
Signal | Num | DefaultAction | Description |
---|---|---|---|
SIGPIPE | n/a | Terminate | broken pipe 를 나타냅니다. writer 가 파이프로 데이터를 쓰고 있는중에 reader 가 종료하거나 또는 connect 되지 않은 socket 에 데이터를 전송하면 발생합니다. |
SIGLOST | n/a | Terminate | Resource lost 를 의미합니다. 가령 NFS 에서 파일에 lock 을 가지고 있는데 NFS 서버가 reboot 된다면 lock 을 잃어버리게 되어 신호가 프로세스에 전달됩니다. |
SIGXCPU | n/a | Terminate | 사용가능한 CPU 시간 ( soft limit ) 을 초과했을 경우 전달되며 신호를 받은 프로세스는 필요한 뒤처리 작업을 할수있고 이후에 OS 의 SIGKILL 신호에 의해 종료됩니다. |
SIGXFSZ | n/a | Terminate | 시스템에서 사용가능한 파일크기 ( soft limit ) 을 초과하려 할때 발생합니다. |
Miscellaneous Signals
Signal | Num | DefaultAction | Description |
---|---|---|---|
SIGUSR1 | n/a | Terminate | 사용자가 정의하여 사용할 수 있는 신호입니다 1. |
SIGUSR2 | n/a | Terminate | 사용자가 정의하여 사용할 수 있는 신호입니다 2. |
SIGWINCH | n/a | Ignore | 터미널 윈도우 사이즈가 변경되었을때 프로세스에 전달됩니다. |
SIGINFO | n/a | Ignore | 터미널의 foreground process group 에 모두 전달되며 group leader 일경우 시스템 상태정보와 프로세스가 실행중인 작업에 대한 정보를 표시합니다. |
Real time signals
SIGRTMIN ~ SIGRTMAX 는 real time signal 입니다. 유닉스 표준 시그널에서는 시그널이 블록될 경우 하나의 시그널만 유지하고 나머지는 모두 잃어 버리지만 RTS 는 블록되더라도 시그널의 queue 를 유지합니다. 또한 ordering 도 보장하는데 블록된 상태에서 각기 다른 번호의 신호들이 도착될 경우 작은 번호의 신호가 먼저 전달됩니다. RTS 은 커널에 의해 사용되지 않습니다.
RTS 는 비동기 이벤트를 전달하기 위한 목적으로 만들어 졌으며, 주로 네트워크 애플리케이션 작성시 소켓 이벤트를 통보하기 위해서 사용합니다. RTS 는 네트워크 입출력에 있어서 polling 에 비해 월등한 성능 향상을 보장해 줍니다. 시그널의 장점인 실시간성을 유지하면서 단점인 queue 부재의 문제를 해결한 향상된 시그널 도구라고 할 수 있습니다.
유닉스 표준 시그널
- 시그널이 블록된 상태에서 동일한 시그널이 여러개 전달되면 하나만 유지가 된다.
( 이때 앞선 시그널이 overwrite 되지는 않습니다 ) - 블록된 상태에서 다른 종류의 시그널이 여러개 전달될 경우 실행 순서는 undefined 이다.
real time 시그널
- 시그널이 블록된 상태에서 동일한 시그널이 여러개 전달되어도 queue 가 유지된다.
- 같은 종류의 시그널일 경우 전달된 순서대로 실행이 되고
- 다른 종류의 시그널일 경우는 작은 번호의 시그널이 먼저 실행된다.
유닉스 표준 시그널과 real time 시그널이 동시에 블록 상태가 되면 리눅스의 경우 표준 시그널이 우선순위를 갖습니다.
다음은 RTS 의 ordering ( 작은 번호의 신호가 먼저 전달 ) 되는지 테스트해보는 영상입니다.
Num
신호이름 대신에 번호를 사용할 수 있는데 POSIX standard 로 정해진 것은 몇개 안되고 나머지는 OS 마다 틀리다고 합니다.
$
LESS='+/Signal numbering for standard signals' man 7 signal
참조
trap '...' HUP INT QUIT TERM
trap '...' 1 2 3 15
--------------------------------
trap '...' EXIT
trap '...' 0
DefaultAction
Terminate
프로세스를 종료합니다.Terminate (core dump)
프로세스를 종료하고 추가적으로 core 파일을 생성합니다.Ignore
신호를 무시합니다.Stop
프로세스를 stop 합니다. ( 종료하는 것은 아닙니다. )Continue
프로세스가 stopped 상태일 경우 실행을 재개합니다. ( 실행 중에는 무시됩니다. )
Quiz
다음은 assembly 로 작성한 코드를 byte 배열로 변환하여 Go 언어에서 실행하는 예입니다.
그런데 실행해보면 SIGILL
신호에 의해 프로그램 실행이 중단되는 것을 볼 수 있습니다.
이것은 byte 배열이 정상적인 CPU instruction 이 아니라는 뜻인데요.
byte 배열의 오른쪽에 위치한 assembly 코드를 참고하여 정상적으로 실행될 수 있도록 수정하는 것입니다.
여기에는
rasm2
명령이 필요합니다.
$ cat test.go
package main
import (
. "syscall"
"unsafe"
)
func main() {
/*
+----------+--------+--------+--------+--------+--------+--------+
| Syscall #| Param 1| Param 2| Param 3| Param 4| Param 5| Param 6|
+----------+--------+--------+--------+--------+--------+--------+
| rax | rdi | rsi | rdx | r10 | r8 | r9 |
+----------+--------+--------+--------+--------+--------+--------+
rax rdi rsi rdx
ssize_t write(int fd, const void *buf, size_t count); write 시스템콜 함수 원형
*/
code := []uint8 {
0x48, 0xc7, 0xd0, 0x01, 0x00, 0x00, 0x00, // mov rax, 1 (write 시스템콜)
0x48, 0xc7, 0xc7, 0x01, 0x00, 0x00, 0x00, // mov rdi, 1 (stdout 출력)
0x48, 0xc7, 0xc2, 0x0c, 0x00, 0x00, 0x00, // mov rdx, 0xc (총 12 bytes)
0x48, 0x8d, 0x35, 0x03, 0x00, 0x00, 0x00, // lea rsi, [rip + 3] (여기서 3 은
0x0f, 0x05, // syscall (2 bytes +
0xc3, // ret (1 byte = 3
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, // H,e,l,l,o,space
0x57, 0x6f, 0x72, 0x6c, 0x64, 0x0a, // W,o,r,l,d,newline
}
mmap, _ := Mmap( -1, 0, len(code),
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_ANONYMOUS | MAP_PRIVATE )
copy(mmap, code)
// Go 언어에서 함수는 pointer to C function pointer 로
type printer func() // two levels of pointers 입니다.
funcPtr := (uintptr)(unsafe.Pointer(&mmap))
hello := *(*printer)(unsafe.Pointer(&funcPtr))
hello()
}
$ go run test.go
SIGILL: illegal instruction // SIGILL 신호에 의해 프로그램 실행이 중단된다.
PC=0x7f35c7daa000 m=0 sigcode=2
instruction bytes: 0x48 0xc7 0xd0 0x1 0x0 0x0 0x0 0x48 0xc7 0xc7 0x1 0x0 0x0 0x0 0x48 0xc7
. . .
# 먼저 byte 배열의 첫번째 라인부터 disassemble 을 해봅니다.
# 정상적으로 disassemble 이 되지 않는 것으로 보아 문제가 있는 코드입니다.
$ rasm2 -a x86 -b 64 -d 48c7d001000000
invalid
invalid
rol byte [rcx], 1
add byte [rax], al
invalid
# 이번에는 오른쪽 주석에 있는 assembly 코드를 byte 배열로 출력해봅니다.
# 출력값과 byte 배열을 비교해 보면 0xd0 을 0xc0 으로 변경해야 합니다.
$ rasm2 -a x86 -b 64 'mov rax, 1'
48c7c001000000
$ rasm2 -a x86 -b 64 -d 48c7c001000000
mov rax, 1
# 수정후 다시 실행해보면 정상적으로 실행되는 것을 볼 수 있습니다.
$ go run test.go
Hello World
C 언어의 경우는 SIGILL
신호가 발생했을 때 단순히 Illegal instruction
메시지만 출력됩니다.
$ gcc -xc - <<\@ && ./a.out
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
unsigned char code[] = {
0x48, 0xc7, 0xd0, 0x01, 0x00, 0x00, 0x00,
0x48, 0xc7, 0xc7, 0x01, 0x00, 0x00, 0x00,
0x48, 0xc7, 0xc2, 0x0c, 0x00, 0x00, 0x00,
0x48, 0x8d, 0x35, 0x03, 0x00, 0x00, 0x00,
0x0f, 0x05,
0xc3,
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20,
0x57, 0x6f, 0x72, 0x6c, 0x64, 0x0a,
};
int main () {
puts("start...");
char *ptr = mmap(NULL, sizeof(code), PROT_READ | PROT_WRITE | PROT_EXEC
, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
memcpy(ptr, code, sizeof(code));
((void(*)()) ptr)();
puts("end...");
return 0;
}
@
start...
Illegal instruction (core dumped) // 단순히 Illegal instruction 메시지만 출력된다.
다음과 같이 SIGILL
신호를 trap 하면 원하는 메시지를 출력할 수 있습니다.
$ gcc -xc - <<\@ && ./a.out
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
unsigned char code[] = {
0x48, 0xc7, 0xd0, 0x01, 0x00, 0x00, 0x00,
0x48, 0xc7, 0xc7, 0x01, 0x00, 0x00, 0x00,
0x48, 0xc7, 0xc2, 0x0c, 0x00, 0x00, 0x00,
0x48, 0x8d, 0x35, 0x03, 0x00, 0x00, 0x00,
0x0f, 0x05,
0xc3,
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20,
0x57, 0x6f, 0x72, 0x6c, 0x64, 0x0a,
};
void SIGILL_handler(int sig)
{
printf("SIGILL ( %d번 ) 신호가 발생했습니다. code[] 값을 수정하세요.\n", sig);
exit(sig);
}
int main() {
puts("start...");
struct sigaction sa = { .sa_handler = SIGILL_handler };
sigaction(SIGILL, &sa, NULL);
char *ptr = mmap(NULL, sizeof(code), PROT_READ | PROT_WRITE | PROT_EXEC
, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
memcpy(ptr, code, sizeof(code));
((void(*)()) ptr)();
puts("end...");
return 0;
}
@
start...
SIGILL ( 4번 ) 신호가 발생했습니다. code[] 값을 수정하세요.
2.
shell 에서는 시그널을 전달할 때 누가 보냈는지도 알 수 없고 데이터를 함께 전달할 수도 없는데요. C 프로그래밍에서는 가능합니다. 다음은 child 프로세스가 parent 프로세스에게 TERM 시그널을 전달함과 동시에 integer 값을 전달하는 예입니다.
$ cat test.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
// SA_SIGINFO 를 설정했으므로 void (*sa_handler)(int); 대신에
// void (*sa_sigaction)(int, siginfo_t *, void *); 를 사용
void handler(int signum, siginfo_t *info, void *uc)
{
puts("--------------------------------------");
printf("I'm pid %d TERM signal handler\n", getpid());
printf("received from pid : %d\n", info->si_pid);
printf("received from real uid : %d\n", info->si_uid);
printf("accompanying data : %d\n", info->si_value.sival_int);
puts("--------------------------------------");
}
int main()
{
if (fork() > 0) goto parent;
printf("I'm child pid %d\n" ,getpid());
printf(" \\----> Sending TERM signal to parent pid %d\n", getppid());
sleep(1);
union sigval val = { .sival_int = 12345 };
sigqueue(getppid(), SIGTERM, val); // kill 함수 대신에 sigqueue 함수를
_exit(EXIT_SUCCESS); // 이용해 integer or pointer 값을 전달
parent:
printf("I'm parent pid %d\n", getpid());
struct sigaction sa = {
.sa_sigaction = handler, // signal handler 에서 siginfo_t 값을
.sa_flags = SA_SIGINFO, // 사용하기 위해 SA_SIGINFO 를 설정
};
sigaction(SIGTERM, &sa, NULL);
pause(); wait(NULL);
puts("main end...");
return 0;
}
-----------------------------------------------------
$ gcc test.c
$ ./a.out
Im parent pid 296538
Im child pid 296539
\----> Sending TERM signal to parent pid 296538
--------------------------------------
Im pid 296538 TERM signal handler
received from pid : 296539
received from real uid : 1000
accompanying data : 12345
--------------------------------------
main end...