Notice
Recent Posts
Recent Comments
Link
«   2024/04   »
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
Archives
Today
Total
04-29 19:41
관리 메뉴

nomad-programmer

[Programming/C] 시그널 (Signal) 본문

Programming/C

[Programming/C] 시그널 (Signal)

scii 2020. 6. 20. 01:10

운영체제는 시그널로 프로그램을 제어한다.

시그널은 단지 정수형의 짧은 메시지일 뿐이다. 시그널이 도착하면 프로세스는 하던 일을 멈추고 시그널을 처리해야 한다. 프로세스는 시그널과 시그널 처리기라는 함수를 대응시키는 시그널 매핑 테이블을 살펴본다. 인터럽트 시그널에 대한 기본 시그널 처리기는 단지 exit() 함수를 호출한다.

시그널을 잡아 직접 정의한 코드 실행

때론 프로그램을 인터럽트 걸 때 직접 정의한 코드를 실행하고 싶을 것이다. 예를 들어 프로세스가 열린 파일이나 네트워크 연결을 갖고 있으면 프로그램을 종료하기 전에 리소스를 닫고 정리하고 싶을 것이다. 

이럴때 "sigaction" 을 사용하면 코드를 실행하고 명령시킬 수 있다.

sigaction은 함수 랩퍼이다.

sigaction은 함수에 대한 포인터를 갖고 있는 구조체이다. sigaction은 시그널이 프로세스에 보내졌을 때 어떤 함수를 호출해야 하는지 운영체제에 알려주는 데 사용한다. 따라서 프로세스에 인터럽트 시그널을 보냈을 때 운영체제가 test55() 라는 함수를 호출하기를 원한다면, test55() 함수를 sigaction 구조체에 넣어야 한다.

sigaction을 만드는 방법은 아래와 같다.

// 구조체를 만듦
struct sigaction action;
// 호출할 함수 이름
action.sa_handler = test55;
// sigaction이 처리할 시그널을 걸러내기 위해 마스크 사용 (보통 이 코드처럼 빈 마스크를 사용)
sigemptyset(&action.sa_mask);
// 플래그를 추가로 설정할 수 있다.
action.sa_flags = 0;

sigaction이 래핑하는 함수를 처리기라고 부른다. 프로세스가 받은 메시지를 이 함수가 처리하기 때문이다. 처리기를 만들려면 아래와 같은 방법으로 만들어야 한다.

// sig 인자는 처리기가 받은 시그널 번호
void test55(int sig) {
    puts("test55 function!!!\n");
    exit(1);
}

처리기가 시그널 번호를 받기 때문에 처리기 하나로 여러 시그널을 처리하게 만들 수 있다. 아니면 시그널마다 처리기를 따로 만들 수도 있다.

처리기는 간단하고 빨리 처리해야 하므로, 받은 시그널을 처리하기 위한 최소한의 코드만 갖고 있어야 한다.

처리기 함수 안에서 표준 출력이나 에러에 데이터를 쓸 때 조심

복잡한 프로그램에서 출력할 때는 조심해야 한다. 프로그램에 어떤 나쁜 일이 생겼기 때문에 시그널이 도착했을 것이다. 이때는 표준 출력도 사용할 수 있다고 장담할 수 없기 때문이다.

sigaction() 함수로 sigaction 구조체를 등록

sigaction 구조체를 생성한 후에는 운영체제에 이 구조체를 알려줘야 한다. 알려주기 위해 다음과 같이 sigaction() 함수를 사용한다.

sigaction(signal_no, &new_action, &old_action);

sigaction() 함수는 세개의 인자를 받는다.

1. 시그널 번호 : 처리하려는 시그널의 정수형 값. 보통 SIGINT나 SIGQUIT처럼 표준 시그널 기호 중 하나를 사용.

2. 새로운 처리기 : 새로 등록하려는 sigaction 구조체의 주소.

3. 이전 처리기 : sigaction 구조체에 대한 포인터를 하나 더 보내면 이 구조체를 현재 처리기의 내용으로 채워준다. 현재 처리기에 대해 신경 쓸 필요 없으면 NULL로 설정하면 된다.

sigaction() 함수가 실패하면 -1을 반환하고 errno 변수를 설정한다. 반드시 언제나 에러를 검사해야 한다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
// signal.h 헤더 파일을 인클루드해야 함
#include <signal.h>

// 처리기는 반환값이 없다.
void test55(int sig){
    puts("\ntest55 function!!\n");
    exit(1);
}

int catch_signal(int sig, void (*handler)(int)){
    struct sigaction action;
    action.sa_handler = handler;
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;
    return sigaction(sig, &action, NULL);
}

int main(int argc, char *argv[]) {
    if(catch_signal(SIGINT, test55) == -1){
        fprintf(stderr, "처리기를 매핑할 수 없음\n");
        exit(2);
    }
    char name[30];
    printf("이름 입력: ");
    fgets(name, 30, stdin);
    printf("hi~ %s\n", name);

    return 0;
}

// 결과
/*

이름 입력: ^C
test55 function!!

*/

프로그램은 사용자에게 이름을 물어보고 입력할 때까지 기다린다. 그런데 이름을 입력하지 않고 [Ctrl-C] 키를 누르면 운영체제는 자동으로 프로세스에 인터럽트 시그널(SIGINT)을 보내게 된다. 이 인터럽트 시그널은 catch_signal() 함수로 등록한 처리기에 의해 처리된다. 사용자 입력을 받기 전에 test55() 함수에 대한 포인터를 처리기로 등록했으므로, 인터럽트 시그널이 test55() 함수에 전달되고, test55()는 메시지를 출력하고 exit() 를 호출한다.

시그널

설명

SIGINT

프로세스 인터럽트

SIGQUIT

프로세스를 멈추고 프로세스의 메모리를 코어 덤프(Core Dump) 파일에 기록

SIGFPE

실수형에 오류가 발생

SIGTRAP

디버거가 알고 싶어하는 조건 발생

SIGSEGV

프로세스가 메모리를 부당하게 접근

SIGWINCH

터미널 윈도우의 크기가 바뀜

SIGTERM

프로세스를 종료하라고 커널에 요청

SIGPIPE

프로세스가 아무도 읽지 않고 있는 파이프에 데이터를 썼음

raise() 함수로 시그널 보내기

자신에게 시그널을 보내야 할 때가 있다. 이럴 때 raise() 함수를 사용하면 된다.

raise(SIGTERM);

보통 raise() 함수는 직접 만든 시그널 처리기 안에서 사용된다. 이 함수는 사소한 시그널을 받았지만, 더 중요한 시그널로 확대하기 위해 사용한다. 

이 방법을 시그널 확대(Signal Escalation) 라고 부른다.

시간이 지났을 때 코드에 알려주는 방법

운영체제는 프로세스가 알아야 할 일이 발생했을 때 프로세스에 시그널을 보낸다. 사용자가 프로세스를 인터럽트하거나 종료하기 위한 것일 수도 있고, 아니면 제한된 메모리에 접근하는 것처럼 하지 말아햐 할 일을 프로세스가 했을 수도 있다.

그러나 일이 잘못되었을 때만 시그널을 사용하는 건 아니다. 때로는 프로세스가 자신만의 시그널을 만들 필요가 있다. 그 중 한 예가 알람 시그널인 "SIGALRM" 이다. 알람 시그널은 보통 프로세스의 시간 타이머(Interval Timer)에 의해 발생한다. 시간 타이머는 자명종 시계와 비슷하다. 알람 시간을 설정해놓고 경고가 발생하기 전까지 다른 일을 할 수 있다.

// 120초 후에 알람이 울리게 타이머 설정
alarm(120);
do_important_busy_work();
do_more_busy_work();

타이머가 SIGALRM 시그널을 보낸다. 시그널을 받으면 프로세스는 하던 일을 모두 멈추고 시그널을 처리한다. 

알람 시그널은 여러 일을 동시에 할 수 있게 한다. 흔히 멀티태스킹(Multitasking) 이라고 한다. 어떤 일을 몇 초만 해야 하거나 어떤일을 하는 작업 시간을 제한하고 싶을 때, 프로그램이 자신을 인터럽트 걸게 하는 데는 알람 시그널이 최고의 방법이다.

시그널 재설정과 무시 

기본 처리기로 되돌리려면 signal.h 헤더 파일에 SIG_DFL 이라는 기호를 사용하면 된다. 그러면 기본적인 방법으로 시그널을 처리한다.

catch_signal(SIGTERM, SIG_DFL);

그리고 SIG_IGN 이라는 기호도 있는데, 이 기호를 사용하면 프로세스가 시그널을 완전히 무시하게 한다.

catch_signal(SIGINT, SIG_IGN);

그러나 시그널을 무시하도록 결정하기 전에 매우 조심해야 한다. 시그널은 프로세스를 제어하고 종료하는 중요한 방법이므로 시그널을 무시하면 프로그램을 제어하기 힘들다.

1초보다 작은 시간으로 알람을 설정하려면 "setitimer()" 함수를 사용해야 한다.
타이머는 운영체제 커널이 관리해야 한다. 그러므로 프로세스가 여러 타이머를 갖게 되면 커널이 점점 느려질 것이다. 이런 문제가 생기지 않게 운영체제는 각 프로세가 가질 수 있는 타이머를 1개로 제한한다.

알람 시그널 예제

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

int score = 0;

void end_game(int sig) {
    printf("\n최종 점수: %i\n", score);
    exit(0);
}

int catch_signal(int sig, void (*handler)(int)) {
    struct sigaction action;
    action.sa_handler = handler;
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;
    return sigaction(sig, &action, NULL);
}

void times_up(int sig) {
    puts("\n시간 초과!");
    // SIGINT로 확대하면 프로그램이 end_game() 함수를 호출해 최종 점수를 출력
    raise(SIGINT);
}

void error(char *msg) {
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(1);
}

int main(int argc, char *argv[]) {
    // 시그널 처리 함수 설정
    catch_signal(SIGALRM, times_up);
    catch_signal(SIGINT, end_game);
    // 코드가 실행될 때마다 난수가 달라지도록
    srandom(time(0));
    while (1) {
        int a = random() % 11;
        int b = random() % 11;
        char txt[4];
        // 5초 후에 알람이 발생하게 된다.
        alarm(5);
        printf("\n%i 곱하기 %i는? ", a, b);
        fgets(txt, 4, stdin);
        int answer = atoi(txt);
        if (answer == a * b) {
            score++;
        } else {
            printf("\n틀렸습니다! 점수: %i\n", score);
        }
    }

    return 0;
}

// 결과
/*

사용자가 [Ctrl-C]를 누르거나 5초 동안 답을 입력하지 못하면 프로그램 종료.
프로그램이 종료될 때 최종 점수를 출력하고 상태 코드 0으로 종료.

*/

시그널은 정말 유용하다. 시그널을 사용하면 프로그램을 깔끔하게 종료시킬 수 있고 시간 타이머를 사용하면 시간이 초과되는 문제를 처리할 수 있다.

정리

운영체제는 시그널로 프로세스와 통신한다.

시그널은 일반적으로 프로세스를 종료하기 위해 사용된다.

프로세스가 시그널을 받으면, 프로세스는 처리기를 실행한다.

대부분 에러 시그널의 기본 처리기는 프로그램을 종료한다.

sigaction() 함수로 처리기를 변경할 수 있다.

raise() 함수로 프로세스 자신에게 시그널을 보낼 수 있다.

시간 타이머는 SIGALRM 시그널을 보낸다.

alarm() 함수는 시간 타이머를 설정한다.

각 프로세스는 타이머를 하나만 갖고 있다.

sleep()과 alarm()을 동시에 사용하면 안된다.

kill 명령은 프로세스에 시그널을 보낸다.

kill -KILL 명령은 프로세스를 강제로 종료시킨다.

'Programming > C' 카테고리의 다른 글

[Programming/C] 스레드 (Thread)  (0) 2020.06.20
[Programming/C] 소켓과 서버  (0) 2020.06.20
[Programming/C] pipe() 함수  (0) 2020.06.19
[Programming/C] 입출력의 리다이렉션  (0) 2020.06.19
[Programming/C] fork() 함수  (0) 2020.06.19
Comments