[Linux] 시그널(signal) 정리

mommers·2026년 2월 3일

Linux

목록 보기
32/59

계속 중복되는 내용이 있지만.. 수업 진도에 따라 겹치는 부분이 있어도 한번 씩 더 정리중입니다.


시그널

프로세스에게 보내는 긴급 문자 메시지(비동기 알림)으로, 받으면 하던 일을 멈추고 즉시 확인(처리)해야 합니다.


1. 시그널 Definition

  • 비동기적(Asynchronous): 프로그램이 언제 받을지 예측할 수 없습니다. (예: 사용자가 갑자기 Ctrl+C를 누름).
  • 소프트웨어 인터럽트: 하드웨어 인터럽트(타이머, 키보드)를 흉내 낸 소프트웨어적 메커니즘입니다.
  • IPC의 기본: 가장 원시적이지만 가장 빠른 프로세스 간 통신 수단입니다. (데이터를 담을 순 없고, "사건 번호"만 전달).

2. 시그널 수명주기 (Lifecycle) 3단계

  1. 발생 (Generation):
    • 이벤트 발생 (예: Ctrl+C, kill 명령어, 0으로 나누기 연산 등).
    • 송신자: 커널, 다른 프로세스, 또는 자기 자신.
  2. 대기 (Pending):
    • 시그널이 생성되었으나 아직 프로세스에 전달되지 않은 상태.
    • 프로세스가 해당 시그널을 블록(Block/Mask) 하고 있으면, 블록이 풀릴 때까지 커널 큐에 머무릅니다.
  3. 전달 및 처리 (Delivery):
    • 커널이 프로세스를 깨우거나 실행 흐름을 가로채서 시그널을 넘겨줍니다.

3. 처리 방법 (Disposition)

개발자는 다음 3가지 중 하나를 선택하여 시그널에 대응합니다.

처리 방식매크로 / 함수설명비고
무시 (Ignore)SIG_IGN"그냥 무시해." 아무 일도 일어나지 않음.SIGKILL, SIGSTOP은 무시 불가. (관리자의 통제권 보장)
기본 동작 (Default)SIG_DFL커널이 정한 기본 행동 수행.대부분 종료(Terminate) 또는 코어 덤프(Core Dump).
포착 (Catch)handler_func"내가 처리할게." 시그널 핸들러 함수 실행.실행 중인 코드를 멈추고 핸들러로 점프 → 수행 후 복귀.

4. 시그널 핸들러의 실행 흐름 (Context Jump)

일반 함수 호출과 다르게, 커널이 강제로 실행 흐름을 바꿉니다.

  1. Main 코드 실행 중: i = i + 1; 수행 중.
  2. 시그널 도착: 커널이 개입.
  3. Context 저장: 현재 레지스터와 스택 상태를 저장.
  4. Handler 실행: signal_handler() 함수로 강제 점프.
  5. Return: 핸들러가 끝나면(return), 저장해둔 위치(i = i + 1 다음)로 복귀하여 계속 실행.

5. 주요 시그널 목록

이름번호기본 동작발생 상황
SIGINT2종료키보드 Ctrl + C 입력 시.
SIGQUIT3코어 덤프키보드 Ctrl + \ 입력 시.
SIGKILL9강제 종료kill -9. (포착, 무시, 블록 불가)
SIGSEGV11코어 덤프잘못된 메모리 접근 (Segmentation Fault).
SIGTERM15종료kill 명령의 기본값. (정상 종료 요청).
SIGSTOP19정지실행 일시 정지. (포착, 무시, 블록 불가)
SIGCHLD17무시자식 프로세스가 종료되거나 멈춤.

+ 추가)

6. 코드 예제 (signal 함수 사용)

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

// 시그널 핸들러 함수 (User defined)
void my_handler(int signo) {
    printf("\n[Signal] %d번 시그널을 받았습니다! 죽지 않아요.\n", signo);
    // 보통 여기서 자원 정리나 플래그 설정을 함
}

int main() {
    // 1. 핸들러 등록 (SIGINT 발생 시 my_handler 실행)
    if (signal(SIGINT, my_handler) == SIG_ERR) {
        perror("signal error");
        exit(1);
    }

    // 2. SIGQUIT는 무시하도록 설정 (Ctrl + \)
    signal(SIGQUIT, SIG_IGN);

    printf("Running... (Ctrl+C를 눌러보세요. Ctrl+\\는 무시됩니다.)\n");

    while (1) {
        printf(".");
        fflush(stdout);
        sleep(1);
    }
    return 0;
}

종료를 위해선 다른 터미널을 열어 아래 명령어 입력

ps aux | grep signal_1
kill -9 [Process ID]

팁: Reentrancy (재진입성)

시그널 핸들러는 비동기적으로 호출되므로, 핸들러 내부에서 printf, malloc 같은 일반 라이브러리 함수를 쓰는 것은 위험합니다. (이 함수들이 실행되는 도중에 시그널이 와서 또 호출하면 꼬일 수 있음).


원칙적으로는, 핸들러 내부에서는 전역 변수(플래그)만 1로 세팅하고 최대한 빨리 빠져나오는 것이 정석입니다.

핸들러는 깃발(Flag)만 올리고, 뒤처리는 메인 루프가 담당한다.

시그널 핸들러 내부에서 printf 같은 무거운 함수를 제거하고, volatile sig_atomic_t 타입을 사용하여 비동기 신호 안전(Async-Signal-Safe) 원칙을 지키는 코드로 재작성된 코드입니다.

✅ 안전한 시그널 처리 패턴 코드

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

/* * [핵심 1] volatile sig_atomic_t
 * - volatile: 컴파일러가 이 변수를 최적화(캐싱)하지 못하게 막음 (언제든 값이 변할 수 있음을 알림).
 * - sig_atomic_t: CPU가 한 번의 명령으로 읽고 쓸 수 있음을 보장하는 정수 타입 (쪼개지지 않음).
 */
volatile sig_atomic_t g_exit_flag = 0;

// 시그널 핸들러: 최대한 짧고 단순하게!
void my_handler(int signo) {
    // 여기서는 복잡한 작업(printf, malloc 등) 금지!
    // 오직 플래그 값만 변경하고 즉시 리턴함.
    g_exit_flag = 1;
}

int main() {
    // 핸들러 등록
    if (signal(SIGINT, my_handler) == SIG_ERR) {
        perror("signal error");
        exit(1);
    }

    printf("프로그램 실행 중... (Ctrl+C를 누르면 안전하게 종료합니다)\n");

    while (1) {
        // [핵심 2] 메인 루프에서 플래그 검사
        if (g_exit_flag == 1) {
            printf("\n[Main] 종료 플래그 감지! 자원을 정리하고 종료합니다.\n");
            break; // 루프 탈출
        }

        // 평소 작업 수행
        printf(".");
        fflush(stdout);
        
        // sleep 중에 시그널이 오면 sleep은 즉시 깨어나고(잔여 시간 반환), 
        // 핸들러 실행 후 다음 라인으로 넘어감.
        sleep(1); 
    }

    printf("Bye Bye!\n");
    return 0;
}

1. printf 제거 이유 (Reentrancy)

  • 상황: 메인 함수가 printf를 호출해서 내부적으로 락(Lock)을 걸고 있는데, 시그널이 발생해서 핸들러가 또 printf를 호출하면?
  • 결과: 핸들러는 메인의 락이 풀리길 기다리고, 메인은 핸들러가 끝나길 기다리는 교착 상태(Deadlock)에 빠질 수 있습니다.

2. volatile 키워드

  • 이유: 컴파일러는 while(1) 내부에서 g_exit_flag를 건드리는 코드가 없으면, 성능을 위해 이 값을 레지스터에 캐싱(저장)해버립니다.
  • 결과: 핸들러가 메모리상의 값을 1로 바꿔도, CPU는 레지스터의 0만 계속 보고 있어서 루프가 안 끝나는 버그가 발생합니다. volatile은 "캐싱하지 말고 무조건 메모리에서 다시 읽어!"라고 지시합니다.

3. sig_atomic_t 타입

  • 이유: int가 32비트인데 8비트 CPU에서 돌린다면? 값을 쓰는 도중(상위 16비트 쓰고 하위 16비트 쓰려는 찰나)에 읽어가면 엉뚱한 값이 될 수 있습니다.
  • 결과: 이 타입은 시스템에서 원자적(Atomic) 접근을 보장하는 가장 안전한 정수 타입입니다.

Deadlock 코드

"시그널 핸들러에서 printf 쓰면 위험하다"는 것을 증명하는 교착 상태(Deadlock) 타이밍이 맞으면 프로그램이 그대로 멈춰버립니다.

예제: 시그널 핸들러 데드락 (The "Unsafe printf" Trap)

이 코드는 printf가 내부적으로 사용하는 락(Lock) 때문에 죽습니다.

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h> // 뮤텍스 사용

// 1. 자원을 보호하는 자물쇠 (Mutex)
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void handler(int signo) {
    printf("[Handler] 시그널 수신! 락 획득 시도 중...\n");
    
    // 3. 여기서 멈춤 (DEADLOCK)
    // Main이 이미 락을 가지고 있는데, Handler가 끝나야 Main이 락을 품.
    // 하지만 Handler는 락을 얻어야 끝남. -> 무한 대기
    pthread_mutex_lock(&lock); 
    
    printf("[Handler] 락 획득 성공! (이 메시지는 절대 안 보임)\n");
    pthread_mutex_unlock(&lock);
}

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

    printf("[Main] 시작: 락을 겁니다.\n");
    
    // 1. 메인이 락을 잠금
    pthread_mutex_lock(&lock);
    
    printf("[Main] 락 획득함. 이제 시그널을 스스로 보냄 (자살골).\n");
    
    // 2. 락을 쥔 상태에서 시그널 발생 (Ctrl+C를 코드로 누름)
    // 이 순간 하던 일을 멈추고 handler로 점프함
    raise(SIGINT); 

    // 4. 핸들러가 끝나야 여기로 돌아오는데, 핸들러가 멈춰서 못 돌아옴.
    printf("[Main] 락 해제 (이 메시지도 절대 안 보임)\n");
    pthread_mutex_unlock(&lock);
    
    return 0;
}


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

// 핸들러에서 메모리 할당 시도 (절대 금지 행위)
void handler(int signo) {
    // 여기서 malloc이 메인이 잡고 있는 '힙 락'을 기다리다 멈춤
    void *ptr = malloc(1024); 
    free(ptr);
}

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

    printf("Running... (이 코드는 곧 멈춥니다)\n");

    // 자식 프로세스가 시그널 난사
    pid_t pid = getpid();
    if (fork() == 0) {
        while(1) {
            kill(pid, SIGINT);
            usleep(100); // 0.1ms 간격 폭격
        }
    }

    // 메인 루프: 쉴 새 없이 메모리 할당/해제 반복 (락을 계속 잡았다 풀었다 함)
    while(1) {
        void *p = malloc(1024);
        // [데드락 포인트] 
        // malloc이 내부 락을 잡고, 아직 리턴하기 전에 시그널이 오면 -> 사망
        free(p);
    }
    return 0;
}

위코드는 데드락에 안들어 갑니다. 확률적인 이유로 시그널 핸들러 안에서는 절대 뮤텍스(Lock)를 쓰거나, 락을 쓰는 함수(printf, malloc 등)를 호출하면 안 됩니다.

해결책

시그널 핸들러 안에서는 락을 쓰지 않는 Async-Signal-Safe 함수만 써야 합니다. printf 대신 write를 써야합니다.

// 안전한 버전
void handler(int signo) {
    char *msg = "Handler: Safe Write!\n";
    write(STDOUT_FILENO, msg, strlen(msg)); // 락을 안 씀
}

시그널집합(Signalset)과 집합 제어

"여러 시그널을 비트 마스크(sigset_t)로 묶어, 특정 구간에서 수신을 잠시 막거나(Block) 푸는(Unblock) 기술.


1. 시그널 집합 (sigset_t)

리눅스는 64개의 시그널을 다루기 위해 unsigned long 배열 비트 마스크인 sigset_t 타입을 사용합니다. 직접 비트 연산을 하지 않고 전용 매크로 함수를 사용합니다.

주요 조작 함수

  • sigemptyset(sigset_t *set): 집합 비우기 (모든 비트 0).
  • sigfillset(sigset_t *set): 모든 시그널 포함 (모든 비트 1).
  • sigaddset(sigset_t *set, int signum): 특정 시그널 추가.
  • sigdelset(sigset_t *set, int signum): 특정 시그널 제거.
  • sigismember(sigset_t *set, int signum): 포함 여부 확인 (True/False).

2. 시그널 제어 (Masking/Blocking)

시그널 집합을 커널에 등록하여 "이 구간에서는 이 시그널들을 잠시 보류해줘"라고 요청하는 것입니다.

시스템 콜 sigprocmask 사용 시

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how 옵션동작 설명비고
SIG_BLOCK현재 마스크 + set (합집합)기존 차단 목록에 추가
SIG_UNBLOCK현재 마스크 - set (차집합)차단 목록에서 해제
SIG_SETMASK현재 마스크 = set (대입)아예 새 목록으로 덮어쓰기

3. 예제: Critical Section 보호

중요한 데이터를 쓰고 있을 때 SIGINT(Ctrl+C)가 들어와도 즉시 종료되지 않고, 작업이 끝난 후에 처리되도록 합니다.

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

int main() {
    sigset_t new_set, old_set;

    // 1. 집합 초기화 및 SIGINT(2번) 추가
    sigemptyset(&new_set);
    sigaddset(&new_set, SIGINT);

    printf("[Start] 중요 작업 시작 전 (Ctrl+C 가능)\n");
    sleep(3);

    // 2. 블록 설정 (이제부터 SIGINT는 Pending 큐에 쌓이고 전달 안 됨)
    // old_set에 이전 상태를 백업해둠
    sigprocmask(SIG_BLOCK, &new_set, &old_set);

    printf("\n[Critical Section] 중요 데이터 기록 중... (Ctrl+C 눌러도 안 죽음)\n");
    for(int i=0; i<5; i++) {
        printf("Writing data... %d/5\n", i+1);
        sleep(1); 
    }
    printf("[Critical Section] 완료.\n");

    // 3. 블록 해제 (백업해둔 상태로 복구)
    // 이 시점에 아까 눌렀던 Ctrl+C가 있다면 즉시 배달되어 프로세스 종료됨
    sigprocmask(SIG_SETMASK, &old_set, NULL);

    printf("\n[End] 작업 끝. (여기까지 출력되면 Ctrl+C 안 누른 것)\n");
    sleep(3);

    return 0;
}

실행 시나리오

  1. "중요 데이터 기록 중..." 일 때 Ctrl+C를 누름.
  2. 프로세스가 종료되지 않고 Writing data...를 끝까지 출력함.
  3. 블록이 해제되는 순간(sigprocmask 복구 직후), 아까 참았던 SIGINT가 도착하여 즉시 종료됨. (마지막 [End] 메시지는 출력 안 됨).

개별 시그널 확인 방법

시그널이 64 기존 sig 8bit 구분해서 프린트하게, 왼쪽이 낮은 비트

예) 00000000 00100000 00000000 00000000 00000000 00000000 00000000 00000000

#define _POSIX_C_SOURCE 200809L //IntelliSense (코드 분석기)의 오탐으로 인해 추가
#include <stdio.h>
#include <unistd.h>
#include <signal.h>


void printf_SIG(sigset_t sig){
    int count=1;
    for (int i = 1; i <= 64; i++)
    {
        if(sigismember(&sig, i)==1){
            printf("1");
        }else printf("0");
        if((count%8)==0)printf(" ");
        count++;
    }
    printf("\n");
    
}


int main() {
    sigset_t new_set, old_set;

    // 1. 집합 초기화 및 SIGINT(2번) 추가
    printf_SIG(old_set);

    sigemptyset(&new_set);    
    sigaddset(&new_set, SIGINT);
    printf_SIG(new_set);

    printf("[Start] 중요 작업 시작 전 (Ctrl+C 가능)\n");
    sleep(3);

    printf("[Start] 마스킹처리됨 (Ctrl+C 안됨)\n");
    sigprocmask(0, NULL,&old_set);
    printf_SIG(old_set);

    // 2. 블록 설정 (이제부터 SIGINT는 Pending 큐에 쌓이고 전달 안 됨)
    // old_set에 이전 상태를 백업해둠
    sigprocmask(SIG_BLOCK, &new_set, &old_set); //old_set => 0

    printf_SIG(old_set);
    
    sigprocmask(0, NULL,&old_set); //=> 1

    printf_SIG(old_set);

    printf("\n[Critical Section] 중요 데이터 기록 중... (Ctrl+C 눌러도 안 죽음)\n");
    for(int i=0; i<5; i++) {
        printf("Writing data... %d/5\n", i+1);
        sleep(1); 
    }
    printf("[Critical Section] 완료.\n");

    // 3. 블록 해제 (백업해둔 상태로 복구)
    // 이 시점에 아까 눌렀던 Ctrl+C가 있다면 즉시 배달되어 프로세스 종료됨
    sigprocmask(SIG_SETMASK, &old_set, NULL);
    printf_SIG(old_set);
    printf("\n[End] 작업 끝. (여기까지 출력되면 Ctrl+C 안 누른 것)\n");
    sleep(3);

    return 0;
}

위 코드 한번 더 정리할 것


시그널을 보낸 pid를 통해서 어느 사용자(UID)가 왜? 보내었는지 확인 하는 프로그램

#define _POSIX_C_SOURCE 200809L //IntelliSense (코드 분석기)의 오탐으로 인해 추가
#include <stdio.h>      // printf()
#include <stdlib.h>     // exit()
#include <unistd.h>     // sleep(), getpid()
#include <signal.h>    // sigaction, siginfo_t, SI_USER, SI_KERNEL
#include <string.h>

void detailed_handler(int sig, siginfo_t *info, void *ucontext) {
    printf("\n[Signal Received] 번호: %d\n", sig);
    
    // 1. 누가 보냈는가?
    printf(" - 보낸 프로세스 PID: %d\n", info->si_pid);
    printf(" - 보낸 사용자 UID: %u\n", info->si_uid);

    // 2. 왜 보냈는가? (si_code 분석)
    printf(" - 발생 원인 코드: %d ", info->si_code);
    
    if (info->si_code == SI_USER) {
        printf("(사용자가 kill이나 raise로 직접 전송)\n");
    } else if (info->si_code == SI_KERNEL) {
        printf("(커널에서 전송)\n");
    } else {
        printf("(기타 사유)\n");
    }
}

int main() {
    struct sigaction sa;

    // 구조체 초기화
    sa.sa_sigaction = detailed_handler; // 상세 정보를 받는 핸들러 연결
    sigemptyset(&sa.sa_mask);           // 핸들러 실행 중 블록할 시그널 없음
    sa.sa_flags = SA_SIGINFO;           // 상세 정보 사용 플래그 설정

    // SIGINT(Ctrl+C)에 대해 sigaction 설정
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        exit(1);
    }

    printf("현재 프로세스 PID: %d\n", getpid());
    printf("Ctrl+C를 누르거나, 다른 터미널에서 'kill -2 %d'를 입력하세요.\n", getpid());

    // 시그널 대기
    while (1) {
        sleep(1);
    }

    return 0;
}


썸넬 reference : https://blockdmask.tistory.com/23

profile
임베디드 개발자가 되기 위해 공부중입니다!

2개의 댓글

comment-user-thumbnail
2026년 2월 3일

블로그 다른 글들을 훑어보았는데 굉장히 열정적이시네요!
열정적인 모습 배워가겠습니다.
또한 소개글의 목표도 저와 비슷하여 공감이 많이 가네요~
로우 레벨에 대해 함께 고민하기 위해 종종 찾아오겠습니다 :)

1개의 답글