Linux 内核信号处理深度工程化

Linux 内核信号处理深度工程化:从 sigaction 到用户态 Handler 的完整生命周期

信号处理是 Unix/Linux 系统编程的基石。从 Ctrl+C 触发 SIGINT,到 kill -9 发送 SIGKILL,再到生产环境中 SIGTERM 优雅退出与 SIGSEGV 崩溃捕获,信号无时无刻不在驱动着进程的生命周期。然而,大多数开发者对信号的理解停留在「注册一个 handler,收到就执行」的表象层面。本文将深入 Linux 2.6 至 6.x 内核源码,从数据结构、投递路径、多线程分发、实时信号排队、安全约束到生产级调试,完整拆解信号机制的每一个关键环节。


一、内核数据结构:signal 的三件套

Linux 内核通过三个核心结构体管理进程的信号状态,每个线程组共享部分结构,同时保留线程私有字段以实现 POSIX 合规的多线程信号语义。

1.1 signal_struct:线程组级信号状态


// include/linux/sched/signal.h
struct signal_struct {
    refcount_t      sigcnt;         // 引用计数(线程组中线程数)
    atomic_t        live;           // 存活线程数
    int             nr_threads;     // 活跃线程数
    struct list_head thread_head;   // 线程链表

    wait_queue_head_t wait_chldexit; // wait4() 等待队列

    /* 共享的信号处理 */
    struct sigaction action[_NSIG]; // 线程组级 POSIX sigaction(仅 forward-compatible)
    struct k_sigaction shared[_NSIG]; // 实际上 Linux 通过这里管理

    /* 进程级信号状态 */
    struct list_head posix_timers;  // POSIX 定时器链表
    
    struct hrtimer real_timer;      // ITIMER_REAL 高精度定时器
    struct pid *pgrp, *session;     // 进程组/会话 ID
    ...
};

关键字段解析:

  • sigcnt 和 live:通过引用计数管理线程组生命周期。最后一个线程 _exit() 时 live 降为 0,触发僵尸进程回收。
  • wait_chldexit:父进程调用 wait4() 在此等待子进程状态变更。子进程 do_exit() 会通过 release_task() 唤醒等待者。
  • action[]:现代 Linux 中实际使用 shared 数组,但 action[] 仍是 POSIX 兼容层。
  • real_timer:setitimer(ITIMER_REAL) 的实现基础,使用高精度定时器 (hrtimer) 实现,到期时发送 SIGALRM。
  • 1.2 sighand_struct:信号处理函数表

    
    // include/linux/sched/signal.h
    struct sighand_struct {
        refcount_t      count;
        struct k_sigaction action[_NSIG]; // 信号处理函数表
        spinlock_t      siglock;          // 保护 action[] 的读写锁
        wait_queue_head_t signalfd_wqh;   // signalfd 通知队列
    };
    

    为什么 sighand 独立于 signal? 因为 fork() 时 sighand 通过 copy_sighand() 被新进程共享(引用计数 +1),而 execve() 时重置为默认 handler。clone(CLONE_SIGHAND) 时直接共享同一个 sighand_struct,这是线程组内所有线程 handler 一致的底层机制。

    1.3 线程私有信号状态:pending 与 blocked

    
    // include/linux/sched.h (task_struct 内嵌)
    struct task_struct {
        struct signal_struct *signal;     // 共享 signal(线程组级)
        struct sighand_struct *sighand;   // 共享 sighand
        sigset_t blocked;                 // 线程私有信号掩码(Block mask)
        sigset_t real_blocked;            // 为 SA_RESTORER 保存的原 blocked
        struct sigpending pending;        // 该线程的 pending 信号队列
        
        // 关键:多线程下,信号可能投递到线程私有 pending 或更高层级的组级 pending
    };
    
    
    struct sigpending {
        struct list_head list;    // sigqueue 链表(实时信号排队)
        sigset_t signal;          // pending 信号集(位图)
    };
    

    核心设计洞察:Linux 维护两级 pending 队列——线程私有 pending(用于针对具体线程的信号)和基于 signal_struct 的潜在全局 pending。普通非实时信号在任意未 block 的线程上排队一次,实时信号必须保留在目标线程或进程级队列中直至被消费。

    1.4 sigaction vs k_sigaction

    
    // 用户态看到的 sigaction(glibc 层)
    struct sigaction {
        union {
            __sighandler_t sa_handler;    // SIG_DFL / SIG_IGN / handler 地址
            void (*sa_sigaction)(int, siginfo_t *, void *); // SA_SIGINFO 时
        };
        sigset_t   sa_mask;      // handler 执行期间额外 block 的信号
        int        sa_flags;     // SA_RESTART / SA_NODEFER / SA_SIGINFO 等
        void (*sa_restorer)(void); // 已废弃但保留 ABI
    };
    
    // 内核内部使用 k_sigaction
    struct k_sigaction {
        struct sigaction sa;
        // 内核不暴露给用户的字段
    };
    

    sa_flags 的位定义是理解信号语义的关键:

    标志位 语义
    SA_SIGINFO 使用三参数 sa_sigaction,携带 siginfo_t 上下文
    SA_RESTART 被信号中断的慢速 syscall 自动重启(不返回 EINTR)
    SA_NODEFER handler 执行期间不自动 block 同种信号
    SA_RESETHAND handler 入口重置为 SIG_DFL(单次捕获)
    SA_NOCLDWAIT 子进程退出时不创建 zombie,直接丢弃状态
    SA_ONSTACK 在 alternate signal stack (sigaltstack) 上执行

    二、信号如何产生:从 kill() 到内核投递

    2.1 系统调用入口:kill/tgkill/rt_sigqueueinfo

    
    // kernel/signal.c
    SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
    {
        struct kernel_siginfo info;
        
        prepare_kill_siginfo(sig, &info, SEND_SIG_PRIV);
        return kill_something_info(sig, &info, pid);
    }
    

    内核根据 pid 的正负值分发信号:

    pid 值 投递目标
    > 0 单个进程(PID == pid)
    0 调用者所在进程组内所有进程
    -1 广播给所有有权发送的进程(除 init 和自身)
    < -1 进程组 ` pid ` 内所有进程
    
    // 投递路径关键函数链:
    kill_proc_info()
      → check_kill_permission()   // 权限检查(CAP_KILL / uid 匹配)
        → __send_signal()
          → send_signal()         // 选择目标线程 / 处理 pending
            → complete_signal()   // 唤醒目标线程(设置 _TIF_SIGPENDING)
    

    2.2 权限检查:谁能向谁发信号

    
    // kernel/signal.c: check_kill_permission()
    static int check_kill_permission(int sig, struct kernel_siginfo *info,
                                      struct task_struct *t)
    {
        struct cred *cred = current_cred();
        struct cred *tcred = __task_cred(t);
        ...
        // 超级用户直接通过
        if (cred->euid == 0 || cred->uid == tcred->suid ||
            cred->uid == tcred->uid || ... )
            return 0;
        // SIGCONT 可发送给同 session 的进程(即使不是同一用户)
        ...
        return -EPERM;
    }
    

    关键特权规则:

  • SIGKILL / SIGSTOP 不可被捕获、阻塞或忽略(硬编码在内核 __send_signal 中直接执行 SIG_DFL)
  • SIGCONT 可以跨越 session 边界发送给进程组
  • CAP_KILL capability 可向任意进程发信号
  • 非特权进程只能向 uid 相同或子进程发送
  • 2.3 由硬件异常触发的信号(trap → signal 桥接)

    当 CPU 触发异常(如 #PF 缺页、#GP 通用保护失败),内核通过 do_signalFault() 路径构造 siginfo_t 并投递:

    
    // arch/x86/kernel/traps.c: do_page_fault() 最后阶段
    if (fault_error_code & PF_USER) {
        /* 用户态发生的 #PF,无法修复 → 发送 SIGSEGV */
        force_sig_fault(SIGSEGV, SEGV_MAPERR, (void __user *)address);
        // force_sig_fault → force_sig_info → __send_signal
    }
    
    
    // 各架构都有的 force_sig_fault 实现
    void force_sig_fault(int sig, int code, void __user *addr)
    {
        struct kernel_siginfo info;
        clear_siginfo(&info);
        info.si_signo = sig;
        info.si_code  = code;   // SEGV_MAPERR / SEGV_ACCERR / SEGV_BNDERR 等
        info.si_addr  = addr;   // 触发故障的虚拟地址
        force_sig_info(&info);
        // force_sig_* 不可被 block、捕获或忽略 —— 强制投递
    }
    

    si_code 字段是信号语义的核心:

    si_code 来源 含义
    SI_USER (0) kill/tgkill 用户态发送
    SI_KERNEL (0x80) 内核异常 内核强制触发
    SI_QUEUE (-1) sigqueue() sigqueue 实时排队(携带额外数据)
    SI_TIMER (-2) 定时器 POSIX 定时器到期
    SI_SIGIO (-5) SIGIO IO 事件通知
    SI_TKILL (-6) tkill() tgkill 线程定向发送

    三、信号如何投递:从 return-to-user 到 handler 执行

    信号不会在「产生时」中断用户态代码(除非 SIGSTOP/SIGKILL 立即生效)。Linux 的异步信号投递发生在特定边界点:

    3.1 投递时机:return-to-user / return-to-user 边界

    
                         ┌──────────────────────────────┐
            中断/异常 →  │       内核态                   │
            系统调用 →  │                              │
                         │  work_pending()              │
                         │    → do_signal()            │
                         │      → get_signal()         │
                         │        → handle_signal()    │
                         └──────────┬───────────────────┘
                                    │
    _TIF_SIGPENDING 置位时机:      │  sigreturn 恢复到用户态
      - 离开中断时                  │  执行 handler
      - 系统调用返回前              │
      - 从 schedule() 醒来时        │
    
    
    // arch/x86/kernel/signal.c: exit_to_user_mode_loop()
    exit_to_user_mode_loop(struct regs *regs, u32 cached_flags)
    {
        while (true) {
            cached_flags = READ_ONCE(current_thread_info()->flags);
            
            if (!(cached_flags & _TIF_WORK_MASK))
                break; // 无待处理工作,直接返回用户态
            
            local_irq_enable();
            
            if (cached_flags & _TIF_NEED_RESCHED)
                schedule();
            
            if (cached_flags & _TIF_SIGPENDING)
                do_signal(regs);  // ← 信号在这里被处理
            
            local_irq_disable();
        }
    }
    

    3.2 get_signal — 从 pending 队列取出信号

    
    // kernel/signal.c
    static bool get_signal(struct ksignal *ksig)
    {
    retry:
        spin_lock_irq(¤t->sighand->siglock);
        
        // 1. 检查线程私有 pending
        sig = dequeue_signal(current);
        if (sig == 0) {
            // 2. 检查进程级 pending(非定向信号)
            sig = dequeue_synchronous_signal(¤t->pending);
        }
        
        spin_unlock_irq(¤t->sighand->siglock);
        
        if (sig)
            return handle_signal(ksig) == 0;
        
        // 没有信号且当前被阻塞 → 阻塞在 freezable_schedule() 等
        return false;
    }
    

    dequeue_signal 逻辑:

    
    int dequeue_signal(struct task_struct *tsk)
    {
        int sig = next_signal(&tsk->pending, &tsk->blocked);
        if (sig == 0) {
            // 获取进程级 pending(shared)
            sig = next_signal(&tsk->signal->shared_pending, &tsk->blocked);
            // 从其他线程那里 "偷" 一个非定向信号
            sig = __dequeue_signal(tsk, &tsk->signal->shared_pending);
        }
        return sig;
    }
    

    关键规则:

  • 信号如果被某个线程 blocked,会一直 pending 直到 unblock
  • SIGKILL 和 SIGSTOP 不进入 pending,直接执行 sigaction
  • 非实时信号(1-31):同一信号在 pending 中只占一个位,多次投递只排队一次
  • 实时信号(32-64):通过 sigqueue 链表排队,可同时存在多个实例并携带数据
  • 3.3 handle_signal — 架设用户态堆栈帧

    
    // arch/x86/kernel/signal.c
    static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
    {
        // 1. 检查 syscall restart 逻辑
        if (syscall_get_nr(current, regs) != -1) {
            switch (syscall_get_error(current, regs)) {
            case -ERESTARTNOHAND:
            case -ERESTARTSYS:
                // 根据 SA_RESTART 标志决定是否自动重启 syscall
                if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {
                    regs->ax = -EINTR;
                } else {
                    regs->ax = regs->orig_ax; // 重启
                    regs->ip -= 2;            // 重新执行 syscall 指令
                }
                ...
            }
        }
        
        // 2. 在用户态堆栈上构造 signal frame
        setup_rt_frame(ksig, regs);
        
        // 3. 设置信号 mask(sa_mask 被自动 block)
        signal_delivered(ksig, regs);
        
        // 4. 注册 sigreturn trampoline(__kernel_vsyscall 页面)
    }
    

    setup_rt_frame 在用户态堆栈上构造 ucontext_t + siginfo_t + 返回地址(指向 __kernel_rt_sigreturn),处理器状态(寄存器集合)被完整保存以便 handler 返回后恢复。

    3.4 sys_rt_sigreturn — handler 执行完毕后回到内核

    用户态 handler 执行完毕时,不能直接 ret 返回到中断点,因为内核堆栈已经发生改变。Linux 的设计是:

    
    ; glibc 中的 sigreturn trampoline (x86_64)
    ; __restore_rt:
        mov rax, 15          ; __NR_rt_sigreturn
        syscall              ; 返回到内核态
                                    ; 内核从堆栈中恢复 pt_regs
    
    
    // arch/x86/kernel/signal.c: sys_rt_sigreturn()
    SYSCALL_DEFINE0(rt_sigreturn)
    {
        struct pt_regs *regs = current_pt_regs();
        struct rt_sigframe __user *frame;
        
        // 1. 从用户态堆栈恢复 pt_regs(原始 PC、sp、寄存器)
        // 2. 恢复原始 signal mask
        // 3. 检查是否嵌套信号帧(防止栈溢出攻击)
        set_current_blocked(&set);
        
        // 4. return 到原始用户态中断点
        return regs->ax;
    }
    

    四、多线程环境下的信号分发语义

    POSIX 标准定义了多线程程序中复杂的信号语义。Linux 通过 NPTL(Native POSIX Thread Library)在 glibc 层+内核配合下实现了完全 POSIX 合规的信号分发。

    4.1 信号的四种投递目标范围

    
                        ┌──────────────────────────────────┐
      信号产生方式 →     │ 投递目标范围                      │
      ├─ kill(pid,sig)  │ 任意线程(内核选择第一个未 block 的)│
      ├─ tkill(tid,sig) │ 指定线程                          │
      ├─ 硬件异常       │ 触发该指令的线程自身               │
      ├─ 异步 IO/SIGIO  │ 拥有该 fd 的线程(可能多个竞争)    │
      └─ SIGCHLD/定时器 │ 接收进程的某线程                    │
                        └──────────────────────────────────┘
    

    4.2 内核线程选择算法

    
    // kernel/signal.c: __send_signal()
    if (!wants_signal(sig, p))
        continue; // 该线程已 block 此信号 → 跳过
    
    // 选择一个未 block 的线程
    for_each_thread(p, t) {
        if (t != p && !thread_group_empty(p)) {
            // SIGCONT: 唤醒整个进程组(即使被 STOP)
            // 普通信号: 选择第一个未 block 的线程
            if (!wants_signal(sig, t)) continue;
            sigaddset(&t->pending.signal, sig); // 投递到线程私有 pending
            complete_signal(t, sig);
            goto out;
        }
    }
    // 所有线程都 block → 投递到进程级 shared_pending
    sigaddset(&p->signal->shared_pending.signal, sig);
    

    4.3 同一进程内多信号的 SIGCHLD 合并问题

    
    // 关键:SIGCHLD 默认不排队
    // 多个子进程快速连续退出时,SIGCHLD 只投递一次
    // 父进程 handler 必须循环 waitpid(-1, WNOHANG) 收割所有 zombie
    
    void sigchld_handler(int sig) {
        int saved_errno = errno;
        pid_t pid;
        int status;
        while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
            // 收割每个 zombie
            reap_child(pid, status);
        }
        errno = saved_errno;
    }
    

    生产陷阱:如果在 handler 内只调用一次 waitpid,当多个子进程几乎同时 SIGCHLD 时,只能收割一个,其余成为僵尸直到下次信号触发。这就是著名的 SIGCHLD 合并问题。

    4.4 signalfd——把信号变成可读事件

    Linux 2.6.22 引入的 signalfd 将信号转化为文件描述号上的可读事件,完美融入 epoll/io_uring 事件循环:

    
    // 使用 signalfd 将信号转化为 fd 可读事件
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGTERM);
    sigaddset(&mask, SIGINT);
    
    // 必须先 block 信号!否则信号仍按默认方式投递
    sigprocmask(SIG_BLOCK, &mask, NULL);
    
    int sfd = signalfd(-1, &mask, SFD_CLOEXEC);
    
    struct signalfd_siginfo fdsi;
    // 在 epoll/io_uring 中监听 sfd 的可读事件
    while (true) {
        read(sfd, &fdsi, sizeof(fdsi));
        switch (fdsi.ssi_signo) {
        case SIGTERM:
            graceful_shutdown();
            break;
        case SIGINT:
            quick_save_state();
            break;
        }
    }
    

    关键约束:使用 signalfd 处理信号的进程中,必须首先通过 sigprocmask(SIG_BLOCK) 阻止信号的常规投递。否则信号仍然会中断阻塞的 syscall,而 read() 将永远得不到返回。这是 signalfd 在生产中最常见的错误。


    五、实时信号与高级 POSIX 机制

    5.1 实时信号(SIGRTMIN ~ SIGRTMAX)的排队保证

    Linux 保证 SIGRTMIN(32) ~ SIGRTMAX(64) 共 33 个实时信号具有严格排队语义:

    
    // kernel/signal.c: __send_signal() 中的排队逻辑
    if (sig < SIGRT_MIN) {
        // 非实时信号:只排队一次(sigaddset 到 pending.signal 位图)
        sigaddset(&pending->signal, sig);
    } else {
        // 实时信号:追加到 sigqueue 链表(同时置位 pending.signal)
        list_add_tail(&q->list, &pending->list);
        sigaddset(&pending->signal, sig);
    }
    

    内核限制:实时信号排队数量受 RLIMIT_SIGPENDING 限制(ulimit -i),默认通常 1024。超出限制后 rt_sigqueueinfo 返回 -EAGAIN。

    5.2 sigqueue — 携带数据的信号发送

    
    // 发送携带附加数据(联合体)的实时信号
    union sigval value;
    value.sival_ptr = some_pointer; // 或 sival_int
    
    sigqueue(target_pid, SIGRTMIN + 3, value);
    

    在 handler 中通过 SA_SIGINFO 风格的 handler 接收:

    
    void rt_handler(int sig, siginfo_t *info, void *ucontext) {
        // info->si_value 包含发送方通过 sigqueue 携带的 union sigval
        int val = info->sival_int;
        void *ptr = info->sival_ptr;
        ...
    }
    

    生产场景:timer_create() 即通过 sigqueue + SIGEV_THREAD_ID 将定时器信号定向投递到特定线程,并携带 sigval 中包含 timer ID 和溢出计数。

    5.3 sigaltstack — handler 在独立栈执行

    
    stack_t ss;
    ss.ss_sp = malloc(SIGSTKSZ);  // 分配 alternate stack
    ss.ss_size = SIGSTKSZ;       // 8KB(x86_64 上)
    ss.ss_flags = 0;
    sigaltstack(&ss, NULL);
    
    struct sigaction sa;
    sa.sa_sigaction = handler;
    sa.sa_flags = SA_ONSTACK | SA_SIGINFO; // ← 关键标志
    sigemptyset(&sa.sa_mask);
    sigaction(SIGSEGV, &sa, NULL);
    

    关键场景:处理 SIGSEGV 时,如果 segfault 是由栈溢出(栈已耗尽)引起,handler 必须在 alternate stack 上运行,否则 handler 自身触发另一个 #DF(Double Fault)。这就是为什么 SA_ONSTACK 对 sigsegv handler 是强制要求。

    GLIBC 的 pthread 已为每个线程预分配 alternate stack(thread_stack_addr),通过 setaltstack 设置,确保栈溢出时 handler 有栈可执行。


    六、信号处理中的安全与同步约束

    6.1 Async-Signal-Safe 函数

    从信号 handler 中只能调用异步信号安全(async-signal-safe)的函数。这些函数要保证:不持有全局锁(否则可能死锁)、不可被信号中断、不使用不可重入的资源。

    GLIBC 定义的 async-signal-safe 函数集(POSIX.1-2017):

    
    _exit()        write()        kill()         sigaction()
    sigprocmask()  sigreturn()    pause()       alarm()
    read()         close()        fstat()        getpid()
    gettimeofday() clock_gettime()              ...
    

    不安全示例:malloc/free、printf/fprintf、pthread_mutex_lock(可能导致死锁)、exit()(非 _exit(),会刷新 FILE 缓冲区,可能死锁在 stdio 内部锁上)。

    6.2 自管道技巧(Self-Pipe Trick)

    事件驱动程序中处理信号的经典模式:

    
    // [ obsolete style — modern: use signalfd ]
    static int signal_pipe[2];
    
    void setup_signal_pipe() {
        pipe2(signal_pipe, O_CLOEXEC | O_NONBLOCK);
        
        struct sigaction sa;
        sa.sa_flags = SA_RESTART;
        sa.sa_handler = signal_to_pipe;
        sigemptyset(&sa.sa_mask);
        sigaction(SIGTERM, &sa, NULL);
        sigaction(SIGINT, &sa, NULL);
    }
    
    // handler 只能调用 write()(async-signal-safe)
    static void signal_to_pipe(int sig) {
        int saved_errno = errno;
        (void)write(signal_pipe[1], &sig, sizeof(int));
        errno = saved_errno;
    }
    
    // 在主事件循环中监听管道读端
    int main() {
        setup_signal_pipe();
        int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
        add_fd(epoll_fd, signal_pipe[0]);
        
        while (true) {
            int n = epoll_wait(epoll_fd, events, MAX, -1);
            for (int i = 0; i < n; i++) {
                if (events[i].data.fd == signal_pipe[0]) {
                    int sig;
                    read(signal_pipe[0], &sig, sizeof(int));
                    handle_signal(sig); // 在安全上下文中处理
                }
            }
        }
    }
    

    现代 Linux 中 signalfd 已经取代 self-pipe(更简洁、无需 O_NONBLOCK 保护、不占用额外 fd),但理解 self-pipe 有助于理解信号到事件循环的「延迟处理」哲学。

    6.3 信号与 ptrace(调试器)

    
    // 当进程被 ptrace 跟踪时,内核暂停在信号投递点
    if (current->ptrace & PT_PTRACED) {
        // 内核将信号排队发送 SIGTRAP/暂停子进程(SIGSTOP)
        // 通过 waitpid() 通知 tracer(调试器)
        sigaddset(¤t->pending.signal, sig);
        signal_wake_up_state(current, TASK_TRACED);
        return -EAGAIN; // 不立即投递到 handler
    }
    

    这是 GDB breakpoint、strace -p 等工具的基础:

    
    # GDB 拦截 SIGSEGV 的视觉化展示
    (gdb) catch signal SIGSEGV
    Catchpoint 1 (signal SIGSEGV)
    (gdb) run
    Catchpoint 1 (signal SIGSEGV), 0x... in foo.c:42
    

    七、生产环境常见问题与调试

    7.1 signal handler 死锁问题

    
    // ❌ 错误示例:handler 中调用 malloc(非 async-signal-safe)
    void bad_handler(int sig) {
        char *buf = malloc(256);  // malloc 持有 arena 锁!
        sprintf(buf, "Got signal %d", sig);
        fprintf(stderr, "%s\n", buf); // fprintf 同样不安全
        free(buf);
    }
    
    // ❌ 实际死锁路径:
    // main thread: malloc() 持锁 A → 中断 → handler → malloc() 尝试持锁 A → 死锁
    

    修复方案:write() 替代 fprintf(),预分配静态 buffer 替代 malloc():

    
    // ✅ 正确模式
    void good_handler(int sig) {
        const char msg[] = "Signal received, shutting down\n";
        write(STDERR_FILENO, msg, sizeof(msg) - 1); // write 是 async-signal-safe
        _exit(128 + sig);  // _exit() 异步安全(exit() 不是!)
    }
    

    7.2 通过 /proc/[pid]/status 检查信号状态

    
    # 查看进程的 pending 和 blocked 信号位图
    $ cat /proc/self/status | grep -i sig
    SigQ:    0/15311           # 当前排队信号数 / RLIMIT_SIGPENDING
    SigPnd:  0000000000000000  # pending 信号位图(hex)
    SigBlk:  0000000000010000  # blocked 信号(SIGUSR2=17 被 block)
    SigCgt:  00000001800004ec  # caught 信号位图
    

    扫描 SigCgt 是审计进程信号处理配置的有效方法:

    
    # 解析 SigCgt 位图以确定哪些信号被捕捉
    def parse_sigcgt(hex_str):
        bits = int(hex_str, 16)
        caught = []
        sig_names = {1:"SIGHUP", 2:"SIGINT", 3:"SIGQUIT", 4:"SIGILL", 
                     5:"SIGTRAP", 6:"SIGABRT", 7:"SIGBUS", 8:"SIGFPE",
                     9:"SIGKILL", ...}
        for i in range(1, 65):
            if bits & (1 << (i-1)):
                caught.append(sig_names.get(i, f"SIG({i})"))
        return caught
    

    7.3 使用 GDB 调试信号相关崩溃

    
    # 让 GDB 在 SIGSEGV 时不停止(为了继续 run 到 handler)
    (gdb) handle SIGSEGV nostop print pass
    
    # 在 handler 入口设置断点
    (gdb) break my_handler
    
    # 恢复原始上下文查看信号触发位置
    (gdb) info registers
    (gdb) bt
    (gdb) frame 2  # test.c:42 处的崩溃上下文
    

    7.4 Rust 生态中的信号处理最佳实践

    Rust 的 tokio 和 signal-hook crate 提供了比 C 更安全的信号处理抽象:

    
    // 使用 tokio 的 Unix 信号通知
    use tokio::signal::unix::{signal, SignalKind};
    
    async fn handle_signals() {
        let mut sigterm = signal(SignalKind::terminate()).unwrap();
        let mut sigint = signal(SignalKind::interrupt()).unwrap();
        let mut sighup = signal(SignalKind::hangup()).unwrap();
    
        tokio::select! {
            _ = sigterm.recv() => {
                println!("SIGTERM received, starting graceful shutdown");
                // 执行优雅关闭逻辑
            }
            _ = sigint.recv() => {
                println!("SIGINT received");
            }
            _ = sighup.recv() => {
                println!("SIGHUP received, reloading config");
            }
        }
    }
    

    Tokio 内部使用 signalfd (Linux) 或 kqueue note (macOS) 将信号包装为 async Stream,使其与选择器完全集成。

    
    // signal-hook crate:解决传统 signal handler 不能执行任何非
    // async-signal-safe 操作的限制
    use signal_hook::iterator::Signals;
    use signal_hook::consts::signal::*;
    
    fn main() {
        let mut signals = Signals::new(&[SIGTERM, SIGINT, SIGHUP]).unwrap();
        
        // 在独立线程中消费信号
        std::thread::spawn(move || {
            for sig in signals.forever() {
                match sig {
                    SIGTERM => { /* 异步信号安全上下文中可任意操作 */ }
                    SIGINT  => { /* ... */ }
                    _ => {}
                }
            }
        });
    }
    

    八、总结:信号处理的工程决策矩阵

    
    ┌───────────────────────────────────────────────────────────┐
    │              信号处理的工程决策矩阵                          │
    ├───────────────────────────────────────────────────────────┤
    │                                                           │
    │  1. 选择投递模型                                            │
    │     ├─ 单线程事件循环: signalfd + epoll/io_uring           │
    │     ├─ 多线程 + 专用: 一个线程 signalfd, 其余 block 信号   │
    │     ├─ 简单场景: sigaction + volatile sig_atomic_t flag    │
    │     └─ 高级 IPC: sigqueue + SA_SIGINFO + sigval 携带数据   │
    │                                                           │
    │  2. 安全约束优先级                                          │
    │     ├─ handler 内仅调用 async-signal-safe 函数             │
    │     ├─ 全局状态修改用 sig_atomic_t 或 volatile             │
    │     ├─ 避免 handler 内加锁 → 改用主循环延迟处理            │
    │     └─ SIGSEGV 栈溢出场景强制 SA_ONSTACK                   │
    │                                                           │
    │  3. 多线程信号分布                                          │
    │     ├─ 目标明确用 tgkill/tkill 定向发送                    │
    │     ├─ 非定向信号让内核自动选择未 block 线程                │
    │     └─ SIGCHLD handler 必须循环 waitpid(WNOHANG)           │
    │                                                           │
    │  4. 实时信号使用                                            │
    │     ├─ 优先 SIGRTMIN+3 ~ SIGRTMAX 自定义语义               │
    │     ├─ 注意 RLIMIT_SIGPENDING 限制                        │
    │     └─ 监控 /proc/[pid]/status SigQ 字段                  │
    │                                                           │
    └───────────────────────────────────────────────────────────┘
    

    Linux 信号处理不是简单的「注册→调用→返回」模型,而是横跨中断返回、进程调度、线程同步、安全边界和生产调试的复杂系统工程。理解内核的 signal_struct → sighand_struct → sigpending 三级数据模型、current->flags & _TIF_SIGPENDING 的投递触发点、以及 sys_rt_sigreturn 的上下文恢复机制,才能在生产环境中写出真正可靠的信号处理逻辑。

    Modern Linux (6.x) 通过 signalfd / pidfd_send_signal 不断丰富信号机制的原语,但核心原理——信号是异步事件、投递是延迟队列、handler 是不安全上下文——自 1960s CTSS 以来从未改变。理解这些本质,方能在 JDK Flight Recorder 的 SIGQUIT 线程 dump、Redis 的 SIGTERM 持久化收尾、以及 Go runtime 的 SIGURM 抢占调度中,游刃有余。

    点赞(0) 打赏

    评论列表 共有 0 条评论

    暂无评论
    立即
    投稿
    网站二维码

    微信公众账号

    微信扫一扫加关注

    发表
    评论
    返回
    顶部
    /* 跳过导航链接 (无障碍) */ .skip-link { position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } .skip-link:focus { top: 0; outline: 3px solid #0056b3; }