본문 바로가기
Computer Science/UNIX & Linux

[UNIX/Linux] ep8) 시그널

by 클레어몬트 2024. 10. 23.

https://claremont.tistory.com/entry/%EC%BB%B4%ED%93%A8%ED%84%B0-%EA%B5%AC%EC%A1%B0-ep4-CPU%EC%99%80-%EC%9D%B8%ED%84%B0%EB%9F%BD%ED%8A%B8

 

[컴퓨터 구조] ep4) CPU와 인터럽트

컴퓨터의 두뇌 CPU == 프로세서에 대해서 자세히 알아보자ㅁCPU(Central Processing Unit) = ALU + 제어장치 + 레지스터  ㅇALU(Arithmetic Logic Unit) 산술연산장치 ALU는 플래그를 내보낸다 아래의 표를 보자 

claremont.tistory.com

우리가 흔히 사용하는 ctrl + c 가 바로 비동기적 인터럽트 명령이고, 더 자세히 들어가면 SIGINT 시그널을 보내 프로그램에 중단 요청을 하는 것이다

 

 

 

시그널(signal): SW 인터럽트로 프로세스에 뭔가 발생했음을 알리는 간단한 메시지를 비동기적으로 보내는 것

프로그램에 예외적인 상황이 일어나는 경우에 발생

e.g. 0으로 나누기, 프로세스가 함수를 사용해 다른 프로세스에 시그널을 보내는 경우

 

보통 시그널로 전달되는 메시지는 무엇이 발생했는지를 표시하는 미리 정의된 상수를 사용

시그널을 받은 프로세스는 시그널에 따른 기본 동작을 수행하거나, 시그널을 무시하거나, 시그널 처리를 위해 특별히 지정

된 함수를 수행

 

 

ㅁ시그널 핸들러: 프로세스를 종료하기 전에 처리할 작업이 남아 있는 경우, 특정 시그널은 종료하고 않고자 하는 경우 시그널을 처리하는 함수를 지정하는 것

 

 

ㅁ시그널의 발생

시그널은 SW 인터럽트

시그널은 비동기적으로 발생하며, Linux 운영체제가 프로세스에 전달

시그널은 다음과 같은 3가지 경우에 발생

① 0으로 나누기 같은 프로그램에서 예외적인 상황이 일어나는 경우

② 프로세스가 kill() 함수와 같이 시그널을 보낼 수 있는 함수를 사용해 다른 프로세스에 시그널을 보내는 경우

③ 사용자가 ctrl + c 같은 인터럽트 키를 입력한 경우

 

 

시그널 처리 방법

기본 동작 수행

- 대부분 시그널의 기본 동작은 프로세스를 종료하는 것

- 이 외에 시그널을 무시하거나 프로세스 수행 일시 중지/재시작 등을 기본 동작으로 수행

 

시그널 무시

- 프로세스가 시그널을 무시하기로 지정하면 시스템은 프로세스에 시그널을 전달하지 않음

 

지정된 함수 호출

- 프로세스는 시그널의 처리를 위해 미리 함수를 지정해 놓고 시그널을 받으면 해당 함수를 호출해 처리

- 시그널 핸들러: 시그널 처리를 위해 지정하는 함수

- 시그널을 받으면 기존 처리 작업을 중지한 후 시그널 핸들러를 호출

- 시그널 핸들러의 동작이 완료되면 기존 처리 작업을 계속 수행

 

 

[시그널의 종류] signal.h 파일에 정의

코어 덤프(Core Dump)는 프로그램이 비정상적으로 종료될 때, 해당 프로그램의 메모리 상태를 파일로 저장한 것을 말한다

 

 

 

ㅇ int kill(pid_t pid, int sig): 시그널 전송1

- pid : 시그널을 받을 프로세스의 PID

- sig : pid로 지정한 프로세스에 보내는 시그널

pid에 대응하는 프로세스에 sig로 지정한 시그널을 전송

pid는 특정 프로세스 또는 프로세스 그룹을 의미

sig에 0(null 시그널)을 지정하면 실제로 시그널을 보내지 않고 오류를 확인, 예를 들면 pid가 정상인지 검사

 

(pid에 지정한 값에 따라 시그널을 어떻게 보낼 것인지를 결정)

pid가 0보다 큰 수 : pid로 지정한 프로세스에 시그널을 전송

pid가-1이 아닌 음수 : 프로세스 그룹 ID가 pid의 절댓값인 프로세스 그룹에 속하고 시그널을 보낼 권한이 있는 모든

프로세스에 시그널을 전송

pid가 0 : 특별한 프로세스(스케줄러 등)를 제외하고 프로세스 그룹 ID가 시그널을 보내는 프로세스의 프로세스 그룹 ID와

같은 모든 프로세스에 시그널을 전송

pid가 -1 : 시그널을 보내는 프로세스의 유효 사용자 ID가 root(수퍼 유저)가 아니면, 특별한 프로세스를 제외하고 프로세스

의 실제 사용자 ID가 시그널을 보내는 프로세스의 유효 사용자 ID와 같은 모든 프로세스에게 시그널을 전송

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    // 위의 시그널 표에 따르면 SIGCONT 시그널의 기본 처리는 무시이므로 특별한 처리를 하지 않음
    // 하지만 SIGQUIT 시그널은 코어 덤프를 발생시키며 프로세스를 종료
    // 따라서 kill(getppid(), SIGCONT) 문장이 실행된 후 바로 종료된다
    printf("Before SIGCONT Signal to parent.\n");
    kill(getppid(), SIGCONT); // 부모 프로세스에 SIGCONT 시그널을 전송

    printf("Before SIGQUIT Signal to me.\n");
    kill(getpid(), SIGQUIT); // 자신에게 SIGQUIT 시그널을 전송

    printf("After SIGQUIT Signal.\n");

    return 0;
}
// 실행 결과: 07행과 10행은 출력되었지만 13행은 출력되지 않고 코어 덤프를 생성한 뒤 종료했음을 알 수 있음

 

 

ㅇ int raise(int sig): 시그널 전송2

- sig : 보내려는 시그널 번호

raise() 함수는 호출한 프로세스에 인자로 지정한 시그널을 전송

만약 시그널 핸들러가 호출되면, 시그널 핸들러의 수행이 끝날 때까지 raise() 함수는 리턴하지 않는다

성공하면 0을, 실패하면 –1을 반환

 

 

ㅇ void abort(void): 시그널 전송3

호출한 프로세스에 SIGABRT 시그널을 전송

 

SIGABRT 시그널: 프로세스를 비정상적으로 종료시키고 코어 덤프 파일을 생성, 최소한 해당 프로세스가 연 파일은 모두 닫음

SIGABRT 시그널은 abort() 함수는 raise(SIGABRT)와 같은 동작을 수행하지만 프로세스를 종료시키므로 리턴하지 않는다

 

 

ㅇ sighandler_t signal(int signum, sighandler_t handler): 시그널 핸들러 지정1

시그널을 처리하는 가장 단순한 함수로, 시그널을 받을 때 해당 시그널을 처리할 함수나 상수를 지정할 수 있다

- signum : 시그널 핸들러로 처리하려는 시그널

- handler : 시그널 핸들러의 함수명

첫 번째 인자인 signum에는 SIGKILL과 SIGSTOP 시그널을 제외한 모든 시그널을 지정할 수 있다

두 번째 인자인 handler에는 signum으로 지정한 시그널을 받았을 때 처리할 방법을 지정한다

 

(handler 설정)

시그널 핸들러 주소

SIG_IGN : 시그널을 무시하도록 지정

SIG_DFL : 시그널의 기본 처리 방법을 수행하도록 지정

 

[버전 별 signal() 함수의 동작]

시스템 V : 시그널을 처리한 후 시그널 처리 방법을 기본 처리 방법(SIG_DFL)으로 재설정, 따라서 시그널 처리를 계속하려면

signal() 함수를 호출해 시그널을 처리한 후 다시 signal() 함수를 설정해야 함

BSD : 시그널을 처리한 후 시그널 처리 방법을 기본 처리 방법(SIG_DFL)으로 재설정하지 않음, 따라서 시그널 핸들러가

계속 동작

• Linux : 커널의 signal(2) 시스템 콜은 시스템 V와 같은 방식으로 동작, 그러나 gcc의 glibc 2부터 signal(3) 함수는

호출하지 않고 sigaction(2)를 호출해 BSD 형식으로 동작

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void sig_handler(int signo) { // 시그널 핸들러 함수 정의
    printf("Signal Handler signum: %d\n", signo);
    psignal(signo, "Received Signal"); // psignal 함수는 시그널 번호를 이름으로 바꿔서 출력하는 함수로 뒤에서 배운다
}

int main() {
    void (*hand)(int); // signal() 함수의 리턴값인 함수 포인터 선언

    hand = signal(SIGINT, sig_handler); // signal() 함수로 SIGINT 시그널의 시그널 핸들러를 지정
    if (hand == SIG_ERR) { // 예외 처리
        perror("signal");
        exit(1);
    }

    printf("Wait 1st Ctrl+C... : SIGINT\n");
    pause(); // 시그널이 입력될 때까지 기다리는 함수로 8절에서 다룬다 (여기서는 사용자가 ctrl + c 를 입력하기를 기다린다)
    printf("After 1st Signal Handler\n"); // pause() 함수 때문에 대기하고 있다가 사용자가 ctrl + c 를 입력하면 sig_handler() 함수가 호출되어 수행한 후 다시 이 줄로 복귀

    printf("Wait 2nd Ctrl+C... : SIGINT\n");
    pause(); // pause() 함수를 호출해 사용자가 다시 ctrl + c 를 입력하기를 기다린다
    printf("After 2nd Signal Handler\n");

    return 0;
}
// 실행 결과: "Wait 1st ..." 문장을 출력한 후 대기하다가 ctrl + c 를 입력받고 sig_handler() 함수를 실행한 후 다시 복귀
// 다시 ctrl + c 를 입력받으면 sig_handler() 함수를 호출해 처리한 후 "After 2nd ..." 문장을 출력

ctrl + c: 현재 실행 중인 프로그램을 종료하거나 인터럽트(interrupt) 신호를 보내는 데 사용

 

 

(참고) 솔라리스에서 실행한다면?

솔라리스에서 실행하면 다음과 같이 두 번째 ctrl + c 를 처리하지 못하고 종료하게 된다

솔라리스는 signal() 함수를 시스템 V 형식으로 처리해 시그널 처리를 기본값으로 재설정하기 때문이다

솔라리스에서 SIGINT 시그널이 들어올 때마다 시그널 핸들러를 호출하려면 매번 시그널 핸들러를 아래 코드와 같이 재지정 해야 한다.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void sig_handler(int signo) {
    void (*hand)(int);

    hand = signal(SIGINT, sig_handler); // signal() 함수로 시그널 핸들러를 다시 지정
    if (hand == SIG_ERR) {
        perror("signal");
        exit(1);
    }

    printf("Signal Handler Signal Number : %d\n", signo);
    psignal(signo, "Received Signal");
}

int main() {
(생략)

// 실행 결과: 두 번째 SIGINT를 받아도 sig_handler() 함수가 호출
// 그러나 위와 같이 시그널 핸들러를 재지정한다고 해도 이것이 100% 동작한다는 보장은 없음
// 시스템에 많은 부하가 걸려 있는 경우에는 시그널 핸들러를 재지정하기 전에 SIGINT 시그널을 받으면 그냥 종료될 수도 있음

 

 

(Linux에서는 사용 권장 x)

ㅇ sighandler_t sigset(int sig, sighandler_t disp): 시그널 핸들러 지정2

- sig : 시그널 핸들러로 처리하려는 시그널

- disp : 시그널 핸들러의 함수명

sigset() 함수의 인자 구조는 signal() 함수와 동일

sigset() 함수도 첫 번째 인자인 sig 에 SIGKILL과 SIGSTOP 시그널을 제외한 어떤 시그널이든 지정할 수 있다

두 번째 인자인 disp에도 signal() 함수처럼 시그널 핸들러 함수의 주소나 SIG_IGN, SIG_DFL 중 하나를 지정해야 한다

시그널 핸들러 함수의 주소를 반환, 실패하면 SIG_ERR을 반환

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void sig_handler(int signo) {
    printf("Signal Handler Signal Number : %d\n", signo);
    psignal(signo, "Received Signal");
}

int main() {
    void (*hand)(int);

    hand = sigset(SIGINT, sig_handler); // sigset() 함수를 사용해 시그널 핸들러를 지정
    if (hand == SIG_ERR) {
        perror("signal");
        exit(1);
    }

    printf("Wait 1st Ctrl+C... : SIGINT\n");
    pause();
    printf("After 1st Signal Handler\n");

    printf("Wait 2nd Ctrl+C... : SIGINT\n");
    pause();
    printf("After 2nd Signal Handler\n");

    return 0;
}
// 실행 결과: 시그널 핸들러가 한 번 호출된 후에 재지정하지 않아도 됨을 알 수 있음

 

 

 

POSIX에서 복수의 시그널을 처리하기 위해 도입한 개념

ㅁ시그널 집합: 시그널을 비트 마스크로 표현한 것으로, 각 비트가 특정 시그널과 연결되어 있다(1 : 1)

비트값이 1이면 해당 시그널이 설정된 것이고(true) 0이면 시그널이 설정되지 않은 것(false)

시그널 집합을 사용하면 여러 시그널을 지정해 처리할 수 있다

시스템에서는 시그널 집합의 처리를 위해 sigset_t 라는 구조체를 제공한다

 

(Linux)

#typedef struct {
    unsigned long __val[_NSIG_WORDS];
} sigset_t;

 

(솔라리스)

#typedef struct {
    unsigned int __sigbits[4];
} sigset_t;

 

 

ㅇ int sigemptyset(sigset_t* set): 시그널 집합 비우기

- set : 비우려는 시그널 집합의 주소

시스템에서 정의한 모든 시그널을 배제해 인자로 지정한 시그널 집합을 빈 집합으로 만든다( = 시그널 집합의 모든 비트를 0으로 설정)

성공하면 0을, 실패하면 -1을 반환

 

 

ㅇ int sigfillset(sigset_t* set): 시그널 집합에 모든 시그널 설정

- set : 설정하려는 시그널 집합의 주소

인자로 받은 시그널 집합을 시스템에서 정의한 모든 시그널을 포함하는 집합으로 만든다( = 시그널 집합의 모든 비트를 1로 설정)

성공하면 0을, 실패하면 -1을 반환

 

 

ㅇ int sigaddset(sigset_t* set, int signum): 시그널 집합에 시그널 설정 추가

- set : 시그널을 추가하려는 시그널 집합의 주소

- signum : 시그널 집합에 추가로 설정하려는 시그널

signum으로 지정한 시그널을 set으로 지정한 시그널 집합에 추가

성공하면 0을, 실패하면 –1을 반환

 

 

ㅇ int sigdelset(sigset_t* set, int signum): 시그널 집합에서 시그널 설정 삭제

- set : 시그널을 삭제하려는 시그널 집합의 주소

- signum : 시그널 집합에 추가로 설정하려는 시그널

ignum으로 지정한 시그널을 set으로 지정한 시그널 집합에서 제거

성공하면 0을, 실패하면 –1을 반환

 

 

ㅇ int sigismember(const sigset_t* set, int signum): 시그널 집합에 설정된 시그널 확인

- set : 확인하려는 시그널 집합의 주소

- signum : 시그널 집합에 설정되었는지 확인하려는 시그널

signum으로 지정한 시그널이 set으로 지정한 시그널 집합에 포함되어 있으면 1을, 포함되어 있지 않으면 0을 반환

#include <stdio.h>
#include <signal.h>

int main() {
    sigset_t st;

    sigemptyset(&st); // 시그널 집합을 모두 비움(모두 0으로 초기화)

    sigaddset(&st, SIGINT); // 시그널 집합에 SIGINT 시그널 추가
    sigaddset(&st, SIGQUIT); // 시그널 집합에 SIGQUIT 시그널 추가

    if (sigismember(&st, SIGINT)) { //  SIGINT가 현재 시그널 집합에 설정되어 있는지 확인해 결과를 출력
        printf("SIGINT has been set.\n");
    }

    printf("** Bit Pattern: %lx\n",st.__val[0]); // 시그널 집합에 설정된 값을 16진수로 출력

    return 0;
}
// 실행 결과: 시그널 집합에 SIGINT가 설정되어 있음을 알 수 있음
// 시그널 집합의 값은 0x06 으로 출력
// 0x06을 2진수로 표현하면 0000 0110으로, 오른쪽에서 두 번째와 세 번째 비트가 1로 설정되어 있음
// 위의 시그널 표에 따르면 SIGINT는 2번, SIGQUIT은 3번 시그널이므로 2번, 3번 비트가 이에 대응하고 있음을 알 수 있음

 

 


ㅁ시그널 제어 함수: sigaction()

 

[sigaction 구조체]

struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t*, void*);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

/* Linux 에서 sigaction 구조체의 멤버
   1. 시그널 처리를 위한 시그널 핸들러 주소
   2. 시그널 핸들러가 수행하는 동안 블로킹될 시그널
   3. 추가 기능을 설정할 수 있는 플래그
*/

 

sigaction() 함수는 시그널을 받아 이를 처리할 시그널 핸들러를 지정할 뿐만 아니라 플래그를 설정해 시그널 처리 과정을

제어할 수도 있다

struct sigaction {
    int sa_flags;
    union {
        void (*sa_handler)();
        void (*sa_sigaction)(int, siginfo_t *, void *);
    } _funcptr;
    sigset_t sa_mask;
};

- sa_flags에 SA_SIGINFO가 설정되어 있지 않는 경우: sa_handler에는 시그널을 처리할 동작을 지정

- sa_flags에 SA_SIGINFO가 설정되어 있는 경우: sa_sigaction 멤버를 사용

 

 

ㅇsa_mask

sa_mask에는 시그널 핸들러가 동작 중일 때 블로킹할 시그널을 시그널 집합으로 지정함

시그널 핸들러가 시작되어 시그널을 전달할 때 이미 블로킹된 시그널 집합에 sa_mask로 지정한 시그널 집합을 추가

sa_flags에 SA_NODEFER를 설정하지 않으면 시그널 핸들러를 호출하게 한 시그널도 블로킹

 

 

ㅇsa_flags: 시그널 전달 방법을 수정할 플래그를 지정

sa_flags는 값을 OR 연산(❘)으로 연결해 지정

 

※ sigaction() 함정

ㅇ int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact)

- signum : 처리할 시그널

- act : 시그널을 처리할 방법을 지정한 구조체 주소

- oldact : 기존에 시그널을 처리하던 방법을 저장할 구조체 주소

시그널을 받아 이를 처리할 시그널 핸들러를 지정

플래그를 설정해 시그널을 처리하는 과정을 제어할 수도 있음

시그널 핸들러가 수행되는 동안 다른 시그널을 블로킹할 수도 있음

signum에 지정한 시그널을 받았을 때 처리할 방법을 sigaction 구조체인 act로 받음

성공하면 0을 반환, 실패하면 –1을 반환

 

첫 번째 인자로 SIGKILL과 SIGSTOP 시그널을 제외한 어떤 시그널도 사용할 수 있음

두 번째와 세 번째 인자는 sigaction 구조체를 사용해 지정

두 번째 인자에는 NULL 또는 signum에 지정된 시그널을 받았을 때 처리할 방법을 지정한 구조체 주소를 저장

세 번째 인자에는 NULL 또는 기존 처리 방법을 저장

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void sig_handler(int signo) { // 시그널 핸들러를 지정
    psignal(signo, "Received Signal:");
    sleep(5); // 5초간 실행을 대기
    printf("In Signal Handler, After Sleep\n");
}

int main() {
    struct sigaction act;

    sigemptyset(&act.sa_mask);
    // 시그널 핸들러가 동작하는 중에 SIGQUIT 시그널을 블로킹하기 위해 sa_mask에 SIGQUIT 시그널을 설정
    sigaddset(&act.sa_mask, SIGQUIT);

    act.sa_flags = 0; // sa_flags에는 아무 플래그도 지정하지 않음
    act.sa_handler = sig_handler;
    // sigaction() 함수를 호출해 SIGINT 시그널을 받을 경우의 동작을 지정
    if (sigaction(SIGINT, &act, (struct sigaction*)NULL) < 0) {
        perror("sigaction");
        exit(1);
    }
    fprintf(stderr, "Input SIGINT: ");
    pause(); // pause() 함수를 사용해 시그널을 기다림
    // 사용자가 ctrl + c 를 입력해 SIGINT 시그널을 보내면 즉시 시그널 핸들러인 sig_handler() 함수가 호출
    // sig_handler() 함수는 sleep() 함수로 잠시 기다리고 있는데, 이때 SIGQUIT( Ctrl + \ ) 시그널을 보내면
    // 블로킹되어 기다리고 있다가 sig_handler() 함수의 동작이 끝난 후 처리
    fprintf(stderr, "After Signal Handler\n");

    return 0;
}
// 실행 결과: sig_handler() 함수의 동작이 끝나고 곧바로 SIGQUIT 시그널의 기본 처리대로 코어 덤프를 생성하고 종료했음을 알 수 있음
// 따라서 "After Signal Handler" 문장 출력은 실행되지 않음

SIGQUIT 시그널의 기본 처리대로 코어 덤프를 생성하고 종료

 

(위와 같이 act.sa_flagsSA_RESETHAND 플래그를 지정하지 않을 경우)

위 코드를 실행한 상태에서 SIGINT 시그널을 보내면 sig_handler() 함수가 호출

sleep() 함수에서 기다리는 동안 다시 SIGINT 시그널을 보내면 시그널은 블로킹되어 있다가 시그널 핸들러의 동작이 끝난 후 다시 시그널 핸들러 함수를 호출해 처리

상자 부분이 두 번째 SIGINT 시그널과 관련된 부분이다

 

처음 sigaction() 함수로 지정한 시그널 처리 방법이 계속 유지되고 있어 sig_handler() 함수가 다시 호출된 것

 

 

(act.sa_flags 에 0 값이 아닌 SA_RESETHAND 플래그를 지정한 경우)

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void sig_handler(int signo) { // 시그널 핸들러를 지정
    psignal(signo, "Received Signal:");
    sleep(5); // 5초간 실행을 대기
    printf("In Signal Handler, After Sleep\n");
}

int main() {
    struct sigaction act;

    sigemptyset(&act.sa_mask);
    // 시그널 핸들러가 동작하는 중에 SIGQUIT 시그널을 블로킹하기 위해 sa_mask에 SIGQUIT 시그널을 설정
    sigaddset(&act.sa_mask, SIGQUIT);

    act.sa_flags = SA_RESETHAND; // sa_flags에는 아무 플래그도 지정하지 않음
    act.sa_handler = sig_handler;
    // sigaction() 함수를 호출해 SIGINT 시그널을 받을 경우의 동작을 지정
    if (sigaction(SIGINT, &act, (struct sigaction*)NULL) < 0) {
        perror("sigaction");
        exit(1);
    }
    fprintf(stderr, "Input SIGINT: ");
    pause(); // pause() 함수를 사용해 시그널을 기다림
    // 사용자가 ctrl + c 를 입력해 SIGINT 시그널을 보내면 즉시 시그널 핸들러인 sig_handler() 함수가 호출
    // sig_handler() 함수는 sleep() 함수로 잠시 기다리고 있는데, 이때 SIGQUIT( Ctrl + \ ) 시그널을 보내면
    // 블로킹되어 기다리고 있다가 sig_handler() 함수의 동작이 끝난 후 처리
    fprintf(stderr, "After Signal Handler\n");

    return 0;
}
// 실행 결과: 두 번째 SIGINT(^C)에 의해 프로세스가 종료된다
// 이는 SA_RESETHAND 플래그가 설정되어 SIGINT 시그널의 처리 방법이 기본 처리 방법으로 재설정되었기 때문

두 번째 SIGINT(^C)에 의해 프로세스가 종료

 

 

ㅁ시그널 발생 원인 검색

#void handler(int sig, siginfo_t* info, void* ucontext) [함수 원형]

- sig : 시그널 핸들러를 호출할 시그널

- info : 시그널이 발생한 원인을 담은 siginfo_t 구조체 포인터

- ucontext : 시그널이 전달될 때 시그널을 받는 프로세스의 내부 상태를 나타내는 ucontext_t 구조체 포인터

typedef struct {
    int si_signo; // 관련된 시그널 번호
    int si_errno; // 0 또는 시그널과 관련된 오류 번호
    int si_code; // 시그널 발생 원인을 정의하는 코드
    // si_code의 값이 SI_NOINFO이면 si_signo만 의미가 있고
    // siginfo_t 구조체의 나머지 멤버는 사용되지 않음

    union {
        int si_trapno;
        pid_t si_pid;
        uid_t si_uid;
        int si_status;
        clock_t si_utime;
        clock_t si_stime;
        sigval_t si_value;
        int si_int;
        void *si_ptr;
        int si_overrun;
        int si_timerid;
        void *si_addr;
        long si_band;
        int si_fd;
        short si_addr_lsb;
        void *si_lower;
        void *si_upper;
        int si_pkey;
        void *si_call_addr;
        int si_syscall;
        unsigned int si_arch;
    }
} siginfo_t;

 

 

사용자 프로세스에 의한 시그널 발생 원인 코드

사용자 프로세스의 시그널 생성

si_code의 값이 0과 같거나 작다면 사용자 프로세스가 kill(), raise(), abort() 등의 함수로 시그널을 생성한 것

사용자 프로세스에서 시그널을 생성하면 si_code에는 아래 표에 정의된 주요 값이 저장

(그 이외의 특정 시그널이 발생한 경우에는 유효한 코드들 man sigaction으로 확인이 가능하다)

 

 

ㅇ void psiginfo(const siginfo_t* pinfo, const char* s): 시그널 발생 원인 출력 함수

- pinfo : 시그널 발생 원인에 관한 정보를 저장하고 있는 구조체 포인터

- s : 출력할 문자열

siginfo_t 구조체 포인터를 인자로 받아 시그널이 발생한 원인을 표준 오류로 출력

첫 번째 인자인 pinfo에는 시그널 핸들러의 두 번째 인자로 받은 siginfo_t 구조체의 주소를 지정

함수를 실행하면 두 번째 인자인 s에 지정한 문자열이 먼저 출력되고 시그널 정보가 출력

 

(예시 코드 - SA_SIGINFO 플래그 설정)

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/ucontext.h>
#include <unistd.h>

void sig_handler(int signo, siginfo_t *sf, ucontext_t *uc) {
    psiginfo(sf, "Received Signal:"); // 시그널 핸들러 함수에서 psiginfo() 함수를 호출해 시그널 정보를 호출
    printf("si_code : %d\n", sf->si_code);
}

int main() {
    struct sigaction act;

    act.sa_flags = SA_SIGINFO; // SA_SIGINFO 플래그를 지정
    act.sa_sigaction = (void(*)(int, siginfo_t*, void*))sig_handler; // sa_handler 대신 sa_sigaction 멤버를 사용해 시그널 핸들러를 지정
    sigemptyset(&act.sa_mask); // sa_mask 값을 모두 비움
    if (sigaction(SIGUSR1, &act, (struct sigaction*)NULL) < 0) { // SIGUSR1 시그널을 지정해 sigaction() 함수로 시그널 처리를 등록
        perror("sigaction");
        exit(1);
    }
    pause();
    
    return 0;
}
/*
    실헹 결과: 실행 파일을 백그라운드로 실행한 후 kill 명령으로 SIGUSR1 시그널을 전송
    15072는 이 파일을 실행한 프로세스의 PID

    시그널 핸들러에서 pisinfo() 함수가 아래의 문장을 출력
    “Received Signal:: User defined signal 1 (Signal sent by kill() 13651 1000)”

    PID 13651인 프로세스가 SIGUSR1 시그널을 보냈다는 의미
    13651은 kill 명령을 실행한 셸의 PID
    1000은 시그널을 보낸 사용자의 UID
*/

 

 


ㅁ알람 시그널: 
일정 시간이 지난 후에 자동으로 시그널이 발생하도록 하는 것

일정 시간 후에 한 번 발생시킬 수도 있고, 일정한 시간 간격을 두고 주기적으로 알람 시그널을 발생시킬 수도 있다

 

ㅇ unsigned int alarm(unsigned int seconds): 알람 시그널 생성

- seconds : 알람을 발생시킬 때까지 남은 시간(초 단위)

인자로 초 단위 시간을 받는다

인자로 지정한 시간이 지나면 SIGALRM 시그널이 생성되어 프로세스에 전달

이전에 호출한 alarm() 함수의 시간이 남아 있으면 해당 시간을 반환, 그렇지 않으면 0을 반환

 

프로세스별로 알람시계가 하나밖에 없으므로 알람은 하나만 설정할 수 있다. 따라서 알람 시그널을 생성하기 전에 다시 alarm() 함수를 호출하면 이전 설정은 없어지고 재설정이 된다.

※ 인자로 0을 지정하면 이전에 설정한 알람 요청은 모두 취소

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sig_handler(int signo) {
    psignal(signo, "Received Signal");
}

int main() {
    signal(SIGALRM, sig_handler); // SIGALRM 시그널 처리를 위한 시그널 핸들러를 지정

    alarm(2); // alarm() 함수의 인자로 2초를 지정해 함수를 호출
    printf("Wait...\n");
    sleep(3); // 3초 동안 sleep 하고 있는 중에 2초가 지나면 SIGALRM 시그널이 발생해 시그널 핸들러가 호출

    return 0;
}
// 실행 결과: 프로그램이 종료되기 전에 시그널 핸들러를 호출한 사실을 알 수 있음

 

 

인터벌 타이머

Linux 시스템은 프로세스별로 타이머 3개를 제공하며, 각 타이머가 사용하는 시간 유형에 따라 각기 다른 시그널이 생성된다

(타이머에 관한 정보는 sys/time.h 파일에 정의)

 

ITIMER_REAL

실제 시간을 사용, 이 타이머가 만료되면 SIGALRM 시그널이 생성

 

 ITIMER_VIRTUAL

프로세스가 사용하는 사용자 모드 CPU 시간을 사용

이 시간은 프로세스가 동작 중일 때만 작동하며, 만료되면 SIGVTALRM 시그널이 생성

 

 ITIMER_PROF

프로세스가 사용하는 시스템 모드와 사용자 모드 CPU 시간을 합해 사용

이 타이머가 만료되면 SIGPROF 시그널이 생성

ITIMER_VIRTUAL과 함께 사용하면 프로세스 사용한 사용자 모드 CPU 시간과 시스템 모드 CPU 시간을 알 수 있음

 

 

ㅇ int getitimer(int which, struct itimerval* curr_value): 타이머 정보 검색

- which : 검색할 타이머의 종류

- curr_value : 타이머 정보를 저장할 구조체 포인터

which에는 검색할 타이머의 종류를 지정

curr_value에는 타이머의 현재 시간과 타이머 간격 정보를 저장할 itimerval 구조체 포인터를 지정

 

[itimerval 구조체]

struct itimerval {
    struct timeval it_interval; // 타이머의 간격 정보가 저장 (0이면 다음에 타이머가 만료될 때 타이머 기능이 정지)
    struct timeval it_value; // 타이머가 만료될 때까지 남은 시간이 저장 (0이면 타이머 기능 정지)
};

 

 

이 시간 정보들은 timeval 구조체에 정의

struct timeval {
    time_t tv_sec;
    suseconds_t tv_usec;
};

 

timeval 구조체에는 초[s]와 마이크로초[μs] 단위로 시간을 저장

 

 

ㅇ int setitimer(int which, const struct itimerval* value, struct itimerval* ovalue): 타이머 설정

- which : 설정할 타이머의 종류

- value : 설정할 타이머의 정보를 저장한 구조체 포인터 (itimerval 구조체를 가리키는 포인터)

- ovalue : 이전 타이머의 정보를 저장할 구조체 포인터 (itimerval 구조체를 가리키는 포인터)

which에는 설정할 타이머의 종류를 지정하고, value에는 설정할 타이머 정보를 저장해 인터벌 타이머를 설정

ovalue는 NULL 또는 이전 설정값을 저장

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>

void sig_handler() {
    printf("Timer Invoked..\n");
}

int main() {
    struct itimerval it;

    signal(SIGALRM, sig_handler); // SIGALRM 시그널에 대한 시그널 핸들러를 지정
    it.it_value.tv_sec = 3; // 타이머에 현재 남은 시간을 3초로 설정
    it.it_value.tv_usec = 0;
    it.it_interval.tv_sec = 2; // 타이머의 간격을 2초로 설정
    it.it_interval.tv_usec = 0;
    // 최초의 시그널은 3초 후에 발생하고, 이후로는 2초 간격으로 타이머가 동작하게 설정하는 것

    if (setitimer(ITIMER_REAL, &it, (struct itimerval*)NULL) == -1) {
        perror("setitimer");
        exit(1);
    }

    while (1) { // 반복문으로 1초 동안 sleep() 했다가 getitimer() 함수를 호출해 남은 시간 정보를 출력하게 함
        if (getitimer(ITIMER_REAL, &it) == -1) {
            perror("getitimer");
            exit(1);
        }
        printf("%d sec, %d msec.\n", (int)it.it_value.tv_sec, (int)it.it_value.tv_usec);
        sleep(1);
    }

    return 0;
}
// 실행 결과: 처음 남은 시간이 3초였으므로 while문을 세 번 반복한 후에 타이머가 만료되어 SIGALRM 시그널을 생성하고
// 이후에는 2초 간격으로 타이머가 작동함을 알 수 있음
// 무한 반복문이므로 ctrl + c 로 종료해야 함

처음에만 3초, 이후로는 2초 간격(무한 반복문이므로 ^C로 탈출해줘야 한다)

 

 

 

+ 기타 시그널 처리 함수

ㅇ void psignal(int sig, const char* s): 시그널 정보 출력

- sig : 정보를 출력할 시그널

- s : 출력할 문자열

두 번째 인자인 s로 지정한 문자열을 출력

첫 번째 인자인 sig로 지정한 시그널을 가리키는 이름을 붙여 표준 오류로 출력

 

 

ㅇ char* strsignal(int sig): 시그널 정보 출력

- sig : 정보를 출력할 시그널

인자로 받은 시그널을 가리키는 이름을 문자열로 반환, 만약 인자로 받은 시그널이 없으면 NULL을 반환

 

 

ㅇ int sighold(int sig): 시그널 블로킹

ㅇ int sigrelse(int sig): 시그널 블로킹 해제

- sig : 블로킹하거나 해제할 시그널

인자로 받은 시그널을 프로세스의 시그널 마스크에 추가

시그널 마스크에 추가된 시그널은 블로킹되어 해당 시그널을 받지 않는다

sigrelse() 함수는 프로세스의 시그널 마스크에서 시그널을 해제

 

이 함수들은 시스템 V 유닉스에서 유래된 것으로, Linux에서는 이 함수 대신 POSIX 표준인 sigaction(), sigprocmask() 함수를 사용할 것을 추천

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>

void sig_handler(int signo) {
    char* s;

    s = strsignal(signo);
    printf("Received Signal : %s\n", s);
}

int main() {
    if (signal(SIGINT, sig_handler) == SIG_ERR) {
        perror("signal");
        exit(1);
    }

    // sighold() 함수를 사용해 SIGINT 시그널을 블로킹
    // 사용자가 ctrl + c 를 입력해도 프로세스가 종료되거나 시그널 핸들러가 호출되지 않음
    // ctrl + \ 로 강제 종료
    sighold(SIGINT);
    
    pause();

    return 0;
}

 

 

POSIX 표준 함수이며 sighold(), sigrelse() 함수의 두 기능을 합친 거라 생각하면 된다

ㅇ int sigprocmask(int how, const sigset_t* set, sigset_t* oldset): 시그널 집합 블로킹과 해제

- how : 시그널을 블로킹할 것인지 해제할 것인지 여부

- set : 블로킹하거나 해제할 시그널 집합의 주소

- oldset : NULL 또는 이전 설정값을 저장할 시그널 집합의 주소

시그널 집합을 사용해 한 번에 여러 시그널을 블로킹할 수 있다

set에 지정한 시그널 집합을 블로킹할 것인지 해제할 것인지를 how 에 지정해 호출할 수 있다

 

첫 번째 인자인 how에 올 수 있는 값

- SIG_BLOCK : set에 지정한 시그널 집합을 시그널 마스크에 추가

- SIG_UNBLOCK : set에 지정한 시그널 집합을 시그널 마스크에서 제거

- SIG_SETMASK : set에 지정한 시그널 집합으로 현재 시그널 마스크를 대체

 

두 번째 인자인 set은 블로킹하거나 해제할 시그널 집합을 가리킴

세 번째 인자인 oldset 에는 NULL이 아니면 이전 설정값이 저장

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main() {
    sigset_t new;
    
    sigemptyset(&new);
    sigaddset(&new, SIGINT); // 시그널 집합에 SIGINT 시그널 설정
    sigaddset(&new, SIGQUIT); // 시그널 집합에 SIGQUIT 시그널 설정
    sigprocmask(SIG_BLOCK, &new, (sigset_t*)NULL); // SIGINT 시그널과 SIGQUIT 시그널을 블로킹

    printf("Blocking Signals : SIGINT, SIGQUIT\n");
    printf("Send SIGQUIT\n");
    kill(getpid(), SIGQUIT); // SIGQUIT 시그널을 전송

    printf("UnBlocking Signals\n");
    sigprocmask(SIG_UNBLOCK, &new, (sigset_t*)NULL); // 블로킹된 SIGQUIT와 SIGINT 시그널을 해제

    return 0;
}
// 실행 결과: 15행에서 보낸 SIGQUIT 시그널에 의해 바로 종료되지 않았음
// sigprocmask() 함수로 블록이 해제되자 기다리고 있던 SIGQUIT 시그널을 받아 프로세스가 코어 덤프를 생성하며 종료

 

 

ㅇ int pause(void): 시그널 대기(잡기)

프로세스가 종료하거나 시그널 대기(잡기) 함수를 호출하는 시그널을 받을 때까지 프로세스를 대기시킨다

시그널을 잡았을 때만 –1을 반환

 

 

ㅇ int sigsuspend(const sigset_t* mask): 시그널 기다리기

- mask : 기다리려는 시그널을 지정한 시그널 집합의 주소

인자로 지정한 시그널 집합에 설정된 시그널들로 프로세스의 시그널 마스크를 교체

블로킹되지 않은 시그널이 도착할 때까지 프로세스의 수행을 멈추고 기다린다

(시그널이 도착하면 프로세스의 시그널 마스크는 이전 설정으로 되돌아간다)

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sig_handler(int signo) {
    psignal(signo, "Received Signal:");
}

int main() {
    sigset_t set;

    signal(SIGALRM, sig_handler); // 알람 시그널을 받으면 시그널 핸들러를 호출하도록 설정

    // 시그널 마스크에서 모든 시그널을 블로킹한 후, SIGALRM 만 해제
    sigfillset(&set);
    sigdelset(&set, SIGALRM);

    alarm(3); // 3초 후에 알람 시그널이 발생하도록 설정

    printf("Wait...\n");
    
    sigsuspend(&set); // sigsuspend() 함수를 사용해 시그널이 도착하기를 기다림

    return 0;
}
// 실행 결과: 알람 시그널이 발생하기 전에 SIGINT 시그널을 보내도 모두 블로킹되고 
// 3초 후에 알람 시그널만 처리됨을 알 수 있음

 

 

 

 

 

 

 

참고 및 출처: 시스템 프로그래밍 리눅스&유닉스(이종원)