본문 바로가기
CS 지식/시스템 프로그래밍

[C언어] Signal 프로그래밍 개념과 예제

by 코딩하는 동현😎 2024. 10. 17.

시그널 프로그래밍의 이해

시그널은 운영체제가 프로세스 간 통신을 위해 제공하는 비동기적 메시지 전달 메커니즘이다. 시그널은 프로세스에 특정 이벤트가 발생했음을 알리거나 작업을 중단하도록 요청하기 위해 사용된다. 이 글에서는 시그널의 동작 원리, System V와 POSIX 표준에 기반한 시그널 프로그래밍, 그리고 운영체제와 시그널의 관계를 자세히 설명한다.


시그널의 기본 개념

  1. 시그널의 정의
    시그널은 정수로 표현되는 작은 메시지로, 특정 이벤트를 알리기 위해 사용된다. 프로세스는 시그널을 받으면 이를 처리하거나 무시한다.
  2. 시그널의 주요 상태
    • Pending: 시그널이 도착했지만 아직 처리되지 않은 상태.
    • Blocked: 프로세스가 시그널을 처리하지 않고 차단한 상태.
    • Not Queued: 동일한 시그널이 여러 번 발생해도 한 번만 처리됨.
  3. 시그널의 기본 동작 각 시그널은 고유의 기본 동작을 가진다:
    • Terminate: 프로세스 종료.
    • Core Dump: 프로세스 종료 및 코어 덤프 생성.
    • Stop: 프로세스 일시 중지.
    • Ignore: 시그널 무시.
    • Continue: 중지된 프로세스 재개.
  4. 시그널 발생 방법 시그널은 다음과 같은 방법으로 발생할 수 있다:
    • 키보드 입력: Ctrl+CSIGINT 발생, Ctrl+ZSIGTSTP 발생.
    • 시스템 호출: kill() 함수를 통해 특정 시그널을 보냄.
    • 시스템 이벤트: 예를 들어, SIGCHLD는 자식 프로세스 종료 시 발생.
    • 알람 타이머: alarm() 함수로 특정 시그널을 설정.

운영체제와 시그널의 관계

운영체제는 커널 내부에서 시그널을 처리하며, 다음과 같은 단계로 동작한다:

  1. 시그널 생성
    키보드 입력, 프로세스 상태 변화, 시스템 호출 등의 이벤트로 인해 시그널이 생성된다.
  2. 시그널 대기
    생성된 시그널은 프로세스의 시그널 비트 벡터에 추가된다. 비트 벡터는 각 시그널의 상태를 나타내며, 동일한 시그널이 여러 번 발생해도 한 번만 처리된다.
  3. 시그널 전달
    운영체제는 프로세스가 실행 가능한 상태가 될 때 시그널 핸들러를 호출하거나, 기본 동작을 수행하도록 한다.

시그널 핸들러

시그널 핸들러는 프로세스가 특정 시그널을 처리하기 위해 호출하는 사용자 정의 함수이다. 시그널 핸들러는 시그널이 발생했을 때 운영체제가 자동으로 실행하며, 시그널 번호를 매개변수로 전달받아 동작을 정의할 수 있다. 핸들러는 다음과 같은 방식으로 동작한다:

  1. 등록 및 호출:
    • System V에서는 signal() 함수로 핸들러를 등록하며, POSIX에서는 sigaction()을 통해 더 안전하고 세밀한 설정이 가능하다.
    • 핸들러는 시그널이 발생하면 즉시 실행된다. 예를 들어, SIGINT 시그널이 발생하면 핸들러는 이를 처리하고 기본 동작(프로세스 종료)을 대체할 수 있다.
  2. 비동기 동작:
    • 핸들러는 실행 중인 프로그램의 흐름과 독립적으로 호출된다. 이는 프로세스가 다른 작업을 수행하는 동안에도 시그널을 처리할 수 있음을 의미한다.
  3. 제한사항:
    • 핸들러 내에서 호출되는 함수는 반드시 Async-Signal-Safe이어야 한다. 예를 들어, printf()는 안전하지 않을 수 있으므로 대신 write()를 사용하는 것이 권장된다.
    • SIGKILLSIGSTOP 시그널에는 핸들러를 등록할 수 없다. 이는 시스템 안정성을 보장하기 위한 제한이다.
  4. 재진입 문제:
    • 핸들러가 실행되는 동안 동일한 시그널이 다시 발생할 수 있다. 이를 방지하려면 POSIX 표준의 sa_mask를 사용해 특정 시그널을 블록해야 한다.
  5. 실제 사용 예:
    • 자원 정리: 파일 닫기, 메모리 해제 등.
    • 사용자 피드백 제공: 종료 알림 메시지 출력.

System V 시그널 프로그래밍

System V는 signal() 함수를 사용하여 시그널 핸들러를 등록한다. 아래는 SIGINT 시그널에 대한 핸들러 등록 예제이다:

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

// SIGINT 핸들러
void handle_sigint(int sig) {
    printf("Caught signal %d (SIGINT). Exiting gracefully...\n", sig);
    exit(0);
}

int main() {
    // SIGINT 핸들러 등록
    signal(SIGINT, handle_sigint);

    printf("Press Ctrl+C to exit.\n");
    while (1) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}

주요 개념

  1. signal()
    • signal(int signum, void (*handler)(int)): 특정 시그널에 대한 핸들러를 설정한다.
    • 위 예제에서는 SIGINT 시그널에 대해 handle_sigint 핸들러를 등록한다.
  2. SIGINT
    • Ctrl+C 입력으로 발생하며, 핸들러에서 이를 처리한다.
  3. 한계
    System V 방식은 비재진입성을 보장하지 않으며, 다중 스레드 환경에서 안전하지 않을 수 있다.

POSIX 시그널 프로그래밍

POSIX 표준은 sigaction() 함수를 사용하여 시그널을 처리하며, System V보다 더 안전하고 유연한 방식으로 동작한다.

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

// SIGINT 핸들러
void handle_sigint(int sig) {
    printf("Caught SIGINT: %d. Cleaning up and exiting...\n", sig);
    exit(0);
}

int main() {
    struct sigaction sa;

    // 핸들러 함수 설정
    sa.sa_handler = handle_sigint;
    sa.sa_flags = 0; // 추가 옵션 비활성화
    sigemptyset(&sa.sa_mask); // 블록할 시그널 없음

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

    printf("Running... Press Ctrl+C to exit.\n");
    while (1) {
        printf("Waiting for SIGINT...\n");
        sleep(2);
    }
    return 0;
}

POSIX 코드의 주요 개념

  1. sigaction 구조체
    • sa_handler: 시그널 핸들러 함수 포인터.
    • sa_mask: 시그널 처리 중 블록할 추가 시그널 집합.
    • sa_flags: 핸들링 동작을 제어하는 플래그.
  2. 유연성과 안전성
    • sigaction()은 핸들러 설정 시 시그널 블록 및 재진입 방지 설정이 가능하다.
    • System V 방식보다 더 안전하며, 다중 스레드 환경에서 권장된다.
  3. 추가 설정
    • sa_flagsSA_RESTART를 설정하면, 시그널 발생 후 차단된 시스템 호출이 자동 재시도된다.

시그널 프로그래밍 관련 함수

kill()

  • kill(pid, signo)는 특정 프로세스나 프로세스 그룹에 시그널을 보낸다.
  • PID에 따라 다음과 같이 동작한다:
    • pid > 0: 해당 프로세스에 시그널을 보낸다.
    • pid == 0: 같은 프로세스 그룹에 시그널을 보낸다.
    • pid < 0: 절대값이 프로세스 그룹 ID인 모든 프로세스에 시그널을 보낸다.

raise()

  • raise(signo)는 자기 자신에게 시그널을 보낸다.

alarm()

  • alarm(seconds)는 지정된 초 후에 SIGALRM 시그널을 보낸다.
  • 타이머가 만료되면 프로세스가 기본적으로 종료된다.

pause()

  • pause()는 시그널이 발생할 때까지 프로세스를 대기 상태로 둔다.

결론

시그널은 프로세스 간 비동기적 소통을 가능하게 하는 중요한 메커니즘이다. System V의 signal() 함수는 간단한 구현에 적합하지만, POSIX 표준의 sigaction()은 더 안전하고 유연한 기능을 제공한다. 시그널의 동작 원리를 이해하고 적절히 활용하면, 안정적이고 효율적인 소프트웨어를 개발할 수 있다.

반응형

댓글