시그널 처리 (Signal Handling)

Linux 커널의 시그널 메커니즘을 자료구조와 실행 경로 중심으로 분석합니다. 프로세스/스레드 그룹별 pending 관리, signal delivery 선택 규칙, sigaction과 signal frame 구성, sigreturn 복구, 실시간 시그널 큐잉, signalfd 기반 이벤트 루프 연계, ptrace·seccomp·job control 상호작용과 장애 분석 기법까지 포괄적으로 다룹니다.

관련 표준: POSIX.1-2017 (Signal Handling) — 시그널 처리의 POSIX 표준 시맨틱입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 프로세스 스케줄러프로세스 문서를 먼저 읽으세요. 실행 단위 관리 주제는 태스크 상태 전이와 큐 정책이 핵심이므로, 스케줄링 기준과 wakeup 경로를 먼저 이해해야 합니다.

핵심 요약

  • 시그널 — 프로세스에 비동기 이벤트를 알리는 소프트웨어 인터럽트입니다.
  • SIGKILL(9) / SIGTERM(15) — 프로세스 종료 시그널. SIGKILL은 무시·차단 불가합니다.
  • sigaction() — 시그널 핸들러를 등록하는 시스템 콜. 기본 동작을 사용자 정의 함수로 교체합니다.
  • 시그널 마스크sigprocmask()로 특정 시그널을 일시적으로 차단(블록)할 수 있습니다.
  • 실시간 시그널 — SIGRTMIN~SIGRTMAX. 큐잉되고 순서가 보장되는 확장 시그널입니다.

단계별 이해

  1. 시그널 보내기kill -SIGTERM <pid>로 프로세스에 시그널을 보냅니다.

    Ctrl+C는 SIGINT, Ctrl+Z는 SIGTSTP을 현재 포그라운드 프로세스에 보냅니다.

  2. 시그널 받기 — 커널은 시스템 콜에서 복귀할 때 보류(pending) 시그널을 확인하고 전달합니다.

    핸들러가 등록되어 있으면 사용자 공간에서 핸들러 함수를 실행합니다.

  3. 핸들러 작성sigaction()으로 핸들러를 등록합니다. 핸들러 내에서는 async-signal-safe 함수만 사용해야 합니다.

    signal()보다 sigaction()이 이식성과 안정성이 높습니다.

  4. 디버깅strace -e signal ./app으로 시그널 전달을 추적할 수 있습니다.

    /proc/<pid>/status에서 SigPnd, SigBlk, SigCgt 필드로 시그널 상태를 확인합니다.

시그널 개요

시그널(signal)은 프로세스에 비동기 이벤트를 알리는 소프트웨어 인터럽트입니다. 커널, 다른 프로세스, 또는 프로세스 자신이 시그널을 보낼 수 있으며, 수신 프로세스는 기본 동작(종료, 코어 덤프, 중지 등)을 따르거나, 사용자 정의 핸들러를 설치하거나, 시그널을 무시할 수 있습니다.

시그널의 역할

표준 시그널 목록

번호시그널기본 동작설명
1SIGHUP종료제어 터미널 끊김, 데몬 설정 재로드 관례
2SIGINT종료Ctrl+C, 포그라운드 프로세스 인터럽트
3SIGQUIT코어 덤프Ctrl+\, 종료 + 코어 덤프 생성
6SIGABRT코어 덤프abort() 호출
9SIGKILL종료무조건 종료, 핸들러 설치/블록 불가
11SIGSEGV코어 덤프잘못된 메모리 접근 (segmentation fault)
13SIGPIPE종료읽는 쪽이 닫힌 파이프/소켓에 쓰기
14SIGALRM종료alarm() 타이머 만료
15SIGTERM종료정상 종료 요청 (기본 kill 시그널)
17SIGCHLD무시자식 프로세스 상태 변경
19SIGSTOP중지프로세스 중지, 핸들러 설치/블록 불가
20SIGTSTP중지Ctrl+Z, 터미널 중지
18SIGCONT재개중지된 프로세스 재개
ℹ️

시그널 번호는 아키텍처마다 다릅니다. 위 표는 x86/x86_64 기준입니다. MIPS, Alpha 등에서는 번호가 다를 수 있습니다. 커널 소스에서 arch/<arch>/include/uapi/asm/signal.h를 확인하세요.

시그널 자료구조

커널은 시그널을 처리하기 위해 여러 자료구조를 사용합니다. 이들은 task_struct에 연결되어 프로세스/스레드별 시그널 상태를 관리합니다.

sigset_t - 시그널 비트마스크

/* include/linux/signal_types.h */
typedef struct {
    unsigned long sig[_NSIG_WORDS];  /* _NSIG=64, 64비트에서 1개 워드 */
} sigset_t;

/* 시그널 비트마스크 조작 매크로 */
sigaddset(&set, SIGTERM);     /* 시그널 추가 */
sigdelset(&set, SIGTERM);     /* 시그널 제거 */
sigismember(&set, SIGTERM);   /* 포함 여부 확인 */
sigemptyset(&set);            /* 모든 비트 클리어 */
sigfillset(&set);             /* 모든 비트 설정 */
sigandsets(&dst, &a, &b);    /* AND 연산 */
sigorsets(&dst, &a, &b);     /* OR 연산 */
signotset(&set);              /* NOT 연산 (반전) */

struct k_sigaction - 시그널 핸들러 정보

/* include/linux/signal_types.h */
struct k_sigaction {
    struct sigaction sa;
};

/* include/uapi/asm-generic/signal-defs.h */
struct sigaction {
    __sighandler_t  sa_handler;   /* SIG_DFL, SIG_IGN, 또는 핸들러 주소 */
    unsigned long   sa_flags;     /* SA_SIGINFO, SA_RESTART, SA_ONSTACK 등 */
    __sigrestore_t sa_restorer;   /* sigreturn trampoline (VDSO) */
    sigset_t        sa_mask;      /* 핸들러 실행 중 추가 블록할 시그널 */
};

struct sigpending - 대기 중인 시그널

/* include/linux/signal_types.h */
struct sigpending {
    struct list_head list;   /* sigqueue 연결 리스트 */
    sigset_t signal;          /* 대기 중인 시그널 비트마스크 */
};

/* 개별 시그널 큐 항목 */
struct sigqueue {
    struct list_head   list;
    int                flags;
    struct siginfo     info;    /* 시그널 상세 정보 */
    struct ucounts     *ucounts;
};

sighand_struct - 핸들러 테이블

/* include/linux/sched/signal.h */
struct sighand_struct {
    refcount_t          count;          /* 참조 카운트 */
    struct k_sigaction  action[_NSIG];  /* 64개 시그널 핸들러 */
    spinlock_t          siglock;        /* 시그널 처리 동기화 */
    wait_queue_head_t   signalfd_wqh;   /* signalfd 대기 큐 */
};

/* sighand_struct는 같은 스레드 그룹의 모든 스레드가 공유합니다.
 * clone(CLONE_SIGHAND)로 생성된 스레드들이 동일한 sighand를 참조합니다.
 * action[] 배열 인덱스는 시그널 번호 - 1 입니다 (0-based). */

signal_struct - 프로세스 그룹 시그널 정보

/* include/linux/sched/signal.h (주요 필드만) */
struct signal_struct {
    refcount_t          sigcnt;
    atomic_t            live;            /* 살아있는 스레드 수 */

    struct sigpending   shared_pending;  /* 프로세스 단위 대기 시그널 */

    int                 group_exit_code;
    int                 group_stop_count;
    unsigned int        flags;           /* SIGNAL_GROUP_EXIT 등 */

    struct rlimit       rlim[RLIM_NLIMITS]; /* RLIMIT_SIGPENDING 포함 */
    struct task_struct  *group_exec_task;
};

/* task_struct 내 시그널 관련 필드 */
struct task_struct {
    /* ... */
    struct signal_struct   *signal;     /* 스레드 그룹 공유 */
    struct sighand_struct  *sighand;    /* 핸들러 테이블 (공유) */
    struct sigpending      pending;     /* 스레드 개별 대기 시그널 */
    sigset_t               blocked;     /* 블록된 시그널 마스크 */
    sigset_t               real_blocked;
    sigset_t               saved_sigmask;
    unsigned long          sas_ss_sp;   /* 대체 시그널 스택 포인터 */
    size_t                 sas_ss_size; /* 대체 시그널 스택 크기 */
    /* ... */
};
💡

두 가지 pending 큐: task_struct.pending은 특정 스레드에게 보낸 시그널(tkill/tgkill)을, signal_struct.shared_pending은 프로세스 전체에 보낸 시그널(kill)을 저장합니다. 시그널 전달 시 두 큐를 모두 확인합니다.

시그널 전송

시그널 전송은 사용자 공간의 시스템 콜에서 시작하여 커널 내부 함수 체인을 통해 대상 프로세스의 pending 큐에 시그널을 추가하는 과정입니다.

전송 시스템 콜

/* 유저 공간 시스템 콜 인터페이스 */

/* 프로세스(스레드 그룹) 단위 시그널 전송 */
kill(pid_t pid, int sig);
/* pid > 0 : 해당 PID 프로세스
 * pid == 0: 같은 프로세스 그룹의 모든 프로세스
 * pid == -1: 시그널 전송 가능한 모든 프로세스
 * pid < -1 : 프로세스 그룹 ID == |pid|인 모든 프로세스 */

/* 특정 스레드에 시그널 전송 (Linux 전용) */
tkill(pid_t tid, int sig);               /* 구형, race 조건 가능 */
tgkill(pid_t tgid, pid_t tid, int sig);  /* 권장: tgid로 검증 */

/* 시그널 + 데이터 전송 (RT 시그널용) */
sigqueue(pid_t pid, int sig, const union sigval value);

/* 파일 디스크립터 소유자에게 시그널 전송 */
fcntl(fd, F_SETOWN, pid);   /* 소유자 설정 */
fcntl(fd, F_SETSIG, sig);   /* SIGIO 대신 사용할 시그널 */

커널 내부 전송 경로

/* kernel/signal.c - 시그널 전송의 핵심 경로 */

/* kill() 시스템 콜 → 커널 진입점 */
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
    struct kernel_siginfo info;
    prepare_kill_siginfo(sig, &info);
    return kill_something_info(sig, &info, pid);
}

/* kill_something_info → kill_pid_info → group_send_sig_info */
int group_send_sig_info(int sig, struct kernel_siginfo *info,
                        struct task_struct *p, enum pid_type type)
{
    int ret;
    rcu_read_lock();
    ret = check_kill_permission(sig, info, p);  /* 권한 검사 */
    if (!ret)
        ret = do_send_sig_info(sig, info, p, type);
    rcu_read_unlock();
    return ret;
}

/* do_send_sig_info → send_signal_locked */
static int send_signal_locked(int sig, struct kernel_siginfo *info,
                               struct task_struct *t, enum pid_type type)
{
    struct sigpending *pending;
    struct sigqueue *q;

    /* 프로세스 단위(PIDTYPE_TGID) → shared_pending
     * 스레드 단위(PIDTYPE_PID)  → task.pending */
    pending = (type != PIDTYPE_PID) ?
              &t->signal->shared_pending : &t->pending;

    /* 이미 같은 비-RT 시그널이 pending이면 무시 (중복 방지) */
    if (legacy_queue(pending, sig))
        goto ret;

    /* sigqueue 할당 및 큐에 추가 */
    q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
    if (q) {
        list_add_tail(&q->list, &pending->list);
        copy_siginfo(&q->info, info);
    }
    sigaddset(&pending->signal, sig);

    /* 대상 스레드 깨우기 */
    complete_signal(sig, t, type);
    return 0;
}

complete_signal - 대상 스레드 선택

/* kernel/signal.c */
static void complete_signal(int sig, struct task_struct *p,
                             enum pid_type type)
{
    struct task_struct *t, *signal_target;

    /* 프로세스 단위 시그널: 시그널을 블록하지 않는 스레드를 찾음 */
    signal_target = p;
    if (wants_signal(sig, p))
        goto found;

    /* 메인 스레드가 블록하면 다른 스레드를 순회 */
    t = p;
    while ((t = next_thread(t)) != p) {
        if (wants_signal(sig, t)) {
            signal_target = t;
            goto found;
        }
    }
    return;  /* 모든 스레드가 블록 → 시그널은 pending으로 남음 */

found:
    /* TIF_SIGPENDING 설정 → 다음 커널→유저 전환 시 시그널 처리 */
    signal_wake_up(signal_target, sig == SIGKILL);
}

/* wants_signal(): 스레드가 시그널을 받을 수 있는지 확인 */
static inline bool wants_signal(int sig, struct task_struct *p)
{
    if (sigismember(&p->blocked, sig))
        return false;      /* 블록된 시그널 */
    if (p->flags & PF_EXITING)
        return false;      /* 종료 중 */
    if (sig == SIGKILL)
        return true;       /* SIGKILL은 항상 전달 */
    if (task_is_stopped_or_traced(p))
        return false;      /* 중지/추적 중 */
    return task_curr(p) || !task_sigpending(p);
}
⚠️

legacy_queue() 함수는 비-RT 시그널(1~31)에 대해 이미 같은 시그널이 pending이면 새 시그널을 무시합니다. 따라서 표준 시그널은 큐잉되지 않으며, 여러 번 보내도 한 번만 전달됩니다. RT 시그널(32~64)은 이 제한이 없어 큐잉됩니다.

시그널 전달 (Delivery)

시그널 전달은 pending 큐에 있는 시그널을 실제로 처리하는 단계입니다. 커널에서 유저 공간으로 복귀하기 직전(exit_to_user_mode_loop)에 TIF_SIGPENDING 플래그를 확인하고 시그널을 처리합니다.

전달 경로 흐름

시그널 전달 (Delivery) 흐름 syscall/IRQ 리턴 TIF_SIGPENDING 확인 get_signal() dequeue 핸들러? SIG_DFL 기본 동작 종료/코어덤프/중지 핸들러 handle_signal() setup_rt_frame() 유저 스택에 프레임 구성 유저 핸들러
시그널이 커널에서 유저 공간 핸들러로 전달되는 과정

get_signal() - 시그널 디큐

/* kernel/signal.c */
bool get_signal(struct ksignal *ksig)
{
    struct task_struct *tsk = current;
    struct sighand_struct *sighand = tsk->sighand;
    int signr;

    for (;;) {
        /* 그룹 중지/종료 처리 */
        if (unlikely(tsk->jobctl & JOBCTL_STOP_PENDING))
            do_jobctl_trap();

        /* pending 큐에서 다음 시그널 가져오기 */
        signr = dequeue_signal(tsk, &tsk->blocked, &ksig->info);
        if (!signr)
            break;  /* pending 시그널 없음 */

        /* 핸들러 확인 */
        struct k_sigaction *ka = &sighand->action[signr - 1];

        if (ka->sa.sa_handler == SIG_IGN)
            continue;  /* 무시 */

        if (ka->sa.sa_handler != SIG_DFL) {
            /* 사용자 핸들러 → handle_signal()로 전달 */
            ksig->ka = *ka;
            if (ka->sa.sa_flags & SA_ONESHOT)
                ka->sa.sa_handler = SIG_DFL;
            return true;
        }

        /* SIG_DFL: 기본 동작 수행 */
        if (sig_kernel_ignore(signr))
            continue;
        if (sig_kernel_stop(signr))
            do_signal_stop(signr);
        if (sig_kernel_coredump(signr))
            do_coredump(&ksig->info);
        do_group_exit(signr);  /* 종료 */
    }
    return false;
}

/* dequeue_signal: 두 pending 큐를 모두 확인 */
int dequeue_signal(struct task_struct *tsk, sigset_t *mask,
                    struct kernel_siginfo *info)
{
    int signr;
    /* 스레드 개별 pending 먼저 확인 */
    signr = __dequeue_signal(&tsk->pending, mask, info);
    if (!signr)
        /* 프로세스 공유 pending 확인 */
        signr = __dequeue_signal(&tsk->signal->shared_pending, mask, info);
    return signr;
}

handle_signal() - 유저 핸들러 설정

/* arch/x86/kernel/signal.c */
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
    /* 1. 유저 스택에 signal frame 구성 */
    bool failed = setup_rt_frame(ksig, regs);

    if (failed) {
        force_sigsegv(ksig->sig);
        return;
    }

    /* 2. 시그널 블록 마스크 갱신 */
    signal_setup_done(failed, ksig, stepping());
    /*
     * signal_setup_done()이 수행하는 작업:
     * - sa_mask에 지정된 시그널을 blocked에 추가
     * - SA_NODEFER가 없으면 현재 시그널도 blocked에 추가
     * → 핸들러 실행 중 같은 시그널의 재진입 방지
     */
}

Signal Frame

시그널 핸들러를 호출하려면 커널이 유저 스택에 signal frame을 구성해야 합니다. 이 프레임에는 핸들러 리턴 후 원래 실행을 복구하기 위한 모든 정보가 들어있습니다.

rt_sigframe 구조 (x86_64)

/* arch/x86/include/asm/sigframe.h */
struct rt_sigframe {
    char __user         *pretcode;    /* sigreturn trampoline 주소 */
    struct ucontext     uc;           /* 실행 컨텍스트 */
    struct siginfo      info;         /* SA_SIGINFO일 때 siginfo */
};

struct ucontext {
    unsigned long       uc_flags;
    struct ucontext     *uc_link;
    stack_t             uc_stack;     /* 대체 시그널 스택 정보 */
    struct sigcontext   uc_mcontext;  /* 레지스터 상태 */
    sigset_t            uc_sigmask;   /* 블록 마스크 */
};

/* sigcontext: 유저 공간 레지스터 저장 */
struct sigcontext {
    __u64 r8, r9, r10, r11, r12, r13, r14, r15;
    __u64 rdi, rsi, rbp, rbx, rdx, rcx, rax;
    __u64 trapno, err;
    __u64 rip;               /* 복귀 주소 (인터럽트된 지점) */
    __u64 cs, eflags, rsp, ss;
    struct _fpstate *fpstate; /* FPU/SSE/AVX 상태 */
};

setup_rt_frame 동작

/* arch/x86/kernel/signal.c (간략화) */
static int setup_rt_frame(struct ksignal *ksig,
                           struct pt_regs *regs)
{
    struct rt_sigframe __user *frame;

    /* 1. 유저 스택에 프레임 공간 확보 */
    frame = get_sigframe(ksig, regs, sizeof(*frame));
    /* SA_ONSTACK이면 대체 스택(sigaltstack) 사용 */

    /* 2. siginfo 복사 (SA_SIGINFO인 경우) */
    copy_siginfo_to_user(&frame->info, &ksig->info);

    /* 3. ucontext 저장: 레지스터, 시그널 마스크, FPU 상태 */
    setup_sigcontext(&frame->uc.uc_mcontext, regs);
    __put_user(current->blocked, &frame->uc.uc_sigmask);
    save_fpu_state(&frame->uc.uc_mcontext);

    /* 4. pretcode에 VDSO sigreturn trampoline 주소 설정 */
    __put_user(vdso_sigreturn, &frame->pretcode);

    /* 5. pt_regs 수정: 핸들러가 실행되도록 설정 */
    regs->sp  = (unsigned long)frame;    /* 스택 → 프레임 */
    regs->ip  = (unsigned long)ksig->ka.sa.sa_handler; /* RIP → 핸들러 */
    regs->di  = ksig->sig;               /* 1번째 인자: 시그널 번호 */
    regs->si  = (unsigned long)&frame->info;  /* 2번째: siginfo* */
    regs->dx  = (unsigned long)&frame->uc;    /* 3번째: ucontext* */

    return 0;
}
ℹ️

스택 프레임 구성 후 유저 복귀: 커널은 pt_regs의 RIP를 시그널 핸들러 주소로, RSP를 signal frame으로 변경합니다. 유저 공간으로 복귀하면 마치 핸들러가 호출된 것처럼 실행이 이어집니다. 핸들러가 리턴하면 스택의 pretcode (VDSO sigreturn trampoline)로 점프하여 rt_sigreturn 시스템 콜을 호출합니다.

sigreturn 시스템 콜

/* arch/x86/kernel/signal.c */
SYSCALL_DEFINE0(rt_sigreturn)
{
    struct pt_regs *regs = current_pt_regs();
    struct rt_sigframe __user *frame;
    sigset_t set;

    frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long));

    /* 1. signal frame에서 블록 마스크 복원 */
    __get_user(set, &frame->uc.uc_sigmask);
    set_current_blocked(&set);

    /* 2. 저장된 레지스터 복원 (RIP, RSP 포함) */
    restore_sigcontext(regs, &frame->uc.uc_mcontext);

    /* 3. FPU 상태 복원 */
    restore_fpu_state(&frame->uc.uc_mcontext);

    /* 원래 인터럽트된 지점으로 복귀 */
    return regs->ax;
}

VDSO sigreturn trampoline

설명 요약:
  • arch/x86/entry/vdso/vdso32/sigreturn.S (32-bit 예시) */
  • VDSO에 매핑된 sigreturn 트램펄린 코드 */
  • 핸들러가 return하면 스택에서 pretcode 주소를 pop하여 여기로 점프합니다.
  • 이 코드는 rt_sigreturn 시스템 콜을 호출하여 원래 실행을 복원합니다.
  • __vdso_rt_sigreturn:
  • mov $__NR_rt_sigreturn, %eax
  • syscall (또는 int $0x80)
  • VDSO를 사용하는 이유:
  • 유저 스택에 실행 코드를 넣으면 NX(No-Execute) 보호와 충돌
  • VDSO는 커널이 유저 주소 공간에 매핑한 읽기+실행 페이지
  • 모든 프로세스가 공유하므로 메모리 효율적
  • ASLR 적용으로 주소가 프로세스마다 다름

SROP (Sigreturn-Oriented Programming) 공격: 공격자가 가짜 signal frame을 스택에 구성하고 sigreturn을 호출하면 임의의 레지스터 값을 설정할 수 있습니다. 이에 대한 대응으로 Linux는 signal frame에 cookie/canary 검증을 추가하고 있으며, x86에서는 shadow stack (CET)이 추가 보호를 제공합니다.

시그널 마스킹

프로세스는 sigprocmask()를 사용하여 특정 시그널의 전달을 일시적으로 차단(블록)할 수 있습니다. 블록된 시그널은 사라지지 않고 pending 상태로 유지되다가 블록 해제 시 전달됩니다.

sigprocmask 시스템 콜

/* 유저 공간 인터페이스 */
#include <signal.h>

sigset_t mask, oldmask;

sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);

/* SIG_BLOCK: 기존 블록 마스크에 추가 */
sigprocmask(SIG_BLOCK, &mask, &oldmask);

/* 크리티컬 섹션: SIGINT, SIGTERM이 전달되지 않음 */
do_critical_work();

/* SIG_SETMASK: 이전 마스크로 복원 */
sigprocmask(SIG_SETMASK, &oldmask, NULL);
/* 이 시점에서 pending이었던 SIGINT/SIGTERM이 전달됨 */

/* SIG_UNBLOCK: 특정 시그널만 블록 해제 */
sigprocmask(SIG_UNBLOCK, &mask, NULL);

커널 내부 마스킹 처리

/* kernel/signal.c */
SYSCALL_DEFINE4(rt_sigprocmask, int, how, sigset_t __user *, nset,
                sigset_t __user *, oset, size_t, sigsetsize)
{
    sigset_t old_set, new_set;

    if (oset)
        old_set = current->blocked;

    if (nset) {
        copy_from_user(&new_set, nset, sizeof(sigset_t));
        /* SIGKILL, SIGSTOP은 절대 블록 불가 */
        sigdelsetmask(&new_set, sigmask(SIGKILL) | sigmask(SIGSTOP));

        switch (how) {
        case SIG_BLOCK:
            sigorsets(&new_set, ¤t->blocked, &new_set);
            break;
        case SIG_UNBLOCK:
            sigandnsets(&new_set, ¤t->blocked, &new_set);
            break;
        case SIG_SETMASK:
            break;
        }
        set_current_blocked(&new_set);
        /* → recalc_sigpending() 호출: TIF_SIGPENDING 재계산 */
    }
}

SA_FLAGS 플래그

플래그설명
SA_SIGINFO3인자 핸들러 사용: void handler(int sig, siginfo_t *info, void *ucontext)
SA_RESTART시그널로 인터럽트된 시스템 콜을 자동 재시작
SA_NODEFER핸들러 실행 중 같은 시그널을 블록하지 않음 (재진입 허용)
SA_RESETHAND핸들러 실행 후 SIG_DFL로 리셋 (일회성)
SA_ONSTACKsigaltstack으로 설정한 대체 스택에서 핸들러 실행
SA_NOCLDSTOP자식이 중지될 때 SIGCHLD를 받지 않음
SA_NOCLDWAIT자식이 종료 시 좀비 생성 안 함 (자동 reap)
SA_RESTORERsa_restorer 필드가 유효함 (libc가 VDSO 주소 설정)
💡

SA_RESTART와 시스템 콜: SA_RESTART가 설정되면 read(), write(), waitpid() 등 느린(slow) 시스템 콜이 시그널에 의해 중단된 후 자동으로 재시작됩니다. 그렇지 않으면 -EINTR을 반환하며, 유저 프로그램이 직접 재시도 루프를 구현해야 합니다.

실시간 시그널 (RT Signals)

POSIX 실시간 시그널(SIGRTMIN~SIGRTMAX, 일반적으로 32~64)은 표준 시그널의 한계를 극복합니다.

표준 시그널 vs RT 시그널

특성표준 시그널 (1~31)RT 시그널 (32~64)
큐잉안 됨 (하나만 pending)됨 (여러 개 큐잉)
전달 순서보장 안 됨시그널 번호 순 (낮은 번호 먼저)
데이터 전달불가sigqueue()로 sigval 전달 가능
의미사전 정의 (SIGTERM 등)애플리케이션 정의
개수 제한없음 (비트마스크)RLIMIT_SIGPENDING 제한

RT 시그널 전송

#include <signal.h>

/* sigqueue()를 이용한 데이터 전달 */
union sigval value;
value.sival_int = 42;
value.sival_ptr = my_data;

sigqueue(target_pid, SIGRTMIN + 3, value);

/* 핸들러에서 데이터 수신 (SA_SIGINFO 필수) */
void rt_handler(int sig, siginfo_t *info, void *ucontext)
{
    int data = info->si_value.sival_int;  /* 42 */
    pid_t sender = info->si_pid;          /* 보낸 프로세스 PID */
    uid_t uid = info->si_uid;              /* 보낸 프로세스 UID */
    printf("RT signal %d from PID %d, data=%d\\n",
           sig, sender, data);
}

/* 핸들러 등록 */
struct sigaction sa;
sa.sa_sigaction = rt_handler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGRTMIN + 3, &sa, NULL);

커널 내부 RT 시그널 처리

/* kernel/signal.c - legacy_queue()에서 RT 시그널 구분 */
static inline bool legacy_queue(struct sigpending *signals, int sig)
{
    /* RT 시그널(sig >= 32)은 항상 큐잉 → false 반환 */
    return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
}

/* __dequeue_signal에서 RT 시그널의 우선순위 처리 */
/*
 * next_signal()은 sigset_t에서 가장 낮은 번호의 설정된 비트를 반환합니다.
 * 따라서 RT 시그널 중 SIGRTMIN이 SIGRTMAX보다 먼저 전달됩니다.
 * 같은 번호의 RT 시그널이 여러 개 큐잉된 경우 FIFO 순서로 전달됩니다.
 */

/* RLIMIT_SIGPENDING: 큐잉 가능한 시그널 수 제한 */
/* 프로세스당 최대 sigqueue 수를 제한하여 DoS 방지 */
/* ulimit -i 로 확인/설정, 기본값은 보통 약 128000 */
⚠️

RLIMIT_SIGPENDING 초과 시: sigqueue 할당이 실패하면 시그널은 여전히 전달되지만 siginfo 데이터는 손실될 수 있습니다. si_codeSI_USER로 설정되어 수신 측에서 데이터 유무를 구분할 수 없게 됩니다.

signalfd

signalfd는 시그널을 파일 디스크립터를 통해 수신하는 메커니즘입니다. epoll, select, poll과 통합하여 이벤트 루프 기반 프로그래밍에서 시그널을 통합 처리할 수 있습니다.

signalfd 사용법

#include <sys/signalfd.h>
#include <sys/epoll.h>
#include <signal.h>

/* 1. 대상 시그널을 블록 (핸들러 전달 방지) */
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL);

/* 2. signalfd 생성 */
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);

/* 3. epoll에 등록 */
int epfd = epoll_create1(EPOLL_CLOEXEC);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = sfd };
epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);

/* 4. 이벤트 루프에서 시그널 읽기 */
struct signalfd_siginfo fdsi;
while (1) {
    int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == sfd) {
            ssize_t s = read(sfd, &fdsi, sizeof(fdsi));
            if (s == sizeof(fdsi)) {
                printf("Signal %d from PID %d\\n",
                       fdsi.ssi_signo, fdsi.ssi_pid);
                if (fdsi.ssi_signo == SIGTERM)
                    graceful_shutdown();
            }
        }
    }
}

signalfd 커널 구현

/* fs/signalfd.c */

/* signalfd_poll(): epoll이 호출하는 poll 콜백 */
static __poll_t signalfd_poll(struct file *file,
                              struct poll_table_struct *wait)
{
    struct signalfd_ctx *ctx = file->private_data;
    __poll_t events = 0;

    /* sighand->signalfd_wqh에 등록 → 시그널 전달 시 깨어남 */
    poll_wait(file, ¤t->sighand->signalfd_wqh, wait);

    /* pending 시그널 중 관심 있는 것이 있으면 POLLIN */
    if (next_signal(¤t->pending, &ctx->sigmask) ||
        next_signal(¤t->signal->shared_pending, &ctx->sigmask))
        events |= EPOLLIN;

    return events;
}

/* signalfd_read(): read() 시 호출 */
/* dequeue_signal()을 호출하여 pending 큐에서 시그널을 꺼내고,
 * signalfd_siginfo 구조체로 변환하여 유저 버퍼에 복사합니다.
 * 시그널이 없고 O_NONBLOCK이 아니면 대기합니다. */
💡

signalfd의 장점: (1) 시그널 핸들러의 비동기성 문제 회피 - 핸들러 내에서 async-signal-safe 함수만 호출해야 하는 제약이 없음. (2) epoll/select로 I/O 이벤트와 시그널을 통합 대기. (3) 멀티스레드 환경에서 특정 스레드가 시그널을 처리하도록 구성 용이.

시그널과 스레드

POSIX 스레드(pthread) 모델에서 시그널 처리는 복잡합니다. 시그널은 프로세스 수준과 스레드 수준의 두 가지 관점을 가집니다.

프로세스 vs 스레드 시그널

리눅스 스레드 시그널 모델 프로세스 (thread group) — 공유 구조 signal_struct (공유) shared_pending ← kill()로 보낸 시그널 sighand_struct (공유) action[64] ← 핸들러 테이블 (전 스레드 공유) kill(pid, SIGTERM) → shared_pending에 추가 → 블록되지 않은 임의 스레드가 처리 pthread_kill(tid, SIGIO) / tgkill → 특정 스레드의 pending에 직접 추가 Thread 1 (task_struct) pending (개별 시그널) blocked (블록 마스크) saved_sigmask Thread 2 pending blocked (독립) Thread 3 pending blocked (독립) 시그널 전달 흐름 kill() → signal_struct.shared_pending → 임의 스레드 처리 (blocked 확인) tgkill() → task_struct.pending → 지정 스레드만 처리 시그널 처리: 스케줄 반환 시 do_signal() → sigaction 핸들러 또는 기본 동작 ④ sigprocmask() → blocked 비트맵 조작 (RT 시그널 큐잉, 일반 시그널은 1개만 펜딩)

멀티스레드 시그널 처리 패턴

/* 권장 패턴: 전용 시그널 처리 스레드 */

static void *signal_thread(void *arg)
{
    sigset_t *mask = arg;
    int sig;

    for (;;) {
        /* sigwait: 동기적으로 시그널 대기 */
        sigwait(mask, &sig);
        switch (sig) {
        case SIGTERM:
            initiate_shutdown();
            return NULL;
        case SIGHUP:
            reload_config();
            break;
        case SIGCHLD:
            reap_children();
            break;
        }
    }
}

int main(void)
{
    sigset_t mask;
    pthread_t sig_thread;

    /* 메인 스레드에서 모든 시그널 블록 */
    sigfillset(&mask);
    pthread_sigmask(SIG_BLOCK, &mask, NULL);
    /* 이후 생성되는 모든 스레드도 이 마스크를 상속 */

    /* 시그널 전용 스레드 생성 */
    sigemptyset(&mask);
    sigaddset(&mask, SIGTERM);
    sigaddset(&mask, SIGHUP);
    sigaddset(&mask, SIGCHLD);
    pthread_create(&sig_thread, NULL, signal_thread, &mask);

    /* 워커 스레드들 생성 (시그널 블록 상태) */
    create_worker_threads();

    pthread_join(sig_thread, NULL);
    return 0;
}
⚠️

주의: signal()/sigaction()으로 설정한 핸들러는 프로세스 전체에 적용됩니다. 멀티스레드 프로그램에서 시그널 핸들러를 사용하면 어느 스레드에서 핸들러가 실행될지 예측하기 어렵습니다. 전용 스레드 + sigwait() 또는 signalfd 패턴을 권장합니다.

커널 내부 시그널 사용

커널 자체도 여러 서브시스템에서 시그널을 활용합니다. 유저 공간 프로세스에 중요한 이벤트를 알리거나 강제 종료하는 데 사용됩니다.

SIGKILL/SIGSTOP 특수 처리

/* SIGKILL과 SIGSTOP은 커널이 특별히 처리합니다:
 *
 * 1. 핸들러 설치 불가: sigaction()에서 SIG_DFL 강제
 * 2. 블록 불가: sigprocmask()에서 자동 제거
 * 3. 무시 불가: SIG_IGN 설정이 거부됨
 */

/* kernel/signal.c - do_sigaction() */
int do_sigaction(int sig, struct k_sigaction *act,
                  struct k_sigaction *oact)
{
    /* SIGKILL, SIGSTOP에 대한 핸들러 변경 거부 */
    if (sig == SIGKILL || sig == SIGSTOP)
        return -EINVAL;
    /* ... */
}

/* SIGKILL 전달 시: 프로세스의 모든 스레드에 전파 */
static void complete_signal(int sig, struct task_struct *p, ...)
{
    if (sig_fatal(p, sig)) {
        /* SIGKILL: 모든 스레드에 TIF_SIGPENDING 설정 */
        /* signal->flags |= SIGNAL_GROUP_EXIT */
        signal_wake_up(t, 1);  /* 1 = SIGKILL, 대기 중이면 강제 깨움 */
    }
}

OOM Killer와 시그널

/* mm/oom_kill.c */
static void oom_kill_process(struct oom_control *oc,
                             const char *message)
{
    struct task_struct *victim = oc->chosen;

    /* TIF_MEMDIE 설정: 메모리 할당 우선권 부여 */
    mark_oom_victim(victim);

    /* SIGKILL 전송: 프로세스 종료하여 메모리 회수 */
    do_send_sig_info(SIGKILL, SEND_SIG_PRIV, victim, PIDTYPE_TGID);

    /* 자식 프로세스에도 SIGKILL 전송 가능 */
    pr_err("Killed process %d (%s) total-vm:%lukB\\n",
           task_pid_nr(victim), victim->comm,
           victim->mm->total_vm << (PAGE_SHIFT - 10));
}

코어 덤프 생성

/* fs/coredump.c */
void do_coredump(const struct kernel_siginfo *siginfo)
{
    /* 코어 덤프를 유발하는 시그널:
     * SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGFPE,
     * SIGSEGV, SIGBUS, SIGSYS, SIGXCPU, SIGXFSZ */

    /* 1. 스레드 그룹의 모든 스레드 중지 */
    zap_threads(tsk, mm, core_state, exit_code);

    /* 2. 코어 파일 생성 (core_pattern에 따라) */
    /* /proc/sys/kernel/core_pattern 설정:
     *   "core"                    → 현재 디렉토리에 core 파일
     *   "/var/cores/core.%e.%p"   → 프로그램명.PID
     *   "|/usr/bin/coredumpctl"   → 파이프로 외부 프로그램에 전달
     */

    /* 3. 바이너리 포맷 핸들러의 core_dump() 호출 */
    /* ELF: elf_core_dump() → PT_NOTE + 레지스터 + 메모리 매핑 */
}
💡

코어 덤프 제어: ulimit -c unlimited로 코어 파일 크기 제한 해제, /proc/sys/kernel/core_pattern으로 파일 경로 패턴 설정, prctl(PR_SET_DUMPABLE, 1)로 setuid 프로세스 코어 덤프 허용. systemd 환경에서는 coredumpctl을 통해 관리합니다.

시그널 보안

시그널 전송에는 권한 검사가 필수입니다. 무분별한 시그널 전송은 DoS 공격이나 권한 상승에 악용될 수 있기 때문입니다.

권한 검사

/* kernel/signal.c */
static int check_kill_permission(int sig,
    struct kernel_siginfo *info, struct task_struct *t)
{
    struct pid *sid;
    int error;

    /* 시그널 번호 유효성 검사 */
    if (!valid_signal(sig))
        return -EINVAL;

    /* 커널 내부 시그널은 항상 허용 */
    if (!si_fromuser(info))
        return 0;

    error = audit_signal_info(sig, t);  /* 감사 로그 */
    if (error)
        return error;

    /* POSIX 권한 검사:
     * - 같은 UID (real 또는 saved set-user-ID)
     * - CAP_KILL capability
     * - 같은 세션의 SIGCONT */
    if (!same_thread_group(current, t) &&
        !kill_ok_by_cred(t)) {
        /* LSM (SELinux/AppArmor) 검사 */
        error = security_task_kill(t, info, sig, NULL);
        if (error)
            return error;
    }
    return 0;
}

PID 네임스페이스와 시그널

/*
 * PID 네임스페이스 경계에서의 시그널 전송 규칙:
 *
 * 1. 자식 네임스페이스 → 부모 네임스페이스:
 *    - 부모 NS의 프로세스 PID를 알 수 없으므로 불가
 *    - 네임스페이스 init(PID 1)에 시그널은 가능하나
 *      핸들러가 설치된 시그널만 전달됨
 *
 * 2. 부모 네임스페이스 → 자식 네임스페이스:
 *    - 정상적으로 전달됨
 *    - 자식 NS의 init 프로세스에 SIGKILL 가능 (NS 전체 종료)
 *
 * 3. 네임스페이스 init (PID 1) 보호:
 *    - 자기 NS 내에서: 핸들러 없는 시그널은 무시 (SIGKILL/SIGSTOP 포함)
 *    - 부모 NS에서: SIGKILL/SIGSTOP은 전달됨
 */

/* kernel/signal.c - sig_task_ignored() */
static bool sig_task_ignored(struct task_struct *t, int sig,
                              bool force)
{
    /* 네임스페이스 init 프로세스 보호 */
    if (is_global_init(t) || is_child_reaper(task_pid(t)))
        if (!force && sig_handler_ignored(handler, sig))
            return true;
    return false;
}

seccomp과 시그널

/* seccomp 필터는 시스템 콜을 검사하여 시그널을 발생시킬 수 있습니다 */

/* seccomp 동작 중 시그널 관련 */
/* SECCOMP_RET_KILL_THREAD → SIGSYS 전달 (스레드 종료) */
/* SECCOMP_RET_KILL_PROCESS → SIGSYS 전달 (프로세스 종료) */
/* SECCOMP_RET_TRAP → SIGSYS 전달 (핸들러에서 처리 가능) */

/* seccomp과 시그널 시스템 콜 필터링 예시 */
/* kill(), tgkill(), rt_sigaction() 등을 필터링하여
 * 컨테이너 내 프로세스가 보낼 수 있는 시그널을 제한 가능 */

/* SIGSYS 핸들러에서 seccomp 위반 정보 확인 */
void sigsys_handler(int sig, siginfo_t *info, void *ucontext)
{
    /* info->si_syscall: 거부된 시스템 콜 번호 */
    /* info->si_arch: 아키텍처 (AUDIT_ARCH_X86_64 등) */
    fprintf(stderr, "Blocked syscall %d\\n", info->si_syscall);
}

시그널 기반 공격 벡터: (1) SROP: 앞서 설명한 sigreturn 악용. (2) 시그널 race condition: 시그널 핸들러 내에서 non-reentrant 함수 호출로 인한 취약점. (3) TOCTOU: 시그널 핸들러가 공유 상태를 수정하는 동안 메인 코드에서 같은 상태를 읽는 race. async-signal-safe 함수만 핸들러 내에서 호출하는 것이 근본적 대응입니다.

디버깅

시그널 관련 문제는 비동기적 특성 때문에 재현과 진단이 어려울 수 있습니다. 다음 도구와 기법을 활용하세요.

strace를 이용한 시그널 추적

# 시그널 관련 시스템 콜만 추적
strace -e trace=signal -p <PID>

# 출력 예시:
# rt_sigaction(SIGTERM, {sa_handler=0x4011a0, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7f...}, ...) = 0
# rt_sigprocmask(SIG_BLOCK, [INT], [], 8) = 0
# --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=1234, si_uid=1000} ---
# rt_sigreturn({mask=[]}) = 0

# 시그널 전달 과정 상세 추적
strace -e trace=signal -e signal=all -f -p <PID>

# -f: fork된 자식도 추적
# -e signal=all: 모든 시그널 표시
# 특정 시그널만: -e signal=SIGTERM,SIGCHLD

proc 파일시스템으로 시그널 상태 확인

# /proc/<pid>/status에서 시그널 정보 확인
cat /proc/1234/status | grep -i sig

# SigQ:   1/128206     ← 현재 큐잉된 시그널 수 / 최대 제한
# SigPnd: 0000000000000000  ← 스레드별 pending 시그널 (비트마스크)
# ShdPnd: 0000000000004000  ← 프로세스 shared pending (비트 14 = SIGALRM)
# SigBlk: 0000000000010000  ← 블록된 시그널 (비트 16 = SIGSTKFLT)
# SigIgn: 0000000000000004  ← 무시된 시그널 (비트 2 = SIGQUIT)
# SigCgt: 0000000180004002  ← 핸들러 설치된 시그널

# 비트마스크 해석: 비트 N은 시그널 N을 의미
# 예: 0x4000 = 비트 14 = SIGALRM

# 대체 시그널 스택 정보
cat /proc/1234/status | grep SigAlt
# (sigaltstack이 설정된 경우 표시)

# 모든 스레드의 시그널 상태 확인
ls /proc/1234/task/
# 각 TID 디렉토리의 status를 확인
for tid in /proc/1234/task/*; do
    echo "=== $(basename $tid) ==="
    grep Sig "$tid/status"
done

ftrace로 시그널 추적

# 시그널 생성(generate) 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/signal/signal_generate/enable

# 시그널 전달(deliver) 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/signal/signal_deliver/enable

# 추적 결과 확인
cat /sys/kernel/debug/tracing/trace

# 출력 예시:
# bash-1234  [002] signal_generate: sig=15 errno=0 code=0 comm=myapp pid=5678
# myapp-5678 [001] signal_deliver:  sig=15 errno=0 code=0 sa_handler=4011a0 sa_flags=14000004

# 특정 프로세스만 필터링
echo 'common_pid == 5678' > /sys/kernel/debug/tracing/events/signal/signal_generate/filter
echo 'common_pid == 5678' > /sys/kernel/debug/tracing/events/signal/signal_deliver/filter

# 추적 비활성화
echo 0 > /sys/kernel/debug/tracing/events/signal/enable

시그널 관련 일반적인 문제와 해결

증상원인해결
kill이 안 먹힘시그널이 블록됨 (SigBlk 확인)SIGKILL 사용 (블록 불가)
좀비 프로세스부모가 SIGCHLD를 처리 안 함SA_NOCLDWAIT 또는 waitpid()
SIGPIPE로 서버 종료클라이언트 연결 끊김 후 writesignal(SIGPIPE, SIG_IGN)
EINTR 오류 반복시스템 콜이 시그널로 중단SA_RESTART 또는 재시도 루프
핸들러 내 데드락핸들러에서 non-reentrant 함수async-signal-safe 함수만 사용
SIGSEGV 무한 루프SIGSEGV 핸들러가 원인을 해결 못함핸들러에서 _exit() 또는 longjmp
멀티스레드 시그널 누락잘못된 스레드가 시그널 수신전용 시그널 스레드 패턴 사용
💡

async-signal-safe 함수 목록: POSIX에서 시그널 핸들러 내에서 안전하게 호출할 수 있는 함수는 제한되어 있습니다. write(), _exit(), signal(), sigaction() 등은 안전하지만, printf(), malloc(), pthread_mutex_lock() 등은 안전하지 않습니다. 전체 목록은 man 7 signal-safety를 참조하세요.

sigaltstack을 이용한 스택 오버플로 디버깅

/* 대체 시그널 스택 설정 - 스택 오버플로 시 SIGSEGV를 처리 */
#include <signal.h>
#include <stdlib.h>

void setup_alt_stack(void)
{
    stack_t ss;

    /* MINSIGSTKSZ: 최소 시그널 스택 크기 (보통 2KB) */
    /* SIGSTKSZ: 권장 시그널 스택 크기 (보통 8KB) */
    ss.ss_sp = malloc(SIGSTKSZ);
    ss.ss_size = SIGSTKSZ;
    ss.ss_flags = 0;
    sigaltstack(&ss, NULL);

    /* SIGSEGV 핸들러를 대체 스택에서 실행 */
    struct sigaction sa;
    sa.sa_sigaction = segv_handler;
    sa.sa_flags = SA_SIGINFO | SA_ONSTACK;  /* SA_ONSTACK 필수 */
    sigemptyset(&sa.sa_mask);
    sigaction(SIGSEGV, &sa, NULL);
}

void segv_handler(int sig, siginfo_t *info, void *ucontext)
{
    /* info->si_addr: 폴트가 발생한 주소 */
    /* 스택 오버플로: si_addr이 스택 근처 */
    /* 일반 SEGV: si_addr이 접근한 잘못된 주소 */

    /* 에러 메시지 출력 (write는 async-signal-safe) */
    const char msg[] = "Stack overflow detected!\\n";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);
    _exit(1);
}

시그널 전달 상태 머신

시그널은 생성(generation)부터 처리(handling)까지 명확한 상태 전이를 거칩니다. 이 상태 머신을 이해하면 시그널이 "사라지는" 현상이나 예기치 않은 지연의 원인을 파악할 수 있습니다.

상태 전이 개요

시그널 전달 상태 머신 Generated (생성) kill()/tgkill()/커널 내부 send_signal_locked() Pending (대기) sigpending.signal 비트 설정 sigqueue 리스트에 추가 Blocked (차단) sigprocmask로 블록됨 언블록 시 dequeue_signal() Dequeued (디큐) get_signal()에서 추출 핸들러 확인 SIG_IGN Ignored (폐기) SIG_DFL Default Action 종료/코어덤프/중지/무시 사용자 핸들러 handle_signal() setup_rt_frame() User Handler 유저 공간 실행 rt_sigreturn() 원래 실행 복귀
시그널의 생성(Generation)부터 처리(Handling)까지의 전체 상태 전이

상태 전이 상세

/* 시그널 상태 전이의 핵심 포인트 */

/* 1. Generated → Pending
 * send_signal_locked()에서 sigpending에 추가
 * legacy_queue()가 표준 시그널 중복 방지 */
static int send_signal_locked(int sig, ...)
{
    /* 비-RT 시그널이 이미 pending이면 → 생성 즉시 폐기 */
    if (legacy_queue(pending, sig))
        return 0;  /* 상태: 생성 → (폐기) */

    /* sigqueue 할당 후 리스트에 추가 */
    sigaddset(&pending->signal, sig);  /* 상태: → Pending */

    /* TIF_SIGPENDING 설정으로 커널→유저 전환 시 확인 유도 */
    complete_signal(sig, t, type);
}

/* 2. Pending → Blocked (sigprocmask로 블록된 경우)
 * 시그널은 pending 큐에 남아있지만 전달되지 않음
 * 블록 해제 시 recalc_sigpending()이 TIF_SIGPENDING 재설정 */

/* 3. Pending → Dequeued
 * get_signal() → dequeue_signal()
 * private pending 먼저, 그 다음 shared pending 확인 */

/* 4. Dequeued → Handler 또는 Default Action
 * sighand->action[sig-1] 검사
 * SIG_DFL: sig_kernel_stop/coredump/ignore/terminate 분기
 * SIG_IGN: 시그널 폐기
 * 사용자 핸들러: handle_signal() → setup_rt_frame() */

/* 5. Handler → Return (rt_sigreturn)
 * 핸들러 완료 후 VDSO trampoline → rt_sigreturn 시스템 콜
 * 원래 레지스터/마스크 복원 → 인터럽트된 지점으로 복귀 */
상태 머신과 TIF_SIGPENDING: TIF_SIGPENDING 플래그는 커널이 유저 공간으로 복귀할 때마다 확인합니다. 이 플래그가 설정되어 있어야 get_signal()이 호출됩니다. recalc_sigpending()은 pending 큐와 blocked 마스크를 비교하여 전달 가능한 시그널이 있을 때만 이 플래그를 설정합니다.

상태별 타이밍 특성

상태지속 시간관찰 방법
Generated수 나노초 (send_signal_locked 내부)ftrace signal_generate 이벤트
Pending다음 커널→유저 전환까지 (수 마이크로~밀리초)/proc/PID/status SigPnd/ShdPnd
Blocked+Pending블록 해제까지 (무한정 가능)/proc/PID/status SigBlk 교차 확인
Dequeued→Delivered수 마이크로초ftrace signal_deliver 이벤트
Handler 실행핸들러 로직에 의존strace 또는 핸들러 내 타임스탬프

sigpending 구조체 심화

struct sigpending은 시그널 시스템의 핵심 자료구조입니다. 프로세스(shared)와 스레드(private) 두 종류의 pending 큐가 있으며, 각각 다른 시나리오에서 사용됩니다.

sigpending 구조체: shared vs private task_struct (스레드) pending (private sigpending) tgkill()/pthread_kill()로 도착한 시그널 signal->shared_pending kill()로 도착한 프로세스 단위 시그널 struct sigpending (private) signal (sigset_t): 비트 1~64 1 2 3 ... 15 ... 34 34 ... 64 list (list_head) → sigqueue 연결 리스트 RT 시그널: 여러 sigqueue 노드 | 표준: 최대 1개 struct sigpending (shared) signal (sigset_t): 비트 1~64 프로세스 전체 대상 시그널 비트맵 list (list_head) → sigqueue 연결 리스트 모든 스레드 중 블록 안 한 스레드가 처리 dequeue_signal() 우선순위 1순위: task->pending (private) 먼저 확인 2순위: task->signal->shared_pending 확인 두 큐 모두에서 next_signal()로 가장 낮은 번호 시그널 선택 blocked (sigset_t) 마스크 적용 pending & ~blocked = 전달 가능한 시그널 (recalc_sigpending)
sigpending의 private/shared 구조와 dequeue 우선순위

sigpending 연산 상세

/* include/linux/signal.h - 핵심 sigpending 연산 */

/* next_signal(): pending & ~blocked에서 최소 시그널 번호 반환 */
static inline int next_signal(struct sigpending *pending,
                                sigset_t *mask)
{
    unsigned long i, *s, *m, x;
    int sig = 0;

    s = pending->signal.sig;
    m = mask->sig;

    /* _NSIG_WORDS 만큼 순회 (64비트에서 1워드) */
    for (i = 0; i < _NSIG_WORDS; ++i, ++s, ++m) {
        x = *s & ~*m;  /* pending AND NOT blocked */
        if (x) {
            sig = ffz(~x) + i * _NSIG_BPW + 1;
            break;
        }
    }
    return sig;
}

/* __dequeue_signal(): sigpending에서 시그널 하나 추출 */
static int __dequeue_signal(struct sigpending *pending,
                             sigset_t *mask,
                             struct kernel_siginfo *info)
{
    int sig = next_signal(pending, mask);
    if (sig)
        sig = collect_signal(sig, pending, info);
    return sig;
}

/* collect_signal(): sigqueue 리스트에서 해당 시그널의 siginfo 추출 */
static int collect_signal(int sig, struct sigpending *list,
                           struct kernel_siginfo *info)
{
    struct sigqueue *q, *first = NULL;

    /* 리스트에서 해당 시그널의 첫 sigqueue 찾기 */
    list_for_each_entry(q, &list->list, list) {
        if (q->info.si_signo == sig) {
            first = q;
            break;
        }
    }

    if (first) {
        list_del_init(&first->list);
        copy_siginfo(info, &first->info);
        __sigqueue_free(first);

        /* 같은 시그널의 다른 sigqueue가 없으면 비트 클리어 */
        if (!sigismember(&list->signal, sig))
            ;  /* 이미 clear됨 */
        else {
            /* RT 시그널: 같은 번호가 더 있는지 확인 */
            struct sigqueue *next;
            int still_pending = 0;
            list_for_each_entry(next, &list->list, list) {
                if (next->info.si_signo == sig) {
                    still_pending = 1;
                    break;
                }
            }
            if (!still_pending)
                sigdelset(&list->signal, sig);
        }
    }
    return sig;
}
sigpending 잠금 규칙: sigpending 구조체에 접근할 때는 반드시 sighand->siglock을 잡아야 합니다. 이 스핀락은 시그널 전송(send_signal_locked)과 수신(dequeue_signal) 양쪽에서 사용됩니다. 인터럽트 컨텍스트에서도 시그널이 전송될 수 있으므로 spin_lock_irqsave를 사용합니다.

코어 덤프 생성 흐름

코어 덤프는 프로세스가 비정상 종료할 때 메모리 상태를 파일로 저장하는 메커니즘입니다. do_coredump()의 전체 워크스루를 따라가며 커널이 어떻게 ELF 코어 파일을 생성하는지 분석합니다.

코어 덤프 생성 흐름 (do_coredump) 코어 덤프 유발 시그널 SIGQUIT/SIGILL/SIGABRT/SIGSEGV/SIGFPE/... sig_kernel_coredump() 1. do_coredump() 진입 RLIMIT_CORE, dumpable 검사 2. zap_threads() 모든 스레드 중지 + 대기 3. core_pattern 해석 파일 경로 또는 | 파이프 프로그램 파일 경로 |프로그램 filp_open(core_name) 코어 파일 생성 call_usermodehelper() coredumpctl 등 외부 프로그램 4. binfmt->core_dump() elf_core_dump() - ELF 코어 파일 작성 5. ELF 코어 파일 구조 ELF Header | PT_NOTE (레지스터/상태) | PT_LOAD (메모리 매핑) coredump_filter로 덤프할 VMA 유형 제어
do_coredump() 함수의 전체 실행 흐름

do_coredump() 내부 구현

/* fs/coredump.c - do_coredump() 상세 워크스루 */

void do_coredump(const struct kernel_siginfo *siginfo)
{
    struct core_state core_state;
    struct coredump_params cprm;
    struct task_struct *tsk = current;

    /* 1단계: 덤프 가능 여부 검사 */
    if (!__get_dumpable(tsk->mm->flags))
        return;  /* prctl(PR_SET_DUMPABLE, 0) 또는 setuid */

    if (!tsk->signal->rlim[RLIMIT_CORE].rlim_cur)
        return;  /* ulimit -c 0 인 경우 */

    /* 2단계: 동시 코어 덤프 방지 */
    if (!dump_interrupted()) {
        /* coredump_task_exit()가 다른 스레드를 정리할 때까지 대기 */
    }

    /* 3단계: 스레드 그룹 전체 정지 */
    coredump_wait(tsk->mm, &core_state);
    /* zap_threads(): 모든 스레드에 SIGKILL + exit_code 설정
     * 모든 스레드가 exit_mm() 호출까지 대기 */

    /* 4단계: core_pattern 해석 */
    format_corename(&cn, &cprm, &argv, &argc);
    /* core_pattern 치환 문자:
     *   %p = PID, %u = UID, %g = GID
     *   %s = 시그널 번호, %t = 타임스탬프
     *   %h = 호스트명, %e = 실행 파일명
     *   %E = 실행 파일 경로 (/ → !)
     *   %c = RLIMIT_CORE 값 */

    /* 5단계: 파이프 모드 또는 파일 모드 */
    if (cn.corename[0] == '|') {
        /* 파이프: call_usermodehelper_exec()
         * 예: |/usr/lib/systemd/systemd-coredump %P %u %g %s ... */
        call_usermodehelper_setup(cn.corename + 1, ...);
    } else {
        /* 파일: filp_open()으로 코어 파일 생성 */
        cprm.file = filp_open(cn.corename, O_CREAT|O_WRONLY, 0600);
    }

    /* 6단계: 바이너리 포맷 핸들러의 core_dump() 호출 */
    cprm.mm = tsk->mm;
    cprm.siginfo = siginfo;
    tsk->mm->core_state->dumper.task->binfmt->core_dump(&cprm);
    /* ELF 바이너리: elf_core_dump() 호출 */
}

coredump_filter 제어

/* /proc/PID/coredump_filter 비트마스크
 * 어떤 VMA 유형을 코어 덤프에 포함할지 제어합니다.
 *
 * 비트 0: 익명 private 매핑 (스택, 힙)
 * 비트 1: 익명 shared 매핑
 * 비트 2: file-backed private 매핑
 * 비트 3: file-backed shared 매핑
 * 비트 4: ELF 헤더 (ELF 매핑의 첫 페이지)
 * 비트 5: 대형(huge) private 매핑
 * 비트 6: 대형(huge) shared 매핑
 * 비트 7: DAX private 매핑
 *
 * 기본값: 0x33 (비트 0,1,4,5) */

/* 커널 내부 구현 */
static bool vma_dump_filter(struct vm_area_struct *vma,
                             unsigned long filter)
{
    if (vma_is_anonymous(vma)) {
        if (vma->vm_flags & VM_SHARED)
            return filter & CORE_DUMP_ANON_SHARED;
        return filter & CORE_DUMP_ANON_PRIVATE;
    }
    if (vma->vm_flags & VM_SHARED)
        return filter & CORE_DUMP_MAPPED_SHARED;
    return filter & CORE_DUMP_MAPPED_PRIVATE;
}

/* 코어 덤프 크기를 줄이는 실용적 설정 */
/* echo 0x31 > /proc/self/coredump_filter
 * → file-backed shared 제외 (공유 라이브러리 텍스트 제외)
 * 코어 파일 크기를 크게 줄일 수 있음 */

ELF 코어 파일 구조

/* fs/binfmt_elf.c - elf_core_dump()가 생성하는 구조 */

/* ELF 코어 파일 레이아웃:
 *
 * +-------------------+
 * | ELF Header        |  ← e_type = ET_CORE
 * +-------------------+
 * | Program Headers   |
 * |  PT_NOTE          |  ← 레지스터, 시그널 정보, 프로세스 상태
 * |  PT_LOAD (1)      |  ← 첫 번째 메모리 세그먼트
 * |  PT_LOAD (2)      |  ← 두 번째 메모리 세그먼트
 * |  ...              |
 * +-------------------+
 * | NOTE section      |
 * |  NT_PRSTATUS      |  ← 스레드별 레지스터 (struct elf_prstatus)
 * |  NT_PRFPREG       |  ← FPU 레지스터
 * |  NT_PRPSINFO      |  ← 프로세스 정보 (이름, 상태)
 * |  NT_SIGINFO       |  ← 코어 덤프 유발 siginfo
 * |  NT_AUXV          |  ← 보조 벡터
 * |  NT_FILE          |  ← 메모리 매핑된 파일 목록
 * +-------------------+
 * | Memory segments   |  ← VMA 내용 (coredump_filter에 따라)
 * +-------------------+ */

/* GDB에서 코어 파일 분석 */
/* $ gdb ./program core.12345
 * (gdb) bt          ← 백트레이스
 * (gdb) info regs   ← 레지스터 상태
 * (gdb) info sig    ← 시그널 정보
 * (gdb) thread apply all bt  ← 모든 스레드 백트레이스 */
systemd-coredump 통합: 현대 리눅스 배포판에서는 core_pattern|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h로 설정되어 있습니다. coredumpctl list로 코어 덤프 목록을 조회하고, coredumpctl debug PID로 바로 GDB 분석이 가능합니다.

시그널 핸들러 설치/실행 심화

시그널 핸들러의 설치부터 실행, 복귀까지의 전체 생명주기를 커널 내부 관점에서 분석합니다. sigaction 구조체의 각 필드가 어떻게 핸들러 동작에 영향을 미치는지 상세히 살펴봅니다.

시그널 핸들러 생명주기 1. 설치 (Install) sigaction() 시스템 콜 do_sigaction() sighand->action[sig-1] = k_sigaction { sa_handler, sa_flags, sa_mask } 2. 트리거 (Trigger) 시그널 수신 시 get_signal() 내부에서 ka = sighand->action[sig-1] SA_ONESHOT 검사: 설정 시 → sa_handler = SIG_DFL ksig 구조체에 복사 3. 프레임 설정 handle_signal() setup_rt_frame() 유저 스택에 rt_sigframe 구성 RIP → sa_handler 주소 RDI=sig, RSI=siginfo* RDX=ucontext* 4. 핸들러 실행 (유저 공간) sa_mask에 지정된 시그널 + 현재 시그널이 blocked에 추가 (SA_NODEFER가 없으면) SA_SIGINFO: 3인자 핸들러 void handler(int sig, siginfo_t *info, void *uctx) 5. 복귀 (sigreturn) 핸들러 return → pretcode (VDSO) → rt_sigreturn 시스템 콜 blocked 마스크 원래 값으로 복원 레지스터/FPU 상태 복원 → 원래 코드 계속 SA_RESTART와 시스템 콜 재시작 시그널로 인터럽트된 시스템 콜의 처리: SA_RESTART 설정 → 시스템 콜 자동 재시작 (RIP를 syscall 명령으로 되돌림) SA_RESTART 미설정 → -EINTR 반환 (유저가 직접 재시도해야 함) 주의: connect(), poll(), sigsuspend() 등은 SA_RESTART와 무관하게 항상 -EINTR 반환
시그널 핸들러의 설치부터 복귀까지 전체 생명주기

do_sigaction() 내부

/* kernel/signal.c - sigaction 설치의 커널 내부 구현 */

int do_sigaction(int sig, struct k_sigaction *act,
                  struct k_sigaction *oact)
{
    struct task_struct *p = current;
    struct k_sigaction *k;
    sigset_t mask;

    /* SIGKILL, SIGSTOP은 핸들러 변경 불가 */
    if (!valid_signal(sig) || sig < 1 ||
        (act && sig_kernel_only(sig)))
        return -EINVAL;

    k = &p->sighand->action[sig - 1];

    spin_lock_irq(&p->sighand->siglock);

    /* 이전 핸들러 정보 저장 */
    if (oact)
        *oact = *k;

    /* 새 핸들러 설치 */
    if (act) {
        /* sa_mask에서 SIGKILL/SIGSTOP 제거 (블록 불가) */
        sigdelsetmask(&act->sa.sa_mask,
                      sigmask(SIGKILL) | sigmask(SIGSTOP));

        *k = *act;

        /* SIG_IGN으로 변경 시: pending 큐에서 해당 시그널 제거 */
        if (sig_handler_ignored(sig_handler(p, sig), sig)) {
            sigemptyset(&mask);
            sigaddset(&mask, sig);
            flush_sigqueue_mask(&mask, &p->signal->shared_pending);
            for_each_thread(p, t)
                flush_sigqueue_mask(&mask, &t->pending);
        }
    }

    spin_unlock_irq(&p->sighand->siglock);
    return 0;
}

SA_SIGINFO 3인자 핸들러

/* SA_SIGINFO 핸들러의 siginfo_t 주요 필드 */

typedef struct {
    int      si_signo;   /* 시그널 번호 */
    int      si_errno;   /* 에러 번호 (보통 0) */
    int      si_code;    /* 시그널 발생 원인 코드 */

    /* si_code 값에 따른 추가 정보 */
    union {
        /* kill()/sigqueue()로 보낸 경우 (SI_USER/SI_QUEUE) */
        struct {
            pid_t si_pid;     /* 보낸 프로세스 PID */
            uid_t si_uid;     /* 보낸 프로세스 실제 UID */
        };
        /* SIGCHLD의 경우 */
        struct {
            pid_t si_pid;
            int   si_status;  /* 자식의 종료 상태 또는 시그널 */
            clock_t si_utime; /* 자식의 사용자 CPU 시간 */
            clock_t si_stime; /* 자식의 시스템 CPU 시간 */
        };
        /* SIGSEGV, SIGBUS 등 (메모리 폴트) */
        struct {
            void *si_addr;    /* 폴트 발생 주소 */
            short si_addr_lsb; /* LSB (SIGBUS+BUS_MCEERR_*) */
        };
        /* SIGIO/SIGPOLL */
        struct {
            long  si_band;    /* poll 이벤트 비트 */
            int   si_fd;      /* 파일 디스크립터 */
        };
        /* SIGSYS (seccomp) */
        struct {
            void  *si_call_addr; /* syscall 명령 주소 */
            int   si_syscall;    /* 거부된 시스템 콜 번호 */
            unsigned int si_arch; /* AUDIT_ARCH 값 */
        };
    };
    union sigval si_value; /* sigqueue()로 전달된 데이터 */
} siginfo_t;

/* si_code 주요 값 */
/* SI_USER    (0)  — kill()/raise()
 * SI_KERNEL  (128) — 커널 내부 생성
 * SI_QUEUE   (-1) — sigqueue()
 * SI_TIMER   (-2) — POSIX timer 만료
 * SI_MESGQ   (-3) — POSIX 메시지 큐
 * SI_ASYNCIO (-4) — AIO 완료
 * SI_TKILL   (-6) — tkill()/tgkill() */
signal() vs sigaction(): signal()은 BSD/System V 시맨틱이 플랫폼마다 달라 이식성이 떨어집니다. glibc의 signal()은 내부적으로 sigaction()SA_RESTART 플래그와 함께 호출합니다. 새 코드에서는 항상 sigaction()을 직접 사용하세요.

프로세스 그룹/세션 시그널

시그널은 개별 프로세스뿐 아니라 프로세스 그룹(process group)이나 세션(session) 단위로도 전송됩니다. 터미널 제어 시그널(SIGHUP, SIGINT, SIGTSTP)과 job control의 동작을 커널 내부 관점에서 분석합니다.

프로세스 그룹/세션과 시그널 전파 Session (SID: 1000) 제어 터미널 (/dev/pts/0) Ctrl+C → SIGINT / Ctrl+Z → SIGTSTP Foreground Process Group (PGID: 2000) Process A PID=2000 (그룹리더) Process B PID=2001 Ctrl+C → SIGINT 모든 포그라운드 프로세스에 전달 kill_pgrp(tty->pgrp, SIGINT, 1) Ctrl+C/Z Background Process Group (PGID: 3000) Process C PID=3000 (그룹리더) Process D PID=3001 TTY 읽기 시도 → SIGTTIN TTY 쓰기 시도 → SIGTTOU (TOSTOP 설정 시) SIGHUP 전파 흐름 (터미널 끊김/세션 리더 종료) 1. 터미널 연결 끊김 (SSH 종료, modem hangup) 2. 세션 리더(shell)에 SIGHUP 전달 3. 세션 리더 종료 시 → 모든 프로세스 그룹에 SIGHUP + SIGCONT 커널 함수 흐름: tty_vhangup() → tty_signal_session_leader() → __kill_pgrp_info(SIGHUP, pgrp) 고아 프로세스 그룹(orphaned pgrp): 중지(stopped) 멤버가 있으면 SIGHUP + SIGCONT → is_current_pgrp_orphaned() + has_stopped_jobs()
세션/프로세스 그룹 구조와 시그널 전파 경로

kill_pgrp() 구현

/* kernel/signal.c - 프로세스 그룹 시그널 전송 */

int kill_pgrp(struct pid *pid, int sig, int priv)
{
    int ret;
    read_lock(&tasklist_lock);
    ret = __kill_pgrp_info(sig,
              priv ? SEND_SIG_PRIV : SEND_SIG_NOINFO,
              pid);
    read_unlock(&tasklist_lock);
    return ret;
}

int __kill_pgrp_info(int sig, struct kernel_siginfo *info,
                      struct pid *pgrp)
{
    struct task_struct *p = NULL;
    int ret = -ESRCH;

    /* 프로세스 그룹의 모든 프로세스에 시그널 전송 */
    do_each_pid_task(pgrp, PIDTYPE_PGID, p) {
        int err = group_send_sig_info(sig, info, p, PIDTYPE_PGID);
        if (!ret)
            ret = err;
    } while_each_pid_task(pgrp, PIDTYPE_PGID, p);

    return ret;
}

/* 유저 공간에서의 프로세스 그룹 시그널 전송 */
/* kill(0, sig)    → 같은 프로세스 그룹 전체 */
/* kill(-pgid, sig) → 지정된 프로세스 그룹 전체 */
/* killpg(pgid, sig) → kill(-pgid, sig)과 동일 */

SIGHUP 전파 메커니즘

/* drivers/tty/tty_jobctrl.c - 터미널 끊김 시 SIGHUP 전파 */

void tty_signal_session_leader(struct tty_struct *tty, int exit_session)
{
    struct task_struct *p;
    struct pid *session;

    session = tty->session;

    /* 세션의 모든 프로세스에 SIGHUP 전송 */
    do_each_pid_task(session, PIDTYPE_SID, p) {
        if (p->signal->tty == tty) {
            group_send_sig_info(SIGHUP, SEND_SIG_PRIV, p, PIDTYPE_TGID);
            group_send_sig_info(SIGCONT, SEND_SIG_PRIV, p, PIDTYPE_TGID);
        }
    } while_each_pid_task(session, PIDTYPE_SID, p);
}

/* nohup 명령의 원리:
 * 1. SIGHUP을 SIG_IGN으로 설정
 * 2. 표준 출력/에러를 nohup.out으로 리다이렉트
 * 3. exec으로 실제 프로그램 실행
 *
 * disown 명령의 원리 (bash 내장):
 * 쉘의 job table에서 프로세스를 제거
 * → 쉘 종료 시 SIGHUP을 보내지 않음
 *
 * setsid() 시스템 콜:
 * 새 세션 생성 → 제어 터미널 없음 → SIGHUP 수신 안 함
 * 데몬 프로세스의 표준 패턴 */
고아 프로세스 그룹(Orphaned Process Group): 프로세스 그룹의 부모가 모두 같은 세션의 다른 그룹이 아닐 때 "고아"가 됩니다. 고아 그룹에 중지(stopped)된 멤버가 있으면 커널은 SIGHUP + SIGCONT를 보내 중지 상태에서 벗어나게 합니다. 이는 제어 터미널이 없는 상태에서 프로세스가 영구적으로 중지되는 것을 방지합니다.

실시간 시그널 심화

실시간(RT) 시그널은 POSIX.1b에서 정의된 확장으로, 표준 시그널의 한계인 큐잉 불가와 순서 미보장 문제를 해결합니다. 커널 내부에서 RT 시그널이 어떻게 관리되는지 상세히 분석합니다.

실시간 시그널 큐잉 메커니즘 표준 시그널 (1~31): 비트마스크만 sigset_t.signal 비트맵: 15 14 13 ... 2 1 SIGTERM 3번 전송 → 비트 15만 1번 설정 (중복 무시) sigqueue 리스트: 비어있을 수 있음 (RLIMIT 초과 시 siginfo 손실) legacy_queue() → true → 두번째부터 폐기 RT 시그널 (32~64): 비트맵 + 큐 sigset_t.signal 비트맵: 34 33 32 ... SIGRTMIN+2 (=34) 3번 전송 → sigqueue 3개 + 비트 유지 sq(val=1) sq(val=2) sq(val=3) legacy_queue() → false → 항상 큐잉 전달 우선순위 (dequeue_signal) 1순위: 표준 시그널 (낮은 번호 먼저): SIGHUP(1) > SIGINT(2) > ... > SIGSYS(31) 2순위: RT 시그널 (낮은 번호 먼저): SIGRTMIN(32) > SIGRTMIN+1(33) > ... > SIGRTMAX(64) 같은 RT 시그널 번호 내에서는 FIFO 순서 (먼저 보낸 것이 먼저 전달) RLIMIT_SIGPENDING 제한 사용자당 최대 큐잉 가능한 sigqueue 수 (기본값: /proc/sys/kernel/rtsig-max 또는 ~128000) 초과 시: sigqueue 할당 실패 → 시그널은 전달되지만 siginfo 데이터 손실 가능 ulimit -i 로 확인, 프로세스당이 아닌 사용자(ucounts)당 제한
표준 시그널과 RT 시그널의 큐잉 메커니즘 비교

sigqueue 할당과 제한

/* kernel/signal.c - sigqueue 할당 */

static struct sigqueue *__sigqueue_alloc(
    int sig, struct task_struct *t,
    gfp_t gfp_flags, int override_rlimit)
{
    struct sigqueue *q = NULL;
    struct ucounts *ucounts;
    long sigpending;

    ucounts = task_ucounts(t);

    /* RLIMIT_SIGPENDING 검사 */
    sigpending = inc_rlimit_ucounts(ucounts, UCOUNT_RLIMIT_SIGPENDING,
                                     1);

    if (!override_rlimit &&
        sigpending > task_rlimit(t, RLIMIT_SIGPENDING)) {
        /* 제한 초과: sigqueue 할당 실패
         * 시그널 비트는 여전히 설정되므로 시그널 자체는 전달됨
         * 하지만 siginfo 데이터가 손실됨 */
        dec_rlimit_ucounts(ucounts, UCOUNT_RLIMIT_SIGPENDING, 1);
        return NULL;
    }

    q = kmem_cache_alloc(sigqueue_cachep, gfp_flags);
    if (!q) {
        dec_rlimit_ucounts(ucounts, UCOUNT_RLIMIT_SIGPENDING, 1);
        return NULL;
    }

    INIT_LIST_HEAD(&q->list);
    q->flags = 0;
    q->ucounts = ucounts;

    return q;
}

/* override_rlimit이 true인 경우:
 * - 커널 내부 시그널 (SIGKILL, OOM killer 등)
 * - SEND_SIG_PRIV로 보낸 시그널
 * → RLIMIT 무시하고 항상 할당 시도 */

RT 시그널 실용적 활용

/* RT 시그널을 활용한 이벤트 알림 시스템 예제 */
#include <signal.h>
#include <stdio.h>

#define SIG_WORKER_DONE    (SIGRTMIN + 0)
#define SIG_DATA_READY     (SIGRTMIN + 1)
#define SIG_HEARTBEAT      (SIGRTMIN + 2)

/* RT 시그널 핸들러: 큐잉되므로 이벤트 손실 없음 */
void rt_event_handler(int sig, siginfo_t *info, void *ctx)
{
    switch (sig) {
    case SIG_WORKER_DONE:
        printf("Worker %d finished, result=%d\n",
               info->si_pid, info->si_value.sival_int);
        break;
    case SIG_DATA_READY: {
        void *buf = info->si_value.sival_ptr;
        process_data(buf);
        break;
    }
    }
}

/* 워커 프로세스에서 완료 알림 전송 */
void worker_complete(pid_t parent, int result)
{
    union sigval val;
    val.sival_int = result;
    /* 큐잉되므로 여러 워커의 알림이 모두 전달됨 */
    sigqueue(parent, SIG_WORKER_DONE, val);
}

/* POSIX 타이머와 RT 시그널 연동 */
struct sigevent sev;
sev.sigev_notify = SIGEV_SIGNAL;
sev.sigev_signo = SIG_HEARTBEAT;
sev.sigev_value.sival_int = 0;

timer_t timerid;
timer_create(CLOCK_MONOTONIC, &sev, &timerid);
/* 타이머 만료 시 SIG_HEARTBEAT가 큐잉됨 */
glibc와 RT 시그널: glibc는 내부적으로 처음 두 개의 RT 시그널(SIGRTMIN ~ SIGRTMIN+1)을 NPTL(pthread 구현)에서 사용합니다. 따라서 애플리케이션에서 사용할 수 있는 RT 시그널은 SIGRTMIN+2부터입니다. SIGRTMIN의 실제 값은 glibc 버전과 스레드 구현에 따라 달라질 수 있으므로 항상 SIGRTMIN 매크로를 사용하세요.

시그널 마스킹 심화

시그널 마스킹은 크리티컬 섹션 보호, 시그널 기반 동기화, 원자적 대기 등 다양한 패턴에서 핵심적으로 사용됩니다. blocked 마스크의 커널 내부 동작과 실용적 패턴을 분석합니다.

시그널 마스킹 동작 흐름 task_struct 시그널 마스크 관계 blocked (sigset_t) 현재 활성 블록 마스크 real_blocked (sigset_t) sigsuspend 이전 마스크 백업 saved_sigmask 시스콜 복귀 시 복원할 마스크 sigprocmask() 동작 SIG_BLOCK: blocked |= new_mask SIG_UNBLOCK: blocked &= ~new_mask SIG_SETMASK: blocked = new_mask 항상: SIGKILL/SIGSTOP 비트 제거됨 sigsuspend() 동작 1. real_blocked = blocked (백업) 2. blocked = temp_mask (임시 마스크 적용) 3. sleep (시그널 대기) 4. 시그널 수신 → blocked = real_blocked 복원 recalc_sigpending() - TIF_SIGPENDING 재계산 deliverable = (private_pending | shared_pending) & ~blocked deliverable != 0 → set_tsk_thread_flag(TIF_SIGPENDING) deliverable == 0 → clear_tsk_thread_flag(TIF_SIGPENDING)
sigprocmask/sigsuspend와 recalc_sigpending의 관계

sigsuspend 원자적 대기 패턴

/* sigsuspend()의 원자성이 필요한 이유 */

/* 잘못된 패턴 (race condition!) */
sigset_t empty;
sigemptyset(&empty);
sigprocmask(SIG_SETMASK, &empty, NULL);
/* ← 여기서 시그널이 도착하면?
 * sigprocmask와 pause 사이에 시그널이 전달되어
 * pause()가 영원히 블록될 수 있음! */
pause();  /* 시그널 대기 */

/* 올바른 패턴: sigsuspend (원자적) */
sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGUSR1);

/* SIGUSR1 블록 */
sigprocmask(SIG_BLOCK, &mask, &oldmask);

/* 작업 수행... */
prepare_work();

/* 원자적으로: 마스크 교체 + sleep + 마스크 복원 */
sigsuspend(&oldmask);
/* sigsuspend 내부:
 * 1. blocked = oldmask (SIGUSR1 언블록)
 * 2. 시그널 대기 (sleep)
 * 3. 시그널 도착 → 핸들러 실행
 * 4. blocked = 원래 값 복원
 * 5. -EINTR 반환
 * 이 전체가 원자적으로 수행됨 */

/* pselect()/ppoll(): 시그널 마스크 + I/O 대기 결합 */
sigset_t origmask;
sigprocmask(SIG_BLOCK, &mask, &origmask);

/* pselect는 원자적으로 마스크 교체 + select 수행 */
pselect(nfds, &readfds, NULL, NULL, NULL, &origmask);
/* 시그널과 I/O 이벤트를 동시에 안전하게 대기 */

커널 내부 마스크 처리

/* kernel/signal.c - set_current_blocked() 상세 */

void set_current_blocked(sigset_t *newset)
{
    /* SIGKILL/SIGSTOP은 절대 블록 불가 */
    sigdelsetmask(newset, sigmask(SIGKILL) | sigmask(SIGSTOP));

    __set_current_blocked(newset);
}

void __set_current_blocked(const sigset_t *newset)
{
    struct task_struct *tsk = current;

    /* 변경이 없으면 빠른 경로 */
    if (sigequalsets(&tsk->blocked, newset))
        return;

    spin_lock_irq(&tsk->sighand->siglock);
    tsk->blocked = *newset;
    recalc_sigpending();  /* TIF_SIGPENDING 재계산 */
    spin_unlock_irq(&tsk->sighand->siglock);
}

/* recalc_sigpending() 구현 */
void recalc_sigpending(void)
{
    /* JOBCTL 관련 플래그도 확인 */
    if (!recalc_sigpending_tsk(current) &&
        !freezing(current))
        clear_thread_flag(TIF_SIGPENDING);
}

static int recalc_sigpending_tsk(struct task_struct *t)
{
    /* pending & ~blocked에 전달 가능한 시그널이 있는지 확인 */
    if ((has_pending_signals(&t->pending.signal, &t->blocked)) ||
        (has_pending_signals(&t->signal->shared_pending.signal,
                             &t->blocked)) ||
        (t->jobctl & (JOBCTL_PENDING_MASK | JOBCTL_TRAP_FREEZE))) {
        set_tsk_thread_flag(t, TIF_SIGPENDING);
        return 1;
    }
    return 0;
}
핸들러 실행 중 자동 마스킹: 시그널 핸들러가 실행될 때, signal_setup_done()sa_mask에 지정된 시그널과 현재 시그널 자체를 blocked에 추가합니다(SA_NODEFER가 아닌 경우). 핸들러가 rt_sigreturn으로 복귀하면 원래 blocked 마스크가 복원됩니다. 이 메커니즘은 핸들러의 재진입(reentrancy)을 방지합니다.

do_signal()/get_signal() 커널 코드 워크스루

시그널 처리의 진입점인 do_signal()부터 시그널 디큐, 핸들러 선택, 프레임 설정까지의 전체 코드 경로를 따라갑니다. TIF_SIGPENDING 플래그가 설정되는 조건부터 유저 공간 핸들러 실행까지의 완전한 흐름입니다.

시그널 처리 진입점

/* kernel/entry/common.c - 커널→유저 복귀 경로 */

static unsigned long exit_to_user_mode_loop(
    struct pt_regs *regs, unsigned long ti_work)
{
    while (ti_work & EXIT_TO_USER_MODE_WORK) {

        /* TIF_SIGPENDING 확인 */
        if (ti_work & _TIF_SIGPENDING)
            arch_do_signal_or_restart(regs);
            /* x86: arch/x86/kernel/signal.c
             * ARM64: arch/arm64/kernel/signal.c */

        /* 다른 작업: TIF_NEED_RESCHED, TIF_NOTIFY_RESUME 등 */
        if (ti_work & _TIF_NEED_RESCHED)
            schedule();

        ti_work = read_thread_flags();
    }
    return ti_work;
}

/* 이 함수가 호출되는 시점:
 * 1. 시스템 콜 리턴 (syscall_exit_to_user_mode)
 * 2. 인터럽트/예외 리턴 (irqentry_exit_to_user_mode)
 * 3. 시그널 관련 시스콜 (rt_sigreturn 등) 후
 *
 * TIF_SIGPENDING가 설정되는 시점:
 * - send_signal_locked() → complete_signal() → signal_wake_up()
 * - set_current_blocked() → recalc_sigpending()
 * - 프로세스 freezer 관련 */

arch_do_signal_or_restart() 흐름

/* arch/x86/kernel/signal.c */

void arch_do_signal_or_restart(struct pt_regs *regs)
{
    struct ksignal ksig;

    /* get_signal(): 시그널 디큐 + 핸들러 결정 */
    if (get_signal(&ksig)) {
        /* 사용자 핸들러가 있는 시그널 발견 */

        /* 인터럽트된 시스템 콜 재시작 처리 */
        if (regs->orig_ax >= 0) {
            /* 시스템 콜이 인터럽트된 경우 */
            switch (regs->ax) {
            case -ERESTARTSYS:
                if (!(ksig.ka.sa.sa_flags & SA_RESTART)) {
                    regs->ax = -EINTR;
                    break;
                }
                /* fall through - SA_RESTART이면 재시작 */
            case -ERESTARTNOINTR:
                /* 시스콜 재시작: RIP를 syscall 명령으로 되돌림 */
                regs->ax = regs->orig_ax;
                regs->ip -= 2;  /* syscall 명령 길이 */
                break;
            case -ERESTARTNOHAND:
                /* 핸들러가 있으면 -EINTR, 없으면 재시작 */
                regs->ax = -EINTR;
                break;
            case -ERESTART_RESTARTBLOCK:
                /* restart_syscall() 사용 */
                regs->ax = -EINTR;
                break;
            }
        }

        /* 시그널 프레임 설정 + 유저 핸들러 호출 */
        handle_signal(&ksig, regs);
        return;
    }

    /* 핸들러가 없는 경우: 시스템 콜 재시작만 처리 */
    if (regs->orig_ax >= 0) {
        switch (regs->ax) {
        case -ERESTARTNOHAND:
        case -ERESTARTSYS:
        case -ERESTARTNOINTR:
            regs->ax = regs->orig_ax;
            regs->ip -= 2;
            break;
        case -ERESTART_RESTARTBLOCK:
            regs->ax = __NR_restart_syscall;
            regs->ip -= 2;
            break;
        }
    }

    /* saved_sigmask 복원 (sigsuspend/pselect 후) */
    restore_saved_sigmask();
}

TIF_SIGPENDING 플래그 관리

/* include/linux/sched/signal.h */

/* signal_wake_up(): 시그널 전송 시 대상 스레드 깨우기 */
static inline void signal_wake_up(struct task_struct *t,
                                    bool fatal)
{
    unsigned int state = 0;

    /* TIF_SIGPENDING 설정 */
    set_tsk_thread_flag(t, TIF_SIGPENDING);

    if (fatal) {
        /* SIGKILL: UNINTERRUPTIBLE 상태에서도 깨움 */
        state = TASK_WAKEKILL;
    }

    /* 대기 중이면 깨우기 */
    if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
        kick_process(t);  /* CPU에서 실행 중이면 IPI로 강제 리턴 유도 */
}

/* kick_process(): 다른 CPU에서 실행 중인 스레드에 IPI 전송
 * → 해당 CPU가 커널→유저 전환 경로를 다시 확인하도록 유도
 * → exit_to_user_mode_loop에서 TIF_SIGPENDING 확인
 *
 * TASK_WAKEKILL의 의미:
 * 일반 시그널: TASK_INTERRUPTIBLE만 깨움
 * SIGKILL: TASK_KILLABLE (= TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)도 깨움
 * → 디스크 I/O 대기 등 UNINTERRUPTIBLE 상태에서도 SIGKILL은 작동 */
시스템 콜 재시작 코드의 의미:
  • -ERESTARTSYS: SA_RESTART에 따라 재시작 또는 -EINTR
  • -ERESTARTNOINTR: 항상 재시작 (clock_nanosleep 등)
  • -ERESTARTNOHAND: 핸들러가 없을 때만 재시작
  • -ERESTART_RESTARTBLOCK: restart_syscall()로 재시작 (nanosleep 잔여시간)
이 코드들은 유저 공간에는 보이지 않으며 커널 내부에서만 사용됩니다.

ftrace/bpftrace 시그널 추적

시그널의 비동기적 특성 때문에 기존 디버거로는 시그널 관련 문제를 추적하기 어렵습니다. ftrace의 signal 이벤트와 bpftrace를 활용한 실시간 시그널 모니터링 기법을 소개합니다.

ftrace signal 이벤트 상세

# ftrace signal 이벤트 구조 확인
cat /sys/kernel/debug/tracing/events/signal/signal_generate/format
# name: signal_generate
# field:int sig;
# field:int errno;
# field:int code;        ← si_code (SI_USER, SI_KERNEL 등)
# field:char comm[16];   ← 대상 프로세스 이름
# field:pid_t pid;       ← 대상 PID
# field:int group;       ← 그룹 시그널 여부
# field:int result;      ← 결과 (0=성공, SIGQUEUE_PREALLOC 등)

cat /sys/kernel/debug/tracing/events/signal/signal_deliver/format
# name: signal_deliver
# field:int sig;
# field:int errno;
# field:int code;
# field:unsigned long sa_handler;  ← 핸들러 주소
# field:unsigned long sa_flags;    ← SA_SIGINFO|SA_RESTART 등

# 두 이벤트를 동시에 활성화하여 생성~전달 추적
echo 1 > /sys/kernel/debug/tracing/events/signal/enable

# 추적 시작
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 테스트: 다른 터미널에서 kill -SIGUSR1 PID
cat /sys/kernel/debug/tracing/trace_pipe

# 출력 예시:
#  bash-1234  [002] signal_generate: sig=10 errno=0 code=0
#                                    comm=myapp pid=5678 group=1 result=0
#  myapp-5678 [001] signal_deliver:  sig=10 errno=0 code=0
#                                    sa_handler=4011a0 sa_flags=14000004

# 특정 시그널만 필터링 (예: SIGKILL=9)
echo 'sig == 9' > /sys/kernel/debug/tracing/events/signal/signal_generate/filter

# function_graph로 시그널 함수 호출 체인 추적
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 'do_send_sig_info' > /sys/kernel/debug/tracing/set_graph_function
echo 'send_signal_locked' >> /sys/kernel/debug/tracing/set_graph_function
echo 'get_signal' >> /sys/kernel/debug/tracing/set_graph_function

bpftrace 시그널 모니터링

# 시그널 전송 실시간 모니터링
bpftrace -e '
tracepoint:signal:signal_generate {
    printf("%-8d %-16s -> %-16s sig=%-3d code=%d\n",
           pid, comm, str(args->comm), args->sig, args->code);
}'

# SIGKILL만 추적 (OOM killer, 강제 종료 감지)
bpftrace -e '
tracepoint:signal:signal_generate /args->sig == 9/ {
    printf("%s[%d] sent SIGKILL to %s[%d]\n",
           comm, pid, str(args->comm), args->pid);
    printf("  stack: %s\n", kstack);
}'

# 시그널 전달 지연 측정
bpftrace -e '
tracepoint:signal:signal_generate {
    @gen[args->pid, args->sig] = nsecs;
}
tracepoint:signal:signal_deliver {
    $key = (pid, args->sig);
    if (@gen[$key]) {
        $delay = nsecs - @gen[$key];
        printf("sig=%d delay=%d ns\n", args->sig, $delay);
        @latency = hist($delay);
        delete(@gen[$key]);
    }
}'

# 프로세스별 시그널 수신 카운트
bpftrace -e '
tracepoint:signal:signal_deliver {
    @[comm, args->sig] = count();
}
END { print(@); }'

# kill() 시스템 콜 추적 (누가 누구에게 보냈는지)
bpftrace -e '
tracepoint:syscalls:sys_enter_kill {
    printf("%-6d %-16s kill(pid=%d, sig=%d)\n",
           pid, comm, args->pid, args->sig);
}'

strace vs ftrace 비교

항목straceftrace/bpftrace
관점유저 공간 (시스템 콜 인터페이스)커널 공간 (내부 함수/이벤트)
메커니즘ptrace (프로세스별 연결)tracepoint/kprobe (시스템 전체)
오버헤드높음 (모든 시스콜에 context switch)낮음 (tracepoint는 거의 제로 오버헤드)
시그널 정보시스콜 인자/리턴, 시그널 전달 로그generate/deliver 이벤트, 커널 스택
대상특정 프로세스시스템 전체 또는 필터링
커널 내부볼 수 없음함수 호출 체인, 락 경합 등 가시화
실무 사용애플리케이션 디버깅시스템 수준 시그널 문제 분석
perf로 시그널 이벤트 통계 수집: perf stat -e 'signal:signal_generate' -e 'signal:signal_deliver' -p PID sleep 10으로 10초간 시그널 발생/전달 횟수를 카운트할 수 있습니다. 시그널 폭풍(signal storm) 진단에 유용합니다.

시그널 디버깅 심화

시그널 관련 버그는 비동기 특성으로 인해 재현이 어렵고 진단이 까다롭습니다. 실무에서 자주 발생하는 시그널 race condition 패턴과 체계적인 디버깅 방법론을 다룹니다.

시그널 디버깅: Race Condition 패턴 패턴 1: EINTR 미처리 Thread A: read(fd, buf, len) ← SIGCHLD 도착! read() 반환: -1, errno=EINTR 해결: SA_RESTART 또는 재시도 루프 while ((n = read(fd, buf, len)) == -1 && errno == EINTR) ; 패턴 2: 핸들러 내 비안전 함수 main(): malloc() 실행 중 힙 메타데이터 수정 중 ← 시그널! handler(): printf() → malloc() → 힙 손상/데드락! 해결: async-signal-safe만 사용 패턴 3: 시그널 손실 SIGUSR1 핸들러 설정 (SA_RESETHAND) 핸들러 실행 → SIG_DFL로 리셋 두 번째 SIGUSR1 → 프로세스 종료! 해결: SA_RESETHAND 제거 또는 핸들러 내에서 재등록 패턴 4: 마스크/대기 Race sigprocmask(SIG_UNBLOCK, ...); ← 시그널 도착! pause(); ← 영원히 블록! 해결: sigsuspend() 사용 (원자적 마스크 교체 + 대기) 시그널 디버깅 체크리스트 1. /proc/PID/status → SigPnd, ShdPnd, SigBlk, SigCgt 확인 2. strace -e signal -f → 시그널 시스콜 추적 (핸들러 주소, 마스크 변경) 3. ftrace signal_generate/deliver → 커널 내부 시그널 흐름 (누가 보냈는지) 4. GDB: handle SIGXXX nostop noprint pass → 특정 시그널 무시하고 디버깅 계속
실무에서 자주 발생하는 시그널 race condition 패턴과 디버깅 체크리스트

EINTR 처리 패턴

/* 올바른 EINTR 처리 패턴 */

/* 패턴 1: SA_RESTART 사용 (가장 간단) */
struct sigaction sa;
sa.sa_handler = my_handler;
sa.sa_flags = SA_RESTART;  /* slow 시스템 콜 자동 재시작 */
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);

/* 패턴 2: 재시도 래퍼 매크로 */
#define RETRY_EINTR(expr) ({     \
    typeof(expr) __ret;          \
    do {                          \
        __ret = (expr);           \
    } while (__ret == -1 && errno == EINTR); \
    __ret;                        \
})

/* 사용 예시 */
ssize_t n = RETRY_EINTR(read(fd, buf, len));
int ret = RETRY_EINTR(waitpid(child, &status, 0));

/* 패턴 3: SA_RESTART가 효과 없는 시스템 콜 */
/* connect(), poll(), select(), epoll_wait(),
 * sleep(), usleep(), nanosleep(),
 * semop(), msgrcv(), msgsnd()
 * → 이들은 항상 직접 재시도 필요 */

int ret;
do {
    ret = connect(sockfd, addr, addrlen);
} while (ret == -1 && errno == EINTR);

/* nanosleep: 남은 시간으로 재시도 */
struct timespec req = { .tv_sec = 5 }, rem;
while (nanosleep(&req, &rem) == -1 && errno == EINTR)
    req = rem;  /* 남은 시간으로 재시도 */

async-signal-safe 함수 가이드

/* 시그널 핸들러에서 안전하게 호출할 수 있는 함수 (POSIX 보장) */

/* 안전한 함수 (일부 발췌): */
/*
 * _exit(), _Exit()
 * abort()
 * accept(), bind(), connect(), listen(), recv(), send(), socket()
 * close(), dup(), dup2(), fcntl(), open(), read(), write()
 * kill(), raise(), sigaction(), sigaddset(), sigdelset(),
 *   sigemptyset(), sigfillset(), signal(), sigprocmask()
 * alarm(), sleep()
 * fork(), execve(), wait(), waitpid()
 * access(), chdir(), chmod(), chown(), link(), unlink(),
 *   mkdir(), rmdir(), rename(), stat()
 * sem_post()
 */

/* 안전하지 않은 함수 (절대 핸들러에서 호출 금지): */
/*
 * printf(), fprintf(), sprintf() → write()로 대체
 * malloc(), free(), realloc() → 사전 할당 버퍼 사용
 * pthread_mutex_lock() → 데드락 위험
 * exit() → _exit()로 대체
 * syslog() → write()로 대체
 */

/* 실용적 패턴: volatile sig_atomic_t 플래그 */
static volatile sig_atomic_t got_signal = 0;

void handler(int sig)
{
    /* 핸들러에서는 플래그만 설정 */
    got_signal = 1;
    /* sig_atomic_t: 원자적 읽기/쓰기 보장 */
}

int main(void)
{
    /* 메인 루프에서 플래그 확인 */
    while (!got_signal) {
        do_work();
    }
    /* 여기서 안전하게 정리 작업 수행 */
    cleanup();
    return 0;
}

/* self-pipe trick: 시그널을 파일 디스크립터 이벤트로 변환 */
static int pipefd[2];

void handler(int sig)
{
    int saved_errno = errno;
    write(pipefd[1], "x", 1);  /* write()는 async-signal-safe */
    errno = saved_errno;
}

/* 메인 루프에서 pipefd[0]을 epoll/select로 감시
 * → 시그널이 I/O 이벤트로 변환되어 안전하게 처리
 * signalfd()가 이 패턴을 커널 수준에서 구현한 것 */

GDB 시그널 디버깅

# GDB에서 시그널 처리 제어

# 모든 시그널의 GDB 동작 확인
(gdb) info signals

# 특정 시그널의 GDB 동작 변경
(gdb) handle SIGUSR1 stop print  # 시그널 시 중단 + 출력
(gdb) handle SIGCHLD nostop noprint pass  # 무시하고 프로그램에 전달
(gdb) handle SIGSEGV stop print nopass  # 중단하되 프로그램에는 전달 안 함

# 시그널 보내기
(gdb) signal SIGUSR1  # 현재 프로세스에 시그널 전송 + 계속 실행
(gdb) signal 0  # pending 시그널을 취소하고 계속 실행

# 시그널 핸들러에 브레이크포인트 설정
(gdb) break my_signal_handler
(gdb) catch signal SIGSEGV  # SIGSEGV 전달 시 캐치

# 시그널 프레임 분석
(gdb) bt  # 핸들러 실행 중 백트레이스에 <signal handler called> 표시
# #0  my_handler (sig=11) at app.c:42
# #1  <signal handler called>
# #2  0x00004011a0 in buggy_function () at app.c:100

# signal frame의 레지스터 확인
(gdb) frame 2  # 시그널 발생 지점으로 이동
(gdb) info registers  # 시그널 발생 시점의 레지스터

# /proc/PID/status 시그널 비트마스크 디코딩 스크립트
python3 -c "
mask = 0x0000000180004002
for i in range(1, 65):
    if mask & (1 << (i-1)):
        print(f'Signal {i}')
"
시그널 디버깅 주의사항: GDB 자체가 ptrace를 사용하므로, 디버깅 대상의 시그널 동작이 변경될 수 있습니다. 특히 SIGSTOP/SIGCONT는 GDB가 내부적으로 사용합니다. 시그널 타이밍에 민감한 문제를 디버깅할 때는 ftrace/bpftrace가 GDB보다 적합합니다. GDB의 handle 명령으로 각 시그널의 stop/print/pass 동작을 정확히 제어하세요.

시그널 처리와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.