C 언어에서 시그널 핸들러의 실행 순서 완벽 이해

시그널은 운영 체제에서 발생하는 비동기 이벤트를 프로그램에 알리기 위한 메커니즘입니다. C 언어에서는 이러한 시그널을 처리하기 위해 시그널 핸들러라는 개념을 제공합니다. 시그널 핸들러는 특정 시그널이 발생했을 때 실행되는 사용자 정의 함수입니다. 이를 통해 프로그램은 예상치 못한 상황(예: Ctrl+C 입력, 파일 크기 초과 등)에 유연하게 대응할 수 있습니다. 본 기사에서는 시그널 핸들러의 기본 개념부터 실행 순서, 다중 처리, 그리고 실제 응용까지 전반적인 내용을 심도 있게 다룹니다.

목차

시그널과 시그널 핸들러의 기본 개념


시그널은 운영 체제에서 특정 이벤트가 발생했음을 프로그램에 알리는 메커니즘으로, 비동기 이벤트 처리에 사용됩니다. 예를 들어, SIGINT는 사용자가 Ctrl+C를 눌렀을 때 발생하며, SIGSEGV는 메모리 접근 오류가 발생했을 때 전달됩니다.

시그널의 역할


시그널은 아래와 같은 다양한 상황에서 발생합니다.

  • 키보드 인터럽트(Ctrl+C): SIGINT
  • 잘못된 메모리 접근: SIGSEGV
  • 프로세스 종료 요청: SIGTERM

시그널 핸들러란?


시그널 핸들러는 특정 시그널이 발생했을 때 호출되는 사용자 정의 함수입니다. 핸들러는 프로그램이 시그널 발생 시 기본 동작을 재정의하거나, 필요한 작업을 수행할 수 있도록 설계됩니다. 예를 들어, SIGINT의 기본 동작은 프로그램 종료이지만, 이를 무시하거나 특정 작업을 수행하도록 핸들러를 작성할 수 있습니다.

기본 동작과 사용자 정의


대부분의 시그널에는 기본 동작이 정의되어 있습니다. 사용자는 시그널 핸들러를 통해 이 동작을 변경할 수 있습니다.

  • 기본 동작 예시:
  • SIGINT: 프로그램 종료
  • SIGSEGV: 프로그램 강제 종료
  • 사용자 정의 예시:
  • SIGINT를 무시하거나, 파일 저장 후 종료하도록 설정

시그널 처리의 유의점

  • 비동기적으로 실행되므로 전역 변수나 I/O 작업과의 충돌에 주의해야 합니다.
  • 일부 시그널은 핸들러 등록이 불가능하거나 제한됩니다(예: SIGKILL).

시그널과 시그널 핸들러를 이해하는 것은 C 언어의 비동기 이벤트 처리의 핵심이며, 이를 통해 더 안전하고 효율적인 프로그램을 작성할 수 있습니다.

시그널 핸들러 등록 방법


C 언어에서는 시그널 핸들러를 등록하기 위해 주로 두 가지 함수를 사용합니다: signal() 함수와 sigaction() 함수. 이 두 함수는 시그널 발생 시 호출될 핸들러를 설정하는 데 사용됩니다.

1. `signal()` 함수


signal() 함수는 간단한 시그널 핸들러 등록 방법으로, 다음과 같은 형식을 가집니다.

#include <signal.h>

void signal_handler(int signum) {
    // 시그널 처리 코드
}

int main() {
    signal(SIGINT, signal_handler);  // SIGINT 시그널 핸들러 등록
    while (1) {
        // 메인 프로그램 실행
    }
    return 0;
}
  • signal(signum, handler)
  • signum: 처리할 시그널 번호 (예: SIGINT, SIGTERM)
  • handler: 호출할 핸들러 함수

장점: 사용법이 간단함
단점: 일부 플랫폼에서는 비표준 동작을 보일 수 있음

2. `sigaction()` 함수


sigaction() 함수는 더 많은 제어를 제공하며, 더 안전한 방법으로 시그널 핸들러를 등록할 수 있습니다.

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

void signal_handler(int signum) {
    printf("Received signal %d\n", signum);
}

int main() {
    struct sigaction sa;
    sa.sa_handler = signal_handler;
    sa.sa_flags = 0;  // 추가 플래그 설정 가능
    sigemptyset(&sa.sa_mask);  // 차단할 시그널 초기화

    sigaction(SIGINT, &sa, NULL);  // SIGINT 핸들러 등록

    while (1) {
        // 메인 프로그램 실행
    }
    return 0;
}
  • struct sigaction: 시그널 처리에 필요한 정보를 포함하는 구조체
  • sa_handler: 호출할 핸들러 함수
  • sa_flags: 핸들러 동작을 변경하는 플래그
  • sa_mask: 추가적으로 차단할 시그널 집합

장점: 더 세밀한 제어 가능, 이식성이 높음
단점: 코드가 더 복잡해짐

3. 두 방법의 비교

함수장점단점
signal()간단하고 직관적비표준적 동작 가능성 있음
sigaction()높은 제어력, 표준 준수상대적으로 복잡

핸들러 등록 시 유의점

  • 시그널 처리 중 다른 시그널이 발생하면, 처리 순서와 중첩에 주의해야 합니다.
  • 재진입 가능 함수(reentrant functions)만 사용해야 충돌을 방지할 수 있습니다.

적절한 함수와 방법을 선택해 시그널 핸들러를 등록하면, 비동기 이벤트를 안전하고 효율적으로 처리할 수 있습니다.

실행 순서의 기본 원칙


C 언어에서 시그널 핸들러의 실행 순서는 시그널의 발생 시점과 프로세스 상태에 따라 결정됩니다. 시그널 처리 시 올바른 순서를 이해하고 관리하는 것이 중요하며, 이는 프로그램의 안정성과 신뢰성에 직접적인 영향을 미칩니다.

1. 시그널 처리의 기본 동작


시그널 핸들러는 등록된 순서에 따라 특정 시그널이 발생했을 때 호출됩니다. 기본적인 실행 순서는 다음과 같습니다.

  • 시그널 발생 시 운영 체제는 해당 시그널 번호를 확인합니다.
  • 해당 시그널에 등록된 핸들러가 있으면 실행됩니다.
  • 핸들러 실행이 완료되면 프로그램은 원래 작업을 재개합니다.

2. 주요 실행 순서 규칙

  • 단일 시그널 처리
    동일한 시그널이 연속적으로 발생할 경우, 이전 핸들러가 실행 중이면 다음 시그널은 대기 상태가 됩니다.
  • 다중 시그널 처리
    다른 유형의 시그널이 발생하면, 각 시그널의 우선순위와 발생 시점에 따라 처리됩니다.

3. 블록된 시그널


시그널 마스크에 의해 블록된 시그널은 핸들러가 호출되지 않고 대기 상태로 유지됩니다. 블록이 해제되면 대기 중이던 시그널이 처리됩니다.

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

void handler(int signum) {
    printf("Handling signal %d\n", signum);
}

int main() {
    signal(SIGINT, handler);
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);

    sigprocmask(SIG_BLOCK, &mask, NULL);  // SIGINT 블록
    printf("SIGINT is blocked\n");
    sleep(5);  // 블록 상태 유지
    sigprocmask(SIG_UNBLOCK, &mask, NULL);  // SIGINT 블록 해제
    printf("SIGINT is unblocked\n");

    while (1);
    return 0;
}

4. 재진입 문제


시그널 핸들러가 실행 중에 동일한 시그널이 발생하면, 재진입 문제가 발생할 수 있습니다. 이를 방지하려면 재진입 가능 함수(reentrant function)를 사용하거나 시그널을 일시적으로 차단해야 합니다.

5. 실행 순서 보장을 위한 전략

  • 시그널 마스크 설정: sigprocmask를 사용해 특정 시그널의 차단 및 허용 상태를 관리합니다.
  • 핸들러 내 작업 최소화: 핸들러에서 수행하는 작업은 간단하고 신속하게 유지해야 합니다.
  • 시스템 호출 사용: sigaction과 같은 표준 함수는 실행 순서를 더욱 안정적으로 제어합니다.

시그널 처리의 실행 순서를 이해하고 적절히 관리하면, 비동기 이벤트의 혼란을 방지하고 프로그램의 안정성을 높일 수 있습니다.

중첩된 시그널 처리


중첩된 시그널 처리란 한 시그널의 핸들러가 실행되는 동안 동일한 시그널이나 다른 시그널이 발생하는 상황을 의미합니다. 이런 경우 적절한 처리가 이루어지지 않으면 프로그램이 예기치 않게 동작하거나 충돌할 수 있습니다.

1. 중첩 문제의 원인

  • 핸들러의 재진입성 부족: 시그널 핸들러가 비재진입 함수(예: printf나 동적 메모리 할당 함수)를 호출하는 경우, 중첩된 시그널이 발생하면 데이터 손상이나 프로그램 충돌이 발생할 수 있습니다.
  • 시그널 처리 중 다른 시그널의 도착: 시그널 처리 중에 발생하는 새로운 시그널은 처리 순서를 복잡하게 만듭니다.

2. 중첩 방지 방법


중첩된 시그널 처리 문제를 방지하기 위해 다음과 같은 방법을 사용할 수 있습니다.

2.1 시그널 블록


시그널 핸들러 실행 중에 동일한 시그널을 차단하여 중첩을 방지합니다.

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

void handler(int signum) {
    printf("Handling signal %d\n", signum);
    sleep(2);  // 긴 작업 시 시그널 중첩 가능성 증가
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);  // 시그널 블록 초기화
    sigaddset(&sa.sa_mask, SIGINT);  // SIGINT 차단
    sa.sa_flags = 0;

    sigaction(SIGINT, &sa, NULL);

    while (1) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}

2.2 재진입 가능 함수 사용


시그널 핸들러 내에서는 반드시 재진입 가능 함수만 사용해야 합니다. 재진입 가능 함수는 호출 중단 시 다른 호출이 이루어져도 안전하게 동작합니다.

  • 예: write(), _exit()
  • 비추천: printf(), malloc()

2.3 플래그 기반 처리


중첩된 시그널을 직접 처리하지 않고 플래그를 설정한 뒤, 메인 루프에서 처리하도록 설계할 수 있습니다.

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

volatile sig_atomic_t flag = 0;

void handler(int signum) {
    flag = 1;
}

int main() {
    signal(SIGINT, handler);

    while (1) {
        if (flag) {
            printf("Signal handled in main loop\n");
            flag = 0;
        }
    }
    return 0;
}

3. 시그널 중첩 처리의 주요 유의점

  • 핸들러 실행 중 중첩된 시그널은 처리 순서가 뒤섞일 수 있으므로 실행 순서를 명확히 정의해야 합니다.
  • 긴 작업이나 복잡한 연산은 핸들러 내부에서 수행하지 않는 것이 좋습니다.

4. 중첩된 시그널 처리 사례


시그널 처리 중 다른 시그널의 발생을 예측하고 적절히 대응하면, 복잡한 비동기 이벤트 환경에서도 안정적인 프로그램 동작을 보장할 수 있습니다.

다중 시그널 처리 시 우선순위 관리


C 언어에서 다중 시그널 처리 상황은 여러 시그널이 동시에 발생하거나 빠르게 연속적으로 발생할 때 흔히 발생합니다. 이 경우 각 시그널의 우선순위를 설정하고 관리하는 것이 중요합니다. 우선순위 관리가 제대로 이루어지지 않으면 시그널 처리 순서가 엉킬 수 있으며, 이는 예기치 않은 동작을 초래할 수 있습니다.

1. 우선순위 관리의 기본 원칙

  • 시그널 번호에 따른 기본 우선순위
    일반적으로 시그널 번호가 낮은 시그널이 높은 우선순위를 가집니다. 예를 들어, SIGKILL과 같은 시스템 필수 시그널은 항상 즉시 처리됩니다.
  • 등록된 시그널 핸들러
    등록된 핸들러는 먼저 발생한 시그널부터 처리됩니다. 대기 중인 시그널은 처리 후 실행됩니다.

2. 다중 시그널 처리 전략

2.1 시그널 블록을 통한 처리 순서 지정


sigprocmask 함수를 사용해 특정 시그널을 블록하거나 해제하여 우선순위를 제어할 수 있습니다.

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

void handler1(int signum) {
    printf("Handling signal %d (High priority)\n", signum);
}

void handler2(int signum) {
    printf("Handling signal %d (Low priority)\n", signum);
}

int main() {
    struct sigaction sa1, sa2;
    sa1.sa_handler = handler1;
    sa2.sa_handler = handler2;

    sigemptyset(&sa1.sa_mask);
    sigemptyset(&sa2.sa_mask);

    sa1.sa_flags = 0;
    sa2.sa_flags = 0;

    sigaction(SIGUSR1, &sa1, NULL);  // High-priority signal
    sigaction(SIGUSR2, &sa2, NULL);  // Low-priority signal

    while (1) {
        printf("Waiting for signals...\n");
        sleep(1);
    }
    return 0;
}

위 코드에서 SIGUSR1이 먼저 처리되고, 그다음 SIGUSR2가 처리됩니다.

2.2 시그널 큐를 활용한 순차 처리


실시간 시그널(SIGRTMIN~SIGRTMAX)은 큐잉 메커니즘을 지원하여 발생 순서대로 처리됩니다.

sigqueue(pid, SIGRTMIN, value);

2.3 우선순위 기반 설계


시그널의 처리 우선순위를 명확히 정의하고, 특정 시그널을 블록하여 중요한 시그널이 먼저 처리되도록 설계합니다.

3. 우선순위 처리 중의 유의점

  • 핸들러 내 블록된 시그널 처리
    핸들러 내에서 다른 시그널을 블록하거나 대기 상태로 두어 충돌을 방지합니다.
  • 핸들러 작업 최소화
    핸들러 내 작업을 단순화하여 대기 중인 다른 시그널이 지연되지 않도록 합니다.
  • 실시간 시그널 사용
    실시간 시그널은 자동으로 순차적 처리 및 우선순위를 지원합니다.

4. 다중 시그널 처리 응용


다중 시그널 처리와 우선순위 관리는 특히 복잡한 시스템에서 비동기 이벤트를 안정적으로 처리하기 위해 필수적입니다. 우선순위를 명확히 정의하고 관리하면 비동기 작업에서도 안정적인 프로그램 동작을 유지할 수 있습니다.

실제 사례와 구현 예시


시그널 핸들러는 실시간 시스템, 서버 애플리케이션, 그리고 디버깅 도구와 같은 다양한 분야에서 활용됩니다. 이번 섹션에서는 시그널 핸들러를 활용한 대표적인 사례와 이를 구현하는 방법을 다룹니다.

1. 서버 애플리케이션의 종료 처리


서버 애플리케이션에서 종료 시 리소스를 정리하고 종료 상태를 저장하는 것이 중요합니다. SIGTERM 시그널을 활용하면 이를 구현할 수 있습니다.

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

void cleanup_and_exit(int signum) {
    printf("Received signal %d. Cleaning up...\n", signum);
    // 리소스 정리 코드
    printf("Cleanup completed. Exiting now.\n");
    exit(0);
}

int main() {
    struct sigaction sa;
    sa.sa_handler = cleanup_and_exit;
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);

    sigaction(SIGTERM, &sa, NULL);  // SIGTERM 핸들러 등록

    printf("Server running. PID: %d\n", getpid());

    while (1) {
        // 서버 동작
        sleep(1);
    }
    return 0;
}

설명:

  • SIGTERM 시그널을 받으면 종료 준비 작업을 수행하고 프로그램을 안전하게 종료합니다.
  • 서버가 장시간 실행될 경우 적합한 리소스 관리 도구로 사용됩니다.

2. 디버깅을 위한 시그널 로그 작성


프로그램 실행 중 발생한 시그널을 기록하여 디버깅에 활용할 수 있습니다.

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

void log_signal(int signum) {
    FILE *logfile = fopen("signal_log.txt", "a");
    if (logfile) {
        fprintf(logfile, "Signal %d received\n", signum);
        fclose(logfile);
    }
}

int main() {
    struct sigaction sa;
    sa.sa_handler = log_signal;
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);

    sigaction(SIGUSR1, &sa, NULL);  // 사용자 정의 시그널 로그
    sigaction(SIGUSR2, &sa, NULL);

    printf("Logging signals to signal_log.txt. PID: %d\n", getpid());

    while (1) {
        sleep(1);
    }
    return 0;
}

설명:

  • SIGUSR1SIGUSR2 시그널을 받을 때마다 로그 파일에 기록됩니다.
  • 발생 빈도 및 시점 기록에 적합한 방법입니다.

3. 시그널을 통한 간단한 IPC (프로세스 간 통신)


두 개의 프로세스 간에 시그널을 사용하여 데이터를 전달하거나 상태를 알릴 수 있습니다.

예제: 부모 프로세스와 자식 프로세스 간의 통신

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

void child_handler(int signum) {
    printf("Child process received signal %d\n", signum);
}

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // 자식 프로세스
        signal(SIGUSR1, child_handler);
        while (1) {
            pause();  // 시그널 대기
        }
    } else {
        // 부모 프로세스
        printf("Parent sending SIGUSR1 to child (PID: %d)\n", pid);
        sleep(1);
        kill(pid, SIGUSR1);  // 자식에게 SIGUSR1 전송
        sleep(1);
    }
    return 0;
}

설명:

  • 부모 프로세스는 kill() 함수를 사용하여 자식 프로세스에 시그널을 보냅니다.
  • 자식 프로세스는 signal() 함수로 시그널을 처리하고 응답합니다.

4. 주요 구현에서의 유의점

  • 리소스 관리: 종료 시 리소스 정리를 반드시 수행해야 합니다.
  • 재진입 가능 함수: 핸들러 내에서는 write()와 같은 안전한 함수만 사용합니다.
  • 안정성 테스트: 시그널 처리 동작은 복잡한 환경에서의 안정성을 확인해야 합니다.

이러한 예제들은 실무에서 시그널 핸들러를 활용할 수 있는 실질적인 방법을 보여줍니다. 각 상황에 맞는 적절한 처리 전략을 설계하여 비동기 이벤트를 효과적으로 관리할 수 있습니다.

요약


본 기사에서는 C 언어의 시그널 핸들러와 관련된 핵심 개념과 실무적 활용 방안을 다루었습니다. 시그널의 기본 정의와 핸들러 등록 방법, 실행 순서 및 중첩 시그널 처리, 다중 시그널 처리에서의 우선순위 관리까지 다양한 측면을 설명했습니다. 또한, 실제 사례와 구현 예시를 통해 시그널 핸들러의 강력함과 실용성을 보여주었습니다.

시그널 핸들러는 비동기 이벤트를 효율적으로 처리하고, 프로그램의 안정성과 유연성을 향상시키는 데 필수적인 도구입니다. 이를 통해 예기치 않은 상황에서도 안전하고 신뢰성 있는 프로그램을 작성할 수 있습니다.

목차