Home [Linux System Programming] Ch10 시그널
Post
Cancel

[Linux System Programming] Ch10 시그널

[Ch10 시그널]

Ch10 시그널

리눅스 환경에서 Robustness Test (강건성 테스트) 나 디버깅을 진행하다보면 여러가지 오류로 인해서 프로그램이 종료되는데, 이 때 Core dump가 있으면 디버깅에 유용하지만 로그만 남아 있는 경우도 있음.

이 때 단서가 되는 부분이 시그널

시그널은 비동기 이벤트 처리를 위한 메커니즘을 제공하는 소프트웨어 인터럽트이다.

Hardware interrupt : 외부에서 전기적 신호(이벤트)가 발생했을 때

Software interrupt : CPU가 연산중에 어떠한 조건에 맞는 이벤트가 발생했을 때

  • 유저가 Ctrl+C 를 눌러 시스템 외부에서 발생시키거나, 프로세스가 0으로 나누는 연산을 수행한 경우처럼 프로그램이나 커널 내부 작업 중에 발생할 수도 있다.
  • 또는 IPC(Inter-Process Communication) 기법으로 프로세스간 시그널 송수신도 가능하다.

중요한 점은 이벤트가 비동기적으로 발생할 뿐만 아니라 해당 프로그램도 시그널을 비동기적으로 처리할 수 있다는 점.

  • 시그널 처리 함수는 커널에 등록되어 시그널이 전달되었을 때 그 함수가 비동기식으로 호출된다.
  • → 프로세스 입장에서 일을 하고 있는 도중에 시그널이 오면 잠시 일을 멈추고 시그널에 대한 처리를 한 뒤 다시 본래의 일로 돌아온다는 의미

image

  • 시그널 전달 가능 흐름
    • Kernel → Process
    • Process → Process
    • Thread → Thread

10.1 시그널 개념

시그널의 생명 주기

  • 시그널 발생 → 커널은 해당 시그널을 전달 가능할 때까지 쌓아둠 → 커널은 가능한 시점에 적절하게 시그널을 처리
  • 커널은 프로세스 요청에 따라 세 가지 동작 중 하나를 수행함.

시그널을 무시한다.

  • 무시할 수 없는 시그널은 SIGKILLSIGSTOP 두 가지
    • 시스템 관리자가 프로세스를 종료하거나 멈출 수 있어야 하기 떄문

시그널을 붙잡아 처리한다.

  • 커널은 프로세스의 현재 코드 실행을 중단하고 이전에 등록했던 함수로 건너뛰어서 해당 함수를 실행함.
  • SIGINTSIGTERM 은 가장 흔하게 잡을 수 있는 시그널.
    • ex. 터미널 프로그램은 시그널을 잡아서 프롬프트로 다시 돌아간다.
    • ex. 프로그램이 종료되기 전에 SIGTERM을 붙잡아서 네트워크 연결을 끊거나, 임시파일 삭제 등 종료와 관련된 작업을 수행할 수 있음
  • SIGKILL 과 SIGSTOP은 잡을 수 없다.

기본 동작을 수행한다.

  • 기본 동작은 시그널에 따라 다름.
    • 대부분은 프로세스 종료

image

  • 시그널을 전달 받게 되면 진행중인 테스크를 잠시 중단하고, Signal Handler를 수행한 후 다시 프로세스로 돌아옴

image

  • 내부적으로는 조금 더 복잡하게 동작함
  • Signal을 처리하는 것은 Kernel 이지만, handler를 등록했다면 signal handler를 수행하기 위해서 다시 user 영역으로 돌아옴.
  • handler를 호출하고 다시 Kernel 영역으로 돌아가서 본래 Task의 context를 이용해서 signal이 불린 시점으로 돌아감.

10.1.1 시그널 식별자

시그널은 모두 <signal.h> 파일에 정의되어 있음

  • 시그널은 단순 양의 정수를 나타내는 선행처리기의 정의이다.

시그널 번호는 1(보통 SIGHUP)에서 시작해서 선형적으로 증가하고, 전체 시그널이 대략 31개지만 대다수 프로그램은 몇 개만 처리함.

10.1.2 리눅스에서 지원하는 시그널

Table 10-1. Signals

SIGABRT

  • abort() 함수를 호출한 프로세스에 이 시그널을 보낸다. 프로세스는 종료되고 코어 파일을 생성함.
  • 리눅스에서는 assert() 호출이 실패할 경우 abort()를 호출함
    • abort() : 현재 상태를 core dump 하고 프로세스를 비정상적으로 종료하는 함수
    • exit() : 정상적으로 종료하는 함수
    1
    
      **core dump** : UNIX 계열에서 프로그램이 비정상적으로 종료되는 경우에 프로그램이 종료될 당시의 메모리 상태를 기록하여 생성된 파일. 디버깅 용도로 사용됨 
    

SIGBUS

  • 프로세스가 메모리 보호 이외에 다른 하드웨어 장애를 유발한 경우 커널에서 이 시그널을 보냄.
    • 프로세스가 mmap() 으로 만든 메모리 영역에 부적절하게 접근할 때 커널에서 이 시그널을 보냄

SIGHUP

  • 제어터미널 상에서 부모 프로세스가 죽거나 멈춘 게(hangup) 감지되면 SIGHUP 시그널을 보냄.
  • 세션의 터미널 접속이 끊어질 때마다 커널에서 해당 세션 리더에게 이 시그널을 보냄.
  • 또한, 커널은 세션 리더가 종료될 때 foreground process 그룹에 속한 모든 프로세스에 이 시그널을 보냄.
  • 기본동작
    • 이 시그널은 사용자의 로그아웃을 의미 → 프로세스 종료
    • 데몬프로세스일 경우 자신의 설정을 다시 읽도록 하는 의미
    • ex. 아파치에 SIGHUP 을 보내면 httpd.conf를 다시 읽음.
      • 데몬 프로세스는 제어 터미널이 없어서 정상적인 상황에서는 이 시그널을 절대 받을 수 없음.

SIGINT

  • 사용자가 인터럽트 문자(보통 Ctrl + C)를 입력했을 때 커널은 포어그라운드 프로세스 그룹에 속한 모든 프로세스에 이 시그널을 보냄.
  • 기본동작
    • 프로세스 종료.
    • 하지만 프로세스에서 이 시그널을 붙잡아 처리할 수 있고, 일반적으로 종료 직전에 마무리 목적으로 사용

SIGKILL

  • kill() 시스템 콜에서 보냄
  • 시스템 관리자가 프로세스를 무조건 종료하도록 만드는 방법을 제공
  • 잡거나 무시할 수 없으며 결과는 항상 해당 프로세스의 종료

SIGSEGV

  • 세그멘테이션 위반(Segmentation Violation) 에서 유래된 이름
  • 유효하지 않은 메모리 접근을 시도하는 프로세스에 보냄
    • 맵핑되지 않은 메모리에 접근하거나,
    • 읽기를 허용하지 않는 메모리를 읽거나,
    • 메모리에서 실행 가능하지 않은 코드를 실행하거나,
    • 쓰기를 허용하지 않는 메모리에 쓰는 경우
  • 기본동작
    • 프로세스의 종료와 코어 덤프 생성

SIGSTOP

  • kill() 에서만 보낸다.
  • 무조건 프로세스를 정지시키며 잡을 수도 무시할 수도 없음.

SIGWINCH

  • 터미널 윈도우 크기가 변한 경우, 커널에서 포어그라운드 프로세스 그룹에 속한 모든 프로세스에 이 시그널을 보냄.
  • 기본적으로는 무시하지만, 붙잡아 처리할 수 있음.

image

10.2 시그널 관리 기초

시그널 관리를 위한 가장 단순하면서도 오래된 인터페이스는 signal() 함수이다.

1
2
3
4
5
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal (int signo, sighandler_t handler);
  • signal() 호출이 성공되면 signo 시그널을 받았을 때 수행할 현재 핸들러를 handler로 명시된 새로운 시그널 핸들러로 옮겨 해당 시그널을 처리한다.
  • handler 함수는 일반 함수와는 달리 이 함수의 반환값을 받아 처리할 수 있는 곳이 없기 때문에 반드시 void 를 반환해야 한다.
    • 유일한 인자는 처리될 시그널의 시그널 식별자(ex. SIGUSR2) 를 나타내는 정수이다.
1
void my_handler (int signo);
  • 현재 프로세스에 대해 시그널을 무시하게 하거나 시그널을 기본 동작으로 재설정하는 용도로도 커널에 signal() 함수를 사용할 수 있음. (handler 위치에 넣어줄 수 있음 )
    • SIG_DFL
      • signo로 지정한 시그널에 대한 동작을 기본값으로 설정한다.
    • SIG_IGN
      • signo로 지정한 시그널을 무시한다.
  • return
    • 해당 시그널의 이전 동작인 시그널 핸들러에 대한 포인터 or SIG_DFL, SIG_IGN 을 반환
    • 에러 발생 시 SIG_ERR 반환

10.2.1 모든 시그널 기다리기

pause() 시스템 콜은 프로세스를 종료시키는 시그널을 받을 때까지 해당 프로세스를 잠재운다.

(테스트와 디버깅에 유용함)

1
2
3
#include <unistd.h>

int pause (void);
  • pause() 는 붙잡을 수 있는 시그널을 받았을 때만 반환되며 -1을 반환.
  • 리눅스 커널에서 가장 단순한 시스템 콜 중 하나이다.
    1. 해당 프로세스를 인터럽트 가능한 잠들기 상태로 만듬
    2. 실행 가능한 다른 프로세스를 찾기 위해 schedule() 을 호출하여 리눅스 프로세스 스케줄러를 실행한다.

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

/* handler for SIGINT and SIGTERM */
static void signal_handler (int signo)
{
        if (signo == SIGINT)
                printf ("Caught SIGINT!\n");
        else if (signo == SIGTERM)
                printf ("Caught SIGTERM!\n");
        else {
                /* this should never happen */
                fprintf (stderr, "Unexpected signal!\n");
                exit (EXIT_FAILURE);
        }
        exit (EXIT_SUCCESS);
}

int main (void)
{
        /*
         * Register signal_handler as our signal handler
         * for SIGINT.
         */
        if (signal (SIGINT, signal_handler) == SIG_ERR) {
                fprintf (stderr, "Cannot handle SIGINT!\n");
                exit (EXIT_FAILURE);
        }

        /*
         * Register signal_handler as our signal handler
         * for SIGTERM.
         */
        if (signal (SIGTERM, signal_handler) == SIG_ERR) {
                fprintf (stderr, "Cannot handle SIGTERM!\n");
                exit (EXIT_FAILURE);
        }

        /* Reset SIGPROF's behavior to the default. */
        if (signal (SIGPROF, SIG_DFL) == SIG_ERR) {
                fprintf (stderr, "Cannot reset SIGPROF!\n");
                exit (EXIT_FAILURE);
        }

        /* Ignore SIGHUP. */
        if (signal (SIGHUP, SIG_IGN) == SIG_ERR) {
                fprintf (stderr, "Cannot ignore SIGHUP!\n");
                exit (EXIT_FAILURE);
        }

        for (;;)
                pause ();

        return 0;
}

10.2.3 실행과 상속

  • fork() 시스템 콜을 통해서 프로세스가 생성되면 자식 프로세스는 부모 프로세스의 시그널에 대한 동작을 상속받는다.
    • 대기 중인 시그널은 상속되지 않는데, 대기 중인 시그널은 특정 pid로 보낸 것이지, 자식 프로세스로 보낸 것이 아니기 때문.
  • exec 시스템 콜 을 통해서 프로세스가 처음 생성되면 모든 시그널은 부모 프로세스가 이를 무시하는 경우를 제외하고 모두 기본 동작으로 설정 됨

실행과 상속

10.2.4 시그널 번호를 문자열에 맵핑하기

시그널 이름으로 코드를 작성하면 힘듦

→ 시그널 번호를 시그널 이름의 문자열로 변환할 수 있음

sys_siglist

1
2
3
4
5
6
extern const char * const sys_siglist[];

static void signal_handler (int signo)
{
        printf ("Caught %s\n", sys_siglist[signo]);
}
  • 최선의 선택
  • 시스템에서 지원하는 시그널 이름을 담고 있는 문자열의 배열
  • 시그널 번호를 색인으로 이용함

BSD에서 정의된 psignal() 인터페이스

1
2
3
#include <signal.h>

void psignal (int signo, const char *msg);
  • msg 인자로 전달한 문자열을 stderr에 출력하는데, 콜론과 공백 그리고 signo로 지정한 시그널 이름이 따라옴

더 나은 인더페이스 strsignal()

1
2
3
4
#define _GNU_SOURCE
#include <string.h>

char * strsignal (int signo);
  • signo로 지정한 시그널의 설명을 가리키는 포인터를 반환함
  • 하지만 반환된 문자열은 다음에 strsignal() 을 호출하기 전까지만 유효하기 때문에 Thread-safe 하지 않다.

    strsignal() uses a static buffer and is not thread safe. Use bsd_strsignal() for thread safety.

10.3 시그널 보내기

kill() 시스템 콜은 특정 프로세스에서 다른 프로세스로 시그널을 보낸다.

1
2
3
4
#include <sys/types.h>
#include <signal.h>

int kill (pid_t pid, int signo);
  • pid가 0보다 큰 경우 (일반적)
    • pid가 가리키는 프로세스에 signo 시그널을 보냄
  • pid 0
    • 호출한 프로세스의 프로세스 그룹에 속한 모든 프로세스에 signo 시그널을 보냄
  • pid -1
    • 호출한 프로세스가 시그널을 보낼 권한이 있는 모든 프로세스에 signo를 보냄
    • 호출한 프로세스 자신과 init은 제외
  • pid < -1
    • 프로세스 그룹 -pid 에 signo를 보냄

10.3.1 권한

다른 프로세스에 시그널을 보내기 위해서는 보내는 프로세스가 적절한 권한을 가지고 있어야함

  • CAP_KILL 기능이 있는 (root process) 프로세스는 모든 프로세스에 시그널을 보낼 수 있음
  • 이 기능이 없을 경우 프로세스의 유효 사용자 ID 나 실제 사용자 ID는 반드시 시그널을 받는 프로세스의 실제 사용자 ID나 저장된 사용자 ID와 유효해야함
    • → 즉, 사용자는 자신이 소유하고 있는 프로세스에만 시그널을 보낼 수 있음

    SIGCONT(프로세스 정지 후 계속 수행) 에 대한 예외를 정의함.

  • signo가 0 (null 시그널) 이라면 시그널을 보내진 않지만, 에러 검사는 수행하기 때문에 권한 체크가 가능함!
1
2
3
4
5
6
7
int ret;

ret = kill (1722, 0);
if (ret)
        ; /* we lack permission */
else
        ; /* we have permission */

10.3.3 자신에게 시그널 보내기

raise() 함수는 자기 자신에게 시그널을 보낼 수 있는 간단한 방법을 제공함

1
2
3
#include <signal.h>

int raise (int signo);
1
2
3
raise (signo);
===
kill (getpid(), signo);

10.3.4 프로세스 그룹 전체에 시그널 보내기

프로세스 그룹 ID를 음수로 바꿔서 kill() 을 사용하는 것이 아니라 프로세스 그룹에 속한 모든 프로세스에 시그널을 보낼 수 있는 함수도 있음

1
2
3
#include <signal.h>

int killpg (int pgrp, int signo); 
1
2
3
killpg (pgrp, signo);
===
kill (-pgrp, signo);

10.4 재진입성 (Reenterancy)

  • Reenterant 함수
    • 둘 이상의 스레드에 의해서 호출되었을 때, 호출된 순서에 상관없이 하나가 수행되고 난 다음 다른 함수 호출이 수행된 것처럼 제대로 된 결과를 반환해주는 함수를 의미
    • interrupt handlersignal handler에서 찾아볼 수 있음
    • 특성
      • no static (or global) non-constant data
      • not return the address to static (or global) non-constant data
    • 예시
      • function_a()가 호출되고 있는 도중 interrupt가 발생
      • interrupt_handler() 가 수행
      • interrupt_handler() 내부에서 function_a()를 다시 사용해도, 기존에 수행중이던 function_a() 의 수행 결과에 영향을 주면 안된다는 것이 Reenterancy 이다.
    • Thread-safe VS Reenterancy
      • Thread-safe : A Function that may be safely invoker soncurrently by multiple threads
        • 즉, 멀티 스레드 환경에서 올바른 결과를 내어주는 함수를 의미
      • 모든 reenterant 함수는 thread-safe 하지만, 모든 thread-safe 함수가 reenterant 함수인 것은 아니다.

커널이 시그널을 보낼 때, 프로세스는 코드 어디선가에서 실행 중인 상태이다.

해당 시그널의 핸들러는 어떤 작업 도중에도 실행이 가능하다. 따라서 프로세스에 설정된 시그널 핸들러는 자신이 실행하는 작업과 자신이 손대는 데이터(특히 Global Data 를 수정할 때)를 아주 조심스럽게 다뤄야 함 !

10.5 시그널 모음

여러개의 시그널을 간편하게 다루기 위해서는 시그널을 집합으로 표시하는 자료 형식이 필요

int 형식을 한 비트마다 하나의 신호로 대응시켜서 표시할 수 있지만 signal은 32개보다 많음

sigset_t 라는 자료 형식이 만들어짐.

sigset_t 와 같은 신호 집합을 사용하는 이유는 많은 신호를 간편하게 다루기 위함

모든 신호를 막는다거나 (BLOCK), 막은 신호를 다시 푼다거나(UNBLOCK), 신호가 발생했지만 Block 되어서 대기(PENDING) 중인 신호가 무엇이 있는가 를 쉽게 파악할 수 있음

SIGSTOPSIGKILL은 절대 제어할 수 없음

1
2
3
4
5
6
7
8
9
10
#include <signal.h>

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 sigismember (const sigset_t *set, int signo);

image

  • sigemptyset()
    • set으로 지정된 시그널 모음을 비어있다고 표시하며 초기화 함
  • sigfillset()
    • set으로 지정된 시그널 모음을 가득 차 있다고 표시하며 초기화 함

image

  • sigaddset()
    • set으로 지정된 시그널 집합에 signo를 추가함
  • sigdelset()
    • set으로 지정한 시그널 모음에서 signo를 제거함
  • sigismember()
    • set으로 지정한 시그널 모음에서 signo가 있으면 1을 반환, 그렇지 않아면 0을 반환

10.5.1 추가적인 시그널 모음 함수

  • 이 함수들은 유용하지만 POSIX 호환이 중요한 프로그램에서는 사용하면 안됨
1
2
3
4
5
6
7
8
#define _GNU_SOURCE
#define <signal.h>

int sigisemptyset (sigset_t *set);

int sigorset (sigset_t *dest, sigset_t *left, sigset_t *right);

int sigandset (sigset_t *dest, sigset_t *left, sigset_t *right);
  • sigisemptyset()
    • set으로 지정된 시그널 모음이 비어있는 경우에는 1, 그렇지 않으면 0을 반환
  • sigorset()
    • 시그널 모음인 left와 right의 합집합을 dest에 넣음
  • sigandset()
    • 시그널 모음인 left와 right의 교집합을 dest에 넣음

10.6 시그널 블록

시그널 핸들러와 프로그램의 다른 부분이 데이터를 공유해야 할 필요가 있다면 어떻게 할까?

image

일시적으로 시그널 전달을 보류하여 이 영역을 보호한다.

→ 시그널은 블록 되었다고 표현함

  • 블록되는 동안 발생하는 어떤 시그널도 블록이 해제될 때까지는 처리되지 않음.
  • 프로세스는 여러 시그널을 블록할 수 있으며 프로세스가 블록한 시그널 모음을 해당 프로세스의 시그널 마스크 라고 한다.

  • sigprocmask() 는 how값에 따라 다르게 동작하며 프로세스 시그널 마스크를 관리한다.
1
2
3
4
5
#include <signal.h>

int sigprocmask (int how,
                 const sigset_t *set,
                 sigset_t *oldset);

how

  1. SIG_SETMASK : 호출한 프로세스의 시그널 마스크를 set으로 변경함
  2. SIG_BLOCK : 호출한 프로세스의 시그널 마스크를 현재 마스크와 set의 합집합으로 변경
  3. SIG_UNBLOCK : 기존의 블록된 시그널에서 set의 시그널을 제거
    • oldset 이 null이 아니라면 이전 시그널 모음을 oldset에 넣는다.
    • set 이 null인 경우 how를 무시하고 시그널 마스크를 변경하지 않지만, 시그널 마스크를 oldset에 넣는다
    • → set에 null 값을 넣어 전달하면 현재 시그널 마스크를 조회할 수 있음

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(){
        sigset_t set, oldset;

		// set과 oldset을 깨끗이 비워줌
        sigemptyset(&set);
        sigemptyset(&oldset);

		// sigaddset으로 set에 SIGINT와 SIGQUIT을 추가
        sigaddset(&set,SIGINT);
        sigaddset(&set,SIGQUIT);
		// set에 있는 시그널들을 block 시키기 위해서 sigprocmask를 호출하는데 how의 인자는 SIG_BLOCK
        sigprocmask(SIG_BLOCK,&set,NULL);

        printf("SIGINT와 SIGQUIT는 블록되었습니다.\n");
        printf("Ctrl+C와 Ctrl+\\ 눌러도 반응이 없습니다.\n");

		//만약 Ctrl + \(SIGQUIT)을 눌렀다면 5초후 Coredump가 생기고 종료
        //SIGQUIT의 기본동작은 Coredump + 종료
        sleep(5);

		//현재 set에서 SIGINT를 뺌. set에는 SIGQUIT만 있는 상태
        //중요한것은 프로세스에 적용하지 않은 상태
        sigdelset(&set,SIGINT);
        
        //프로세스에 Unblock을 set에 적용. SIGQUIT은 이제 Block되지 않음
        sigprocmask(SIG_UNBLOCK,&set,&oldset);

        printf("만약 Ctrl+\\을 눌렀다면 종료합니다.\n");
        printf("현재 남은 시그널은 SIGINT입니다.\n");
        printf("Ctrl+C를 눌러도 반응이 없습니다.\n");

        sleep(5);

        set=oldset;
        sigprocmask(SIG_SETMASK,&set,NULL);

        printf("다시 SIGINT와 SIGQUIT이 블록됩니다.\n");
        printf("Ctrl+C와 Ctrl+\\ 눌러도 반응이 없습니다.\n");

        sleep(5);

        sigprocmask(SIG_UNBLOCK,&set,NULL);
        //아무 시그널(Cntl +C 혹은 Cntl+\)을 주지 않았다면 아래의 메시지가 출력되고 종료
        printf("모든 시그널이 해제되었습니다.\n");

}

10.6.1 대기 중인 시그널 조회하기

1
2
3
#include <signal.h>

int sigpending (sigset_t *set);
  • 커널에서 블록된 시그널이 발생할 경우, 이 시그널은 전달되지 않는다.
  • sigpending() 는 대기 중인 시그널 모음을 조회할 수 있음
  • 호출이 성공하면 대기 중인 시그널 모음을 set에 넣고 0을 반환함.
  • 실패하면 -1을 반환

10.6.2 여러 시그널 기다리기

  • 프로세스가 자신의 시그널 마스크를 일시적으로 변경하고, 자신을 종료시키거나 자신이 처리할 시그널이 발생할 때까지 기다리게 만든다.
1
2
3
#include <signal.h>

int sigsuspend (const sigset_t *set);
  • 시그널을 BLOCK시킴과 동시에 대기함.
  • sigprocmask같은 경우 how를 SIG_BLOCK이나 SIG_SETMASK로 설정하면 블록하기만 할뿐 대기하지는 않는데, sigsuspend는 블록과 대기를 동시에 할 수 있음.
  • 성공시 0, 실패시 -1을 반환
  • 활용 방법
    • 프로그램이 critical section에 머물러 있을 때 도착해서 블록되었던 시그널을 조회할 수 있음.

10.7 고급 시그널 관리

  • signal()은 시그널 핸들러를 구축하는 오래되고 가장 간단한 방법이지만, 매우 기초적이고 시그널 관리를 위한 최소한의 부분만 제공함
  • sigaction() 시스템 콜이 훨씬 더 훌륭한 시그널 관리 능력을 제공한다.
1
2
3
4
5
#include <signal.h>

int sigaction (int signo,
               const struct sigaction *act,
               struct sigaction *oldact);
  • sigaction() 을 호출하면 signo로 지정한 시그널의 동작 방식을 변경한다.
    • SIGKILL, SIGSTOP을 제외
  • act 가 NULL이 아닌 경우 시스템 콜은 해당 시그널의 현재 동작 방식을 act가 지정한 내용으로 변경함
  • oldact 가 NULL이 아닌 경우 해당 호출은 이전의 동작 방식( act가 NULL인 경우에는 현재의 방식) 을 oldact에 저장한다.

  • sigaction 구조체는 시그널을 세세하게 제어할 수 있게 함
1
2
3
4
5
6
7
struct sigaction {
        void (*sa_handler)(int);   /* signal handler or action */
        void (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t sa_mask;          /* signals to block */
        int sa_flags;              /* flags */
        void (*sa_restorer)(void); /* obsolete and non-POSIX */
};
  • sa_handlersa_sigaction유니언이라는 사실을 주의하고, 두 필드 모두에 값을 할당하지 않도록 해야 함
  • sa_handler
    • 해당 시그널을 받았을 때 수행할 동작을 지정함.
    • signal()과 마찬가지로 SIG_DFL, SIG_IGN, 시그널을 처리하는 함수를 가리키는 포인터가 들어올 수 있음
  • sa_sigaction

    1
    
      void my_handler (int signo, siginfo_t *si, void *ucontext);
    
    • 시그널 번호, siginfo_t 구조체, ucontext_t 구조체를 void 포인터로 타입 변환하여 받음.
  • sa_flags
    • 0개 혹은 하나 이상의 플래그에 대한 비트마스크. 해당 플래그들은 signo로 지정한 시그널의 처리를 변경함. (p.456 플래그 설명)
    • SA_SIGINFO 를 설정하면 sa_handler 가 아니라 sa_signaction이 시그널을 처리하는 함수를 명시한다.
    • SA_NODEFER 를 설정하지 않으면 현재 처리 중인 시그널도 블록됨.
  • sa_mask
    • 시그널 핸들러를 실행하는 동안 시스템이 블록해야 할 시그널 모음을 제공함
    • 이 필드를 사용해서 여러 시그널 핸들러 간의 재진입을 적절하게 막을 수 있다.

10.7.1 siginfo_t 구조체

siginfo_t 구조체에는 sa_handler 대신 sa_sigaction 을 이용하는 경우 시그널 핸들러로 전달할 정보가 가득함

시그널을 보낸 프로세스에 대한 정보와 시그널을 일으킨 원인에 대한 정보를 포함하여 흥미로운 데이터가 많음.

(p.458 각 필드 설명)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct siginfo_t {
        int si_signo;      /* signal number */
        int si_errno;      /* errno value */
        int si_code;       /* signal code */
        pid_t si_pid;      /* sending process's PID */
        uid_t si_uid;      /* sending process's real UID */
        int si_status;     /* exit value or signal */
        clock_t si_utime;  /* user time consumed */
        clock_t si_stime;  /* system time consumed */
        sigval_t si_value; /* signal payload value */
        int si_int;        /* POSIX.1b signal */
        void *si_ptr;      /* POSIX.1b signal */
        void *si_addr;     /* memory location that caused fault */
        int si_band;       /* band event */
        int si_fd;         /* file descriptor */
};
  • si_code
    • 프로세스가 왜 그리고 어디서부터 시그널을 받았는지에 대한 설명이 있음
  • si_addr
    • SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP 의 경우 이 void 포인터는 장애를 일으킨 주소를 저장함.
  • si_value
    • si_intsi_ptr 의 유니언이다.

10.7.2 si_code 의 멋진 세계

  • si_code 필드는 시그널을 일으킨 원인을 알려주는데, 커널이 시그널을 보냈을 때 이 필드를 보면 왜 시그널을 보냈는지 알 수 있다.
  • 모든 시그널에 대해서 유효한 값이 있고, 각 시그널에 대해서 유효한 값들도 존재한다. (p.459 참고)

10.8 페이로드와 함께 시그널 보내기

앞에서 확인했듯이 sigaction에서 SA_SIGINFO 플래그가 함께 등록된 시그널 핸들러는 siginfo_t 인자를 전달하는데, siginfo_t 구조체는 si_value 라는 필드를 포함하고있다.

si_value 필드는 시그널을 생성한 곳에서 시그널을 받는 곳까지 전달되는 선택적인 페이로드이다.

sigqueue() 함수를 이용해서 페이로드와 함께 시그널을 보낼 수 있다.

1
2
3
4
5
#include <signal.h>

int sigqueue (pid_t pid,
              int signo,
              const union sigval value);
  • sigqueue()kill()과 유사하게 동작
    • 호출이 성공하면 signo 시그널pid 프로세스나 프로세스 그룹 큐에 들어가고 0을 반환
  • value
    • intvoid 포인터의 유니언임
    1
    2
    3
    4
    
      union sigval {
              int sival_int;
              void *sival_ptr;
      };
    

10.8.1 시그널 페이로드 예제

1
2
3
4
5
6
7
8
sigval value;
int ret;

value.sival_int = 404;

ret = sigqueue (1722, SIGUSR2, value);
if (ret)
        perror ("sigqueue");
  • pid가 1722 인 프로세스에 404라는 정수 값을 페이로드에 담아 SIGUSR2 시그널과 함께 보낸다.

10.9 시그널은 미운 오리 새끼?

  • 시그널은 커널과 사용자 간 통신을 위한 구식 메커니즘이며 IPC의 원시적인 형태로 볼 수 있다.
  • 멀티스레딩 프로그램과 이벤트 루프 세계에서 시그널은 적절하지 않음.
  • 하지만 시그널은 커널에서 수많은 통보를 수신할 수 있는 유일한 방법이다!

ref. https://d-yong.tistory.com/10 / http://slideplayer.com/slide/10812592/

https://reakwon.tistory.com/53

This post is licensed under CC BY 4.0 by the author.