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 直到 unblockSIGKILL 和 SIGSTOP 不进入 pending,直接执行 sigactionsigqueue 链表排队,可同时存在多个实例并携带数据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 抢占调度中,游刃有余。

发表评论 取消回复