리눅스 시그널의 우선순위와 비동기 신호 안전성 이해

시그널 우선순위의 개념

리눅스 환경에서 시그널은 소프트웨어적인 인터럽트로 취급됩니다. 하드웨어 인터럽트와 마찬가지로 시그널에도 처리 우선순위가 존재하며, 여러 시그널이 동시에 대기 중(Pending)일 때 어떤 시그널을 먼저 처리할지 결정하는 규칙이 있습니다.

  • 실시간 시그널(Real-time Signals): 동일한 시그널 값이 여러 개 대기 중일 경우, 전송된 순서대로(FIFO) 처리됩니다. 서로 다른 실시간 시그널이 대기 중이라면 시그널 번호가 작을수록 우선순위가 높습니다.
  • 비실시간 시그널(Unreliable Signals) vs 실시간 시그널: 엄격한 POSIX 규정은 없으나, 일반적인 리눅스 커널 구현에서는 비실시간 시그널을 실시간 시그널보다 먼저 전달합니다.
  • 비실시간 시그널 간의 우선순위:
    • 하드웨어 예외와 관련된 시그널(SIGSEGV, SIGBUS, SIGILL, SIGTRAP, SIGFPE, SIGSYS)이 최우선적으로 처리됩니다.
    • 그 외에는 시그널 번호가 작은 것이 먼저 전달되는 경향이 있습니다.

다음은 시그널의 우선순위와 전달 순서를 테스트하기 위한 예제 코드입니다.

// signal_sender.c - 시그널을 생성하여 특정 프로세스에 송신하는 프로그램
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>

int is_restricted(int sig, int* list, int size) {
    for (int i = 0; i < size; i++) {
        if (list[i] == sig) return 1;
    }
    return 0;
}

int main(int argc, char* argv[]) {
    if (argc < 3) return 1;

    pid_t target_pid = atoi(argv[1]);
    int burst_count = atoi(argv[2]);
    
    int restricted_list[] = {SIGKILL, SIGSTOP, 32, 33, SIGSEGV, SIGBUS, SIGILL, SIGTRAP, SIGFPE, SIGSYS};
    int res_size = sizeof(restricted_list) / sizeof(int);
    
    int critical_sigs[] = {SIGSEGV, SIGBUS, SIGILL, SIGTRAP, SIGFPE, SIGSYS};
    int crit_size = sizeof(critical_sigs) / sizeof(int);

    srand(time(NULL));
    union sigval value;

    printf("Sender PID: %d\n", getpid());

    for (int i = 0; i < burst_count; i++) {
        int target_sig;
        do {
            target_sig = (rand() % 64) + 1;
        } while (is_restricted(target_sig, restricted_list, res_size));

        value.sival_int = i + 1;
        sigqueue(target_pid, target_sig, value);
        printf("Sent: Index %d, Signal %d\n", value.sival_int, target_sig);
    }

    for (int i = 0; i < crit_size; i++) {
        value.sival_int = -(i + 1);
        sigqueue(target_pid, critical_sigs[i], value);
        printf("Sent Critical: Index %d, Signal %d\n", value.sival_int, critical_sigs[i]);
    }

    return 0;
}
// signal_receiver.c - 시그널을 블록한 후 순차적으로 수신하여 확인하는 프로그램
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

typedef struct {
    int signo;
    int data;
} SignalRecord;

static volatile int record_count = 0;
static SignalRecord history[128];

void custom_handler(int sig, siginfo_t* info, void* context) {
    if (record_count < 128) {
        history[record_count].signo = info->si_signo;
        history[record_count].data = info->si_value.sival_int;
        record_count++;
    }
}

int main() {
    struct sigaction sa = {0};
    sigset_t full_mask, empty_mask;

    printf("Receiver PID: %d\n", getpid());

    sa.sa_sigaction = custom_handler;
    sa.sa_flags = SA_SIGINFO | SA_RESTART;
    sigfillset(&sa.sa_mask);

    for (int i = 1; i <= 64; i++) {
        sigaction(i, &sa, NULL);
    }

    sigfillset(&full_mask);
    sigprocmask(SIG_SETMASK, &full_mask, NULL);

    for (int i = 1; i <= 10; i++) {
        printf("Holding signals... %d sec\n", i);
        sleep(1);
    }

    sigemptyset(&empty_mask);
    sigprocmask(SIG_SETMASK, &empty_mask, NULL);

    printf("\nExecution Order:\n");
    for (int i = 0; i < record_count; i++) {
        printf("[%d] Signal: %d, Payload: %d\n", i, history[i].signo, history[i].data);
    }

    return 0;
}

비동기 신호 안전성 (Async-Signal Safety)

시그널 처리에 있어 가장 중요한 개념 중 하나는 '안전성'입니다. 프로그램이 시그널에 의해 중단되었을 때, 핸들러 내부의 동작이 주 프로그램의 상태를 오염시키지 않아야 합니다.

  • 재진입성(Reentrancy): 시그널 핸들러는 주 루프의 어느 지점에서든 실행될 수 있습니다. 만약 핸들러가 주 루프에서 사용 중인 공유 자원(전역 변수, 정적 변수)을 수정하거나 잠금(Lock)을 시도하면 데드락이나 데이터 오염이 발생할 수 있습니다.
  • 비보안 함수 호출 금지: 시그널 핸들러 내부에서는 '비동기 신호 안전'이 보장되지 않는 함수를 호출해서는 안 됩니다.
    • malloc(), free(): 힙 메모리 관리 구조체를 수정하는 도중 시그널이 발생하면 메모리 오염이 발생할 수 있습니다.
    • 표준 I/O 함수(printf() 등): 내부적으로 버퍼와 락을 사용하므로 핸들러 내에서 호출 시 프로그램이 중단될 수 있습니다.
    • 뮤텍스나 세마포어를 사용하는 임계 구역 함수: 데드락의 위험이 큽니다.

따라서 시그널 핸들러는 최대한 간결하게 작성해야 하며, 플래그 변수를 설정하거나 write()와 같은 시스템 호출(비동기 신호 안전 함수)만을 사용하는 것이 권장됩니다.

태그: linux Signal ipc POSIX system-programming

7월 30일 09:48에 게시됨