C언어로 배우는 리눅스 커널 시그널 처리 기법

리눅스 운영체제는 커널 레벨에서 시그널이라는 강력한 메커니즘을 통해 프로세스 간 통신과 시스템 이벤트 처리를 제공합니다. C언어는 이러한 시그널 처리를 다루는 데 가장 기본적이고 중요한 도구로 활용됩니다. 본 기사에서는 리눅스 시그널의 개념부터 C언어를 활용한 기본 및 고급 시그널 처리 기법을 다루며, 실무에서 이를 효과적으로 활용할 수 있는 지침을 제공합니다.

목차

시그널의 개념과 역할


시그널은 리눅스 운영체제에서 프로세스 간 통신(IPC) 및 이벤트 처리를 위해 사용되는 메커니즘입니다. 이는 소프트웨어 인터럽트의 한 형태로, 커널이나 다른 프로세스에서 특정 프로세스에 알림을 전달할 때 사용됩니다.

시그널의 정의


시그널은 프로세스에 비동기적으로 전달되는 이벤트 알림으로, 특정 사건(예: 종료 요청, 메모리 접근 오류, 타이머 만료 등)을 나타냅니다.

시그널의 역할

  1. 프로세스 종료 요청: 사용자가 키보드로 Ctrl+C를 입력할 때 전송되는 SIGINT처럼, 프로세스 종료를 제어합니다.
  2. 예외 처리: 잘못된 메모리 접근(SIGSEGV)이나 산술 오류(SIGFPE)와 같은 예외를 처리합니다.
  3. 프로세스 간 통신: 프로세스 간의 간단한 메시지 전달 수단으로 활용됩니다.

시그널의 작동 방식

  1. 발생: 시그널은 커널, 다른 프로세스, 또는 프로세스 자체에 의해 발생할 수 있습니다.
  2. 전달: 커널은 대상 프로세스의 PCB(Process Control Block)에 시그널 정보를 저장하여 전달을 예약합니다.
  3. 처리: 프로세스는 기본 동작(예: 종료) 또는 사용자 정의 핸들러를 통해 시그널을 처리합니다.

시그널은 리눅스의 유연성과 제어력을 강화하는 중요한 도구이며, 이를 효과적으로 이해하는 것이 안정적인 소프트웨어 개발의 핵심입니다.

리눅스 시그널의 종류와 용도


리눅스에서는 다양한 시그널이 정의되어 있으며, 각각 특정한 상황에서 사용됩니다. 이러한 시그널들은 프로세스 제어와 디버깅, 시스템 자원 관리 등에 중요한 역할을 합니다.

주요 시그널 종류

  1. SIGINT (Interrupt Signal)
  • 용도: 키보드 입력(Ctrl+C)을 통해 프로세스를 종료.
  • 기본 동작: 프로세스 종료.
  1. SIGTERM (Termination Signal)
  • 용도: 프로세스 종료 요청.
  • 기본 동작: 프로세스 종료(사용자가 이를 무시하거나 핸들러를 설정 가능).
  1. SIGKILL (Kill Signal)
  • 용도: 강제 프로세스 종료.
  • 기본 동작: 즉각적인 종료(사용자가 무시할 수 없음).
  1. SIGSEGV (Segmentation Fault Signal)
  • 용도: 잘못된 메모리 접근 감지.
  • 기본 동작: 프로세스 종료와 코어 덤프 생성.
  1. SIGALRM (Alarm Signal)
  • 용도: 타이머 알람 시 발생.
  • 기본 동작: 사용자 정의 핸들러 실행 또는 프로세스 종료.

시그널의 용도

  1. 프로세스 관리
  • 시그널은 프로세스를 시작, 중단, 재개하거나 종료하는 데 사용됩니다. 예를 들어, SIGSTOP은 프로세스를 일시 정지시키고, SIGCONT는 이를 다시 실행합니다.
  1. 에러 감지
  • SIGFPE(산술 오류)나 SIGBUS(버스 오류)와 같은 시그널은 프로그램 실행 중 발생하는 오류를 감지하는 데 유용합니다.
  1. 타이머 기반 동작
  • SIGALRM을 사용하여 특정 시간 후에 이벤트를 트리거할 수 있습니다.
  1. 프로세스 간 통신
  • SIGUSR1SIGUSR2는 사용자 정의 이벤트 처리를 위한 시그널로, 개발자가 임의의 동작을 지정할 수 있습니다.

시그널의 분류

  • 표준 시그널: 시스템에서 일반적으로 제공하는 시그널.
  • 실시간 시그널: 추가적인 사용자 정의 및 우선순위 제어를 위한 시그널로, SIGRTMIN부터 시작.

리눅스 시그널의 다양한 종류와 용도를 숙지하면, 프로세스와 시스템 자원 관리가 더욱 효율적으로 이루어질 수 있습니다.

시그널 핸들링 기초


C언어에서 시그널 핸들링은 시그널 발생 시 기본 동작(예: 종료, 무시)을 변경하거나 사용자 정의 동작을 수행할 수 있도록 지원합니다. 이를 통해 프로세스가 특정 이벤트에 유연하게 반응할 수 있습니다.

시그널 처리의 기본 메커니즘

  1. 시그널 발생
  • 시그널은 키보드 입력(Ctrl+C), 소프트웨어 이벤트, 또는 커널에 의해 생성됩니다.
  1. 기본 동작 수행
  • 시그널이 발생하면 해당 프로세스는 미리 정의된 기본 동작(예: 종료, 무시, 중단)을 수행합니다.
  1. 사용자 정의 핸들러 설정
  • 개발자는 signal() 함수를 사용하여 특정 시그널에 대해 사용자 정의 동작을 지정할 수 있습니다.

C언어에서 시그널 처리의 주요 함수

  1. signal()
  • 특정 시그널에 대해 핸들러 함수를 등록합니다.
  • 문법:
    c void (*signal(int signum, void (*handler)(int)))(int);
  • 예제: #include <stdio.h> #include <signal.h> void handle_signal(int signal) { printf("Received signal: %d\n", signal); } int main() { signal(SIGINT, handle_signal); // Ctrl+C 처리 while (1); // 무한 루프 return 0; }
  1. raise()
  • 프로세스 내부에서 시그널을 발생시킵니다.
  • 예제:
    c raise(SIGINT);
  1. kill()
  • 특정 프로세스에 시그널을 보냅니다.

시그널 핸들링의 기본 원칙

  1. 비동기적 처리
  • 시그널은 예측할 수 없는 시점에 발생하므로, 핸들러는 항상 비동기적으로 동작해야 합니다.
  1. 재진입 가능 코드 작성
  • 핸들러 함수는 재진입 가능한 코드로 작성해야 합니다(공유 자원 접근 주의).
  1. 기본 동작 복구
  • 필요 시, 처리 후 기본 동작을 복구하거나 반복적으로 설정해야 합니다.

시그널 핸들링은 리눅스 프로세스의 유연한 제어를 가능하게 하며, 이를 적절히 활용하면 복잡한 시스템에서 이벤트 기반 처리를 구현할 수 있습니다.

`signal()` 함수 사용법


signal() 함수는 C언어에서 시그널 처리를 위해 가장 기본적으로 사용되는 함수로, 특정 시그널에 대한 핸들러를 등록하거나 기본 동작을 변경할 수 있습니다.

`signal()` 함수의 구조


signal() 함수는 다음과 같은 형태로 정의됩니다:

void (*signal(int signum, void (*handler)(int)))(int);
  • signum: 설정할 시그널의 번호입니다(e.g., SIGINT, SIGTERM).
  • handler: 시그널 발생 시 실행될 함수 포인터 또는 기본 동작을 지정하는 매크로(SIG_DFL, SIG_IGN)입니다.
  • SIG_DFL: 시그널의 기본 동작 수행.
  • SIG_IGN: 시그널을 무시.

핸들러 등록 예제


다음은 signal()을 사용하여 사용자 정의 핸들러를 설정하는 코드입니다:

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

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

int main() {
    // SIGINT에 대한 사용자 정의 핸들러 등록
    signal(SIGINT, signal_handler);

    // 무한 루프 (Ctrl+C 입력 시 SIGINT 발생)
    while (1) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}
  • 출력 결과:
  Running...
  Running...
  Received signal: 2
  Running...

시그널 무시


다음 코드는 SIGINT 시그널을 무시하도록 설정합니다:

signal(SIGINT, SIG_IGN);
  • 사용 사례: 특정 상황에서 시그널을 일시적으로 무시하여 프로세스가 종료되지 않도록 보호.

기본 동작 복구


기본 동작으로 복구하려면 SIG_DFL을 사용합니다:

signal(SIGINT, SIG_DFL);

주의점

  1. 비동기성: 핸들러는 예측할 수 없는 시점에 실행되므로, 전역 변수 접근이나 동기화에 유의해야 합니다.
  2. 제한된 함수 사용: 핸들러 내부에서는 재진입 가능 함수만 호출해야 합니다(예: printf 대신 write 사용).

장단점

  • 장점: 간단하고 직관적으로 사용할 수 있음.
  • 단점: 일부 환경에서의 비정상 동작 가능성(멀티스레드 환경 등).

signal() 함수는 단순한 시그널 처리 작업에 유용하며, 복잡한 환경에서는 더 강력한 sigaction()을 사용하는 것이 적합합니다.

고급 시그널 처리: `sigaction()` 함수


sigaction() 함수는 signal() 함수보다 안전하고 유연한 방식으로 시그널 처리를 설정할 수 있는 고급 기능을 제공합니다. 멀티스레드 환경이나 비동기 처리에서도 안정적인 동작을 보장합니다.

`sigaction()` 함수의 구조


sigaction() 함수는 다음과 같은 형태로 정의됩니다:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum: 처리할 시그널의 번호.
  • act: 새로운 시그널 핸들러 설정을 담고 있는 구조체 포인터.
  • oldact: 이전 시그널 핸들러 설정을 저장할 구조체 포인터(필요하지 않을 경우 NULL).
  • 반환값: 성공 시 0, 실패 시 -1.

`sigaction` 구조체


sigaction 구조체는 다음과 같이 정의됩니다:

struct sigaction {
    void (*sa_handler)(int);     // 핸들러 함수
    sigset_t sa_mask;            // 블록할 시그널 집합
    int sa_flags;                // 동작 플래그
};
  • sa_handler: 시그널 발생 시 실행할 함수 포인터.
  • sa_mask: 핸들러 실행 중 블록할 추가 시그널 집합.
  • sa_flags: 동작 플래그(예: SA_RESTART, SA_SIGINFO).

핸들러 등록 예제


다음은 sigaction()을 사용하여 사용자 정의 핸들러를 설정하는 코드입니다:

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

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

int main() {
    struct sigaction sa;

    // 핸들러 설정
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);    // 추가 블록할 시그널 없음
    sa.sa_flags = 0;            // 기본 플래그 설정

    // SIGINT에 대한 핸들러 등록
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    // 무한 루프
    while (1) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}
  • 출력 결과:
  Running...
  Running...
  Received signal: 2

`sigaction()`의 주요 플래그

  1. SA_RESTART
  • 시스템 호출이 시그널로 중단되었을 경우 자동으로 재시작.
  • 사용 사례: 읽기/쓰기 작업 중단 방지.
  1. SA_SIGINFO
  • 핸들러에 추가 정보를 전달하기 위해 사용.
  • 핸들러 함수는 void (*sa_sigaction)(int, siginfo_t *, void *) 형태로 작성해야 함.

고급 핸들러 예제: `SA_SIGINFO`

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

void signal_handler(int sig, siginfo_t *info, void *context) {
    printf("Received signal: %d from PID: %d\n", sig, info->si_pid);
}

int main() {
    struct sigaction sa;

    sa.sa_flags = SA_SIGINFO;    // 추가 정보 전달 활성화
    sa.sa_sigaction = signal_handler;
    sigemptyset(&sa.sa_mask);

    if (sigaction(SIGUSR1, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    // 대기
    while (1) pause();
    return 0;
}
  • 출력 결과:
  Received signal: 10 from PID: 12345

`sigaction()` 사용의 장점

  1. 안전성: 멀티스레드 환경에서도 안정적.
  2. 유연성: 추가 플래그와 마스크로 세밀한 제어 가능.
  3. 확장성: 핸들러에 추가 정보를 전달할 수 있어 다양한 활용 가능.

주요 사용 사례

  • 멀티스레드 환경에서의 시그널 처리.
  • 시스템 호출 재시작(SA_RESTART) 요구 시.
  • 시그널 발생 정보를 필요로 하는 디버깅 및 로깅.

sigaction()은 강력하고 세부적인 제어를 제공하므로, 복잡한 시스템 개발 시 signal()보다 선호됩니다.

시그널 처리에서의 예외 상황 처리


시그널 처리 중에는 예상치 못한 예외 상황이 발생할 수 있습니다. 이러한 상황을 적절히 처리하지 않으면 프로그램의 불안정성이나 예기치 않은 동작으로 이어질 수 있습니다.

대표적인 예외 상황

  1. 중첩된 시그널 처리
  • 동일한 시그널이 처리 중에 다시 발생할 경우, 이전 핸들러가 중단될 수 있습니다.
  • 해결책: 시그널 블록 집합을 설정하여 핸들러 실행 중 해당 시그널을 무시합니다.
   struct sigaction sa;
   sigemptyset(&sa.sa_mask);  // 블록할 시그널 초기화
   sigaddset(&sa.sa_mask, SIGINT);  // SIGINT 블록 추가
   sa.sa_handler = signal_handler;
   sa.sa_flags = 0;
   sigaction(SIGINT, &sa, NULL);
  1. 비재진입 함수 호출
  • 핸들러 내에서 비재진입 함수(예: printf)를 호출하면, 예상치 못한 동작이 발생할 수 있습니다.
  • 해결책: 재진입 가능한 함수(예: write)를 사용하거나, 공유 자원 접근 시 동기화 메커니즘을 적용합니다.
   void signal_handler(int sig) {
       const char *message = "Signal received\n";
       write(STDOUT_FILENO, message, strlen(message));
   }
  1. 시스템 호출 중단
  • 시그널이 시스템 호출(예: read, write) 중에 발생하면, 호출이 EINTR 오류로 중단될 수 있습니다.
  • 해결책: SA_RESTART 플래그를 설정하여 시스템 호출이 자동으로 재시작되도록 합니다.
   sa.sa_flags = SA_RESTART;
   sigaction(SIGINT, &sa, NULL);

예외 상황 해결을 위한 모범 사례

  1. 핸들러 최소화
  • 핸들러는 가능한 한 간단하게 작성하여 비동기적인 동작에 의한 문제를 줄입니다.
  1. 공유 자원 접근 보호
  • 핸들러 내부에서 전역 변수나 공유 자원을 다룰 때는 동기화 기법(예: 뮤텍스)을 사용합니다.
  • 예제: volatile sig_atomic_t signal_flag = 0; void signal_handler(int sig) { signal_flag = 1; // 시그널 발생 플래그 설정 }
  1. 핸들러의 재설정 방지
  • 일부 시스템에서는 핸들러가 실행된 후 기본 동작으로 복원될 수 있습니다. 이를 방지하려면 sigaction()을 사용하여 지속적으로 핸들러가 유지되도록 설정합니다.
  1. 다중 시그널 관리
  • 다중 시그널 처리 시 충돌을 방지하기 위해 시그널 우선순위를 정의하고, 필요한 경우 마스크를 설정합니다.

예외 상황 처리 예제

다음 코드는 시그널 처리 중 중첩과 시스템 호출 중단을 방지하는 방법을 보여줍니다:

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

volatile sig_atomic_t signal_flag = 0;

void signal_handler(int sig) {
    signal_flag = 1;  // 플래그 설정
}

int main() {
    struct sigaction sa;
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  // 시스템 호출 재시작 설정

    sigaction(SIGINT, &sa, NULL);

    while (1) {
        if (signal_flag) {
            printf("Signal caught!\n");
            signal_flag = 0;
        }
        sleep(1);  // 시스템 호출
    }
    return 0;
}

예외 상황 처리가 중요한 이유

  • 안정적인 프로그램 동작 보장.
  • 예기치 않은 종료 방지.
  • 디버깅 및 유지보수 용이성 향상.

효율적인 예외 상황 처리는 시그널 처리의 신뢰성을 높이고, 복잡한 시스템에서도 안정성을 유지하는 데 필수적입니다.

멀티스레딩 환경에서의 시그널 처리


멀티스레딩 프로그램에서 시그널 처리는 단일 스레드 프로그램보다 복잡하며, 잘못 구현하면 예기치 않은 동작이나 성능 문제가 발생할 수 있습니다. 따라서 멀티스레딩 환경에서는 적절한 설계와 관리가 필요합니다.

멀티스레딩과 시그널 처리의 주요 이슈

  1. 시그널 전달의 불확실성
  • 시그널은 프로세스 전체에 전달되며, 어느 스레드가 시그널을 처리할지는 예측할 수 없습니다.
  • 해결책: 특정 스레드로 시그널을 지정하여 처리.
  1. 시그널 핸들러와 스레드 간 충돌
  • 공유 자원이나 변수 접근 시 경쟁 조건이 발생할 수 있습니다.
  • 해결책: 시그널 핸들러에서 재진입 가능 코드를 작성하거나 동기화 메커니즘 사용.
  1. 블록되지 않은 시그널의 영향
  • 특정 스레드에서 시그널을 블록하지 않으면, 예상하지 못한 시점에 시그널이 처리될 수 있습니다.
  • 해결책: 스레드별로 적절히 시그널 블록 집합을 설정.

멀티스레드에서 시그널 처리 방법

  1. 시그널 블록 설정
    각 스레드가 처리하지 않아야 할 시그널을 블록합니다.
   sigset_t set;
   sigemptyset(&set);
   sigaddset(&set, SIGINT);
   pthread_sigmask(SIG_BLOCK, &set, NULL);
  1. 시그널 처리 전용 스레드 생성
    특정 스레드에서만 시그널을 처리하도록 설정합니다.
   void *signal_handler_thread(void *arg) {
       int sig;
       sigset_t *set = (sigset_t *)arg;

       while (1) {
           sigwait(set, &sig);  // 시그널 대기
           printf("Signal received: %d\n", sig);
       }
       return NULL;
   }
  1. 시그널 대기와 처리
    sigwait() 함수는 특정 시그널이 발생할 때까지 대기하여 스레드에서 안정적으로 처리할 수 있도록 지원합니다.

전체 예제: 시그널 처리 전용 스레드 구현


다음 코드는 멀티스레드 환경에서 시그널 처리 전용 스레드를 구현한 예제입니다:

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

void *signal_handler_thread(void *arg) {
    int sig;
    sigset_t *set = (sigset_t *)arg;

    while (1) {
        sigwait(set, &sig);  // 지정된 시그널 대기
        printf("Signal received: %d\n", sig);
    }
    return NULL;
}

int main() {
    pthread_t thread;
    sigset_t set;

    // 시그널 블록 설정
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGTERM);
    pthread_sigmask(SIG_BLOCK, &set, NULL);  // 메인 스레드 시그널 블록

    // 시그널 처리 전용 스레드 생성
    pthread_create(&thread, NULL, signal_handler_thread, (void *)&set);

    // 메인 스레드 작업
    while (1) {
        printf("Main thread running...\n");
        sleep(1);
    }

    return 0;
}
  • 출력 결과:
  Main thread running...
  Signal received: 2

주요 함수 및 사용 사례

  1. sigwait()
  • 시그널 발생 시까지 대기하며 안정적인 처리 환경 제공.
  • 사용 사례: 시그널 처리 전용 스레드.
  1. pthread_sigmask()
  • 스레드별로 시그널 블록 집합을 설정하여 처리 범위를 제한.
  • 사용 사례: 특정 스레드에서만 시그널 처리.
  1. sigaction()
  • 고급 시그널 처리 및 안전한 핸들러 등록.
  • 사용 사례: 스레드 전체에서 공통 처리.

멀티스레드 환경에서의 시그널 처리 모범 사례

  1. 시그널 처리 스레드 분리
  • 시그널 처리를 단일 스레드에 집중시켜 경쟁 조건 및 동기화 문제 최소화.
  1. 시그널 블록 관리
  • 각 스레드에서 불필요한 시그널이 처리되지 않도록 블록 집합 설정.
  1. 비동기 처리 최소화
  • 비동기적 시그널 핸들링 대신 sigwait()와 같은 동기적 처리 사용.

결론


멀티스레딩 환경에서의 시그널 처리는 신중한 설계와 관리가 요구됩니다. 위와 같은 방법과 모범 사례를 활용하면 안정적이고 예측 가능한 시그널 처리가 가능합니다.

시그널 처리 응용 예시


리눅스에서 시그널 처리는 단순한 종료 제어 외에도 다양한 실무 상황에서 활용됩니다. 여기서는 실제 프로젝트에서 사용할 수 있는 응용 사례를 코드와 함께 소개합니다.

응용 사례 1: 타이머 기반 작업


시그널을 활용해 정기적으로 실행해야 하는 작업을 구현할 수 있습니다.

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

void timer_handler(int sig) {
    static int count = 0;
    printf("Timer triggered %d times\n", ++count);
}

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

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

    // 타이머 설정 (1초 간격)
    alarm(1);

    while (1) {
        pause();  // 시그널 대기
        alarm(1); // 다음 알람 설정
    }
    return 0;
}
  • 출력 결과:
  Timer triggered 1 times
  Timer triggered 2 times
  ...

응용 사례 2: 프로세스 간 통신


SIGUSR1SIGUSR2를 사용해 사용자 정의 이벤트를 구현할 수 있습니다.

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

void signal_handler(int sig) {
    if (sig == SIGUSR1) {
        printf("Received SIGUSR1: Start process\n");
    } else if (sig == SIGUSR2) {
        printf("Received SIGUSR2: Stop process\n");
    }
}

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

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

    printf("Process ID: %d\n", getpid());

    while (1) {
        pause();  // 시그널 대기
    }
    return 0;
}
  • 출력 결과:
  • 다른 터미널에서 시그널 전송:
    bash kill -SIGUSR1 <PID> kill -SIGUSR2 <PID>
  • 프로그램 출력:
    Received SIGUSR1: Start process Received SIGUSR2: Stop process

응용 사례 3: 클린업 작업


프로그램 종료 시 자원을 정리하기 위해 SIGTERM 시그널을 활용할 수 있습니다.

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

void cleanup_handler(int sig) {
    printf("Cleanup in progress...\n");
    // 자원 해제 작업
    exit(0);
}

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

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

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

    while (1) {
        sleep(1);  // 실행 유지
    }
    return 0;
}
  • 출력 결과:
  • 터미널에서 kill <PID> 실행 시:
    Cleanup in progress...

응용 사례 4: 멀티스레드에서의 시그널 처리


멀티스레드 환경에서 특정 스레드만 시그널을 처리하도록 설정하는 예제입니다.

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

void *signal_handler_thread(void *arg) {
    int sig;
    sigset_t *set = (sigset_t *)arg;

    while (1) {
        sigwait(set, &sig);
        printf("Signal %d received by handler thread\n", sig);
    }
    return NULL;
}

int main() {
    pthread_t thread;
    sigset_t set;

    // SIGINT 블록 설정
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    pthread_sigmask(SIG_BLOCK, &set, NULL);

    // 시그널 처리 스레드 생성
    pthread_create(&thread, NULL, signal_handler_thread, (void *)&set);

    while (1) {
        printf("Main thread running...\n");
        sleep(1);
    }
    return 0;
}
  • 출력 결과:
  • 터미널에서 Ctrl+C 입력 시:
    Signal 2 received by handler thread

응용 사례의 중요성

  • 시그널 처리 응용은 단순한 종료 제어를 넘어 정기 작업, 이벤트 기반 통신, 클린업 작업 등 다양한 실무 문제를 해결하는 데 사용됩니다.
  • 위 사례들을 이해하고 활용하면, 안정적이고 유연한 프로그램 설계가 가능합니다.

요약


본 기사에서는 리눅스 커널의 시그널 처리 메커니즘을 C언어를 활용해 이해하고, 실무에서 활용할 수 있는 다양한 기법과 응용 사례를 소개했습니다. 시그널의 기본 개념부터 고급 처리 방식인 sigaction(), 멀티스레딩 환경에서의 시그널 관리, 그리고 실용적인 응용 사례까지 다뤘습니다.

적절한 시그널 처리는 안정적인 프로그램 동작을 보장하고, 시스템 이벤트와 프로세스 간 통신을 효율적으로 관리할 수 있도록 돕습니다. 이를 통해 복잡한 소프트웨어 환경에서도 예측 가능하고 신뢰성 높은 애플리케이션을 개발할 수 있습니다.

목차