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 부재의 문제를 해결한 향상된 시그널 도구라고 할 수 있습니다.

유닉스 표준 시그널

  1. 시그널이 블록된 상태에서 동일한 시그널이 여러개 전달되면 하나만 유지가 된다.
    ( 이때 앞선 시그널이 overwrite 되지는 않습니다 )
  2. 블록된 상태에서 다른 종류의 시그널이 여러개 전달될 경우 실행 순서는 undefined 이다.

real time 시그널

  1. 시그널이 블록된 상태에서 동일한 시그널이 여러개 전달되어도 queue 가 유지된다.
  2. 같은 종류의 시그널일 경우 전달된 순서대로 실행이 되고
  3. 다른 종류의 시그널일 경우는 작은 번호의 시그널이 먼저 실행된다.

유닉스 표준 시그널과 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...