Linux内核分析之进程管理-06
14.1 进程终止 -- do_exit() 与资源回收
进程终止是操作系统中最复杂的状态转换之一。一个进程在运行期间持有大量内核资源:内存地址空间、文件描述符、信号处理器、POSIX 定时器、命名空间引用、cgroup 归属等。do_exit() 函数必须按照严格的顺序逐一释放这些资源,确保不产生死锁、不遗漏资源、不破坏内核一致性。本节将结合 Linux 7.0.10 内核源码,逐行分析进程终止的完整流程。
14.1.1 进程终止入口
进程的终止可以从三个不同入口进入,分别对应正常退出、信号触发终止和异常崩溃三种场景。
sys_exit() -- 单线程退出
// kernel/exit.c:1085-1088
SYSCALL_DEFINE1(exit, int, error_code)
{
do_exit((error_code&0xff)<<8);
}
sys_exit() 是最基础的退出系统调用。它将低 8 位错误码左移 8 位后传入 do_exit(),这与 POSIX 规范中 WEXITSTATUS(status) 宏的提取方式一致。值得注意的是,sys_exit() 只终止调用线程本身,不影响同线程组的其他线程。但在 glibc 层面,exit() 函数实际调用的是 sys_exit_group(),会终止整个线程组。
sys_exit_group() -- 线程组退出
// kernel/exit.c:1129-1134
SYSCALL_DEFINE1(exit_group, int, error_code)
{
do_group_exit((error_code & 0xff) << 8);
/* NOTREACHED */
return 0;
}
sys_exit_group() 是 glibc exit() 函数的真正系统调用入口。它调用 do_group_exit(),后者负责终止整个线程组中的所有线程。
do_group_exit() -- 线程组终止的核心协调者
// kernel/exit.c:1095-1122
void __noreturn
do_group_exit(int exit_code)
{
struct signal_struct *sig = current->signal;
if (sig->flags & SIGNAL_GROUP_EXIT) // line 1099
exit_code = sig->group_exit_code; // line 1100
else if (sig->group_exec_task) // line 1101
exit_code = 0;
else {
struct sighand_struct *const sighand = current->sighand;
spin_lock_irq(&sighand->siglock); // line 1106
if (sig->flags & SIGNAL_GROUP_EXIT) // line 1107
/* Another thread got here before we took the lock. */
exit_code = sig->group_exit_code; // line 1109
else if (sig->group_exec_task) // line 1110
exit_code = 0;
else {
sig->group_exit_code = exit_code; // line 1113
sig->flags = SIGNAL_GROUP_EXIT; // line 1114
zap_other_threads(current); // line 1115
}
spin_unlock_irq(&sighand->siglock); // line 1117
}
do_exit(exit_code); // line 1120
/* NOTREACHED */
}
这个函数的逻辑体现了线程组退出的三种场景:
场景一:已有退出在进行中(line 1099)。如果 SIGNAL_GROUP_EXIT 标志已经设置,说明其他线程已经率先启动了线程组退出流程。当前线程直接采用已有的 group_exit_code,不再重复设置。
场景二:exec 正在执行(line 1101)。如果 group_exec_task 非空,说明线程组正在执行 execve(),de_thread() 正在等待线程组领导者退出。此时退出码设为 0,因为 exec 替换了进程映像,旧进程的退出码没有意义。
场景三:首次触发退出(line 1103-1117)。这是最常见的路径。由于获取 siglock 可能存在竞争,获取锁后需要再次检查 SIGNAL_GROUP_EXIT。确认是首次退出后,执行三个关键操作:
sig->group_exit_code = exit_code:记录线程组的统一退出码sig->flags = SIGNAL_GROUP_EXIT:设置退出标志,阻止后续线程再次进入此路径zap_other_threads(current):向线程组中所有其他线程发送 SIGKILL,使它们也进入退出流程
zap_other_threads() 遍历线程组中的每个线程,为其设置 SIGKILL 信号并唤醒。被唤醒的线程在从内核返回用户空间前检测到信号,最终也会调用 do_group_exit(),进入场景一路径。
make_task_dead() -- 异常终止路径
// kernel/exit.c:1024-1083
void __noreturn make_task_dead(int signr)
{
struct task_struct *tsk = current;
unsigned int limit;
if (unlikely(in_interrupt())) // line 1038
panic("Aiee, killing interrupt handler!");
if (unlikely(!tsk->pid)) // line 1040
panic("Attempted to kill the idle task!");
make_task_dead() 处理内核 oops 等灾难性错误导致的进程终止。它首先进行两项关键检查:如果在中断上下文中触发 oops,整个系统必须 panic,因为中断上下文没有关联的 task_struct 可以安全终止;如果试图终止 idle 进程(PID=0),也必须 panic,因为 idle 进程是每个 CPU 的调度兜底。
if (unlikely(irqs_disabled())) { // line 1043
pr_info("note: %s[%d] exited with irqs disabled\n",
current->comm, task_pid_nr(current));
local_irq_enable();
}
if (unlikely(in_atomic())) { // line 1048
pr_info("note: %s[%d] exited with preempt_count %d\n",
current->comm, task_pid_nr(current),
preempt_count());
preempt_count_set(PREEMPT_ENABLED);
}
接下来修复中断和抢占状态。oops 发生时,当前 CPU 可能处于中断禁用或抢占禁用状态。make_task_dead() 强制启用中断和抢占,因为后续的 do_exit() 需要正常的执行环境。
limit = READ_ONCE(oops_limit); // line 1065
if (atomic_inc_return(&oops_count) >= limit && limit)
panic("Oopsed too often (kernel.oops_limit is %d)", limit);
oops 计数限制是 Linux 5.9 引入的安全机制。每次 oops 可能导致引用计数泄漏(因为错误发生时可能正好持有某对象的引用),反复 oops 可能使引用计数溢出,从而将原本不可利用的漏洞变为可利用。oops_limit(通过 kernel.oops_limit sysctl 可调)限制了系统在 panic 之前允许的最大 oops 次数。
if (unlikely(tsk->flags & PF_EXITING)) { // line 1073
pr_alert("Fixing recursive fault but reboot is needed!\n");
futex_exit_recursive(tsk);
tsk->exit_state = EXIT_DEAD;
refcount_inc(&tsk->rcu_users);
preempt_disable();
do_task_dead();
}
do_exit(signr); // line 1082
递归故障检测:如果进程在已经处于 PF_EXITING 状态时再次触发 oops,说明 do_exit() 本身出现了问题。此时无法安全地再次调用 do_exit(),只能直接设置 EXIT_DEAD 状态并调用 do_task_dead() 让进程立即从调度器中消失。这种情况需要系统重启才能完全恢复。
14.1.2 do_exit() 完整流程
do_exit() 是进程终止的主函数,函数签名为 void __noreturn do_exit(long code)(kernel/exit.c:897)。__noreturn 属性告诉编译器此函数永远不会返回,编译器会据此优化调用点的代码生成。整个函数从 line 897 到 line 1022,跨度约 125 行,涵盖了从清理调试状态到最终调度的完整过程。
初始化与 kthread 检查
// kernel/exit.c:897-908
void __noreturn do_exit(long code)
{
struct task_struct *tsk = current;
struct kthread *kthread;
int group_dead;
WARN_ON(irqs_disabled());
WARN_ON(tsk->plug);
kthread = tsk_is_kthread(tsk);
if (unlikely(kthread))
kthread_do_exit(kthread, code);
函数开头获取 current 指针(line 899),声明 group_dead 局部变量用于后续判断是否是线程组的最后一个线程。WARN_ON 断言检查两个不应该出现的情况:中断禁用状态(do_exit() 可能需要睡眠)和 blk plug 未清理(块 IO 插头应该在之前被冲刷)。
内核线程(kthread)有特殊的退出路径:tsk_is_kthread() 检查 PF_KTHREAD 标志,如果为真则调用 kthread_do_exit()。内核线程不拥有用户空间资源,其退出逻辑大为简化。kthread_do_exit() 被标记为 __noreturn,不会返回到 do_exit() 的后续流程。
调试与追踪清理
kcov_task_exit(tsk); // line 910
kmsan_task_exit(tsk); // line 911
kcov_task_exit()(line 910)清理内核代码覆盖率(Kernel COVerage)工具的状态。kcov 用于用户空间程序收集内核代码覆盖率数据,主要用于 fuzzing 工具(如 syzkaller)。进程退出时必须解除 kcov 与当前任务的关联,防止悬挂引用。
kmsan_task_exit()(line 911)清理内核内存消毒器(Kernel Memory Sanitizer)的状态。KMSAN 用于检测内核中未初始化内存的使用,需要在进程退出时释放相关的影子内存元数据。
synchronize_group_exit() -- 协调线程组退出
synchronize_group_exit(tsk, code); // line 913
// kernel/exit.c:868-895
static void synchronize_group_exit(struct task_struct *tsk, long code)
{
struct sighand_struct *sighand = tsk->sighand;
struct signal_struct *signal = tsk->signal;
struct core_state *core_state;
spin_lock_irq(&sighand->siglock);
signal->quick_threads--; // line 875
if ((signal->quick_threads == 0) &&
!(signal->flags & SIGNAL_GROUP_EXIT)) {
signal->flags = SIGNAL_GROUP_EXIT; // line 878
signal->group_exit_code = code;
signal->group_stop_count = 0;
}
synchronize_group_exit() 在 siglock 保护下递减 quick_threads 计数器。这个计数器与 signal->live 不同:live 是原子变量,用于判断线程组是否全部退出;而 quick_threads 在 siglock 保护下操作,用于协调退出过程中的信号处理。
当 quick_threads 降为 0 且没有其他退出操作在进行时,设置 SIGNAL_GROUP_EXIT 标志。这确保即使没有通过 do_group_exit() 进入(例如通过 sys_exit() 单线程退出),线程组退出标志也能被正确设置。
core_state = signal->core_state;
spin_unlock_irq(&sighand->siglock);
if (unlikely(core_state))
coredump_task_exit(tsk, core_state);
如果线程组正在进行 coredump(core_state 非空),当前线程需要与 coredump 操作同步。coredump_task_exit() 将当前线程加入 core_thread 链表,并递减 core_state->nr_threads 计数,通知 coredumper 有线程即将退出。
ptrace 事件通知
ptrace_event(PTRACE_EVENT_EXIT, code); // line 914
user_events_exit(tsk); // line 915
ptrace_event()(line 914)在进程退出时通知正在追踪此进程的 tracer(如 gdb、strace)。PTRACE_EVENT_EXIT 事件允许 tracer 在被追踪进程完全退出之前获取其最终状态。
user_events_exit()(line 915)清理用户事件追踪(user_events)的注册信息。user_events 是 Linux 5.18 引入的轻量级追踪机制,允许用户空间程序通过 write() 系统调用注册追踪事件。
io_uring 清理
io_uring_files_cancel(); // line 917
io_uring_files_cancel() 取消当前进程所有未完成的 io_uring 请求。这一步必须在 exit_files() 之前执行,因为 io_uring 的异步 IO 操作可能持有对文件描述符的引用。如果在释放文件描述符表之后再取消 io_uring 请求,可能导致 use-after-free。io_uring 的清理也必须在 exit_signals() 之前完成,因为 io_uring 可能需要等待 CQE(Completion Queue Entry)的处理。
调度器 mm_cid 清理
sched_mm_cid_exit(tsk); // line 918
sched_mm_cid_exit() 清理调度器中与内存管理上下文 ID(Memory Context ID)相关的状态。mm_cid 是用于 TLB 刷新优化的每 CPU 每进程 ID,在进程退出时需要释放,使其可以被新进程复用。
14.1.3 exit_signals() -- 设置 PF_EXITING
exit_signals(tsk); /* sets PF_EXITING */ // line 919
// kernel/signal.c:3116-3164
void exit_signals(struct task_struct *tsk)
{
int group_stop = 0;
sigset_t unblocked;
cgroup_threadgroup_change_begin(tsk);
if (thread_group_empty(tsk) || (tsk->signal->flags & SIGNAL_GROUP_EXIT)) {
tsk->flags |= PF_EXITING; // line 3128
cgroup_threadgroup_change_end(tsk);
return;
}
spin_lock_irq(&tsk->sighand->siglock);
tsk->flags |= PF_EXITING; // line 3138
exit_signals() 是 do_exit() 中的一个关键转折点。在此之后,进程被标记为 PF_EXITING,信号子系统和其他子系统将以此为依据改变对当前进程的行为。
快速路径(line 3127-3131):如果线程组为空(thread_group_empty())或者线程组退出已经在进行中(SIGNAL_GROUP_EXIT 已设置),只需设置 PF_EXITING 即可返回。这是单线程进程或线程组中最后退出的线程的常见路径。
慢速路径(line 3133-3153):对于多线程进程中的非首个退出线程,需要额外的信号重定向操作。
PF_EXITING 标志的含义和影响
PF_EXITING 标志(定义在 include/linux/sched.h 中)对内核多个子系统有深远影响:
-
信号发送:
wants_signal()函数(signal.c)在为目标线程选择信号投递对象时,会跳过设置了PF_EXITING的线程。这确保正在退出的线程不会被选为新的信号目标。 -
定时器:POSIX 定时器在通知信号时通过
send_sigqueue()检查目标线程的PF_EXITING标志,避免向即将消亡的线程发送信号。 -
ptrace:tracer 在 attach 时会跳过
PF_EXITING的进程,避免竞争条件。 -
cgroup:
cgroup_threadgroup_change_begin/end()确保在设置PF_EXITING前后,cgroup 的线程组迁移操作被阻塞。
信号重定向 -- retarget_shared_pending
cgroup_threadgroup_change_end(tsk);
if (!task_sigpending(tsk)) // line 3142
goto out;
unblocked = tsk->blocked;
signotset(&unblocked); // line 3146
retarget_shared_pending(tsk, &unblocked); // line 3147
当退出的线程有待处理的信号时(task_sigpending() 为真),需要将这些信号重定向到线程组中的其他存活线程。具体操作:
unblocked = tsk->blocked; signotset(&unblocked)-- 计算当前线程未阻塞的信号集合(即可能被当前线程处理的信号)retarget_shared_pending(tsk, &unblocked)-- 从当前线程的共享待处理队列中,将未阻塞的信号移动到其他合适的线程
retarget_shared_pending() 的核心逻辑是:对于每个在 shared_pending 中排队且当前线程可以处理的信号,调用 signal_wake_up() 唤醒线程组中的另一个合适线程来接手处理。信号位图中会移除已重定向的信号,防止重复处理。
seccomp 过滤器释放
seccomp_filter_release(tsk); // line 921
seccomp_filter_release() 释放当前进程关联的所有 seccomp-BPF 过滤器。seccomp 通过 BPF 程序限制进程可以使用的系统调用,每个过滤器通过引用计数共享(fork 时子进程继承父进程的过滤器)。进程退出时需要释放其引用。
注意这一步在 PF_EXITING 设置之后执行,确保在 seccomp 释放过程中不会有新的系统调用检查需要使用这些过滤器。
14.1.4 group_dead 判定
acct_update_integrals(tsk); // line 923
group_dead = atomic_dec_and_test(&tsk->signal->live); // line 924
group_dead 是 do_exit() 中的一个核心布尔变量。atomic_dec_and_test() 原子地将 signal->live 减 1,如果结果为 0 则返回 true,表示当前线程是线程组中最后一个退出的线程。
signal->live(定义在 include/linux/sched/signal.h:96)在线程创建时(copy_process())初始化为新线程组的线程数,每创建一个线程递增 1。当最后一个线程退出时 live 降为 0,触发线程组级别的资源清理。
init 进程保护
if (group_dead) {
if (unlikely(is_global_init(tsk))) // line 930
panic("Attempted to kill init! exitcode=0x%08x\n",
tsk->signal->group_exit_code ?: (int)code); // line 932
init 进程(PID 1)是用户空间的祖先进程,系统中的所有进程最终都是 init 的后代。如果 init 进程退出,整个系统将无法正常运作(没有进程会回收孤儿进程)。因此,当 group_dead 为真且退出的进程是全局 init 时,内核直接调用 panic() 使系统崩溃,以便获取可用的 coredump 用于调试。
is_global_init() 检查进程是否是全局 PID 命名空间(init_pid_ns)中的 PID 1。在容器环境中,容器内的 init 进程(子命名空间的 PID 1)退出不会触发 panic,只会使该命名空间的进程被清理。
POSIX 定时器清理
#ifdef CONFIG_POSIX_TIMERS
hrtimer_cancel(&tsk->signal->real_timer); // line 935
exit_itimers(tsk); // line 936
#endif
只有 group_dead 为真时才清理 POSIX 定时器。real_timer 是 ITIMER_REAL 对应的高精度定时器,exit_itimers() 清理所有 POSIX.1b 间隔定时器。这些定时器关联到线程组(而非单个线程),因此只在线程组最后一个线程退出时清理。
14.1.5 资源释放序列
在 group_dead 判定之后,do_exit() 按照严格的顺序释放进程持有的各种资源。释放顺序至关重要,因为资源之间存在依赖关系。
性能事件和栈展开
tsk->exit_code = code; // line 946
taskstats_exit(tsk, group_dead); // line 947
trace_sched_process_exit(tsk, group_dead); // line 948
perf_event_exit_task(tsk); // line 957
unwind_deferred_task_exit(tsk); // line 963
exit_code 赋值(line 946)将退出码记录在 task_struct 中,父进程通过 wait() 可以获取此值。taskstats_exit() 向 taskstats 接口报告进程的退出信息,用于进程记账(per-task accounting)。trace_sched_process_exit() 触发 sched_process_exit 追踪点,perf 和 ftrace 等工具可以捕获此事件。
perf_event_exit_task()(line 957)必须在 exit_mm() 之前执行。性能采样可能需要访问进程的内存映射(mm_struct),因此在释放地址空间之前必须停止所有 perf 事件。具体来说,perf 的硬件性能计数器(PMU)可能配置了基于用户空间地址的采样,这些采样依赖 mm_struct 中的页表信息。
unwind_deferred_task_exit()(line 963)刷新延迟的栈展开请求。栈展开需要访问进程的内存映射来解析用户空间栈帧,必须在 exit_mm() 之前完成。
exit_mm() -- 释放地址空间
exit_mm(); // line 965
// kernel/exit.c:550-585
static void exit_mm(void)
{
struct mm_struct *mm = current->mm;
exit_mm_release(current, mm); // line 554
if (!mm)
return;
mmap_read_lock(mm); // line 557
mmgrab_lazy_tlb(mm); // line 558
BUG_ON(mm != current->active_mm);
task_lock(current);
smp_mb__after_spinlock(); // line 572
local_irq_disable();
current->user_dumpable = (get_dumpable(mm) == SUID_DUMP_USER);
current->mm = NULL; // line 575
membarrier_update_current_mm(NULL); // line 576
enter_lazy_tlb(mm, current); // line 577
local_irq_enable();
task_unlock(current);
mmap_read_unlock(mm);
mm_update_next_owner(mm); // line 581
mmput(mm); // line 582
if (test_thread_flag(TIF_MEMDIE))
exit_oom_victim();
}
exit_mm() 是资源释放中最复杂的步骤之一,它处理进程地址空间的释放。内核线程(mm == NULL)在 exit_mm_release() 之后直接返回。
exit_mm_release() -- futex 与 robust list
exit_mm_release()(line 554)调用 futex_exit_release(),处理进程持有的所有 futex 锁。futex(Fast Userspace Mutex)允许用户空间程序在无竞争时完全在用户态操作 mutex,只在竞争时进入内核。进程退出时必须:
- 唤醒所有在此进程持有的 futex 上等待的其他进程
- 处理 robust list(通过
set_robust_list()系统调用注册的 robust mutex 列表),为每个锁住的 robust mutex 设置FUTEX_OWNER_DIED标志并唤醒等待者
如果遗漏这些操作,其他进程可能永远等待在一个已经死亡进程持有的锁上。
Lazy TLB 机制
mmgrab_lazy_tlb(mm); // line 558
mmgrab_lazy_tlb() 为 lazy TLB 机制额外增加一个对 mm_struct 的引用。在多线程环境中,当一个线程退出时,其他线程可能仍在使用相同的地址空间。即使当前线程是最后一个用户,lazy TLB 机制允许内核临时保持对 mm 的引用,避免在其他 CPU 仍在使用相关 TLB 条目时释放页表。
内存屏障与 membarrier
smp_mb__after_spinlock(); // line 572
这个内存屏障是 membarrier 系统调用的关键组成部分。membarrier() 允许一个线程向系统中所有其他线程发出内存屏障指令,用于实现用户空间 RCU 等算法。当进程退出时,它必须确保在清除 current->mm 之前,所有之前的用户空间内存访问都已经完成并对其他 CPU 可见。smp_mb__after_spinlock() 保证了这个顺序:先完成所有用户空间内存访问,然后才将 mm 设为 NULL。
地址空间解绑
current->mm = NULL; // line 575
membarrier_update_current_mm(NULL); // line 576
enter_lazy_tlb(mm, current); // line 577
将 current->mm 设为 NULL 后,当前线程不再是任何用户地址空间的所有者。enter_lazy_tlb() 是架构相关的函数,通知 TLB 管理代码当前 CPU 进入 lazy TLB 模式。在此模式下,CPU 不需要维护活跃的地址空间切换。
mm 的最终释放
mm_update_next_owner(mm); // line 581
mmput(mm); // line 582
mm_update_next_owner() 在多线程环境中将 mm 的所有权转移给同线程组的另一个线程(如果有)。这是因为内核中某些操作(如 /proc 文件系统访问)通过 owner 查找 mm_struct。
mmput() 递减 mm_struct 的引用计数。当计数归零时,__mmput() 被调用,执行实际的地址空间释放:卸载所有 VMA(exit_mmap())、释放页表、释放 mm_struct 本身。对于大型进程,exit_mmap() 可能需要释放数百万个页面,是一个潜在耗时的操作。
exit_sem -- System V 信号量
exit_sem(tsk); // line 970
exit_sem()(ipc/sem.c)撤销当前进程在 System V 信号量上的所有操作。当进程通过 semop() 对信号量执行操作时,内核维护一个 undo 列表。进程退出时,这些操作被反向执行(例如,如果进程曾将信号量加 3,退出时减 3),防止进程异常退出导致信号量永远处于不可用状态。
exit_shm -- 共享内存
exit_shm(tsk); // line 971
exit_shm()(ipc/shm.c)处理 System V 共享内存段的分离。当进程通过 shmat() 附加到共享内存段时,内核在其地址空间中创建映射。进程退出时必须解除这些映射,并递减共享内存段的附加计数。当计数降为 0 且段被标记为删除(通过 shmctl(IPC_RMID)),共享内存段才被真正释放。
exit_files -- 文件描述符表
exit_files(tsk); // line 972
exit_files()(fs/file.c)递减当前进程文件描述符表(files_struct)的引用计数。files_struct 通过引用计数在 fork 时共享(CLONE_FILES),只有当所有引用者都退出时才真正释放。释放时关闭所有打开的文件描述符,这包括:
- 调用每个文件的
f_op->release()方法(如 socket 的sock_release()) - 释放文件锁(通过
locks_remove_posix()) - 释放
files_struct结构本身
注意 exit_files() 在 io_uring_files_cancel() 之后执行,确保 io_uring 不会再访问任何文件描述符。
exit_fs -- 文件系统信息
exit_fs(tsk); // line 973
exit_fs()(fs/fs_struct.c)递减 fs_struct 的引用计数。fs_struct 包含进程的根目录(root)、当前工作目录(pwd)和 umask。与 files_struct 类似,fs_struct 通过 CLONE_FS 在线程间共享。
控制终端分离
if (group_dead)
disassociate_ctty(1); // line 975
只有线程组最后一个线程退出时才释放控制终端。disassociate_ctty(1) 将当前进程从其控制终端分离,参数 1 表示在退出时调用。如果当前进程是会话领导者(session leader),还会向前台进程组发送 SIGHUP 信号,通知它们控制终端已关闭。
命名空间退出
exit_nsproxy_namespaces(tsk); // line 976
exit_nsproxy_namespaces()(kernel/nsproxy.c)递减 nsproxy 的引用计数。nsproxy 是命名空间的代理结构,聚合了进程所属的所有命名空间引用(PID、Mount、Network、User、IPC、Cgroup、Time 命名空间)。当引用计数归零时释放 nsproxy,但各个命名空间本身有独立的引用计数,可能被其他进程保持存活。
task_work 队列
exit_task_work(tsk); // line 977
exit_task_work() 执行挂起的 task_work 队列中的所有回调。task_work 机制允许内核在进程上下文中延迟执行工作(类似于用户空间的 signal handler 概念)。例如,io_uring 使用 task_work 在进程上下文中完成 IO 操作。进程退出时必须确保所有挂起的 task_work 都已执行完毕。
架构相关清理
exit_thread(tsk); // line 978
exit_thread() 是架构相关的清理函数。在 x86_64 上(arch/x86/kernel/process.c),它执行以下操作:
- 释放 x86 FPU 状态(fpu__drop())
- 释放 DS(Debug Store)区域
- 清理已分配的 IO 位图
cgroup 退出
cgroup_task_exit(tsk); // line 981
cgroup_task_exit()(kernel/cgroup/cgroup.c)将当前进程从其所属的 cgroup 中移除。cgroup(Control Group)用于资源限制和监控(CPU、内存、IO 等)。进程退出时需要递减 cgroup 的进程计数,并在启用 cgroup v2 压力失速信息(PSI)时更新资源压力统计。
14.1.6 exit_notify() -- 通知父进程
exit_tasks_rcu_start(); // line 988
exit_notify(tsk, group_dead); // line 989
proc_exit_connector(tsk); // line 990
exit_notify() 是进程终止流程中最关键的函数之一,它处理父进程通知和子进程重挂,决定了进程最终是进入 ZOMBIE 状态还是被立即回收。
// kernel/exit.c:737-780
static void exit_notify(struct task_struct *tsk, int group_dead)
{
bool autoreap;
struct task_struct *p, *n;
LIST_HEAD(dead);
write_lock_irq(&tasklist_lock); // line 743
forget_original_parent(tsk, &dead); // line 744
if (group_dead)
kill_orphaned_pgrp(tsk->group_leader, NULL); // line 747
tsk->exit_state = EXIT_ZOMBIE; // line 749
forget_original_parent -- 子进程重挂
// kernel/exit.c:698-731
static void forget_original_parent(struct task_struct *father,
struct list_head *dead)
{
struct task_struct *p, *t, *reaper;
if (unlikely(!list_empty(&father->ptraced)))
exit_ptrace(father, dead); // line 704
reaper = find_child_reaper(father, dead); // line 707
if (list_empty(&father->children))
return;
reaper = find_new_reaper(father, reaper); // line 711
list_for_each_entry(p, &father->children, sibling) {
for_each_thread(p, t) {
RCU_INIT_POINTER(t->real_parent, reaper); // line 714
BUG_ON((!t->ptrace) != (rcu_access_pointer(t->parent) == father));
if (likely(!t->ptrace))
t->parent = t->real_parent; // line 717
if (t->pdeath_signal)
group_send_sig_info(t->pdeath_signal,
SEND_SIG_NOINFO, t,
PIDTYPE_TGID); // line 720
}
if (!same_thread_group(reaper, father))
reparent_leader(father, p, dead); // line 728
}
list_splice_tail_init(&father->children, &reaper->children);
}
当一个进程退出时,它的所有子进程必须被"收养"到另一个进程。forget_original_parent() 的收养策略遵循以下优先级:
第一步:ptrace 子进程处理(line 704)。如果退出进程正在 ptrace 追踪其他进程,这些被追踪进程先通过 exit_ptrace() 交还给它们原来的父进程。
第二步:查找子进程回收者(line 707)。find_child_reaper() 确定当前 PID 命名空间中的 init 进程(child_reaper)作为最终兜底。
第三步:查找新父进程(line 711)。find_new_reaper() 的查找策略:
// kernel/exit.c:636-669
static struct task_struct *find_new_reaper(struct task_struct *father,
struct task_struct *child_reaper)
{
struct task_struct *thread, *reaper;
thread = find_alive_thread(father); // line 641
if (thread)
return thread; // line 643 -- 同线程组的其他线程优先
if (father->signal->has_child_subreaper) { // line 645
unsigned int ns_level = task_pid(father)->level;
for (reaper = father->real_parent;
task_pid(reaper)->level == ns_level;
reaper = reaper->real_parent) {
if (reaper == &init_task)
break;
if (!reaper->signal->is_child_subreaper)
continue;
thread = find_alive_thread(reaper);
if (thread)
return thread;
}
}
return child_reaper; // line 668 -- init 进程兜底
}
-
同线程组的存活线程(line 641-643):如果退出进程所在的线程组中还有其他存活线程,子进程优先被交给同线程组的线程。这是最自然的选择,因为同线程组的线程共享地址空间和信号处理器。
-
子进程回收者(child_subreaper)(line 645-666):Linux 3.4 引入了
PR_SET_CHILD_SUBREAPERprctl 选项,允许进程将自己标记为"子进程回收者"。当这样的进程的任何后代进程退出时,其后代孤儿进程不会被交给 init,而是沿着进程树向上查找标记了is_child_subreaper的祖先。这是 systemd 等服务管理器的重要功能。 -
init 进程(line 668):如果前两种选择都失败,子进程被交给当前 PID 命名空间的 init 进程。
pdeath_signal -- 父进程死亡信号
if (t->pdeath_signal) // line 718
group_send_sig_info(t->pdeath_signal,
SEND_SIG_NOINFO, t,
PIDTYPE_TGID);
子进程可以通过 prctl(PR_SET_PDEATHSIG, signo) 设置在父进程死亡时接收的信号。在子进程被重挂时,如果设置了 pdeath_signal,内核向子进程发送此信号。这是一种"孤儿通知"机制。
EXIT_ZOMBIE 状态
tsk->exit_state = EXIT_ZOMBIE; // line 749
进程的 exit_state 被设置为 EXIT_ZOMBIE。在此状态下:
- 进程已经完全停止执行,不会再被调度
- task_struct 仍然保留在系统中,因为父进程可能需要查询退出状态
- 进程的所有资源(内存、文件等)已经被释放,只留下 task_struct 和少量关联数据
autoreap 判定与 SIGCHLD 发送
if (unlikely(tsk->ptrace)) { // line 751
int sig = thread_group_leader(tsk) &&
thread_group_empty(tsk) &&
!ptrace_reparented(tsk) ?
tsk->exit_signal : SIGCHLD;
autoreap = do_notify_parent(tsk, sig); // line 756
} else if (thread_group_leader(tsk)) { // line 757
autoreap = thread_group_empty(tsk) &&
do_notify_parent(tsk, tsk->exit_signal); // line 759
} else {
autoreap = true; // line 761
/* untraced sub-thread */
do_notify_pidfd(tsk); // line 763
}
autoreap 判定决定进程是停留在 ZOMBIE 状态还是被立即回收。三种情况:
情况一:被 ptrace 追踪的进程(line 751-756)。被追踪进程的退出通知比较复杂。如果进程是线程组领导者、线程组为空、且没有被 ptrace 重新挂接(ptrace_reparented() 为假),则使用进程原始的 exit_signal(通常是 SIGCHLD);否则强制使用 SIGCHLD。do_notify_parent() 的返回值决定是否自动回收。
情况二:非追踪的线程组领导者(line 757-759)。只有当线程组为空(所有其他线程都已退出)且 do_notify_parent() 返回 true 时才自动回收。do_notify_parent() 向父进程发送 SIGCHLD(或进程指定的退出信号),并检查父进程是否设置了 SA_NOCLDWAIT 标志或者明确忽略了 SIGCHLD。如果是,返回 true 表示父进程不关心退出状态,子进程可以被自动回收。
情况三:非领导者的子线程(line 760-763)。子线程不是线程组领导者,它不需要发送 SIGCHLD 给父进程(只有领导者需要),因此直接标记为 autoreap。do_notify_pidfd() 通知通过 pidfd 监视此线程的进程。
EXIT_DEAD 与 release_task
if (autoreap) { // line 766
tsk->exit_state = EXIT_DEAD; // line 767
list_add(&tsk->ptrace_entry, &dead); // line 768
}
/* mt-exec, de_thread() is waiting for group leader */
if (unlikely(tsk->signal->notify_count < 0)) // line 772
wake_up_process(tsk->signal->group_exec_task); // line 773
write_unlock_irq(&tasklist_lock); // line 774
list_for_each_entry_safe(p, n, &dead, ptrace_entry) {
list_del_init(&p->ptrace_entry);
release_task(p); // line 778
}
如果 autoreap 为真,进程直接进入 EXIT_DEAD 状态并放入 dead 列表。在释放 tasklist_lock 后,release_task() 被调用以最终释放 task_struct。
release_task() 执行以下操作:
1. __exit_signal():累计线程的资源使用统计到线程组(utime、stime 等),递减 signal->live(如果尚未归零),释放 sighand_struct 引用
2. __exit_pid():将进程从 PID 哈希表中移除
3. release_task_struct() / free_task():释放 task_struct 占用的内存
exec 通知(line 772-773):当 notify_count < 0 时,说明有 execve() 操作(在 de_thread() 中)正在等待线程组领导者退出。group_exec_task 是执行 exec 的线程,wake_up_process() 唤醒它继续完成 exec 操作。
14.1.7 最终调度:do_task_dead()
// kernel/exit.c:1013-1020
preempt_disable(); // line 1013
if (tsk->nr_dirtied)
__this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied);
exit_rcu(); // line 1016
exit_tasks_rcu_finish(); // line 1017
lockdep_free_task(tsk); // line 1019
do_task_dead(); // line 1020
在 exit_notify() 完成后,进程进入最后的调度准备阶段。
preempt_disable -- 禁止抢占
preempt_disable()(line 1013)禁用内核抢占。从此时起,当前 CPU 不会再发生抢占,进程将一直运行到主动调用 __schedule()。这是必要的,因为后续的清理步骤不允许被中断。
脏页计数转移
if (tsk->nr_dirtied)
__this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied);
如果进程退出时仍有未回写的脏页(nr_dirtied 非零),这些计数被转移到每 CPU 的 dirty_throttle_leaks。脏节流(dirty throttling)机制根据系统中总脏页数量限制进程产生脏页的速率。进程退出时,其贡献的脏页数需要从节流计算中移除。
RCU 退出
exit_rcu(); // line 1016
exit_tasks_rcu_finish(); // line 1017
exit_rcu() 将当前任务从 RCU(Read-Copy-Update)的宽限期(grace period)等待列表中移除。RCU 是 Linux 内核中广泛使用的同步机制,允许读者无锁访问共享数据。进程可能处于 RCU 读侧临界区中,退出时必须确保不在任何临界区内。
exit_tasks_rcu_finish() 清理 Tasks RCU(一种 RCU 变体,用于跟踪任务状态变更)的回调。它确保在 task_struct 被释放之前,所有相关的 RCU 回调都已完成。
lockdep 任务释放
lockdep_free_task(tsk); // line 1019
lockdep_free_task() 清理 lockdep(锁依赖检测器)中与当前任务相关的状态。lockdep 跟踪每个任务持有的锁,用于检测潜在的死锁。任务退出时需要释放其 lockdep 上下文。
do_task_dead -- 最终调度
// kernel/sched/core.c:6901-6915
void __noreturn do_task_dead(void)
{
/* Causes final put_task_struct in finish_task_switch(): */
set_special_state(TASK_DEAD); // line 6904
/* Tell freezer to ignore us: */
current->flags |= PF_NOFREEZE; // line 6907
__schedule(SM_NONE); // line 6909
BUG();
for (;;)
cpu_relax();
}
do_task_dead() 是进程生命周期的最终调用。它将进程状态设置为 TASK_DEAD(line 6904),这是一个特殊状态,调度器不会再将此进程放回运行队列。
set_special_state() 与普通的 set_current_state() 不同,它直接设置 task_struct->state 而不使用 set_current_state() 的优化路径。这确保状态设置对其他 CPU 立即可见。
PF_NOFREEZE(line 6907)防止 freezer 子系统在挂起/恢复操作中尝试冻结此进程。已经死亡的进程不需要被冻结。
__schedule(SM_NONE)(line 6909)触发调度器切换到下一个可运行进程。在 finish_task_switch() 中,调度器检测到前一个进程的状态是 TASK_DEAD,调用 put_task_struct() 递减 task_struct 的引用计数。如果引用计数归零(通常如此),task_struct 的内存被释放。
BUG()(line 6910)是一个安全断言:__schedule() 不应该返回到 do_task_dead() 中,因为进程已经从运行队列中移除。如果由于某种原因 __schedule() 确实返回了,BUG() 会触发内核崩溃。
for (;;) cpu_relax()(line 6913-6914)是在 BUG() 被配置为空操作时的兜底死循环。
14.1.8 ZOMBIE 状态与 wait
EXIT_ZOMBIE 的生命周期
当进程通过 exit_notify() 设置 exit_state = EXIT_ZOMBIE(exit.c:749)后,进程进入僵尸状态。在这个状态下:
- 进程不再消耗 CPU 时间(不会被调度)
- 进程的内存地址空间已被释放(
exit_mm()已执行) - 进程的文件描述符已关闭(
exit_files()已执行) - 但
task_struct结构仍然保留在内核中,因为: - 父进程可能通过
wait()/waitpid()获取退出状态 - 进程的累计资源使用统计(utime/stime 等)需要被父进程读取
- 进程的 PID 仍然被占用,直到被回收
wait()/waitpid() 系统调用
父进程通过 wait()/waitpid()/waitid() 系统调用回收子进程。这些系统调用的核心实现在 kernel/exit.c 的 wait_task_zombie() 函数中:
- 父进程在
wait_chldexit等待队列上睡眠(定义在signal_struct的 line 101) - 当子进程调用
do_notify_parent()发送 SIGCHLD 时,父进程被唤醒 wait_task_zombie()读取子进程的退出信息(exit_code、utime/stime 等)- 将子进程的
exit_state从EXIT_ZOMBIE改为EXIT_DEAD - 调用
release_task()释放task_struct
孤儿进程处理
当父进程先于子进程退出时,子进程成为孤儿进程。forget_original_parent()(exit.c:698-731)负责将这些子进程重挂到新的父进程。
reparent_leader() -- 子进程领导者的重挂
// kernel/exit.c:674-693
static void reparent_leader(struct task_struct *father, struct task_struct *p,
struct list_head *dead)
{
if (unlikely(p->exit_state == EXIT_DEAD))
return;
p->exit_signal = SIGCHLD; // line 681
if (!p->ptrace &&
p->exit_state == EXIT_ZOMBIE && thread_group_empty(p)) {
if (do_notify_parent(p, p->exit_signal)) {
p->exit_state = EXIT_DEAD;
list_add(&p->ptrace_entry, dead);
}
}
kill_orphaned_pgrp(p, father); // line 692
}
重挂子进程时,exit_signal 被强制设置为 SIGCHLD(line 681),因为新父进程期望通过标准的 SIGCHLD 通知获知子进程退出。如果子进程已经处于 ZOMBIE 状态,需要立即通知新父进程,让新父进程有机会通过 wait() 回收它。
PID 命名空间中的孤儿进程
// kernel/exit.c:598-627
static struct task_struct *find_child_reaper(struct task_struct *father,
struct list_head *dead)
{
struct pid_namespace *pid_ns = task_active_pid_ns(father);
struct task_struct *reaper = pid_ns->child_reaper;
struct task_struct *p, *n;
if (likely(reaper != father))
return reaper;
reaper = find_alive_thread(father);
if (reaper) {
pid_ns->child_reaper = reaper;
return reaper;
}
write_unlock_irq(&tasklist_lock);
list_for_each_entry_safe(p, n, dead, ptrace_entry) {
list_del_init(&p->ptrace_entry);
release_task(p);
}
zap_pid_ns_processes(pid_ns);
write_lock_irq(&tasklist_lock);
return father;
}
如果退出进程本身就是当前 PID 命名空间的 init 进程(child_reaper == father),这是特殊情况。首先尝试将 child_reaper 角色转移给同线程组的其他存活线程(line 610-613)。如果线程组中没有其他存活线程,整个 PID 命名空间必须被销毁:zap_pid_ns_processes() 向命名空间中所有进程发送 SIGKILL,并等待它们全部退出。
PR_SET_CHILD_SUBREAPER
// include/linux/sched/signal.h:133-134
unsigned int is_child_subreaper:1;
unsigned int has_child_subreaper:1;
PR_SET_CHILD_SUBREAPER(定义在 include/uapi/linux/prctl.h)通过 prctl 系统调用设置。is_child_subreaper 标记当前进程为子进程回收者,has_child_subreaper 标记当前进程的祖先中是否存在 subreaper(由 fork 时从父进程继承)。
当 has_child_subreaper 为真时(exit.c:645),find_new_reaper() 会沿着进程树向上查找标记了 is_child_subreaper 的祖先。这一机制解决了以下问题:服务管理器(如 systemd)启动的服务进程执行 double-fork 创建工作进程后,如果中间的父进程退出,工作进程成为孤儿并被交给 init。但 init 可能不了解服务的语义,无法正确处理工作进程的退出。有了 subreaper 机制,工作进程会被交给服务管理器,后者可以根据服务策略做出适当的响应。
14.1.9 进程退出中的资源依赖总结
do_exit() 中资源释放的顺序不是任意的,而是由资源之间的依赖关系决定的。以下是关键的依赖约束:
io_uring_files_cancel() 必须在 exit_files() 之前(io_uring 引用文件)
exit_signals(PF_EXITING) 必须在 seccomp_filter_release() 之前
perf_event_exit_task() 必须在 exit_mm() 之前(perf 访问 mm)
unwind_deferred_task_exit() 必须在 exit_mm() 之前(栈展开访问 mm)
exit_mm() 必须在 exit_notify() 之前(OOM killer 相关)
exit_files() 必须在 exit_task_work() 之前(task_work 可能操作文件)
exit_notify() 必须在 preempt_disable() 之前(需要睡眠)
exit_rcu() 必须在 lockdep_free_task() 之前(RCU 回调可能在锁中)
do_task_dead() 必须是最后一步(不可逆)
理解这些依赖关系对于理解内核代码至关重要。在 Linux 内核开发中,修改 do_exit() 的步骤顺序是最容易引入微妙 bug 的操作之一,因为错误顺序可能导致的死锁或 use-after-free 通常只在特定的竞态条件下触发。
14.2 信号机制概述
信号(Signal)是 UNIX/Linux 系统中最古老的进程间通信机制之一,提供了一种异步通知机制:内核或其他进程可以向目标进程发送信号,目标进程在合适的时机对信号做出响应。信号既可以用于进程间的简单通信(如 SIGUSR1/SIGUSR2),也是内核通知进程异步事件的主要手段(如 SIGSEGV 表示段错误、SIGCHLD 表示子进程状态变化)。本节将深入分析 Linux 7.0.10 内核中信号机制的核心数据结构、两级待处理队列模型、信号发送路径以及信号处理方式。
14.2.1 信号的基本概念
软件中断模型
信号本质上是一种"软件中断"机制。与硬件中断类似,信号具有以下特征:
- 异步性:信号可以在进程执行的任何时刻到达,进程无法预知信号何时到来
- 延迟处理:信号不是在到达的瞬间被处理,而是在进程从内核返回用户空间时检查并处理
- 可嵌套:在一个信号处理函数执行期间,可能被另一个信号的处理函数中断
但信号与硬件中断也有重要区别:信号的处理发生在进程上下文中(而非中断上下文),信号处理函数在用户态执行,可以调用任意的用户空间函数。
POSIX 信号模型
Linux 的信号实现遵循 POSIX.1-2001 标准(即 POSIX.1b 实时信号扩展),但在某些细节上有自己的扩展。POSIX 定义了以下信号操作:
- 发送:
kill()、sigqueue()、tgkill()等系统调用 - 阻塞:通过
sigprocmask()设置阻塞掩码 - 处理:通过
sigaction()设置处理方式(默认/忽略/自定义函数) - 等待:通过
sigwaitinfo()、sigsuspend()等同步等待信号
信号编号与分类
Linux 内核支持的信号编号范围如下:
信号编号 类别 说明
1-31 标准信号 POSIX.1 定义的传统信号,每个信号有特定含义
32-33 保留 Linux 内部使用(SIGCANCEL/SIGSETXID for glibc)
34-64 实时信号 POSIX.1b 定义的实时信号(SIGRTMIN-SIGRTMAX)
内核中信号数量的上限由 _NSIG 定义:
// include/uapi/asm-generic/signal.h
#define _NSIG 64
#define _NSIG_BPW __BITS_PER_LONG
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)
_NSIG = 64 意味着 Linux 最多支持 64 个不同的信号编号。_NSIG_WORDS 是 sigset_t 位图中 unsigned long 字的个数:在 64 位系统上为 1,在 32 位系统上为 2。
标准信号(1-31)的编号定义在 include/uapi/asm-generic/signal.h 中,每个信号都有特定的含义:
| 编号 | 名称 | 默认动作 | 说明 |
|---|---|---|---|
| 1 | SIGHUP | Term | 终端挂断或控制进程死亡 |
| 2 | SIGINT | Term | 键盘中断(Ctrl+C) |
| 3 | SIGQUIT | Core | 键盘退出(Ctrl+\) |
| 4 | SIGILL | Core | 非法指令 |
| 5 | SIGTRAP | Core | 跟踪/断点陷阱 |
| 6 | SIGABRT | Core | abort() 调用 |
| 7 | SIGBUS | Core | 总线错误(未对齐访问) |
| 8 | SIGFPE | Core | 浮点异常 |
| 9 | SIGKILL | Term | 强制终止(不可捕获/忽略) |
| 10 | SIGUSR1 | Term | 用户定义信号 1 |
| 11 | SIGSEGV | Core | 段错误(无效内存引用) |
| 12 | SIGUSR2 | Term | 用户定义信号 2 |
| 13 | SIGPIPE | Term | 向无读端的管道写入 |
| 14 | SIGALRM | Term | alarm() 定时器到期 |
| 15 | SIGTERM | Term | 终止信号 |
| 17 | SIGCHLD | Ign | 子进程停止或终止 |
| 18 | SIGCONT | Cont | 停止后继续 |
| 19 | SIGSTOP | Stop | 停止进程(不可捕获/忽略) |
| 20 | SIGTSTP | Stop | 终端停止(Ctrl+Z) |
标准信号与实时信号的关键区别:
- 排队行为:标准信号在同一个信号已有待处理实例时不排队(只记录一次);实时信号保证排队,每个实例都被保留
- 优先级:在 dequeue 时,标准信号按编号从小到大处理;实时信号按 FIFO 顺序处理
- 附带数据:标准信号只携带信号编号;实时信号通过
siginfo_t可附带额外数据(si_value)
14.2.2 核心数据结构
信号子系统涉及多个紧密关联的数据结构,它们分布在三个层次:进程级(task_struct)、线程组级(signal_struct)和信号处理级(sighand_struct)。
signal_struct -- 线程组共享信号状态
// include/linux/sched/signal.h:94-255
struct signal_struct {
refcount_t sigcnt; // line 95
atomic_t live; // line 96
int nr_threads; // line 97
int quick_threads; // line 98
struct list_head thread_head; // line 99
wait_queue_head_t wait_chldexit; // line 101 -- wait4() 等待队列
struct task_struct *curr_target; // line 104 -- 信号负载均衡目标
struct sigpending shared_pending; // line 107 -- 共享待处理信号
struct hlist_head multiprocess; // line 110 -- fork 期间的延迟信号
int group_exit_code; // line 113 -- 线程组退出码
int notify_count; // line 115 -- exec 通知计数
struct task_struct *group_exec_task; // line 116 -- 执行 exec 的线程
int group_stop_count; // line 119 -- 组停止计数
unsigned int flags; // line 120 -- SIGNAL_* 标志
struct core_state *core_state; // line 122 -- coredump 状态
unsigned int is_child_subreaper:1; // line 133
unsigned int has_child_subreaper:1; // line 134
signal_struct 是线程组级别的信号状态结构。在同一线程组中,所有线程共享同一个 signal_struct 实例(通过 fork 时的 CLONE_THREAD 或普通 fork 共享)。
引用计数与存活计数:
sigcnt(line 95):signal_struct结构本身的引用计数。每有一个线程引用此结构时递增,线程退出时递减。归零时释放signal_struct。live(line 96):原子变量,记录线程组中存活的线程数。在线程创建时递增,在do_exit()中通过atomic_dec_and_test()递减(exit.c:924)。当降为 0 时表示线程组完全退出。nr_threads(line 97):线程组中的线程总数(包括正在退出的线程),与live的区别在于nr_threads在线程完全退出后才递减。quick_threads(line 98):在siglock保护下操作的线程计数,用于synchronize_group_exit()中的快速退出同步。
信号负载均衡:
curr_target(line 104):当前线程组中负责接收共享信号的"目标线程"。当发送一个进程级信号时(如kill(pid, sig)),内核使用curr_target来选择接收线程,实现线程组内的信号负载均衡。next_signal()在选择目标时会轮转curr_target,确保信号不会总是发送给同一个线程。
线程组退出支持:
group_exit_code(line 113):在do_group_exit()中设置(exit.c:1113),记录整个线程组的统一退出码。notify_count(line 115)和group_exec_task(line 116):用于execve()中的 de_thread() 操作。de_thread()需要等待线程组中所有其他线程退出后才能完成 exec。notify_count记录剩余需要退出的线程数,当降为 0 时唤醒group_exec_task。
线程组停止支持:
group_stop_count(line 119):记录当前组停止操作中尚未停止的线程数。flags(line 120):SIGNAL_*标志位组合,定义在 include/linux/sched/signal.h:260-270:
// include/linux/sched/signal.h:260-270
#define SIGNAL_STOP_STOPPED 0x00000001 /* job control stop in effect */
#define SIGNAL_STOP_CONTINUED 0x00000002 /* SIGCONT since WCONTINUED reap */
#define SIGNAL_GROUP_EXIT 0x00000004 /* group exit in progress */
#define SIGNAL_CLD_STOPPED 0x00000010 /* 通知父进程:子进程停止 */
#define SIGNAL_CLD_CONTINUED 0x00000020 /* 通知父进程:子进程继续 */
#define SIGNAL_CLD_MASK (SIGNAL_CLD_STOPPED|SIGNAL_CLD_CONTINUED)
#define SIGNAL_UNKILLABLE 0x00000040 /* for init: ignore fatal signals */
SIGNAL_GROUP_EXIT 标志在 do_group_exit() 中设置(exit.c:1114),表示线程组正在退出。一旦设置,后续的退出请求直接使用已有的 group_exit_code。
SIGNAL_UNKILLABLE 标志保护 init 进程(PID 1)不被普通信号杀死。在 prepare_signal() 中检查此标志。
资源统计:
// include/linux/sched/signal.h:188-205
seqlock_t stats_lock;
u64 utime, stime, cutime, cstime; // line 189
u64 gtime; // line 191
u64 cgtime; // line 192
struct prev_cputime prev_cputime; // line 193
unsigned long nvcsw, nivcsw, cnvcsw, cnivcsw; // line 194
unsigned long min_flt, maj_flt, cmin_flt, cmaj_flt; // line 195-196
unsigned long inblock, oublock, cinblock, coublock; // line 196
unsigned long maxrss, cmaxrss; // line 196
struct task_io_accounting ioac; // line 197
这些字段累计了线程组中已退出线程和已回收子进程的资源使用统计。存活线程维护自己的计数,在 __exit_signal() 中(由 release_task() 调用)将自身计数累计到线程组的这些字段中。
资源限制:
struct rlimit rlim[RLIM_NLIMITS]; // line 216
rlim 数组存储了线程组的所有 POSIX 资源限制,包括 RLIMIT_CPU(CPU 时间)、RLIMIT_NOFILE(最大文件描述符数)、RLIMIT_NPROC(最大进程数)等。通过 getrlimit()/setrlimit() 系统调用访问。注意 signal_struct 注释(line 207-214)特别说明:大多数读者不需要同步读取 rlim_cur 和 rlim_max,因为单独读取一个字段本身就是原子的(unsigned long)。
POSIX 定时器:
// include/linux/sched/signal.h:136-163
#ifdef CONFIG_POSIX_TIMERS
unsigned int timer_create_restore_ids:1;
atomic_t next_posix_timer_id;
struct hlist_head posix_timers; // line 141
struct hlist_head ignored_posix_timers; // line 142
struct hrtimer real_timer; // line 145 -- ITIMER_REAL
ktime_t it_real_incr; // line 146
struct cpu_itimer it[2]; // line 153 -- ITIMER_PROF/VIRTUAL
struct thread_group_cputimer cputimer; // line 159
#endif
struct posix_cputimers posix_cputimers; // line 163
POSIX 定时器关联到线程组级别。real_timer 是 ITIMER_REAL 的高精度定时器,到期时向线程组发送 SIGALRM。posix_timers 哈希链表管理所有通过 timer_create() 创建的 POSIX 定时器。这些定时器在 group_dead 时由 exit_itimers()(exit.c:936)清理。
sighand_struct -- 信号处理配置
// include/linux/sched/signal.h:21-26
struct sighand_struct {
spinlock_t siglock; // line 22
refcount_t count; // line 23
wait_queue_head_t signalfd_wqh; // line 24
struct k_sigaction action[_NSIG]; // line 25
};
sighand_struct 是信号子系统中最重要的锁持有者和配置容器。
siglock(line 22):信号子系统的核心自旋锁。它保护所有与信号相关的操作,包括:
- 信号的发送(
__send_signal_locked()) - 信号的出队(
dequeue_signal()) - 信号处理方式的修改(
do_sigaction()) - 信号掩码的修改(
do_sigprocmask()) - 线程组退出的协调(
do_group_exit()中获取 siglock) - 进程停止/继续的操作
内核注释(sched/signal.h:88-92)特别强调:signal_struct 没有自己的锁,因为共享的 signal_struct 总是隐含共享的 sighand_struct,所以锁住 sighand_struct.siglock 就足够保护 signal_struct 的所有字段。
count(line 23):引用计数。通过 CLONE_SIGHAND(无 CLONE_THREAD)fork 的进程共享 sighand_struct,引用计数递增。当最后一个引用者退出时释放。
signalfd_wqh(line 24):signalfd 的等待队列。signalfd 是 Linux 2.6.22 引入的机制,允许进程通过文件描述符读取信号(而非安装信号处理函数)。当新信号到达时,在此等待队列上睡眠的 signalfd 读取操作被唤醒。
action[_NSIG](line 25):数组长度为 _NSIG(64),每个元素是一个 k_sigaction 结构,记录对应编号信号的处理配置:
// include/linux/signal_types.h:37-56
struct sigaction {
__sighandler_t sa_handler; // 信号处理函数或 SIG_DFL/SIG_IGN
unsigned long sa_flags; // SA_* 标志
#ifdef __ARCH_HAS_SA_RESTORER
__sigrestore_t sa_restorer; // sigreturn 恢复函数
#endif
sigset_t sa_mask; // 处理期间要阻塞的信号集
};
struct k_sigaction {
struct sigaction sa;
#ifdef __ARCH_HAS_KA_RESTORER
__sigrestore_t ka_restorer;
#endif
};
sa_handler 字段有三种可能的值:
- SIG_DFL(值为 0):使用默认处理动作
- SIG_IGN(值为 1):忽略该信号
- 其他值:用户空间函数地址,信号到达时调用此函数
sa_mask 指定在信号处理函数执行期间需要额外阻塞的信号集。这是 POSIX 信号处理的关键特性:当处理一个信号时,该信号本身会被自动阻塞(除非设置了 SA_NODEFER),sa_mask 允许额外阻塞其他信号。
sigpending -- 待处理信号集
// include/linux/signal_types.h:32-35
struct sigpending {
struct list_head list; // sigqueue 链表头
sigset_t signal; // 信号位图
};
sigpending 是信号排队的基本容器,包含两个字段:
- signal:
sigset_t类型的信号位图,每一位对应一个信号编号。如果第 N 位置位,表示信号 N 有待处理的实例。对于标准信号,位图只记录信号是否存在(不记录数量);对于实时信号,位图同样只记录是否至少有一个实例,但链表中可能排了多个实例。 - list:
sigqueue结构的双向链表,每个节点代表一个排队信号及其附带信息。
sigqueue -- 排队信号
// include/linux/signal_types.h:22-27
struct sigqueue {
struct list_head list; // 链入 sigpending.list
int flags; // SIGQUEUE_PREALLOC 等
kernel_siginfo_t info; // 信号附带信息
struct ucounts *ucounts; // 用户信号计数(限制每用户排队数)
};
sigqueue 代表一个排队的信号实例:
- list:链入
sigpending.list,所有排队信号形成一个双向链表 - flags:
SIGQUEUE_PREALLOC(值为 1)表示此 sigqueue 是预分配的,用于 POSIX 定时器。预分配的 sigqueue 在信号处理后不会被释放,而是归还给定时器重用 - info:
kernel_siginfo_t结构,携带信号的详细信息
kernel_siginfo_t 的关键字段包括:
// include/uapi/asm-generic/siginfo.h (简化)
typedef struct kernel_siginfo {
__SIGINFO;
} kernel_siginfo_t;
// __SIGINFO 展开后的关键字段:
// int si_signo; -- 信号编号
// int si_errno; -- 关联的 errno
// int si_code; -- 信号来源代码 (SI_USER/SI_KERNEL/SI_QUEUE/...)
// int si_pid; -- 发送者 PID
// uid_t si_uid; -- 发送者 UID
// void *si_addr; -- 触发地址 (SIGSEGV 等使用)
// int si_status; -- 退出状态 (SIGCHLD 使用)
// union sigval si_value; -- 用户附带数据 (实时信号使用)
si_code 字段标识信号的来源,是信号处理中非常重要的信息:
- SI_USER(0):来自 kill() 等用户空间调用
- SI_KERNEL(0x80):来自内核自身
- SI_QUEUE(-1):来自 sigqueue() 实时信号发送
- SI_TIMER(-2):来自 POSIX 定时器到期
- SI_TKILL(-6):来自 tgkill() 或 tkill()
- 对于特定信号还有专用的 si_code:SEGV_MAPERR(SIGSEGV 的未映射地址)、FPE_INTDIV(SIGFPE 的整数除零)等
ucounts 字段用于限制每个用户可以排队的实时信号总数,防止资源耗尽攻击。每个用户的限制通过 /proc/sys/fs/rlimit-nr-signals(或 RLIMIT_SIGPENDING)控制。
ksignal -- 内核信号包装
// include/linux/signal_types.h:67-71
struct ksignal {
struct k_sigaction ka; // 信号处理配置
kernel_siginfo_t info; // 信号信息
int sig; // 信号编号
};
ksignal 是内核在处理(delivering)信号时使用的临时包装结构。在 get_signal() 函数(kernel/signal.c)中,信号被从待处理队列中取出后,连同其处理配置(从 sighand->action[] 查找)一起打包到 ksignal 结构中,传递给架构相关的信号投递函数。
14.2.3 两级待处理队列
Linux 信号子系统使用两级待处理队列模型,这是理解信号投递行为的关键。
task_struct->pending -- 私有待处理队列
// include/linux/sched.h:1205
struct sigpending pending;
每个 task_struct 都有自己的 sigpending 结构。通过 tgkill() 或 tkill() 指定线程 TID 发送的信号(PIDTYPE_PID)被挂入此队列。私有信号只能被目标线程看到和处理。
signal_struct->shared_pending -- 共享待处理队列
// include/linux/sched/signal.h:107
struct sigpending shared_pending;
每个线程组共享一个 shared_pending 队列。通过 kill(pid, sig) 向进程(线程组)发送的信号(PIDTYPE_TGID),以及向进程组(PIDTYPE_PGID)或会话(PIDTYPE_SID)发送的信号,都被挂入此队列。共享信号可以被线程组中的任何一个线程处理。
队列选择逻辑
// kernel/signal.c:1056
pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
在 __send_signal_locked()(signal.c:1042-1157)中,根据发送时的 pid_type 参数选择目标队列:
PIDTYPE_PID:发送到特定线程,使用task_struct->pendingPIDTYPE_TGID:发送到线程组,使用signal_struct->shared_pendingPIDTYPE_PGID:发送到进程组中每个进程的shared_pendingPIDTYPE_SID:发送到会话中每个进程的shared_pending
dequeue 顺序
在 dequeue_signal()(signal.c:616-666)中,信号的出队遵循"私有优先"的原则:
// kernel/signal.c:627-633
again:
*type = PIDTYPE_PID;
timer_sigq = NULL;
signr = __dequeue_signal(&tsk->pending, mask, info, &timer_sigq);
if (!signr) {
*type = PIDTYPE_TGID;
signr = __dequeue_signal(&tsk->signal->shared_pending,
mask, info, &timer_sigq);
先从私有队列(tsk->pending)中取信号,如果为空再从共享队列(tsk->signal->shared_pending)中取。type 输出参数记录了信号来自哪个队列,某些后续处理需要知道这一点。
共享信号的线程选择
当一个信号被挂入 shared_pending 后,哪个线程来处理它?这由 complete_signal() 函数决定:
// kernel/signal.c (complete_signal 的核心逻辑,简化)
complete_signal() 的线程选择算法:
- 目标线程直接检查:如果传入的目标线程
t没有阻塞该信号且没有设置PF_EXITING,选择该线程 - 信号负载均衡:如果目标线程不合适,通过
signal->curr_target轮转选择线程组中的下一个合适线程。next_signal()的wants_signal()函数检查候选线程是否满足条件(未阻塞该信号、非 PF_EXITING) - 唤醒任意线程:如果所有线程都阻塞了该信号,但信号是 SIGKILL 等致命信号,唤醒任意一个线程(致命信号需要立即处理)
14.2.4 信号处理方式(sigaction)
当信号被出队后,内核根据 sighand->action[signo-1].sa_handler 的值决定处理方式。
SIG_DFL -- 默认处理
// include/linux/signal.h:445-448
#define sig_kernel_only(sig) siginmask(sig, SIG_KERNEL_ONLY_MASK)
#define sig_kernel_coredump(sig) siginmask(sig, SIG_KERNEL_COREDUMP_MASK)
#define sig_kernel_ignore(sig) siginmask(sig, SIG_KERNEL_IGNORE_MASK)
#define sig_kernel_stop(sig) siginmask(sig, SIG_KERNEL_STOP_MASK)
默认处理动作通过信号分类掩码定义(include/linux/signal.h:419-436),分为五类:
Term(终止):默认终止进程的信号,如 SIGKILL、SIGTERM、SIGINT、SIGHUP、SIGALRM 等。处理方式是调用 do_group_exit() 使整个线程组退出。
Ign(忽略):默认被忽略的信号:
#define SIG_KERNEL_IGNORE_MASK (\
rt_sigmask(SIGCONT) | rt_sigmask(SIGCHLD) | \
rt_sigmask(SIGWINCH) | rt_sigmask(SIGURG) )
SIGCHLD(子进程状态变化)、SIGURG(紧急数据到达)、SIGWINCH(终端窗口大小变化)、SIGCONT(继续执行,如果进程未停止则忽略)默认被忽略。
Core(终止并生成 core dump):
#define SIG_KERNEL_COREDUMP_MASK (\
rt_sigmask(SIGQUIT) | rt_sigmask(SIGILL) | \
rt_sigmask(SIGTRAP) | rt_sigmask(SIGABRT) | \
rt_sigmask(SIGFPE) | rt_sigmask(SIGSEGV) | \
rt_sigmask(SIGBUS) | rt_sigmask(SIGSYS) | \
rt_sigmask(SIGXCPU) | rt_sigmask(SIGXFSZ) | \
SIGEMT_MASK )
这些信号表示程序发生了严重错误(段错误、非法指令、总线错误等),内核在终止进程前尝试生成 core dump 文件,包含进程的内存映像和寄存器状态,用于事后调试。
Stop(停止):
#define SIG_KERNEL_STOP_MASK (\
rt_sigmask(SIGSTOP) | rt_sigmask(SIGTSTP) | \
rt_sigmask(SIGTTIN) | rt_sigmask(SIGTTOU) )
SIGSTOP(无条件停止)、SIGTSTP(终端停止)、SIGTTIN(后台读终端)、SIGTTOU(后台写终端)使进程进入停止状态(TASK_STOPPED),直到收到 SIGCONT 才恢复执行。
Cont(继续):SIGCONT 使停止的进程继续执行。如果进程未处于停止状态,SIGCONT 默认被忽略。
SIG_IGN -- 显式忽略
当 sa_handler == SIG_IGN 时,信号被忽略。但并非所有信号都可以被忽略(见下文不可忽略信号部分)。
对于 SIGCHLD,如果设置为 SIG_IGN,内核还有特殊行为:子进程退出时自动回收(autoreap),不产生 ZOMBIE 状态。这等价于设置 SA_NOCLDWAIT 标志。这个行为在 do_notify_parent() 中实现:如果父进程对 SIGCHLD 设置了 SIG_IGN 或 SA_NOCLDWAIT,do_notify_parent() 返回 true,导致子进程被 autoreap(exit.c:766-768)。
用户处理函数
当 sa_handler 是一个函数指针时,信号到达时调用该函数。内核通过 setup_rt_frame()(架构相关,如 arch/x86/kernel/signal.c)在用户栈上构建一个特殊的栈帧,使进程从内核返回用户空间时跳转到信号处理函数,而非原来的中断位置。
信号处理函数有两种形式:
传统形式(SA_SIGINFO 未设置):
void handler(int signo);
扩展形式(SA_SIGINFO 已设置):
void handler(int signo, siginfo_t *info, void *context);
扩展形式可以获取信号的详细信息(发送者 PID/UID、触发地址等)和中断时的寄存器上下文(ucontext)。
SA_FLAGS 标志
sigaction 的 sa_flags 字段控制信号处理的细节行为:
| 标志 | 值 | 说明 |
|---|---|---|
| SA_NOCLDSTOP | 0x00000001 | 子进程停止时不发送 SIGCHLD |
| SA_NOCLDWAIT | 0x00000002 | 子进程退出时不产生 ZOMBIE(自动回收) |
| SA_SIGINFO | 0x00000004 | 使用三参数信号处理函数 |
| SA_ONSTACK | 0x08000000 | 在备用信号栈上执行处理函数 |
| SA_RESTART | 0x10000000 | 自动重启被信号中断的系统调用 |
| SA_NODEFER | 0x40000000 | 处理信号时不自动阻塞该信号 |
| SA_RESETHAND | 0x80000000 | 信号处理函数执行后重置为 SIG_DFL |
| SA_ONESHOT | 0x80000000 | SA_RESETHAND 的旧名称 |
SA_RESTART:当系统调用(如 read()、write())被信号中断时,默认行为是返回 EINTR 错误。设置 SA_RESTART 后,内核自动重启被中断的系统调用,对应用程序透明。但并非所有系统调用都支持自动重启(如 select()、poll() 始终返回 EINTR)。
SA_ONSTACK:默认情况下,信号处理函数在正常的用户栈上执行。如果进程的栈已经耗尽(例如由于递归过深),在栈上分配 sigframe 会失败,导致 SIGSEGV 的处理函数无法执行。SA_ONSTACK 使信号处理函数在通过 sigaltstack() 预分配的备用栈上执行,避免栈溢出导致信号处理失败。
SA_NODEFER:正常情况下,当一个信号的处理函数正在执行时,该信号会被自动阻塞,防止信号处理函数被递归调用。SA_NODEFER 取消这种自动阻塞。对于 SIGKILL 和 SIGSTOP 无效(它们永远不能被阻塞)。
14.2.5 信号的不可忽略性
SIGKILL 和 SIGSTOP
// include/linux/signal.h:419-420
#define SIG_KERNEL_ONLY_MASK (\
rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP))
#define sig_kernel_only(sig) siginmask(sig, SIG_KERNEL_ONLY_MASK)
SIGKILL(信号 9)和 SIGSTOP(信号 19)是两个特殊信号,它们有以下限制:
- 不能被捕获:
sigaction()对这两个信号的设置会被拒绝(do_sigaction()在 signal.c 中检查sig_kernel_only(sig)并返回 EINVAL) - 不能被忽略:即使
sa_handler被设置为SIG_IGN,这两个信号仍然会被投递 - 不能被阻塞:
sigprocmask()对这两个信号的阻塞操作无效
这些限制在信号的多个阶段被强制执行:
- 发送阶段:
prepare_signal()对 SIGKILL 总是返回 true(不检查忽略设置) - 阻塞检查:
__dequeue_signal()的掩码参数中 SIGKILL/SIGSTOP 被特殊处理 - 处理阶段:
get_signal()中 SIGKILL 直接调用do_group_exit(),不检查sa_handler
SIGNAL_UNKILLABLE -- init 进程保护
// include/linux/sched/signal.h:270
#define SIGNAL_UNKILLABLE 0x00000040 /* for init: ignore fatal signals */
SIGNAL_UNKILLABLE 标志在 init 进程(PID 1)的 signal_struct 中设置,保护 init 不被普通致命信号杀死。在 prepare_signal() 函数中:
// kernel/signal.c (prepare_signal 核心逻辑,简化)
当信号是致命信号且目标进程设置了 SIGNAL_UNKILLABLE 时,信号被丢弃(除非是 SIGKILL 来自特权级足够的发送者)。这确保了即使用户向 init 发送 SIGTERM 或 SIGSEGV,init 也不会被杀死。只有内核自身(si_code == SI_KERNEL)或具有 CAP_SYS_BOOT 能力的进程才能杀死 init。
在 exit.c:930 中我们看到,如果 init 确实退出了(通过 is_global_init() 检测),内核会调用 panic(),因为 init 退出意味着系统无法继续正常运行。
14.2.6 信号位图 sigset_t
sigset_t 的定义
// include/uapi/asm-generic/signal.h
#define _NSIG 64
#define _NSIG_BPW __BITS_PER_LONG
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
sigset_t 是信号集合的类型定义。在 64 位系统上,_NSIG_WORDS = 1,所以 sigset_t 就是一个 unsigned long(64 位),每一位对应一个信号编号。在 32 位系统上,_NSIG_WORDS = 2,需要两个 unsigned long。
信号编号 N 对应位图中的第 (N-1) 位。例如 SIGKILL(9)对应第 8 位,SIGSEGV(11)对应第 10 位。
位图操作函数
内核提供了一组位图操作函数(定义在 include/linux/signal.h 和 include/uapi/asm-generic/signal.h):
// 位图操作(简化展示)
sigemptyset(set) -- 清空所有位(无信号)
sigfillset(set) -- 设置所有位(所有信号)
sigaddset(set, signo) -- 设置指定信号的位
sigdelset(set, signo) -- 清除指定信号的位
sigismember(set, signo) -- 测试指定信号的位是否设置
sigandsets(d, a, b) -- d = a & b(交集)
sigorsets(d, a, b) -- d = a | b(并集)
signandsets(d, a, b) -- d = a & ~b(差集)
sigisemptyset(set) -- 测试集合是否为空
sigtestsetmask(set, mask) -- 测试集合与掩码是否有交集
siginitset(set, mask) -- 用掩码初始化集合
blocked 掩码
// include/linux/sched.h:1201-1204
sigset_t blocked; // 当前阻塞的信号集
sigset_t real_blocked; // sigsuspend 期间的临时掩码
sigset_t saved_sigmask; // 被信号中断时保存的原掩码
blocked 是进程当前阻塞的信号集合。被阻塞的信号不会被投递给进程,而是保留在待处理队列中,直到解除阻塞。
信号阻塞的关键规则:
- SIGKILL 和 SIGSTOP 不可阻塞:
sigprocmask()操作中这两个信号会被自动从阻塞集中移除 - 标准信号不排队:如果同一个标准信号在阻塞期间被发送多次,解除阻塞后只投递一次
- 实时信号排队:实时信号在阻塞期间排队的每个实例都会被投递
real_blocked 用于 sigsuspend() 系统调用:原子地替换信号掩码并睡眠等待信号。sigsuspend() 保存当前掩码到 real_blocked,设置新掩码,等待信号到达后恢复 real_blocked。
saved_sigmask 用于 ppoll()、pselect() 等系统调用:这些系统调用接受一个临时的信号掩码参数。如果系统调用被信号中断,保存的掩码在返回用户空间前恢复。
14.2.7 signal_wake_up_state() -- 唤醒目标线程
当信号被成功挂入待处理队列后,需要通知目标线程有信号待处理。这通过 signal_wake_up_state() 完成。
// kernel/signal.c:721-736
void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
lockdep_assert_held(&t->sighand->siglock);
set_tsk_thread_flag(t, TIF_SIGPENDING); // line 725
if (!wake_up_state(t, state | TASK_INTERRUPTIBLE)) // line 734
kick_process(t); // line 735
}
这个函数执行两个关键操作:
设置 TIF_SIGPENDING 标志(line 725):
TIF_SIGPENDING(Thread Info Flag: SIGPENDING)设置在线程的 thread_info 中。当线程从内核返回用户空间时(在 exit_to_user_mode_loop() 中),内核检查此标志。如果设置,调用 do_signal()(或架构相关的信号处理入口)处理待处理信号。
在 x86_64 上,TIF_SIGPENDING 的检查发生在系统调用返回和中断返回的慢路径中(entry_64.S 中的 syscall_return_path)。正常情况下,系统调用返回走快速路径(不检查线程标志),但 TIF_SIGPENDING 等标志会使返回走慢速路径。
唤醒目标线程(line 734-735):
wake_up_state() 尝试将目标线程从等待状态唤醒。state 参数指定可以唤醒的状态掩码:
- 通常传入
TASK_INTERRUPTIBLE:可以唤醒处于TASK_INTERRUPTIBLE(可中断睡眠)状态的线程 - 对于致命信号,传入
TASK_WAKEKILL:还可以唤醒处于TASK_KILLABLE(可杀死睡眠)状态的线程 - 对于 SIGSTOP 等停止信号,可能传入
TASK_INTERRUPTIBLE | TASK_WAKEKILL
如果 wake_up_state() 返回 0,表示目标线程不在可唤醒的状态(可能正在另一个 CPU 上运行)。此时 kick_process() 被调用。
kick_process() 向目标线程所在的 CPU 发送一个 IPI(Inter-Processor Interrupt,处理器间中断)。在 x86_64 上,这通常是一个 RESCHEDULE_VECTOR IPI。目标 CPU 收到 IPI 后,在 IPI 处理路径中检查当前线程的 TIF_SIGPENDING 标志,如果设置则进入信号处理流程。
调用上下文
signal_wake_up_state() 在以下场景被调用:
complete_signal():信号成功入队后,选择目标线程并调用此函数唤醒zap_other_threads():在do_group_exit()中向线程组的其他线程发送 SIGKILLptrace_wake_up():ptrace 操作需要目标线程处理信号force_sig_info():强制发送信号(绕过阻塞和忽略检查)
在所有这些场景中,调用者必须持有 sighand->siglock,这由 lockdep_assert_held() 断言确保。
从唤醒到处理的完整时序
1. __send_signal_locked()
-> sigaddset(&pending->signal, sig)
-> complete_signal(sig, t, type)
-> signal_wake_up_state(target, TASK_INTERRUPTIBLE)
-> set_tsk_thread_flag(TIF_SIGPENDING)
-> wake_up_state() 或 kick_process()
2. 目标线程被唤醒或在 IPI 中断返回时
-> 检查 TIF_SIGPENDING
-> exit_to_user_mode_loop()
-> arch/x86/kernel/signal.c: do_signal()
-> get_signal()
-> dequeue_signal()
-> __dequeue_signal(&tsk->pending, ...) // 先查私有
-> __dequeue_signal(&tsk->signal->shared_pending, ...) // 再查共享
-> 根据 ksignal.ka.sa_handler 决定处理方式
-> SIG_DFL: 执行默认动作
-> SIG_IGN: 丢弃
-> 函数指针: setup_rt_frame() 构建用户栈帧
这个时序揭示了信号的延迟处理本质:信号发送只是设置标志并唤醒线程,真正的信号处理(出队、决策、投递)发生在目标线程返回用户空间前的内核代码中。这种设计避免了在信号发送路径中进行复杂的用户空间操作,保持了信号发送的原子性和效率。
14.2.8 __send_signal_locked() -- 信号发送的核心实现
// kernel/signal.c:1042-1157
static int __send_signal_locked(int sig, struct kernel_siginfo *info,
struct task_struct *t, enum pid_type type, bool force)
{
struct sigpending *pending;
struct sigqueue *q;
int override_rlimit;
int ret = 0, result;
lockdep_assert_held(&t->sighand->siglock);
result = TRACE_SIGNAL_IGNORED;
if (!prepare_signal(sig, t, force)) // line 1053
goto ret;
pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending; // line 1056
prepare_signal() 检查(line 1053):prepare_signal() 检查信号是否应该被投递。对于 SIGKILL,总是返回 true。对于 SIGCONT,它会清除停止相关的标志。对于其他信号,检查目标进程是否设置了 SIGNAL_GROUP_EXIT(正在退出)或 SIGNAL_UNKILLABLE(init 进程保护)。如果信号应该被忽略,返回 false,函数直接返回。
队列选择(line 1056):如前所述,根据 type 参数选择私有或共享队列。
result = TRACE_SIGNAL_ALREADY_PENDING;
if (legacy_queue(pending, sig)) // line 1063
goto ret;
legacy_queue 检查(line 1063):对于标准信号(编号 < SIGRTMIN),如果该信号已经有一个实例在队列中(sigismember(&pending->signal, sig) 为真),则不再排队新实例。这就是"标准信号不排队"的实现。对于实时信号,legacy_queue() 总是返回 false,允许排队多个实例。
if ((sig == SIGKILL) || (t->flags & PF_KTHREAD)) // line 1070
goto out_set;
SIGKILL 优化(line 1070):SIGKILL 不需要 siginfo(不需要排队详细信息),因为 SIGKILL 的处理总是立即终止进程。类似地,内核线程也不需要 siginfo。直接跳到设置位图的步骤。
if (sig < SIGRTMIN) // line 1082
override_rlimit = (is_si_special(info) || info->si_code >= 0);
else
override_rlimit = 0;
q = sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit); // line 1087
sigqueue 分配(line 1087):sigqueue_alloc() 分配一个新的 sigqueue 结构。分配受 rlimit(RLIMIT_SIGPENDING) 限制,防止用户耗尽内核内存。但对于来自内核的信号(si_code >= 0 来自用户空间,< 0 来自内核,但此处的判断逻辑是 is_si_special() 或 si_code >= 0 时 override),即使超过限制也允许分配。
if (q) {
list_add_tail(&q->list, &pending->list); // line 1090
switch ((unsigned long) info) {
case (unsigned long) SEND_SIG_NOINFO: // line 1092
clear_siginfo(&q->info);
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_USER;
q->info.si_pid = task_tgid_nr_ns(current,
task_active_pid_ns(t));
rcu_read_lock();
q->info.si_uid = from_kuid_munged(...);
rcu_read_unlock();
break;
case (unsigned long) SEND_SIG_PRIV: // line 1105
clear_siginfo(&q->info);
q->info.si_signo = sig;
q->info.si_code = SI_KERNEL;
break;
default: // line 1113
copy_siginfo(&q->info, info);
break;
}
}
siginfo 填充(line 1090-1116):根据 info 参数的类型填充 sigqueue 的 info 字段。三种情况:
SEND_SIG_NOINFO:来自kill()等系统调用,填充发送者的 PID 和 UIDSEND_SIG_PRIV:来自内核自身,设置si_code = SI_KERNEL- 其他:直接复制传入的
kernel_siginfo_t
out_set:
signalfd_notify(t, sig); // line 1136
sigaddset(&pending->signal, sig); // line 1137
if (type > PIDTYPE_TGID) { // line 1140
// 处理 multiprocess 延迟信号
// SIGCONT 清除停止信号,反之亦然
}
complete_signal(sig, t, type); // line 1153
最终步骤:
signalfd_notify()(line 1136):唤醒在 signalfd 上等待的读取操作sigaddset()(line 1137):在待处理位图中设置该信号的位- multiprocess 延迟(line 1140-1151):如果信号类型是 PGID 或 SID(
type > PIDTYPE_TGID),信号可能需要延迟到正在进行的 fork 操作完成后才能投递。这是为了避免 fork 期间向新建子进程发送信号 complete_signal()(line 1153):选择目标线程并调用signal_wake_up_state()唤醒
这个完整的发送路径展示了 Linux 信号子系统的精巧设计:在 siglock 保护下原子地完成信号排队、位图更新和目标线程唤醒,确保信号不会丢失也不会重复投递。
14.3 信号发送 -- kill、sigqueue 与内核信号
信号发送是信号机制的起点。当用户态程序调用 kill()、tgkill(),或者内核内部需要通知进程某一事件时,信号便进入内核的发送管道。Linux 内核的信号发送路径涉及权限检查、队列管理、进程组语义以及 PID namespace 等多个子系统的协同工作。本节将从系统调用入口开始,逐层深入内核信号发送的完整实现。
14.3.1 kill 系统调用路径
14.3.1.1 SYSCALL_DEFINE2(kill, pid, sig)
kill() 是最经典的信号发送系统调用。其内核入口定义在 kernel/signal.c:3947:
// kernel/signal.c:3947
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct kernel_siginfo info;
prepare_kill_siginfo(sig, &info, PIDTYPE_TGID);
return kill_something_info(sig, &info, pid);
}
该函数首先调用 prepare_kill_siginfo() 填充 kernel_siginfo 结构体。prepare_kill_siginfo() 定义在 kernel/signal.c:3931:
// kernel/signal.c:3931
static void prepare_kill_siginfo(int sig, struct kernel_siginfo *info,
enum pid_type type)
{
clear_siginfo(info);
info->si_signo = sig;
info->si_errno = 0;
info->si_code = (type == PIDTYPE_PID) ? SI_TKILL : SI_USER;
info->si_pid = task_tgid_vnr(current);
info->si_uid = from_kuid_munged(current_user_ns(), current_uid());
}
此处有几点值得注意:
- si_code 的区分: 当
type == PIDTYPE_PID(即tkill/tgkill路径)时,si_code设为SI_TKILL;否则(kill路径)设为SI_USER。接收方可以通过si_code区分信号来源。 - si_pid: 记录发送者的线程组 ID(tgid),使用
task_tgid_vnr()获取调用者在目标进程的 PID namespace 中的可见编号。 - si_uid: 记录发送者的用户 ID,通过
from_kuid_munged()将内核内部kuid_t转换为用户空间可见的 uid。
14.3.1.2 kill_something_info() -- pid 值语义分发
kill_something_info() 是 kill() 系统调用的核心分发函数,定义在 kernel/signal.c:1572:
// kernel/signal.c:1572
static int kill_something_info(int sig, struct kernel_siginfo *info, pid_t pid)
{
int ret;
if (pid > 0)
return kill_proc_info(sig, info, pid);
/* -INT_MIN is undefined. Exclude this case to avoid a UBSAN warning */
if (pid == INT_MIN)
return -ESRCH;
read_lock(&tasklist_lock);
if (pid != -1) {
ret = __kill_pgrp_info(sig, info,
pid ? find_vpid(-pid) : task_pgrp(current));
} else {
int retval = 0, count = 0;
struct task_struct * p;
for_each_process(p) {
if (task_pid_vnr(p) > 1 &&
!same_thread_group(p, current)) {
int err = group_send_sig_info(sig, info, p,
PIDTYPE_MAX);
++count;
if (err != -EPERM)
retval = err;
}
}
ret = count ? retval : -ESRCH;
}
read_unlock(&tasklist_lock);
return ret;
}
此函数根据 pid 参数的值执行完全不同的语义,这正是 POSIX kill(2) 规范所定义的:
| pid 值 | 目标 | 调用路径 |
|---|---|---|
pid > 0 |
指定进程(线程组) | kill_proc_info() |
pid == 0 |
调用者所在进程组 | __kill_pgrp_info(task_pgrp(current)) |
pid == -1 |
所有进程(广播) | for_each_process 遍历 |
pid < -1 |
指定进程组 -pid |
__kill_pgrp_info(find_vpid(-pid)) |
pid > 0 路径:调用 kill_proc_info() (signal.c:1476),它通过 find_vpid(pid) 在调用者的 PID namespace 中查找对应的 struct pid,然后进入 kill_pid_info() -> kill_pid_info_type() -> group_send_sig_info() 链路:
// kernel/signal.c:1476
static int kill_proc_info(int sig, struct kernel_siginfo *info, pid_t pid)
{
int error;
rcu_read_lock();
error = kill_pid_info(sig, info, find_vpid(pid));
rcu_read_unlock();
return error;
}
// kernel/signal.c:1471
int kill_pid_info(int sig, struct kernel_siginfo *info, struct pid *pid)
{
return kill_pid_info_type(sig, info, pid, PIDTYPE_TGID);
}
// kernel/signal.c:1449
static int kill_pid_info_type(int sig, struct kernel_siginfo *info,
struct pid *pid, enum pid_type type)
{
int error = -ESRCH;
struct task_struct *p;
for (;;) {
rcu_read_lock();
p = pid_task(pid, PIDTYPE_PID);
if (p)
error = group_send_sig_info(sig, info, p, type);
rcu_read_unlock();
if (likely(!p || error != -ESRCH))
return error;
/*
* The task was unhashed in between, try again. If it
* is dead, pid_task() will return NULL, if we race with
* de_thread() it will find the new leader.
*/
}
}
kill_pid_info_type() 中有一个重试循环:如果在 RCU 临界区内找到进程后、执行发送之前进程被回收(de_thread() 线程组领导者切换),则重试。这保证了在线程组领导者退出、新领导者接替的竞态窗口中信号不会丢失。
pid == 0 路径:当 pid 为 0 时,条件 pid ? find_vpid(-pid) : task_pgrp(current) 中的三元表达式选择 task_pgrp(current),即调用者自身的进程组。信号被发送到该进程组中的所有进程。
pid < -1 路径:find_vpid(-pid) 查找进程组 ID 为 -pid 的进程组,然后调用 __kill_pgrp_info()。
pid == -1 路径(广播):这是最特殊的路径。内核持有 tasklist_lock 读锁后,使用 for_each_process(p) 遍历系统中的所有进程:
task_pid_vnr(p) > 1:跳过 PID 为 1 的 init 进程(在当前 namespace 中),init 进程受到SIGNAL_UNKILLABLE保护。!same_thread_group(p, current):跳过调用者自身所在的线程组。PIDTYPE_MAX:表示这是一个多进程信号,需要进入共享 pending 队列。- 错误处理逻辑:只要至少一个进程返回非
-EPERM错误,就使用该错误;只有当所有进程都返回-EPERM时才返回-EPERM;如果没有发送到任何进程(count == 0),返回-ESRCH。
14.3.1.3 __kill_pgrp_info() -- 进程组信号
__kill_pgrp_info() 向一个进程组中的所有进程发送信号,定义在 kernel/signal.c:1429:
// kernel/signal.c:1429
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 group_send_sig_info() succeeds at least once ret
* becomes 0 and after that the code below has no effect.
* Otherwise we return the last err or -ESRCH if this
* process group is empty.
*/
if (ret)
ret = err;
} while_each_pid_task(pgrp, PIDTYPE_PGID, p);
return ret;
}
do_each_pid_task / while_each_pid_task 宏遍历具有给定 PGID 的所有进程(准确地说是每个线程组的领导者)。每个进程通过 group_send_sig_info() 接收信号,类型为 PIDTYPE_PGID,表示这是一个进程组级别的信号,进入 shared_pending 共享队列。
14.3.2 group_send_sig_info() -- 权限检查与发送
group_send_sig_info() 是信号发送路径上的关键检查点,定义在 kernel/signal.c:1409:
// kernel/signal.c:1409
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);
rcu_read_unlock();
if (!ret && sig)
ret = do_send_sig_info(sig, info, p, type);
return ret;
}
此函数分为两个阶段:
- 权限检查:在 RCU 读锁保护下调用
check_kill_permission()。 - 信号发送:如果权限检查通过且信号编号非零,调用
do_send_sig_info()实际发送信号。
14.3.2.1 check_kill_permission() -- 权限验证
// kernel/signal.c:799
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;
if (!same_thread_group(current, t) &&
!kill_ok_by_cred(t)) {
switch (sig) {
case SIGCONT:
sid = task_session(t);
if (!sid || sid == task_session(current))
break;
fallthrough;
default:
return -EPERM;
}
}
return security_task_kill(t, info, sig, NULL);
}
权限检查流程如下:
- 信号有效性:
valid_signal(sig)确保信号编号在合法范围内(1 到_NSIG)。 - 内核信号豁免:
si_fromuser(info)判断信号是否来自用户空间。内核自身发送的信号(SEND_SIG_PRIV、SI_KERNEL等)直接跳过权限检查。 - 同线程组豁免:
same_thread_group(current, t)-- 发送者和接收者在同一线程组内时无需权限检查。这允许线程之间通过pthread_kill自由发送信号。 - 凭证检查:
kill_ok_by_cred(t)是核心的凭证比较函数。 - SIGCONT 特殊规则:即使凭证检查失败,如果信号是
SIGCONT且发送者和接收者在同一会话(session)中,仍然允许发送。这是作业控制(job control)的需要 -- 同一会话中的进程可以向前台进程组发送 SIGCONT。 - LSM 检查:最后调用
security_task_kill()执行 Linux Security Module (如 SELinux, AppArmor) 的安全策略检查。
14.3.2.2 kill_ok_by_cred() -- 凭证比较
// kernel/signal.c:783
static bool kill_ok_by_cred(struct task_struct *t)
{
const struct cred *cred = current_cred();
const struct cred *tcred = __task_cred(t);
return uid_eq(cred->euid, tcred->suid) ||
uid_eq(cred->euid, tcred->uid) ||
uid_eq(cred->uid, tcred->suid) ||
uid_eq(cred->uid, tcred->uid) ||
ns_capable(tcred->user_ns, CAP_KILL);
}
kill_ok_by_cred() 执行经典的 4 路 UID 比较加能力检查。POSIX 规定:如果发送者的(真实或有效)用户 ID 等于接收者的(真实或保存的)用户 ID,则允许发送。具体检查以下 4 种等价关系:
| 发送者字段 | 接收者字段 | 说明 |
|---|---|---|
cred->euid |
tcred->suid |
发送者 EUID == 接收者 SUID |
cred->euid |
tcred->uid |
发送者 EUID == 接收者 RUID |
cred->uid |
tcred->suid |
发送者 RUID == 接收者 SUID |
cred->uid |
tcred->uid |
发送者 RUID == 接收者 RUID |
如果以上 4 种比较均不成立,还有最后一条兜底规则:发送者在接收者的 user namespace 中拥有 CAP_KILL 能力。注意 ns_capable() 使用的是 tcred->user_ns,即接收者的用户命名空间,而非发送者的。这意味着即使 root 用户在一个子 namespace 中,也无法向父 namespace 中的进程发送信号。
14.3.2.3 do_send_sig_info() -- 加锁发送
// kernel/signal.c:1262
int do_send_sig_info(int sig, struct kernel_siginfo *info, struct task_struct *p,
enum pid_type type)
{
unsigned long flags;
int ret = -ESRCH;
if (lock_task_sighand(p, &flags)) {
ret = send_signal_locked(sig, info, p, type);
unlock_task_sighand(p, &flags);
}
return ret;
}
do_send_sig_info() 获取目标进程的 sighand->siglock 自旋锁后,调用 send_signal_locked()。如果目标进程已经退出(sighand 已被释放),lock_task_sighand() 返回 NULL,函数返回 -ESRCH。
14.3.3 send_signal_locked() -- PID namespace 处理
send_signal_locked() 是信号发送的中间层,负责处理 PID namespace 相关的转换和 init 进程的特殊保护,定义在 kernel/signal.c:1183:
// kernel/signal.c:1183
int send_signal_locked(int sig, struct kernel_siginfo *info,
struct task_struct *t, enum pid_type type)
{
bool force = false;
if (info == SEND_SIG_NOINFO) {
/* Force if sent from an ancestor pid namespace */
force = !task_pid_nr_ns(current, task_active_pid_ns(t));
} else if (info == SEND_SIG_PRIV) {
/* Don't ignore kernel generated signals */
force = true;
} else if (has_si_pid_and_uid(info)) {
struct user_namespace *t_user_ns;
rcu_read_lock();
t_user_ns = task_cred_xxx(t, user_ns);
if (current_user_ns() != t_user_ns) {
kuid_t uid = make_kuid(current_user_ns(), info->si_uid);
info->si_uid = from_kuid_munged(t_user_ns, uid);
}
rcu_read_unlock();
/* A kernel generated signal? */
force = (info->si_code == SI_KERNEL);
/* From an ancestor pid namespace? */
if (!task_pid_nr_ns(current, task_active_pid_ns(t))) {
info->si_pid = 0;
force = true;
}
}
return __send_signal_locked(sig, info, t, type, force);
}
此函数的核心任务是确定 force 参数并处理跨命名空间信号:
-
SEND_SIG_NOINFO:当
info为SEND_SIG_NOINFO(无附加信息的用户信号)时,检查发送者是否来自祖先 PID namespace。task_pid_nr_ns(current, task_active_pid_ns(t))获取发送者在接收者的 PID namespace 中的编号。如果返回 0,说明发送者不在接收者的 namespace 中可见(即来自祖先 namespace),此时设置force = true。这使得容器 init 进程可以被祖先 namespace 中的kill命令杀死。 -
SEND_SIG_PRIV:内核自身发送的信号,
force始终为true。这类信号不可被忽略。 -
带 siginfo 的信号:进行两项处理: - UID 转换:如果发送者和接收者不在同一 user namespace,需要将
info->si_uid从发送者的 user namespace 映射到接收者的 user namespace。make_kuid()先将数值 UID 映射为发送者 namespace 中的kuid_t,然后from_kuid_munged()将其转换为接收者 namespace 中的数值 UID(如果映射不存在则使用 overflowuid)。 - PID namespace 检查:如果发送者来自祖先 PID namespace,将si_pid设为 0(因为 PID 在接收者的 namespace 中无意义),并设置force = true。
force 参数会传递给 __send_signal_locked(),影响 prepare_signal() 中的 sig_ignored() 判断,从而决定是否跳过 SIGNAL_UNKILLABLE 检查。
14.3.4 __send_signal_locked() -- 核心发送逻辑
__send_signal_locked() 是信号发送的真正核心函数,定义在 kernel/signal.c:1042。该函数执行信号排队的所有关键操作:
// kernel/signal.c:1042
static int __send_signal_locked(int sig, struct kernel_siginfo *info,
struct task_struct *t, enum pid_type type, bool force)
{
struct sigpending *pending;
struct sigqueue *q;
int override_rlimit;
int ret = 0, result;
lockdep_assert_held(&t->sighand->siglock);
14.3.4.1 prepare_signal() -- 预检查
result = TRACE_SIGNAL_IGNORED;
if (!prepare_signal(sig, t, force))
goto ret;
prepare_signal() (signal.c:871) 执行进程级别的信号互斥和状态转换:
SIGNAL_GROUP_EXIT 检查:如果进程正在执行组退出(signal->flags & SIGNAL_GROUP_EXIT),只允许 SIGKILL 通过(且仅当进程正在 core dump 时):
// kernel/signal.c:877
if (signal->flags & SIGNAL_GROUP_EXIT) {
if (signal->core_state)
return sig == SIGKILL;
return false;
}
停止信号处理:当发送停止信号(SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU)时,从所有队列中清除 SIGCONT:
// kernel/signal.c:884
} else if (sig_kernel_stop(sig)) {
siginitset(&flush, sigmask(SIGCONT));
flush_sigqueue_mask(p, &flush, &signal->shared_pending);
for_each_thread(p, t)
flush_sigqueue_mask(p, &flush, &t->pending);
SIGCONT 处理:当发送 SIGCONT 时,从所有队列中清除停止信号,并唤醒所有被停止的线程:
// kernel/signal.c:892
} else if (sig == SIGCONT) {
siginitset(&flush, SIG_KERNEL_STOP_MASK);
flush_sigqueue_mask(p, &flush, &signal->shared_pending);
for_each_thread(p, t) {
flush_sigqueue_mask(p, &flush, &t->pending);
task_clear_jobctl_pending(t, JOBCTL_STOP_PENDING);
if (likely(!(t->ptrace & PT_SEIZED))) {
t->jobctl &= ~JOBCTL_STOPPED;
wake_up_state(t, __TASK_STOPPED);
} else
ptrace_trap_notify(t);
}
最后,prepare_signal() 调用 sig_ignored() 检查信号是否应被忽略(signal.c:935)。sig_ignored() (signal.c:106) 的判断逻辑是:
// kernel/signal.c:106
static bool sig_ignored(struct task_struct *t, int sig, bool force)
{
if (sigismember(&t->blocked, sig) || sigismember(&t->real_blocked, sig))
return false;
if (t->ptrace && sig != SIGKILL)
return false;
return sig_task_ignored(t, sig, force);
}
被阻塞的信号不能被忽略(因为处理器可能在解除阻塞时改变)。被 ptrace 跟踪的进程也不忽略信号(调试器需要看到所有信号)。sig_task_ignored() (signal.c:76) 进一步检查 SIGNAL_UNKILLABLE(init 进程保护)和处理器设置。
14.3.4.2 pending 队列选择
pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
信号被排队到不同的 pending 队列,取决于信号的发送类型:
| type 值 | 含义 | 目标队列 |
|---|---|---|
PIDTYPE_PID |
定向到特定线程 | t->pending(私有队列) |
PIDTYPE_TGID |
线程组共享 | t->signal->shared_pending |
PIDTYPE_PGID |
进程组 | t->signal->shared_pending |
PIDTYPE_MAX |
广播 | t->signal->shared_pending |
struct sigpending 的定义位于 include/linux/signal_types.h:32:
struct sigpending {
struct list_head list; // sigqueue 节点链表
sigset_t signal; // 信号位图(哪些信号有待处理)
};
每个进程有两个 sigpending:私有的 task_struct->pending 和共享的 signal_struct->shared_pending。
14.3.4.3 legacy_queue() -- 标准信号去重
result = TRACE_SIGNAL_ALREADY_PENDING;
if (legacy_queue(pending, sig))
goto ret;
legacy_queue() 定义在 signal.c:1037:
// kernel/signal.c:1037
static inline bool legacy_queue(struct sigpending *signals, int sig)
{
return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
}
标准信号(编号 1-31,即 sig < SIGRTMIN)是不可靠信号。如果该信号已经在 pending 位图中存在,则不再重复排队。这就是所谓的"标准信号可能丢失"的根源 -- 如果在处理一个 SIGTERM 之前又收到了另一个 SIGTERM,后者会被直接丢弃。
实时信号(SIGRTMIN 到 SIGRTMAX,即 34-64)不受此限制,每次发送都会排队一个新的 sigqueue 节点。
14.3.4.4 SIGKILL 快速路径
result = TRACE_SIGNAL_DELIVERED;
if ((sig == SIGKILL) || (t->flags & PF_KTHREAD))
goto out_set;
对于 SIGKILL 信号和内核线程,跳过 siginfo 的分配和填充,直接跳到 out_set 标签。这是一个性能优化:SIGKILL 不需要 siginfo(其处理动作总是终止进程),而内核线程通常也不会检查 siginfo 的详细内容。
14.3.4.5 sigqueue 分配与 siginfo 填充
if (sig < SIGRTMIN)
override_rlimit = (is_si_special(info) || info->si_code >= 0);
else
override_rlimit = 0;
q = sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
override_rlimit 决定是否在 RLIMIT_SIGPENDING 达到上限时仍然分配 sigqueue。对于标准信号(sig < SIGRTMIN),如果信号来自内核(is_si_special)或者 si_code >= 0(用户空间信号),允许超出限制,因为标准信号不保证可靠性,内核承诺 kill() 不会因内存不足而返回 -EAGAIN。
sigqueue_alloc() (signal.c:446) 执行实际的 sigqueue 节点分配:
// kernel/signal.c:446
static struct sigqueue *sigqueue_alloc(int sig, struct task_struct *t, gfp_t gfp_flags,
int override_rlimit)
{
struct ucounts *ucounts = sig_get_ucounts(t, sig, override_rlimit);
struct sigqueue *q;
if (!ucounts)
return NULL;
q = kmem_cache_alloc(sigqueue_cachep, gfp_flags);
if (!q) {
dec_rlimit_put_ucounts(ucounts, UCOUNT_RLIMIT_SIGPENDING);
return NULL;
}
__sigqueue_init(q, ucounts, 0);
return q;
}
sig_get_ucounts() (signal.c:402) 检查 per-user 的 RLIMIT_SIGPENDING 计数,通过 inc_rlimit_get_ucounts() 递增层次化 ucounts 计数器。struct sigqueue 定义在 include/linux/signal_types.h:22:
struct sigqueue {
struct list_head list; // 链入 sigpending.list
int flags; // SIGQUEUE_PREALLOC 等
kernel_siginfo_t info; // 携带的 siginfo
struct ucounts *ucounts; // 所属 user 的计数
};
分配成功后,根据 info 的类型填充 siginfo(signal.c:1089-1116):
if (q) {
list_add_tail(&q->list, &pending->list);
switch ((unsigned long) info) {
case (unsigned long) SEND_SIG_NOINFO:
clear_siginfo(&q->info);
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_USER;
q->info.si_pid = task_tgid_nr_ns(current,
task_active_pid_ns(t));
rcu_read_lock();
q->info.si_uid =
from_kuid_munged(task_cred_xxx(t, user_ns),
current_uid());
rcu_read_unlock();
break;
case (unsigned long) SEND_SIG_PRIV:
clear_siginfo(&q->info);
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_KERNEL;
q->info.si_pid = 0;
q->info.si_uid = 0;
break;
default:
copy_siginfo(&q->info, info);
break;
}
三种填充模式:
- SEND_SIG_NOINFO:来自用户空间的简单信号(如
kill()发送的),填充发送者的 PID 和 UID。 - SEND_SIG_PRIV:内核内部发送的信号,
si_code设为SI_KERNEL,PID/UID 均为 0。 - default(正常 siginfo 指针):直接
copy_siginfo()复制调用者提供的 siginfo,适用于rt_sigqueueinfo()等需要携带自定义数据的场景。
14.3.4.6 队列溢出处理
} else if (!is_si_special(info) &&
sig >= SIGRTMIN && info->si_code != SI_USER) {
result = TRACE_SIGNAL_OVERFLOW_FAIL;
ret = -EAGAIN;
goto ret;
} else {
result = TRACE_SIGNAL_LOSE_INFO;
}
当 sigqueue_alloc() 失败(返回 NULL)时:
- 对于实时信号(
sig >= SIGRTMIN)且非内核/非kill()发送的信号,返回-EAGAIN。POSIX 规定实时信号的sigqueue()应当在资源不足时返回错误。 - 对于其他情况(标准信号或
kill()发送的信号),静默丢失 siginfo 信息。信号本身仍然会被标记为 pending(在out_set中设置位图),只是无法携带详细的 siginfo。这符合 POSIX 对标准信号不保证可靠性的要求。
14.3.4.7 设置 pending 位图与多进程信号延迟
out_set:
signalfd_notify(t, sig);
sigaddset(&pending->signal, sig);
/* Let multiprocess signals appear after on-going forks */
if (type > PIDTYPE_TGID) {
struct multiprocess_signals *delayed;
hlist_for_each_entry(delayed, &t->signal->multiprocess, node) {
sigset_t *signal = &delayed->signal;
if (sig == SIGCONT)
sigdelsetmask(signal, SIG_KERNEL_STOP_MASK);
else if (sig_kernel_stop(sig))
sigdelset(signal, SIGCONT);
sigaddset(signal, sig);
}
}
complete_signal(sig, t, type);
关键步骤:
- signalfd_notify():通知通过
signalfd监听信号的文件描述符。 - sigaddset():在 pending 队列的信号位图中设置该信号的位。这是后续
next_signal()查找待处理信号的基础。 - 多进程信号延迟(
type > PIDTYPE_TGID,即 PGID 或广播信号):在fork()期间,子进程尚未完全加入进程组/会话,信号可能需要延迟到 fork 完成后才投递。multiprocess哈希链表上的节点记录了这些延迟信号。同时,SIGCONT 和停止信号互斥:延迟队列中不会同时存在 SIGCONT 和停止信号。 - complete_signal():选择目标线程并唤醒。
14.3.5 complete_signal() -- 目标线程选择与组退出
complete_signal() 定义在 kernel/signal.c:963,负责从线程组中选择一个合适的线程来处理信号,并在必要时启动整个线程组的退出流程。
14.3.5.1 线程选择策略
// kernel/signal.c:963
static void complete_signal(int sig, struct task_struct *p, enum pid_type type)
{
struct signal_struct *signal = p->signal;
struct task_struct *t;
if (wants_signal(sig, p))
t = p;
else if ((type == PIDTYPE_PID) || thread_group_empty(p))
return;
else {
t = signal->curr_target;
while (!wants_signal(sig, t)) {
t = next_thread(t);
if (t == signal->curr_target)
return;
}
signal->curr_target = t;
}
线程选择逻辑:
- 首先尝试建议的线程:如果信号直接发给某个线程(
type == PIDTYPE_PID),或者建议的线程p愿意接收该信号(wants_signal()返回 true),则选择该线程。 - 单线程直接返回:如果
type不是PIDTYPE_PID且线程组只有一个线程,无需唤醒 -- 该线程运行时会自动从共享队列中取出信号。 - 轮转选择:否则,从
signal->curr_target开始轮转遍历线程组,寻找一个愿意接收信号的线程。curr_target被更新为选中的线程,形成负载均衡效果。
wants_signal() (signal.c:946) 的判断条件:
// kernel/signal.c:946
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;
if (task_is_stopped_or_traced(p))
return false;
return task_curr(p) || !task_sigpending(p);
}
一个线程"想要"接收信号的条件是:未阻塞该信号、不在退出中、未被停止/跟踪(SIGKILL 除外)、且要么正在运行要么没有其他待处理信号。
14.3.5.2 致命信号的组退出
// kernel/signal.c:1003
if (sig_fatal(p, sig) &&
(signal->core_state || !(signal->flags & SIGNAL_GROUP_EXIT)) &&
!sigismember(&t->real_blocked, sig) &&
(sig == SIGKILL || !p->ptrace)) {
if (!sig_kernel_coredump(sig)) {
signal->flags = SIGNAL_GROUP_EXIT;
signal->group_exit_code = sig;
signal->group_stop_count = 0;
__for_each_thread(signal, t) {
task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
sigaddset(&t->pending.signal, SIGKILL);
signal_wake_up(t, 1);
}
return;
}
}
当检测到一个致命信号(sig_fatal() 返回 true,即默认动作为终止)且满足以下条件时:
- 进程未处于
SIGNAL_GROUP_EXIT状态(或者正在 core dump) - 信号未被
real_blocked阻塞 - 信号是 SIGKILL 或者进程未被 ptrace 跟踪
则启动组退出流程:
- 非 coredump 致命信号:设置
SIGNAL_GROUP_EXIT标志和group_exit_code,然后向线程组中的所有线程发送 SIGKILL(直接在pending.signal位图中设置 SIGKILL 位),并通过signal_wake_up(t, 1)唤醒所有线程。参数1表示这是一个致命信号,需要唤醒处于 TASK_STOPPED 或 TASK_TRACED 状态的线程。 - 需要 coredump 的信号:不在此处立即执行组退出,而是等到信号被投递时在
get_signal()中执行vfs_coredump()和do_group_exit()。
14.3.5.3 signal_wake_up_state()
// kernel/signal.c:1033
signal_wake_up(t, sig == SIGKILL);
return;
signal_wake_up() 最终调用 signal_wake_up_state() (signal.c:721):
// kernel/signal.c:721
void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
lockdep_assert_held(&t->sighand->siglock);
set_tsk_thread_flag(t, TIF_SIGPENDING);
if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
kick_process(t);
}
此函数执行两个操作:
- 设置 TIF_SIGPENDING 标志:通过
set_tsk_thread_flag()在目标线程的 thread_info 中设置TIF_SIGPENDING。这告诉内核在返回用户态前需要处理信号。 - 唤醒线程:
wake_up_state()尝试唤醒处于指定状态的线程。对于 SIGKILL,state包含TASK_WAKEKILL,可以唤醒处于 TASK_STOPPED/TASK_TRACED 状态的线程。如果线程正在其他 CPU 上运行(无需唤醒),则调用kick_process()发送 IPI 中断,使目标 CPU 尽快检查信号。
14.3.6 内核内部信号发送 API
内核内部有多个信号发送接口,适用于不同的场景。
14.3.6.1 send_sig / send_sig_info
// kernel/signal.c:1612
int send_sig_info(int sig, struct kernel_siginfo *info, struct task_struct *p)
{
if (!valid_signal(sig))
return -EINVAL;
return do_send_sig_info(sig, info, p, PIDTYPE_PID);
}
// kernel/signal.c:1628
int send_sig(int sig, struct task_struct *p, int priv)
{
return send_sig_info(sig, __si_special(priv), p);
}
send_sig() 是内核中最常用的信号发送接口之一。priv 参数为 1 时使用 SEND_SIG_PRIV(内核信号,不可忽略),为 0 时使用 SEND_SIG_NOINFO(用户信号,需检查权限)。信号类型为 PIDTYPE_PID,定向到特定线程。
14.3.6.2 force_sig 家族
// kernel/signal.c:1635
void force_sig(int sig)
{
struct kernel_siginfo info;
clear_siginfo(&info);
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_KERNEL;
info.si_pid = 0;
info.si_uid = 0;
force_sig_info(&info);
}
// kernel/signal.c:1649
void force_fatal_sig(int sig)
{
struct kernel_siginfo info;
// ... 填充 info ...
force_sig_info_to_task(&info, current, HANDLER_SIG_DFL);
}
// kernel/signal.c:1662
void force_exit_sig(int sig)
{
struct kernel_siginfo info;
// ... 填充 info ...
force_sig_info_to_task(&info, current, HANDLER_EXIT);
}
force_sig() 系列函数的目的是发送不可忽略的信号。它们的核心实现是 force_sig_info_to_task() (signal.c:1293):
// kernel/signal.c:1293
static int
force_sig_info_to_task(struct kernel_siginfo *info, struct task_struct *t,
enum sig_handler handler)
{
unsigned long int flags;
int ret, blocked, ignored;
struct k_sigaction *action;
int sig = info->si_signo;
spin_lock_irqsave(&t->sighand->siglock, flags);
action = &t->sighand->action[sig-1];
ignored = action->sa.sa_handler == SIG_IGN;
blocked = sigismember(&t->blocked, sig);
if (blocked || ignored || (handler != HANDLER_CURRENT)) {
action->sa.sa_handler = SIG_DFL;
if (handler == HANDLER_EXIT)
action->sa.sa_flags |= SA_IMMUTABLE;
if (blocked)
sigdelset(&t->blocked, sig);
}
if (action->sa.sa_handler == SIG_DFL &&
(!t->ptrace || (handler == HANDLER_EXIT)))
t->signal->flags &= ~SIGNAL_UNKILLABLE;
ret = send_signal_locked(sig, info, t, PIDTYPE_PID);
if (!task_sigpending(t))
signal_wake_up(t, 0);
spin_unlock_irqrestore(&t->sighand->siglock, flags);
return ret;
}
force_sig_info_to_task() 的特殊行为:
- 解除阻塞:如果信号被阻塞,从
blocked掩码中移除。 - 重置处理器:如果信号被忽略(
SIG_IGN)或handler不是HANDLER_CURRENT,将处理器重置为SIG_DFL。 - SA_IMMUTABLE:当
handler == HANDLER_EXIT时,设置SA_IMMUTABLE标志,阻止 ptrace 修改信号处理器。 - 清除 SIGNAL_UNKILLABLE:如果处理器被重置为
SIG_DFL且进程未被 ptrace 跟踪(或handler == HANDLER_EXIT),清除SIGNAL_UNKILLABLE标志。这允许强制杀死 init 进程。 - 唤醒:如果目标线程没有其他待处理信号,调用
signal_wake_up()唤醒它。
enum sig_handler 定义了三种模式(signal.c:1276):
| handler | 含义 | 使用场景 |
|---|---|---|
HANDLER_CURRENT |
使用当前处理器 | force_sig() -- 异常信号 |
HANDLER_EXIT |
重置为 SIG_DFL + SA_IMMUTABLE | force_exit_sig() -- 必须终止 |
HANDLER_SIG_DFL |
重置为 SIG_DFL | force_fatal_sig() -- 必须致命 |
14.3.6.3 force_sigsegv -- 信号处理失败的后备
// kernel/signal.c:1681
void force_sigsegv(int sig)
{
if (sig == SIGSEGV)
force_fatal_sig(SIGSEGV);
else
force_sig(SIGSEGV);
}
当信号处理过程中出错时(例如信号栈溢出),内核调用 force_sigsegv()。如果出错的信号本身就是 SIGSEGV,则直接使用 force_fatal_sig()(避免无限递归);否则使用 force_sig() 发送 SIGSEGV。
14.3.6.4 send_sig_fault / force_sig_fault -- 异常信号
// kernel/signal.c:1689
int force_sig_fault_to_task(int sig, int code, void __user *addr,
struct task_struct *t)
{
struct kernel_siginfo info;
clear_siginfo(&info);
info.si_signo = sig;
info.si_errno = 0;
info.si_code = code;
info.si_addr = addr;
return force_sig_info_to_task(&info, t, HANDLER_CURRENT);
}
// kernel/signal.c:1702
int force_sig_fault(int sig, int code, void __user *addr)
{
return force_sig_fault_to_task(sig, code, addr, current);
}
// kernel/signal.c:1707
int send_sig_fault(int sig, int code, void __user *addr, struct task_struct *t)
{
struct kernel_siginfo info;
// ... 填充 info ...
return send_sig_info(info.si_signo, &info, t);
}
这些函数用于发送硬件异常产生的信号(SIGSEGV、SIGBUS、SIGFPE 等)。它们填充 si_addr 字段指向触发异常的地址,si_code 包含具体的异常原因(如 SEGV_MAPERR、BUS_ADRALN 等)。
14.3.6.5 kill_pid / kill_pgrp -- 内核 kill 接口
// kernel/signal.c:1889
int kill_pid(struct pid *pid, int sig, int priv)
{
return kill_pid_info(sig, __si_special(priv), pid);
}
// kernel/signal.c:1883
int kill_pgrp(struct pid *pid, int sig, int priv)
{
return kill_pgrp_info(sig, __si_special(priv), pid);
}
这些是内核子系统向进程/进程组发送信号的接口。priv 参数决定信号优先级:priv=1 使用 SEND_SIG_PRIV(内核信号,不可忽略),priv=0 使用 SEND_SIG_NOINFO。典型使用场景包括:
kill_pid():被 oom killer、分组冻结等子系统使用。kill_pgrp():被终端驱动用于发送 SIGINT(Ctrl+C)、SIGQUIT(Ctrl+\)、SIGTSTP(Ctrl+Z)等信号。
14.3.7 PID namespace 与 init 进程保护
14.3.7.1 SIGNAL_UNKILLABLE 机制
PID namespace 的 init 进程受到 SIGNAL_UNKILLABLE 标志的保护。该标志在进程创建时设置:
- 全局 init(PID 1):始终具有
SIGNAL_UNKILLABLE。 - 容器 init:在创建新的 PID namespace 时设置
SIGNAL_UNKILLABLE。
sig_task_ignored() (signal.c:76) 中的保护逻辑:
// kernel/signal.c:91
if (unlikely(t->signal->flags & SIGNAL_UNKILLABLE) &&
handler == SIG_DFL && !(force && sig_kernel_only(sig)))
return true;
当 SIGNAL_UNKILLABLE 被设置时,只有 force == true 且信号是 SIGKILL 或 SIGSTOP(sig_kernel_only() 返回 true)的信号才能绕过保护。而 force 为 true 的条件是信号来自祖先 PID namespace 或内核自身(见 14.3.3 节 send_signal_locked() 的分析)。
这意味着:
- 容器内的进程无法 kill 容器的 init 进程(SIGKILL 和 SIGSTOP 除外)。
- 主机上的 root(在祖先 namespace 中)可以杀死容器 init。
- 内核自身可以杀死任何 init 进程。
14.3.7.2 force_sig_info_to_task() 对 SIGNAL_UNKILLABLE 的清除
当 force_sig_info_to_task() 被调用时(如硬件异常),如果信号处理器被重置为 SIG_DFL 且进程未被 ptrace 跟踪,则清除 SIGNAL_UNKILLABLE。这确保了硬件异常(如 SIGSEGV)可以使 init 进程崩溃。
14.3.8 tkill/tgkill 系统调用
tkill() 和 tgkill() 用于向指定线程发送信号,而不是像 kill() 那样向线程组发送。
14.3.8.1 tgkill 系统调用
// kernel/signal.c:4165
SYSCALL_DEFINE3(tgkill, pid_t, tgid, pid_t, pid, int, sig)
{
if (pid <= 0 || tgid <= 0)
return -EINVAL;
return do_tkill(tgid, pid, sig);
}
14.3.8.2 tkill 系统调用
// kernel/signal.c:4181
SYSCALL_DEFINE2(tkill, pid_t, pid, int, sig)
{
if (pid <= 0)
return -EINVAL;
return do_tkill(0, pid, sig);
}
14.3.8.3 do_tkill() 实现
// kernel/signal.c:4146
static int do_tkill(pid_t tgid, pid_t pid, int sig)
{
struct kernel_siginfo info;
prepare_kill_siginfo(sig, &info, PIDTYPE_PID);
return do_send_specific(tgid, pid, sig, &info);
}
// kernel/signal.c:4116
static int
do_send_specific(pid_t tgid, pid_t pid, int sig, struct kernel_siginfo *info)
{
struct task_struct *p;
int error = -ESRCH;
rcu_read_lock();
p = find_task_by_vpid(pid);
if (p && (tgid <= 0 || task_tgid_vnr(p) == tgid)) {
error = check_kill_permission(sig, info, p);
if (!error && sig) {
error = do_send_sig_info(sig, info, p, PIDTYPE_PID);
if (unlikely(error == -ESRCH))
error = 0;
}
}
rcu_read_unlock();
return error;
}
tkill 与 tgkill 的区别:
- tkill:只指定目标线程的 PID(
tgid参数为 0),如果该 PID 被其他线程组复用,可能发送到错误的线程。 - tgkill:同时指定
tgid和pid,只有当目标线程的线程组 ID 匹配tgid时才发送。这消除了 PID 复用导致的竞态条件。
两者都使用 PIDTYPE_PID 类型,信号进入目标线程的私有 pending 队列(t->pending),而不是共享队列。si_code 设为 SI_TKILL(通过 prepare_kill_siginfo() 中 type == PIDTYPE_PID 的判断)。
当 do_send_sig_info() 返回 -ESRCH 时(目标进程的 sighand 在获取锁的过程中被释放),错误被静默地改为 0 -- 因为信号是私有的,目标线程即将退出,不影响正确性。
14.3.9 rt_sigqueueinfo 系统调用
rt_sigqueueinfo() 允许用户空间发送带有自定义 siginfo 的实时信号:
// kernel/signal.c:4209
SYSCALL_DEFINE3(rt_sigqueueinfo, pid_t, pid, int, sig,
siginfo_t __user *, uinfo)
{
kernel_siginfo_t info;
int ret = __copy_siginfo_from_user(sig, &info, uinfo);
if (unlikely(ret))
return ret;
return do_rt_sigqueueinfo(pid, sig, &info);
}
// kernel/signal.c:4190
static int do_rt_sigqueueinfo(pid_t pid, int sig, kernel_siginfo_t *info)
{
if ((info->si_code >= 0 || info->si_code == SI_TKILL) &&
(task_pid_vnr(current) != pid))
return -EPERM;
return kill_proc_info(sig, info, pid);
}
关键的安全检查在 do_rt_sigqueueinfo() 中:
- si_code 限制:
si_code >= 0表示信号来自用户空间(内核信号的si_code为负值),si_code == SI_TKILL是 tkill 的标记。这两种情况下,如果发送者不是给自己发信号(task_pid_vnr(current) != pid),则拒绝。这防止用户空间伪造内核信号或 tkill 信号。 - 用户空间可以合法设置的
si_code包括SI_QUEUE、SI_TIMER、SI_MESGQ、SI_ASYNCIO等,这些都是负值。
rt_tgsigqueueinfo() 是线程级别的版本:
// kernel/signal.c:4233
static int do_rt_tgsigqueueinfo(pid_t tgid, pid_t pid, int sig, kernel_siginfo_t *info)
{
if (pid <= 0 || tgid <= 0)
return -EINVAL;
if ((info->si_code >= 0 || info->si_code == SI_TKILL) &&
(task_pid_vnr(current) != pid))
return -EPERM;
return do_send_specific(tgid, pid, sig, info);
}
14.3.10 权限模型总结
Linux 信号权限模型可以总结为以下几个层次:
第一层:进程关系检查
| 关系 | 权限 |
|---|---|
| 自己发给自己 | 始终允许 |
| 同一线程组内 | 始终允许 |
| 同一会话中的 SIGCONT | 允许(作业控制) |
第二层:凭证检查
发送者(UID/EUID) == 接收者(UID/SUID) ? 允许 : 检查 CAP_KILL
四种组合中任一匹配即可,否则检查发送者在接收者 user namespace 中是否有 CAP_KILL。
第三层:PID namespace 保护
- init 进程(
SIGNAL_UNKILLABLE)忽略来自同 namespace 的非 SIGKILL/SIGSTOP 信号。 - 祖先 namespace 的信号通过
force=true绕过保护。 - 内核信号(
SEND_SIG_PRIV)通过force=true绕过保护。
第四层:LSM 安全模块
security_task_kill() 作为最后一道关卡,允许 SELinux、AppArmor、Smack 等安全模块实施额外的访问控制策略。
锁层次关系
信号发送路径中涉及的锁有以下层次关系(从外到内):
tasklist_lock(读锁):保护进程列表,在kill_something_info()和__kill_pgrp_info()中持有。rcu_read_lock():保护task_struct访问,在权限检查和 PID 查找时持有。sighand->siglock(自旋锁,关中断):保护信号发送的整个操作,在do_send_sig_info()/force_sig_info_to_task()中持有。
siglock 是信号子系统最核心的锁,保护 sigpending 队列、blocked 掩码、signal->flags、jobctl 等所有与信号相关的状态。内核要求在持有 siglock 时关中断(spin_lock_irq),因为信号操作可能在中断上下文中发生。
14.3.11 信号发送完整流程图
以 kill(1234, SIGTERM) 为例,完整调用链如下:
用户态 kill(1234, SIGTERM)
|
v
SYSCALL_DEFINE2(kill) // signal.c:3947
|
+-- prepare_kill_siginfo(SIGTERM, &info, PIDTYPE_TGID) // signal.c:3931
| si_code = SI_USER, si_pid = caller_tgid
|
+-- kill_something_info(SIGTERM, &info, 1234) // signal.c:1572
|
+-- kill_proc_info(SIGTERM, &info, 1234) // signal.c:1476
|
+-- kill_pid_info(SIGTERM, &info, pid) // signal.c:1471
|
+-- kill_pid_info_type() // signal.c:1449
|
+-- group_send_sig_info() // signal.c:1409
|
+-- check_kill_permission() // 799
| kill_ok_by_cred()
| security_task_kill()
|
+-- do_send_sig_info() // 1262
|
+-- send_signal_locked() // 1183
| PID namespace 转换
| force 参数确定
|
+-- __send_signal_locked() // 1042
|
+-- prepare_signal() // 871
+-- legacy_queue() // 1037
+-- sigqueue_alloc() // 446
+-- siginfo 填充
+-- sigaddset()
+-- complete_signal() // 963
|
+-- wants_signal()
+-- fatal? -> SIGNAL_GROUP_EXIT
+-- signal_wake_up() // 721
set TIF_SIGPENDING
wake_up_state()
整个过程的关键设计原则是:权限检查在最外层(发送前),状态修改在最内层(持有 siglock 时),而 PID namespace 转换在中间层(send_signal_locked)完成。这种分层设计使得内核代码在不同发送路径(kill、tkill、force_sig 等)之间最大程度复用了核心逻辑。
14.4 信号投递与处理
信号发送(generate)和信号投递(deliver)是两个不同的阶段。发送是将信号排入目标进程的 pending 队列,而投递是目标进程在返回用户态之前检查待处理信号、选择信号并执行相应动作的过程。本节将深入分析从内核返回用户态时的信号检查机制、信号的出队策略、信号帧的构建以及信号返回的完整流程。
14.4.1 信号投递时机
14.4.1.1 从内核返回用户态的检查点
Linux 内核在从内核态返回用户态之前,会检查当前线程的 TIF_SIGPENDING 标志。这一检查点位于架构相关的入口代码中。
在 x86_64 架构上,arch_do_signal_or_restart() 是信号投递的架构入口,定义在 arch/x86/kernel/signal.c:333:
// arch/x86/kernel/signal.c:333
void arch_do_signal_or_restart(struct pt_regs *regs)
{
struct ksignal ksig;
if (get_signal(&ksig)) {
/* Whee! Actually deliver the signal. */
handle_signal(&ksig, regs);
return;
}
/* Did we come from a system call? */
if (syscall_get_nr(current, regs) != -1) {
/* Restart the system call - no handlers present */
switch (syscall_get_error(current, regs)) {
case -ERESTARTNOHAND:
case -ERESTARTSYS:
case -ERESTARTNOINTR:
regs->ax = regs->orig_ax;
regs->ip -= 2;
break;
case -ERESTART_RESTARTBLOCK:
regs->ax = get_nr_restart_syscall(regs);
regs->ip -= 2;
break;
}
}
/*
* If there's no signal to deliver, we just put the saved sigmask
* back.
*/
restore_saved_sigmask();
}
当没有待处理信号时,get_signal() 返回 false,内核处理系统调用重启逻辑,然后恢复之前保存的信号掩码。当有待处理信号时,调用 handle_signal() 进行信号帧构建。
内核通用入口层在系统调用返回和中断返回路径中调用 arch_do_signal_or_restart()。在 x86_64 上,这一调用链是:
entry_64.S: ret_from系统调用/中断
-> entry-common.S: exit_to_user_mode_loop()
-> exit_to_user_mode_prepare()
-> arch_exit_to_user_mode()
-> arch_do_signal_or_restart()
在 exit_to_user_mode_loop() 中,内核会循环检查 _TIF_SIGPENDING、_TIF_NOTIFY_RESUME、_TIF_NEED_RESCHED 等标志,直到所有待处理工作完成才返回用户态。
14.4.1.2 系统调用重启机制
arch_do_signal_or_restart() 中处理了系统调用重启的四种情况:
| 返回值 | 含义 | 重启条件 |
|---|---|---|
-ERESTARTNOHAND |
不自动重启 | 从不重启(被信号中断) |
-ERESTARTSYS |
有条件重启 | 仅当 SA_RESTART 未设置时不重启 |
-ERESTARTNOINTR |
无条件重启 | 始终重启 |
-ERESTART_RESTARTBLOCK |
使用 restart_block | 替换为 restart_syscall |
当有信号要处理时(handle_signal() 路径),系统调用重启逻辑略有不同。在 handle_signal() 内部(signal.c:254):
// arch/x86/kernel/signal.c:254
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
bool stepping, failed;
struct fpu *fpu = x86_task_fpu(current);
// ... vm86 处理 ...
/* Are we from a system call? */
if (syscall_get_nr(current, regs) != -1) {
/* If so, check system call restarting.. */
switch (syscall_get_error(current, regs)) {
case -ERESTART_RESTARTBLOCK:
case -ERESTARTNOHAND:
regs->ax = -EINTR;
break;
case -ERESTARTSYS:
if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {
regs->ax = -EINTR;
break;
}
fallthrough;
case -ERESTARTNOINTR:
regs->ax = regs->orig_ax;
regs->ip -= 2;
break;
}
}
在信号处理路径中,-ERESTARTNOHAND 和 -ERESTART_RESTARTBLOCK 始终转为 -EINTR;-ERESTARTSYS 仅在信号处理器设置了 SA_RESTART 标志时才重启;-ERESTARTNOINTR 始终重启(将 orig_ax 恢复到 rax,ip 回退 2 字节以重新执行 syscall 指令)。
14.4.2 get_signal() 完整分析
get_signal() 是信号投递的核心决策函数,定义在 kernel/signal.c:2800(实际上是名为 get_signal 的函数,接受一个 struct ksignal *ksig 参数)。这个函数决定了从所有待处理信号中选择哪个信号、如何处理以及是否终止进程。
14.4.2.1 入口准备
// kernel/signal.c:2800
{
struct sighand_struct *sighand = current->sighand;
struct signal_struct *signal = current->signal;
int signr;
clear_notify_signal();
if (unlikely(task_work_pending(current)))
task_work_run();
if (!task_sigpending(current))
return false;
if (unlikely(uprobe_deny_signal()))
return false;
try_to_freeze();
入口处执行几个重要的前期处理:
- clear_notify_signal():清除
TIF_NOTIFY_SIGNAL标志。这个标志用于通知机制(如 io_uring),需要与TIF_SIGPENDING区分。 - task_work_run():执行排队到当前任务的回调工作。
task_work机制允许内核在返回用户态前执行一些清理工作,如文件描述表安装等。 - task_sigpending():再次检查是否有待处理信号。这避免了在清除 notify_signal 和运行 task_work 后的无谓锁获取。
- uprobe_deny_signal():如果当前线程处于 uprobe(用户空间探针)处理中,暂时阻止信号投递。
- try_to_freeze():检查是否需要冻结当前任务(用于挂起/休眠),如果需要则将当前任务设为 TASK_FROZEN 状态。
14.4.2.2 relock 标签与 SIGNAL_CLD_MASK 处理
// kernel/signal.c:2822
relock:
spin_lock_irq(&sighand->siglock);
if (unlikely(signal->flags & SIGNAL_CLD_MASK)) {
int why;
if (signal->flags & SIGNAL_CLD_CONTINUED)
why = CLD_CONTINUED;
else
why = CLD_STOPPED;
signal->flags &= ~SIGNAL_CLD_MASK;
spin_unlock_irq(&sighand->siglock);
read_lock(&tasklist_lock);
do_notify_parent_cldstop(current, false, why);
if (ptrace_reparented(current->group_leader))
do_notify_parent_cldstop(current->group_leader,
true, why);
read_unlock(&tasklist_lock);
goto relock;
}
relock 标签是 get_signal() 中反复跳回的关键点。每次跳回都重新获取 siglock。
SIGNAL_CLD_MASK 处理逻辑用于向父进程报告子进程的停止/继续事件。这些事件由 prepare_signal() 中的 SIGCONT/SIGSTOP 处理代码设置(参见 14.3.4.1 节)。当检测到 SIGNAL_CLD_MASK 标志时:
- 解锁
siglock(因为do_notify_parent_cldstop()可能睡眠)。 - 获取
tasklist_lock读锁。 - 通过
do_notify_parent_cldstop()向父进程发送 SIGCHLD。 - 如果线程组领导者被 ptrace 跟踪,也通知 ptracer。
- 跳回
relock重新获取siglock继续处理。
14.4.2.3 信号处理主循环 for(;;)
// kernel/signal.c:2861
for (;;) {
struct k_sigaction *ka;
enum pid_type type;
/* Has this task already been marked for death? */
if ((signal->flags & SIGNAL_GROUP_EXIT) ||
signal->group_exec_task) {
signr = SIGKILL;
sigdelset(¤t->pending.signal, SIGKILL);
trace_signal_deliver(SIGKILL, SEND_SIG_NOINFO,
&sighand->action[SIGKILL-1]);
recalc_sigpending();
goto fatal;
}
主循环的第一步是检查进程是否已被标记为死亡。SIGNAL_GROUP_EXIT 由 complete_signal() 设置(见 14.3.5.2 节),表示整个线程组正在退出。signal->group_exec_task 表示正在执行 execve,需要杀死除领导者外的所有线程。在任一情况下,直接跳到 fatal 标签执行终止。
14.4.2.4 JOBCTL_STOP_PENDING 处理
// kernel/signal.c:2880
if (unlikely(current->jobctl & JOBCTL_STOP_PENDING) &&
do_signal_stop(0))
goto relock;
如果当前线程有待处理的停止请求(JOBCTL_STOP_PENDING),调用 do_signal_stop() 处理组停止。如果 do_signal_stop() 返回 true,线程被设置为 TASK_STOPPED 状态并释放 siglock。当线程被 SIGCONT 唤醒后,它重新跳回 relock 继续处理信号。
do_signal_stop() (signal.c:2550) 的实现分析见 14.4.7 节。
14.4.2.5 JOBCTL_TRAP 处理(ptrace 和 freezer)
// kernel/signal.c:2884
if (unlikely(current->jobctl &
(JOBCTL_TRAP_MASK | JOBCTL_TRAP_FREEZE))) {
if (current->jobctl & JOBCTL_TRAP_MASK) {
do_jobctl_trap();
spin_unlock_irq(&sighand->siglock);
} else if (current->jobctl & JOBCTL_TRAP_FREEZE)
do_freezer_trap();
goto relock;
}
JOBCTL_TRAP_MASK 用于 ptrace 停止:当被 ptrace 跟踪的进程收到信号时,先暂停以通知调试器。JOBCTL_TRAP_FREEZE 用于 cgroup freezer:将线程冻结在 TASK_FROZEN 状态。
14.4.2.6 信号出队
// kernel/signal.c:2911
type = PIDTYPE_PID;
signr = dequeue_synchronous_signal(&ksig->info);
if (!signr)
signr = dequeue_signal(¤t->blocked, &ksig->info, &type);
if (!signr)
break; /* will return 0 */
信号出队分为两步:
-
优先出队同步信号:
dequeue_synchronous_signal()(signal.c:668) 优先处理同步信号(SIGSEGV、SIGBUS、SIGILL、SIGTRAP、SIGFPE、SIGSYS)。同步信号由指令执行产生,需要确保其 siginfo 中的指令指针指向触发异常的指令。如果不优先处理,其他异步信号可能先被投递,导致栈帧中的 RIP 不再指向异常指令。 -
常规出队:如果没有同步信号,调用
dequeue_signal()从 pending 队列中取出优先级最高(编号最小)的未阻塞信号。
如果没有可出队的信号,break 退出循环,get_signal() 返回 false。
14.4.2.7 ptrace 信号拦截
// kernel/signal.c:2919
if (unlikely(current->ptrace) && (signr != SIGKILL) &&
!(sighand->action[signr -1].sa.sa_flags & SA_IMMUTABLE)) {
signr = ptrace_signal(signr, &ksig->info, type);
if (!signr)
continue;
}
当进程被 ptrace 跟踪时,除了 SIGKILL 和 SA_IMMUTABLE 标记的信号外,所有信号都会先经过调试器的拦截。ptrace_signal() 暂停进程并通知调试器,调试器可以选择:
- 放行原信号
- 替换为另一个信号
- 丢弃信号(返回 0,continue 继续下一个信号)
SA_IMMUTABLE 标志阻止 ptrace 修改信号,用于 force_exit_sig() 发送的不可拦截信号。
14.4.2.8 信号处理动作选择
// kernel/signal.c:2926
ka = &sighand->action[signr-1];
trace_signal_deliver(signr, &ksig->info, ka);
if (ka->sa.sa_handler == SIG_IGN) /* Do nothing. */
continue;
if (ka->sa.sa_handler != SIG_DFL) {
/* Run the handler. */
ksig->ka = *ka;
if (ka->sa.sa_flags & SA_ONESHOT)
ka->sa.sa_handler = SIG_DFL;
break; /* will return non-zero "signr" value */
}
信号处理的三种路径:
- SIG_IGN(忽略):直接
continue跳过,取下一个信号。注意,信号在prepare_signal()阶段已经做过忽略检查,但 ptrace 可能修改了处理器设置,所以这里需要再次检查。 - 用户处理函数(非 SIG_DFL 且非 SIG_IGN):保存
k_sigaction到ksig->ka,如果是SA_ONESHOT(一次性处理器),将处理器重置为SIG_DFL,然后break退出循环返回 true。调用者将构建信号帧并跳转到用户处理函数。 - SIG_DFL(默认处理):继续执行下面的默认处理逻辑。
14.4.2.9 默认处理动作
// kernel/signal.c:2946
if (sig_kernel_ignore(signr))
continue;
if (unlikely(signal->flags & SIGNAL_UNKILLABLE) &&
!sig_kernel_only(signr))
continue;
忽略类信号:某些信号在默认情况下被忽略(如 SIGCHLD、SIGURG、SIGWINCH)。sig_kernel_ignore() 检查信号是否属于此类。
init 保护:如果进程设置了 SIGNAL_UNKILLABLE 且信号不是 SIGKILL/SIGSTOP(sig_kernel_only()),则跳过。这保护 init 进程不会被普通信号杀死。
14.4.2.10 停止信号处理
// kernel/signal.c:2963
if (sig_kernel_stop(signr)) {
if (signr != SIGSTOP) {
spin_unlock_irq(&sighand->siglock);
if (is_current_pgrp_orphaned())
goto relock;
spin_lock_irq(&sighand->siglock);
}
if (likely(do_signal_stop(signr))) {
goto relock;
}
continue;
}
停止信号(SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU)的特殊处理:
- 孤儿进程组检查:对于非 SIGSTOP 的停止信号(如 SIGTSTP),先释放
siglock,检查当前进程组是否为孤儿进程组(is_current_pgrp_orphaned())。孤儿进程组的定义是该进程组的所有成员的父进程要么在同一进程组中,要么在另一个会话中。POSIX 规定孤儿进程组中的非 SIGSTOP 停止信号应被丢弃。释放锁的必要性在于is_current_pgrp_orphaned()需要获取tasklist_lock,而锁序要求tasklist_lock在siglock之外。 - 执行停止:
do_signal_stop()执行实际的组停止操作。如果返回 true,线程被设置为 TASK_STOPPED,释放siglock后跳回relock。如果返回 false(被 SIGCONT 竞争),continue取下一个信号。
14.4.2.11 致命信号处理
// kernel/signal.c:2997
fatal:
spin_unlock_irq(&sighand->siglock);
if (unlikely(cgroup_task_frozen(current)))
cgroup_leave_frozen(true);
current->flags |= PF_SIGNALED;
if (sig_kernel_coredump(signr)) {
if (print_fatal_signals)
print_fatal_signal(signr);
proc_coredump_connector(current);
vfs_coredump(&ksig->info);
}
if (current->flags & PF_USER_WORKER)
goto out;
do_group_exit(signr);
/* NOTREACHED */
}
致命信号的最终处理:
- 释放 siglock:此后不再需要锁。
- 解冻 cgroup:如果进程被 cgroup freezer 冻结,先解冻。
- 设置 PF_SIGNALED:标记进程因信号而终止。
- core dump:如果信号需要生成 core dump(
sig_kernel_coredump()检查信号是否为 SIGQUIT、SIGILL、SIGABRT、SIGFPE、SIGSEGV、SIGBUS、SIGSYS、SIGTRAP 或 SIGXCPU),调用vfs_coredump()生成 core 文件。vfs_coredump()会调用zap_other_threads()杀死线程组中的所有其他线程,并等待它们退出。 - do_group_exit():执行线程组的退出操作。注意注释
/* NOTREACHED */--do_group_exit()不会返回。 - PF_USER_WORKER 特殊处理:用户工作者线程(如 io_uring 的 SQ 线程)需要自行处理清理工作,不能直接调用
do_exit()。
14.4.2.12 返回值
// kernel/signal.c:3037
spin_unlock_irq(&sighand->siglock);
ksig->sig = signr;
if (signr && !(ksig->ka.sa.sa_flags & SA_EXPOSE_TAGBITS))
hide_si_addr_tag_bits(ksig);
out:
return signr > 0;
退出循环后(有用户处理函数的信号),设置 ksig->sig 并隐藏地址标签位(MTE 等内存标签扩展),返回 true。调用者(arch_do_signal_or_restart())将使用 ksig 中的信息构建信号帧。
14.4.3 dequeue_signal() -- 信号出队
14.4.3.1 dequeue_signal() 实现
dequeue_signal() 定义在 kernel/signal.c:618:
// kernel/signal.c:618
int dequeue_signal(sigset_t *mask, kernel_siginfo_t *info, enum pid_type *type)
{
struct task_struct *tsk = current;
struct sigqueue *timer_sigq;
int signr;
lockdep_assert_held(&tsk->sighand->siglock);
again:
*type = PIDTYPE_PID;
timer_sigq = NULL;
signr = __dequeue_signal(&tsk->pending, mask, info, &timer_sigq);
if (!signr) {
*type = PIDTYPE_TGID;
signr = __dequeue_signal(&tsk->signal->shared_pending,
mask, info, &timer_sigq);
if (unlikely(signr == SIGALRM))
posixtimer_rearm_itimer(tsk);
}
recalc_sigpending();
if (!signr)
return 0;
出队顺序非常重要:
- 先查私有队列(
tsk->pending):私有信号(通过tkill/tgkill发送)优先于共享信号。type设为PIDTYPE_PID。 - 再查共享队列(
tsk->signal->shared_pending):如果没有私有信号,从共享队列取。type设为PIDTYPE_TGID。 - POSIX 定时器重载:如果取出的信号是 SIGALRM,调用
posixtimer_rearm_itimer()重新装载 interval 定时器。 - recalc_sigpending():重新计算
TIF_SIGPENDING标志 -- 如果两个队列中都没有未阻塞的信号了,清除标志。
if (unlikely(sig_kernel_stop(signr))) {
current->jobctl |= JOBCTL_STOP_DEQUEUED;
}
if (IS_ENABLED(CONFIG_POSIX_TIMERS) && unlikely(timer_sigq)) {
if (!posixtimer_deliver_signal(info, timer_sigq))
goto again;
}
return signr;
}
后续处理:
- JOBCTL_STOP_DEQUEUED:标记已出队一个停止信号。这在 SIGCONT 的竞态处理中使用 -- 如果在出队停止信号后、处理停止信号前收到了 SIGCONT,
SIGCONT的prepare_signal()会清除JOBCTL_STOP_PENDING,但停止信号已经不在队列中了。JOBCTL_STOP_DEQUEUED标记告诉do_signal_stop()已经有停止信号被取出。 - POSIX 定时器信号处理:如果取出的是预分配的定时器 sigqueue(
SIGQUEUE_PREALLOC),调用posixtimer_deliver_signal()处理 overrun 计数。如果 overrun 超限,信号被重新入队(goto again)。
14.4.3.2 __dequeue_signal() 与 next_signal()
// kernel/signal.c:603
static int __dequeue_signal(struct sigpending *pending, sigset_t *mask,
kernel_siginfo_t *info, struct sigqueue **timer_sigq)
{
int sig = next_signal(pending, mask);
if (sig)
collect_signal(sig, pending, info, timer_sigq);
return sig;
}
__dequeue_signal() 首先调用 next_signal() 找到优先级最高的可投递信号,然后调用 collect_signal() 从链表中取出对应的 sigqueue 节点。
14.4.3.3 next_signal() -- 信号优先级
next_signal() 定义在 kernel/signal.c:203:
// kernel/signal.c:203
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;
x = *s &~ *m;
if (x) {
if (x & SYNCHRONOUS_MASK)
x &= SYNCHRONOUS_MASK;
sig = ffz(~x) + 1;
return sig;
}
switch (_NSIG_WORDS) {
default:
for (i = 1; i < _NSIG_WORDS; ++i) {
x = *++s &~ *++m;
if (!x)
continue;
sig = ffz(~x) + i*_NSIG_BPW + 1;
break;
}
break;
case 2:
x = s[1] &~ m[1];
if (!x)
break;
sig = ffz(~x) + _NSIG_BPW + 1;
break;
case 1:
break;
}
return sig;
}
next_signal() 的核心逻辑是:在 pending 位图(pending->signal)中找到最低编号的、不在 mask(blocked 掩码)中的信号。
注意第一个字的特殊处理:
if (x & SYNCHRONOUS_MASK)
x &= SYNCHRONOUS_MASK;
SYNCHRONOUS_MASK 定义在 signal.c:199:
#define SYNCHRONOUS_MASK \
(sigmask(SIGSEGV) | sigmask(SIGBUS) | sigmask(SIGILL) | \
sigmask(SIGTRAP) | sigmask(SIGFPE) | sigmask(SIGSYS))
当第一个字中既有同步信号又有异步信号时,优先选择同步信号。但实际上在正常的 dequeue_signal() 调用路径中,同步信号已经被 dequeue_synchronous_signal() 优先处理了,所以这个掩码过滤主要是保护性的。
ffz(~x) 查找 x 中第一个为 1 的位(即编号最小的待处理信号)。由于信号编号从 1 开始,需要加 1。
14.4.3.4 collect_signal() -- sigqueue 节点收集
// kernel/signal.c:553
static void collect_signal(int sig, struct sigpending *list, kernel_siginfo_t *info,
struct sigqueue **timer_sigq)
{
struct sigqueue *q, *first = NULL;
list_for_each_entry(q, &list->list, list) {
if (q->info.si_signo == sig) {
if (first)
goto still_pending;
first = q;
}
}
sigdelset(&list->signal, sig);
if (first) {
still_pending:
list_del_init(&first->list);
copy_siginfo(info, &first->info);
if (unlikely((first->flags & SIGQUEUE_PREALLOC) && (info->si_code == SI_TIMER)))
*timer_sigq = first;
else
__sigqueue_free(first);
} else {
clear_siginfo(info);
info->si_signo = sig;
info->si_errno = 0;
info->si_code = SI_USER;
info->si_pid = 0;
info->si_uid = 0;
}
}
collect_signal() 在 sigpending 的链表中查找匹配 sig 的 sigqueue 节点:
- 查找第一个匹配节点:遍历链表找到第一个
si_signo == sig的节点。 - 检查是否还有更多:如果找到第一个后继续找到第二个,说明该信号还有更多排队(实时信号的 FIFO 队列),跳到
still_pending不清除信号位图。 - 清除位图:如果只找到一个或没有找到,从 pending 位图中清除该信号的位。
- 取出节点:从链表中摘除
sigqueue节点,复制 siginfo。 - 无节点处理:如果链表中没有
sigqueue节点但位图中有该信号(这种情况发生在标准信号的 siginfo 分配失败时),生成一个最小化的 siginfo(si_code = SI_USER, si_pid = 0, si_uid = 0)。
14.4.3.5 dequeue_synchronous_signal() -- 同步信号优先
// kernel/signal.c:668
static int dequeue_synchronous_signal(kernel_siginfo_t *info)
{
struct task_struct *tsk = current;
struct sigpending *pending = &tsk->pending;
struct sigqueue *q, *sync = NULL;
if (!((pending->signal.sig[0] & ~tsk->blocked.sig[0]) & SYNCHRONOUS_MASK))
return 0;
list_for_each_entry(q, &pending->list, list) {
if ((q->info.si_code > SI_USER) &&
(sigmask(q->info.si_signo) & SYNCHRONOUS_MASK)) {
sync = q;
goto next;
}
}
return 0;
next:
list_for_each_entry_continue(q, &pending->list, list) {
if (q->info.si_signo == sync->info.si_signo)
goto still_pending;
}
sigdelset(&pending->signal, sync->info.si_signo);
recalc_sigpending();
still_pending:
list_del_init(&sync->list);
copy_siginfo(info, &sync->info);
__sigqueue_free(sync);
return info->si_signo;
}
dequeue_synchronous_signal() 仅在私有 pending 队列中查找同步信号(si_code > SI_USER 且信号编号在 SYNCHRONOUS_MASK 中)。关键条件 si_code > SI_USER 确保:
SI_USER(0):来自kill()的用户信号,不算同步信号。- 正值(如
SEGV_MAPERR = 1):来自硬件异常,是同步信号。 - 负值(如
SI_KERNEL = -1、SI_QUEUE = -1):来自内核或其他机制,不是同步信号。
同步信号只可能在私有队列中(因为它们是由当前线程的指令触发的),所以此函数只检查 tsk->pending。
14.4.4 信号帧构建 (setup_rt_frame)
当 get_signal() 返回 true 且信号有用户处理函数时,架构相关的代码负责在用户栈上构建信号帧,然后修改内核栈上的 pt_regs 使返回用户态后跳转到信号处理函数。
14.4.4.1 x86_64 架构的信号帧构建入口
// arch/x86/kernel/signal.c:236
static int setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs)
{
rseq_signal_deliver(ksig, regs);
if (is_ia32_frame(ksig)) {
if (ksig->ka.sa.sa_flags & SA_SIGINFO)
return ia32_setup_rt_frame(ksig, regs);
else
return ia32_setup_frame(ksig, regs);
} else if (is_x32_frame(ksig)) {
return x32_setup_rt_frame(ksig, regs);
} else {
return x64_setup_rt_frame(ksig, regs);
}
}
首先调用 rseq_signal_deliver() 处理 restartable sequences 的信号投递回调。然后根据 ABI 类型选择不同的帧构建函数。
14.4.4.2 get_sigframe() -- 信号栈选择
在构建帧之前,需要确定使用哪个栈。get_sigframe() 定义在 arch/x86/kernel/signal.c:93:
// arch/x86/kernel/signal.c:93
void __user *
get_sigframe(struct ksignal *ksig, struct pt_regs *regs, size_t frame_size,
void __user **fpstate)
{
struct k_sigaction *ka = &ksig->ka;
int ia32_frame = is_ia32_frame(ksig);
bool nested_altstack = on_sig_stack(regs->sp);
bool entering_altstack = false;
unsigned long math_size = 0;
unsigned long sp = regs->sp;
/* redzone */
if (!ia32_frame)
sp -= 128;
if (ka->sa.sa_flags & SA_ONSTACK) {
if (sas_ss_flags(sp) == 0) {
sp = current->sas_ss_sp + current->sas_ss_size;
entering_altstack = true;
}
}
栈选择逻辑:
- Red Zone:x86_64 ABI 定义了一个 128 字节的 "red zone",位于栈指针之下,函数可以使用但不修改
%rsp。非 ia32 帧需要跳过这个区域。 - SA_ONSTACK:如果信号处理器设置了
SA_ONSTACK标志且当前不在信号栈上,则使用sigaltstack设置的信号专用栈。sas_ss_flags(sp) == 0表示sp在信号栈范围内且信号栈未禁用。 - 栈对齐:x86_64 要求 16 字节对齐。
sp = fpu__alloc_mathframe(sp, ia32_frame, &buf_fx, &math_size);
*fpstate = (void __user *)sp;
sp -= frame_size;
if (ia32_frame)
sp = ((sp + 4) & -FRAME_ALIGNMENT) - 4;
else
sp = round_down(sp, FRAME_ALIGNMENT) - 8;
分配 FPU 状态保存空间后,减去帧大小并进行对齐。
14.4.4.3 x64_setup_rt_frame() -- 64位信号帧构建
// arch/x86/kernel/signal_64.c:164
int x64_setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs)
{
sigset_t *set = sigmask_to_save();
struct rt_sigframe __user *frame;
void __user *fp = NULL;
unsigned long uc_flags;
if (!(ksig->ka.sa.sa_flags & SA_RESTORER))
return -EFAULT;
frame = get_sigframe(ksig, regs, sizeof(struct rt_sigframe), &fp);
uc_flags = frame_uc_flags(regs);
if (!user_access_begin(frame, sizeof(*frame)))
return -EFAULT;
/* Create the ucontext. */
unsafe_put_user(uc_flags, &frame->uc.uc_flags, Efault);
unsafe_put_user(0, &frame->uc.uc_link, Efault);
unsafe_save_altstack(&frame->uc.uc_stack, regs->sp, Efault);
unsafe_put_user(ksig->ka.sa.sa_restorer, &frame->pretcode, Efault);
unsafe_put_sigcontext(&frame->uc.uc_mcontext, fp, regs, set, Efault);
unsafe_put_sigmask(set, frame, Efault);
user_access_end();
if (ksig->ka.sa.sa_flags & SA_SIGINFO) {
if (copy_siginfo_to_user(&frame->info, &ksig->info))
return -EFAULT;
}
rt_sigframe 结构体在用户栈上的布局如下:
高地址
|---------------------------|
| siginfo_t (可选) | <- frame->info
|---------------------------|
| struct ucontext | <- frame->uc
| uc_flags |
| uc_link |
| uc_stack (sigaltstack) |
| uc_mcontext (sigcontext) |
| 通用寄存器 |
| FPU 状态 |
| uc_sigmask |
|---------------------------|
| pretcode (sa_restorer) | <- frame->pretcode
|---------------------------|
| FPU/XSAVE 缓冲区 |
|---------------------------|
低地址 (frame = sp)
关键操作:
- sigcontext:
unsafe_put_sigcontext()将所有通用寄存器保存到uc_mcontext中。这就是为什么信号处理器返回时可以恢复原始上下文。实现使用unsafe_put_user()批量写入(signal_64.c:99-136):
// arch/x86/kernel/signal_64.c:98
static __always_inline int
__unsafe_setup_sigcontext(struct sigcontext __user *sc, void __user *fpstate,
struct pt_regs *regs, unsigned long mask)
{
unsafe_put_user(regs->di, &sc->di, Efault);
unsafe_put_user(regs->si, &sc->si, Efault);
// ... 所有通用寄存器 ...
unsafe_put_user(regs->ip, &sc->ip, Efault);
unsafe_put_user(regs->flags, &sc->flags, Efault);
unsafe_put_user(regs->cs, &sc->cs, Efault);
unsafe_put_user(regs->ss, &sc->ss, Efault);
unsafe_put_user(fpstate, (unsigned long __user *)&sc->fpstate, Efault);
unsafe_put_user(mask, &sc->oldmask, Efault);
// ...
}
-
sa_restorer:
frame->pretcode被设置为sa_restorer函数的地址。这个函数是 C 运行时库(如 glibc)提供的,负责调用rt_sigreturn系统调用来恢复信号帧。x86_64 要求使用SA_RESTORER。 -
siginfo:如果设置了
SA_SIGINFO,将完整的 siginfo 复制到用户栈帧中,允许信号处理器访问信号的详细信息。
寄存器设置
// arch/x86/kernel/signal_64.c:201
/* Set up registers for signal handler */
regs->di = ksig->sig;
regs->ax = 0;
regs->si = (unsigned long)&frame->info;
regs->dx = (unsigned long)&frame->uc;
regs->ip = (unsigned long) ksig->ka.sa.sa_handler;
regs->sp = (unsigned long)frame;
regs->cs = __USER_CS;
这是信号帧构建的最关键步骤 -- 修改 pt_regs 使返回用户态时跳转到信号处理函数:
| 寄存器 | 值 | 含义 |
|---|---|---|
%rdi |
ksig->sig |
信号编号(第一个参数) |
%rsi |
&frame->info |
siginfo 指针(第二个参数) |
%rdx |
&frame->uc |
ucontext 指针(第三个参数) |
%rip |
sa_handler |
信号处理函数入口 |
%rsp |
frame |
信号帧栈顶 |
%rax |
0 | 系统调用返回值(为0避免误判) |
%cs |
__USER_CS |
用户态代码段 |
信号处理函数的函数签名为 void handler(int sig, siginfo_t *info, void *ucontext)(当设置 SA_SIGINFO 时),或 void handler(int sig)(简单形式)。x86_64 的调用约定使参数通过 %rdi、%rsi、%rdx 传递。
当信号处理函数返回时,执行 sa_restorer,该函数执行 rt_sigreturn 系统调用恢复原始上下文。
14.4.5 信号返回 (rt_sigreturn)
信号处理函数执行完毕后,通过 sa_restorer 调用 rt_sigreturn 系统调用恢复原始上下文。
14.4.5.1 x86_64 rt_sigreturn 实现
// arch/x86/kernel/signal_64.c:246
SYSCALL_DEFINE0(rt_sigreturn)
{
struct pt_regs *regs = current_pt_regs();
struct rt_sigframe __user *frame;
sigset_t set;
unsigned long uc_flags;
prevent_single_step_upon_eretu(regs);
frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long));
if (!access_ok(frame, sizeof(*frame)))
goto badframe;
if (__get_user(*(__u64 *)&set, (__u64 __user *)&frame->uc.uc_sigmask))
goto badframe;
if (__get_user(uc_flags, &frame->uc.uc_flags))
goto badframe;
set_current_blocked(&set);
if (restore_altstack(&frame->uc.uc_stack))
goto badframe;
if (!restore_sigcontext(regs, &frame->uc.uc_mcontext, uc_flags))
goto badframe;
if (restore_signal_shadow_stack())
goto badframe;
return regs->ax;
badframe:
signal_fault(regs, frame, "rt_sigreturn");
return 0;
}
rt_sigreturn 的处理步骤:
- 定位栈帧:
frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long))。因为sa_restorer在调用rt_sigreturn之前将返回地址压栈,%rsp指向返回地址之下,所以减去一个long跳过返回地址找到帧起始。 - 恢复 blocked 掩码:从
frame->uc.uc_sigmask读取信号掩码,通过set_current_blocked()恢复。这包括信号处理器设置期间自动屏蔽的信号(sa_mask+ 信号本身)。 - 恢复 sigaltstack:
restore_altstack()恢复信号专用栈的状态。 - 恢复寄存器上下文:
restore_sigcontext()(signal_64.c:50) 从用户栈帧恢复所有通用寄存器:
// arch/x86/kernel/signal_64.c:50
static bool restore_sigcontext(struct pt_regs *regs,
struct sigcontext __user *usc,
unsigned long uc_flags)
{
struct sigcontext sc;
current->restart_block.fn = do_no_restart_syscall;
if (copy_from_user(&sc, usc, offsetof(struct sigcontext, reserved1)))
return false;
regs->bx = sc.bx;
regs->cx = sc.cx;
// ... 所有通用寄存器 ...
regs->ip = sc.ip;
regs->sp = sc.sp;
regs->cs = sc.cs | 0x03;
regs->ss = sc.ss | 0x03;
regs->flags = (regs->flags & ~FIX_EFLAGS) | (sc.flags & FIX_EFLAGS);
regs->orig_ax = -1;
return fpu__restore_sig((void __user *)sc.fpstate, 0);
}
注意 regs->orig_ax = -1:这确保在从 sigreturn 返回用户态时,内核不会误判为从系统调用返回(从而避免触发系统调用重启逻辑)。
- 恢复返回值:
return regs->ax返回信号处理前的系统调用返回值。
如果任何步骤失败(帧损坏),跳到 badframe,调用 signal_fault() 记录错误信息,然后发送 SIGSEGV。
14.4.6 signal_delivered() -- 投递后处理
// kernel/signal.c:3057
static void signal_delivered(struct ksignal *ksig, int stepping)
{
sigset_t blocked;
clear_restore_sigmask();
sigorsets(&blocked, ¤t->blocked, &ksig->ka.sa.sa_mask);
if (!(ksig->ka.sa.sa_flags & SA_NODEFER))
sigaddset(&blocked, ksig->sig);
set_current_blocked(&blocked);
if (current->sas_ss_flags & SS_AUTODISARM)
sas_ss_reset(current);
if (stepping)
ptrace_notify(SIGTRAP, 0);
}
signal_delivered() 在信号帧成功构建后调用,执行以下操作:
- 清除 restore_sigmask 标志:因为新的信号掩码已经被保存到信号帧中,
sigreturn会恢复它。 - 更新 blocked 掩码:将
sa_mask(信号处理期间要屏蔽的信号集)添加到当前 blocked 掩码中。除非设置了SA_NODEFER,还将信号本身也加入屏蔽集。这防止信号处理器被同信号递归调用。 - SS_AUTODISARM:如果信号栈设置了
SS_AUTODISARM标志,在进入信号处理器后自动解除信号栈,防止嵌套信号处理器使用同一信号栈。 - 单步通知:如果调试器设置了单步执行,通知 ptracer。
// kernel/signal.c:3077
void signal_setup_done(int failed, struct ksignal *ksig, int stepping)
{
if (failed)
force_sigsegv(ksig->sig);
else
signal_delivered(ksig, stepping);
}
signal_setup_done() 由架构代码在 setup_rt_frame() 之后调用。如果帧构建失败(如栈溢出),发送 SIGSEGV。
14.4.7 SIGSTOP/SIGCONT 机制
14.4.7.1 do_signal_stop() -- 组停止
do_signal_stop() 实现了线程组的停止机制,定义在 kernel/signal.c:2550:
// kernel/signal.c:2550
static bool do_signal_stop(int signr)
__releases(¤t->sighand->siglock)
{
struct signal_struct *sig = current->signal;
if (!(current->jobctl & JOBCTL_STOP_PENDING)) {
unsigned long gstop = JOBCTL_STOP_PENDING | JOBCTL_STOP_CONSUME;
struct task_struct *t;
WARN_ON_ONCE(signr & ~JOBCTL_STOP_SIGMASK);
if (!likely(current->jobctl & JOBCTL_STOP_DEQUEUED) ||
unlikely(sig->flags & SIGNAL_GROUP_EXIT) ||
unlikely(sig->group_exec_task))
return false;
if (!(sig->flags & SIGNAL_STOP_STOPPED))
sig->group_exit_code = signr;
sig->group_stop_count = 0;
if (task_set_jobctl_pending(current, signr | gstop))
sig->group_stop_count++;
for_other_threads(current, t) {
if (!task_is_stopped(t) &&
task_set_jobctl_pending(t, signr | gstop)) {
sig->group_stop_count++;
if (likely(!(t->ptrace & PT_SEIZED)))
signal_wake_up(t, 0);
else
ptrace_trap_notify(t);
}
}
}
当第一个线程进入 do_signal_stop() 且没有正在进行的组停止时(!(current->jobctl & JOBCTL_STOP_PENDING)),该线程发起组停止:
- JOBCTL_STOP_DEQUEUED 检查:确认已从队列中取出一个停止信号。
- group_exit_code:记录导致停止的信号编号。
- group_stop_count:初始化为 0,然后遍历所有其他线程,为每个未被停止的线程设置
JOBCTL_STOP_PENDING,并递增计数器。 - 唤醒线程:向每个需要停止的线程发送信号唤醒,使它们进入
get_signal()并最终到达do_signal_stop()。被 ptrace 跟踪的线程使用ptrace_trap_notify()通知调试器。
// kernel/signal.c:2609
if (likely(!current->ptrace)) {
int notify = 0;
if (task_participate_group_stop(current))
notify = CLD_STOPPED;
current->jobctl |= JOBCTL_STOPPED;
set_special_state(TASK_STOPPED);
spin_unlock_irq(¤t->sighand->siglock);
if (notify) {
read_lock(&tasklist_lock);
do_notify_parent_cldstop(current, false, notify);
read_unlock(&tasklist_lock);
}
/* Now we don't run again until woken by SIGCONT or SIGKILL */
schedule();
每个线程到达自己的 do_signal_stop() 后:
- 参与组停止:
task_participate_group_stop()递减group_stop_count。当最后一个线程到达(计数归零)时返回 true,触发CLD_STOPPED通知。 - 设置 TASK_STOPPED:将当前线程设置为 TASK_STOPPED 状态。
- 通知父进程:如果是最后一个停止的线程,发送 SIGCHLD 给父进程。
- 调度让出 CPU:
schedule()切换到其他进程。线程在此睡眠,直到收到 SIGKILL 或 SIGCONT。
14.4.7.2 SIGCONT 唤醒
SIGCONT 的处理在 prepare_signal() 中(见 14.3.4.1 节),主要操作:
- 清除所有 pending 队列中的停止信号。
- 清除所有线程的
JOBCTL_STOP_PENDING。 - 唤醒所有处于
TASK_STOPPED状态的线程(wake_up_state(t, __TASK_STOPPED))。 - 设置
SIGNAL_CLD_CONTINUED标志,使下一个进入get_signal()的线程通知父进程。
被唤醒的线程从 schedule() 返回,重新获取 siglock,跳回 relock 继续处理信号。
14.4.7.3 孤儿进程组
// kernel/signal.c:2974
if (signr != SIGSTOP) {
spin_unlock_irq(&sighand->siglock);
if (is_current_pgrp_orphaned())
goto relock;
spin_lock_irq(&sighand->siglock);
}
POSIX 规定:如果一个进程组成为孤儿进程组(即该进程组中所有成员的父进程要么在同一会话中,要么进程组领导者已经是孤儿),那么除了 SIGSTOP 之外的停止信号应被丢弃。这是因为没有 shell(会话领导者)来恢复被停止的进程,进程将永远无法继续执行。
SIGSTOP 是例外 -- 它可以被来自任何上下文的 SIGCONT 唤醒(例如 kill -CONT),因此即使是孤儿进程组也需要处理 SIGSTOP。
14.4.8 信号处理中的同步
14.4.8.1 sighand->siglock
siglock 是信号子系统最核心的同步原语。它保护以下数据:
sigpending队列(task_struct->pending和signal_struct->shared_pending)blocked/real_blocked信号掩码signal_struct->flags(SIGNAL_GROUP_EXIT 等)signal_struct->curr_targettask_struct->jobctlsighand_struct->action[](信号处理器数组)
siglock 必须在关中断状态下获取(spin_lock_irq / spin_lock_irqsave),因为:
- 定时器中断可能在持有 siglock 的代码路径中触发。
- 硬件异常(如 SIGSEGV)可能在中断上下文中产生信号。
- 嵌套获取 siglock 会导致死锁。
14.4.8.2 siglock 与 tasklist_lock 的层次关系
锁的获取顺序为:
tasklist_lock (读) -> siglock
在 kill_something_info() 中,tasklist_lock 在 siglock 之外获取。在 do_signal_stop() 中,需要通知父进程时,先释放 siglock,再获取 tasklist_lock。这种"释放-重获取"模式避免了死锁。
14.4.8.3 retarget_shared_pending()
当一个线程改变了其 blocked 掩码或退出时,需要将共享队列中原本由它处理的信号重定向到其他线程:
// kernel/signal.c:3090
static void retarget_shared_pending(struct task_struct *tsk, sigset_t *which)
{
sigset_t retarget;
struct task_struct *t;
sigandsets(&retarget, &tsk->signal->shared_pending.signal, which);
if (sigisemptyset(&retarget))
return;
t = tsk;
while_each_thread(tsk, t) {
if (t->flags & PF_EXITING)
continue;
if (wants_signal(sig, t))
signal_wake_up(t, 0);
}
}
这确保共享信号不会因为处理线程退出或改变掩码而丢失。
14.4.9 信号投递完整流程总结
以一个完整场景为例:进程在执行 read() 系统调用时收到 SIGTERM,且注册了信号处理函数。
1. 用户态执行 read() 系统调用
|
2. 硬件中断或另一个 CPU 上的进程调用 kill()
| -> __send_signal_locked() 将 SIGTERM 排入 shared_pending
| -> complete_signal() 选择一个线程
| -> signal_wake_up() 设置 TIF_SIGPENDING 并唤醒
|
3. 被唤醒的线程在返回用户态前检查 TIF_SIGPENDING
| -> arch_do_signal_or_restart()
| -> get_signal()
| -> clear_notify_signal(), task_work_run()
| -> relock: 获取 siglock
| -> for(;;):
| -> 无 SIGNAL_GROUP_EXIT
| -> 无 JOBCTL_STOP_PENDING
| -> dequeue_signal(): 从 shared_pending 取出 SIGTERM
| -> 无 ptrace
| -> sa_handler != SIG_IGN, != SIG_DFL
| -> break (退出循环)
| -> 返回 true (ksig.sig = SIGTERM)
|
4. handle_signal(&ksig, regs)
| -> 系统调用重启: read() 返回 -EINTR (假设无 SA_RESTART)
| -> setup_rt_frame():
| -> get_sigframe(): 在用户栈上分配帧空间
| -> 保存 sigcontext (所有寄存器)
| -> 保存 siginfo (如果 SA_SIGINFO)
| -> 设置 pretcode = sa_restorer
| -> 修改 pt_regs:
| %rdi = SIGTERM (15)
| %rsi = &frame->info
| %rdx = &frame->uc
| %rip = sa_handler
| %rsp = frame
| -> signal_delivered():
| -> 更新 blocked 掩码 (添加 sa_mask + SIGTERM)
|
5. 返回用户态 -> 跳转到 sa_handler
| 用户态执行信号处理函数
|
6. 信号处理函数返回 -> sa_restorer
| -> syscall rt_sigreturn
| -> restore_sigcontext(): 恢复所有寄存器
| -> set_current_blocked(): 恢复 blocked 掩码
| -> 返回 regs->ax (原始 read() 的返回值 -EINTR)
|
7. 返回用户态 -> 继续从 read() 之后执行
| read() 返回 -1, errno = EINTR
这个完整的流程展示了信号从发送到处理再到恢复的每一个步骤,涉及内核态和用户态的多次切换、栈帧的构建与恢复、信号掩码的管理以及系统调用重启机制。所有这些都在内核信号子系统的精确协调下完成,确保了 POSIX 信号语义的正确实现。
14.5 实时信号与可靠信号
POSIX 实时信号(Real-time Signals)是 POSIX.1b(即 POSIX 1003.1b)标准引入的可靠信号机制。与标准信号(信号编号 1-31)不同,实时信号保证排队投递、携带附加数据且按 FIFO 顺序处理。Linux 内核从早期版本就对实时信号提供了完整支持。本节将深入分析实时信号的内核实现机制,包括队列化策略、资源管理、POSIX 定时器信号以及线程组中的信号分发。
14.5.1 实时信号范围与特性
14.5.1.1 信号编号范围
Linux 定义了以下信号范围常量:
// include/uapi/asm-generic/signal.h
#define SIGRTMIN 32 /* 实际上由 C 运行时库调整,通常为 34 */
#define SIGRTMAX _NSIG /* 通常为 64 */
内核内部使用 SIGRTMIN 作为标准信号和实时信号的分界点。标准信号编号为 1-31(sig < SIGRTMIN),实时信号编号为 34-64(sig >= SIGRTMIN)。注意编号 32 和 33 被 glibc 等运行时库内部使用(如用于线程库实现),因此用户可用的实时信号范围通常为 SIGRTMIN(34) 到 SIGRTMAX(64),共 31 个。
14.5.1.2 实时信号的三大特性
与标准信号相比,实时信号具有三个核心特性:
1. 可靠性(排队保证)
标准信号在同一时刻只能有一个实例处于 pending 状态。如果向一个进程连续发送两次 SIGTERM,只有第一个会被投递,第二个会被 legacy_queue() 丢弃。而实时信号的每次发送都会排队一个新的 sigqueue 节点,不会丢失。
2. 顺序保证(FIFO 投递)
多个同编号的实时信号按照发送顺序投递。sigqueue 节点通过 list_add_tail() 添加到链表尾部,collect_signal() 从链表头部取出,天然保证了 FIFO 语义。
3. 数据携带
实时信号可以通过 siginfo_t 的 si_value 字段(类型为 union sigval,可以携带一个 int 或一个 void*)传递附加数据。标准信号虽然也有 siginfo_t,但其数据字段由内核填充,用户无法自定义。
14.5.2 队列化机制
14.5.2.1 legacy_queue() -- 标准信号与实时信号的分水岭
legacy_queue() 是区分标准信号和实时信号队列策略的关键函数,定义在 kernel/signal.c:1037:
// kernel/signal.c:1037
static inline bool legacy_queue(struct sigpending *signals, int sig)
{
return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
}
这个函数的逻辑非常简洁:
- 对于标准信号(
sig < SIGRTMIN):如果该信号已经在 pending 位图中存在(sigismember返回 true),返回 true,表示信号已经排队,不应重复排队。__send_signal_locked()中:
// kernel/signal.c:1062
result = TRACE_SIGNAL_ALREADY_PENDING;
if (legacy_queue(pending, sig))
goto ret;
返回 true 时直接跳到 ret,信号被丢弃(result 为 TRACE_SIGNAL_ALREADY_PENDING)。
- 对于实时信号(
sig >= SIGRTMIN):条件sig < SIGRTMIN为 false,legacy_queue()始终返回 false,不会跳过排队。每次发送都会分配新的sigqueue节点并添加到链表。
14.5.2.2 sigqueue 数据结构
struct sigqueue 是信号排队的核心数据结构,定义在 include/linux/signal_types.h:22:
// include/linux/signal_types.h:22
struct sigqueue {
struct list_head list; // 链入 sigpending.list
int flags; // SIGQUEUE_PREALLOC 等
kernel_siginfo_t info; // 携带的 siginfo
struct ucounts *ucounts; // 所属 user 的资源计数
};
各字段含义:
- list:双向链表节点,链入
struct sigpending的list字段。多个同编号或不同编号的信号按 FIFO 顺序排列。 - flags:
SIGQUEUE_PREALLOC(值为 1)表示这是预分配的 sigqueue,用于 POSIX 定时器。预分配的 sigqueue 在释放时不会被回收到 slab 分配器,而是归还给定时器。 - info:完整的
kernel_siginfo_t结构,包含信号编号、发送者信息、si_code、si_value 等。 - ucounts:per-user 的资源计数器,关联到
RLIMIT_SIGPENDING限制。
struct sigpending 管理一个信号队列:
// include/linux/signal_types.h:32
struct sigpending {
struct list_head list; // sigqueue 节点链表(FIFO 顺序)
sigset_t signal; // 信号位图(哪些编号有待处理信号)
};
signal 位图用于快速判断哪些信号有待处理实例,list 链表保存所有 sigqueue 节点(包括不同编号的信号)。next_signal() 在位图上找到最低编号的待处理信号,collect_signal() 在链表中找到匹配的 sigqueue 节点。
14.5.2.3 sigqueue_alloc() -- 资源受限分配
sigqueue_alloc() 负责分配 sigqueue 节点并进行资源计数检查,定义在 kernel/signal.c:446:
// kernel/signal.c:446
static struct sigqueue *sigqueue_alloc(int sig, struct task_struct *t, gfp_t gfp_flags,
int override_rlimit)
{
struct ucounts *ucounts = sig_get_ucounts(t, sig, override_rlimit);
struct sigqueue *q;
if (!ucounts)
return NULL;
q = kmem_cache_alloc(sigqueue_cachep, gfp_flags);
if (!q) {
dec_rlimit_put_ucounts(ucounts, UCOUNT_RLIMIT_SIGPENDING);
return NULL;
}
__sigqueue_init(q, ucounts, 0);
return q;
}
分配过程:
- sig_get_ucounts():检查并递增 per-user 的 sigpending 计数。如果超出限制且不允许 override,返回 NULL。
- slab 分配:从
sigqueue_cachepslab 缓存中分配 sigqueue 节点。使用GFP_ATOMIC分配标志(因为调用者持有自旋锁)。 - 初始化:
__sigqueue_init()初始化链表节点和 flags。
14.5.2.4 sig_get_ucounts() -- per-user 资源计数
// kernel/signal.c:402
static struct ucounts *sig_get_ucounts(struct task_struct *t, int sig,
int override_rlimit)
{
struct ucounts *ucounts;
long sigpending;
rcu_read_lock();
ucounts = task_ucounts(t);
sigpending = inc_rlimit_get_ucounts(ucounts, UCOUNT_RLIMIT_SIGPENDING,
override_rlimit);
rcu_read_unlock();
if (!sigpending)
return NULL;
return ucounts;
}
sig_get_ucounts() 使用 Linux 的层次化 ucounts 机制进行资源计数:
- task_ucounts(t):获取目标进程所属用户的 ucounts 层次结构。ucounts 按照用户命名空间的层次组织,每个层级有自己的计数限制。
- inc_rlimit_get_ucounts():递增整个层次链上的
UCOUNT_RLIMIT_SIGPENDING计数。如果任何一层的计数超过对应的RLIMIT_SIGPENDING限制,且override_rlimit为 0,则递增失败,返回 0。 - override_rlimit:在
__send_signal_locked()中计算(signal.c:1082-1085):
// kernel/signal.c:1082
if (sig < SIGRTMIN)
override_rlimit = (is_si_special(info) || info->si_code >= 0);
else
override_rlimit = 0;
对于实时信号,override_rlimit 始终为 0,严格遵守 RLIMIT_SIGPENDING 限制。对于标准信号,内核信号和用户信号可以超出限制(因为标准信号不保证可靠性)。
14.5.2.5 __sigqueue_free() -- 释放
// kernel/signal.c:465
static void __sigqueue_free(struct sigqueue *q)
{
if (q->flags & SIGQUEUE_PREALLOC) {
posixtimer_sigqueue_putref(q);
return;
}
if (!q->ucounts)
return;
dec_rlimit_put_ucounts(q->ucounts, UCOUNT_RLIMIT_SIGPENDING);
kmem_cache_free(sigqueue_cachep, q);
}
释放时,预分配的 sigqueue(POSIX 定时器)归还给定时器而非 slab 分配器。普通的 sigqueue 递减 ucounts 计数后释放到 slab。
14.5.3 标准信号 vs 实时信号对比
| 特性 | 标准信号 (1-31) | 实时信号 (34-64) |
|---|---|---|
| 编号范围 | 1 (SIGHUP) - 31 (SIGSYS) | 34 (SIGRTMIN) - 64 (SIGRTMAX) |
| 排队策略 | 同编号最多排队一个 | 同编号可排队多个 |
| 丢失可能 | 是(重复发送时丢弃) | 否(受 RLIMIT_SIGPENDING 限制) |
| 投递顺序 | 无保证(编号优先) | FIFO(同编号按发送顺序) |
| 携带数据 | 内核填充 siginfo | 用户自定义 si_value |
| 资源限制 | 可 override | 严格受限 |
| 典型来源 | kill(), 终端, 内核 | sigqueue(), 定时器, AIO |
| 内核判定 | sig < SIGRTMIN |
sig >= SIGRTMIN |
| legacy_queue() | 可能跳过排队 | 始终排队 |
| 队列溢出 | 静默丢失 siginfo | 返回 -EAGAIN |
14.5.3.1 投递优先级
next_signal() (signal.c:203) 的实现确保了信号按编号顺序投递:
// kernel/signal.c:215
x = *s &~ *m;
if (x) {
if (x & SYNCHRONOUS_MASK)
x &= SYNCHRONOUS_MASK;
sig = ffz(~x) + 1;
return sig;
}
ffz(~x) 找到位图中的第一个 1 位,对应编号最小的待处理信号。这意味着 SIGRTMIN (34) 的优先级低于所有标准信号。在投递时,低编号信号总是先于高编号信号被取出。
对于实时信号,collect_signal() (signal.c:553) 通过遍历链表找到匹配编号的第一个 sigqueue 节点(FIFO 顺序):
// kernel/signal.c:562
list_for_each_entry(q, &list->list, list) {
if (q->info.si_signo == sig) {
if (first)
goto still_pending;
first = q;
}
}
如果链表中有多个同编号的 sigqueue 节点(实时信号的多次排队),只有第一个被取出,后续的同编号节点仍然保留在链表中(still_pending 标签不清除位图中的信号位)。
14.5.4 sigqueue 系统调用
14.5.4.1 rt_sigqueueinfo()
rt_sigqueueinfo() 系统调用允许用户空间发送带自定义 siginfo 的实时信号,定义在 kernel/signal.c:4209:
// kernel/signal.c:4209
SYSCALL_DEFINE3(rt_sigqueueinfo, pid_t, pid, int, sig,
siginfo_t __user *, uinfo)
{
kernel_siginfo_t info;
int ret = __copy_siginfo_from_user(sig, &info, uinfo);
if (unlikely(ret))
return ret;
return do_rt_sigqueueinfo(pid, sig, &info);
}
do_rt_sigqueueinfo() (signal.c:4190) 执行安全检查:
// kernel/signal.c:4190
static int do_rt_sigqueueinfo(pid_t pid, int sig, kernel_siginfo_t *info)
{
if ((info->si_code >= 0 || info->si_code == SI_TKILL) &&
(task_pid_vnr(current) != pid))
return -EPERM;
return kill_proc_info(sig, info, pid);
}
安全检查防止用户空间伪造信号来源:
si_code >= 0:用户空间不能伪造内核信号(内核信号的 si_code 为负值)。si_code == SI_TKILL:不能伪造 tkill 信号。- 例外:给自己发信号时不受限制(
task_pid_vnr(current) == pid)。
用户空间合法的 si_code 值包括 SI_QUEUE(由 sigqueue() 设置)、SI_TIMER(POSIX 定时器)、SI_MESGQ(消息队列通知)、SI_ASYNCIO(异步 I/O)。这些都是负值。
14.5.4.2 rt_tgsigqueueinfo()
线程级别的版本,定义在 kernel/signal.c:4233:
// kernel/signal.c:4233
static int do_rt_tgsigqueueinfo(pid_t tgid, pid_t pid, int sig, kernel_siginfo_t *info)
{
if (pid <= 0 || tgid <= 0)
return -EINVAL;
if ((info->si_code >= 0 || info->si_code == SI_TKILL) &&
(task_pid_vnr(current) != pid))
return -EPERM;
return do_send_specific(tgid, pid, sig, info);
}
rt_tgsigqueueinfo() 通过 do_send_specific() 将信号定向到特定线程(PIDTYPE_PID),而 rt_sigqueueinfo() 通过 kill_proc_info() 发送到线程组(PIDTYPE_TGID)。
14.5.4.3 si_value 字段
实时信号的核心优势之一是 si_value 字段。siginfo_t 中 _sifields._rt 的布局(x86 架构,参见 arch/x86/kernel/signal_64.c:474):
// include/uapi/asm-generic/siginfo.h (简化)
typedef union sigval {
int sival_int;
void __user *sival_ptr;
} sigval_t;
// siginfo_t 中 _sifields._rt 的布局:
// si_pid -- 发送者 PID
// si_uid -- 发送者 UID
// si_value -- union sigval (用户自定义数据)
union sigval 允许用户空间传递一个 32 位整数或一个指针。内核在 copy_siginfo() 时原样复制这个字段,不做解释。
glibc 的 sigqueue() 函数封装了 rt_sigqueueinfo():
// glibc 封装 (示意)
int sigqueue(pid_t pid, int sig, const union sigval value) {
siginfo_t info;
memset(&info, 0, sizeof(info));
info.si_signo = sig;
info.si_code = SI_QUEUE;
info.si_value = value;
return syscall(SYS_rt_sigqueueinfo, pid, sig, &info);
}
14.5.5 信号队列资源管理
14.5.5.1 RLIMIT_SIGPENDING
RLIMIT_SIGPENDING 限制每个用户(per-user)可以排队的 sigqueue 节点数量。这个限制通过层次化的 ucounts 机制实现。
层次化计数的含义:在一个用户命名空间层次中(如 root namespace -> container namespace),每个层级都有自己的计数和限制。当递增计数时(inc_rlimit_get_ucounts()),从最内层到最外层逐层检查。只有所有层级的计数都在限制之内,才能成功分配。
User Namespace Hierarchy:
init_user_ns (RLIMIT_SIGPENDING = 1000)
|
+-- container_user_ns (RLIMIT_SIGPENDING = 100)
|
sigqueue_alloc() 时逐层递增检查
这种设计确保一个容器中的进程不会耗尽宿主机的 sigqueue 资源。
14.5.5.2 队列溢出处理
当 sigqueue 分配失败时的处理路径在 __send_signal_locked() (signal.c:1117):
// kernel/signal.c:1117
} else if (!is_si_special(info) &&
sig >= SIGRTMIN && info->si_code != SI_USER) {
result = TRACE_SIGNAL_OVERFLOW_FAIL;
ret = -EAGAIN;
goto ret;
} else {
result = TRACE_SIGNAL_LOSE_INFO;
}
三种失败模式的处理策略:
| 信号类型 | 发送方式 | 失败处理 |
|---|---|---|
| 实时信号 | sigqueue (非 kill) | 返回 -EAGAIN |
| 实时信号 | kill() (SI_USER) | 静默丢失 siginfo,信号仍 pending |
| 标准信号 | 任何方式 | 静默丢失 siginfo,信号仍 pending |
对于通过 rt_sigqueueinfo() 发送的实时信号,队列溢出返回 -EAGAIN 给用户空间,符合 POSIX 规范。对于通过 kill() 发送的实时信号,POSIX 允许实现选择是否排队 siginfo,Linux 选择在溢出时不排队 siginfo 但仍然标记信号为 pending(因为 kill() 不允许失败)。
14.5.5.3 SIGQUEUE_PREALLOC -- 预分配机制
POSIX 定时器使用预分配的 sigqueue 节点,避免在定时器到期时因内存不足而无法发送信号。预分配在定时器创建时完成:
// kernel/signal.c:1932
bool posixtimer_init_sigqueue(struct sigqueue *q)
{
struct ucounts *ucounts = sig_get_ucounts(current, -1, 0);
if (!ucounts)
return false;
clear_siginfo(&q->info);
__sigqueue_init(q, ucounts, SIGQUEUE_PREALLOC);
return true;
}
预分配的 sigqueue 在 __sigqueue_free() 中不会被释放到 slab,而是通过引用计数管理:
// kernel/signal.c:465
static void __sigqueue_free(struct sigqueue *q)
{
if (q->flags & SIGQUEUE_PREALLOC) {
posixtimer_sigqueue_putref(q);
return;
}
// ...
}
14.5.6 POSIX 定时器信号
14.5.6.1 定时器信号的发送
POSIX 定时器到期时通过信号通知进程。posixtimer_queue_sigqueue() (signal.c:1943) 负责将预分配的 sigqueue 入队:
// kernel/signal.c:1943
static void posixtimer_queue_sigqueue(struct sigqueue *q, struct task_struct *t, enum pid_type type)
{
struct sigpending *pending;
int sig = q->info.si_signo;
signalfd_notify(t, sig);
pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
list_add_tail(&q->list, &pending->list);
sigaddset(&pending->signal, sig);
complete_signal(sig, t, type);
}
定时器信号的特殊之处:
- 预分配 sigqueue:不经过
sigqueue_alloc(),直接使用定时器创建时预分配的节点。 - si_code = SI_TIMER:标识这是定时器信号。
- si_overrun:如果定时器在信号被投递之前多次到期,overrun 计数器递增。
posixtimer_deliver_signal()在信号投递时处理 overrun:
// kernel/signal.c:659 (在 dequeue_signal 中)
if (IS_ENABLED(CONFIG_POSIX_TIMERS) && unlikely(timer_sigq)) {
if (!posixtimer_deliver_signal(info, timer_sigq))
goto again;
}
如果 overrun 计数超过 DELAYTIMER_MAX,信号会被重新入队(goto again),确保不丢失定时器事件。
14.5.6.2 定时器 siginfo 字段
POSIX 定时器信号的 siginfo 包含以下关键字段(参见 arch/x86/kernel/signal_64.c:468):
si_signo = 配置的通知信号
si_code = SI_TIMER
si_tid = 定时器 ID
si_overrun = 累积的 overrun 计数
si_value = timer_create() 时设置的 sigevent.sigev_value
14.5.6.3 SIGEV_SIGNAL 与 SIGEV_THREAD_ID
定时器创建时通过 sigevent 结构指定通知方式:
- SIGEV_SIGNAL:到期时发送信号到进程(通过
shared_pending)。 - SIGEV_THREAD_ID:到期时发送信号到指定线程(通过私有
pending),type为PIDTYPE_PID。这个扩展是 Linux 特有的,被timer_create()的SIGEV_THREAD_ID模式使用。
14.5.7 异步 IO 信号
14.5.7.1 kill_pid_usb_asyncio()
USB 异步 I/O 完成时使用信号通知进程,kill_pid_usb_asyncio() 定义在 kernel/signal.c:1521:
// kernel/signal.c:1521
int kill_pid_usb_asyncio(int sig, int errno, sigval_t addr,
struct pid *pid, const struct cred *cred)
{
struct kernel_siginfo info;
struct task_struct *p;
unsigned long flags;
int ret = -EINVAL;
if (!valid_signal(sig))
return ret;
clear_siginfo(&info);
info.si_signo = sig;
info.si_errno = errno;
info.si_code = SI_ASYNCIO;
*((sigval_t *)&info.si_pid) = addr;
注意这里的一个微妙之处:si_value(union sigval)通过 *((sigval_t *)&info->si_pid) 写入。这是因为在 siginfo_t 的 SIL_RT 布局中,si_pid/si_uid 和 si_value 位于相同的偏移位置(通过 union 重叠)。内核注释中(signal.c:1506-1518)解释了这种做法的原因:在 64 位大端序内核 + 32 位用户空间的组合中,需要确保 32 位地址被正确编码。
rcu_read_lock();
p = pid_task(pid, PIDTYPE_PID);
if (!p) {
ret = -ESRCH;
goto out_unlock;
}
if (!kill_as_cred_perm(cred, p)) {
ret = -EPERM;
goto out_unlock;
}
ret = security_task_kill(p, &info, sig, cred);
if (ret)
goto out_unlock;
if (sig) {
if (lock_task_sighand(p, &flags)) {
ret = __send_signal_locked(sig, &info, p, PIDTYPE_TGID, false);
unlock_task_sighand(p, &flags);
} else
ret = -ESRCH;
}
out_unlock:
rcu_read_unlock();
return ret;
}
kill_pid_usb_asyncio() 直接调用 __send_signal_locked() 而非 group_send_sig_info(),因为它绕过了 check_kill_permission()(使用自己的凭证检查 kill_as_cred_perm())和 send_signal_locked()(不做 PID namespace 转换)。信号类型为 PIDTYPE_TGID(进入共享队列),force 为 false。
14.5.7.2 si_code 来源标识
不同的异步事件使用不同的 si_code 标识来源:
| si_code | 来源 | siginfo 字段 |
|---|---|---|
SI_ASYNCIO |
异步 I/O 完成 | si_value, si_errno |
SI_TIMER |
POSIX 定时器到期 | si_tid, si_overrun, si_value |
SI_MESGQ |
消息队列通知 | si_value |
SI_QUEUE |
用户 sigqueue() | si_value |
14.5.8 信号与线程
14.5.8.1 线程组信号分发 -- curr_target 轮转
当信号被发送到线程组(PIDTYPE_TGID 或更高级别的 type)时,complete_signal() (signal.c:963) 负责选择一个线程来处理信号。选择策略是轮转(round-robin):
// kernel/signal.c:985
t = signal->curr_target;
while (!wants_signal(sig, t)) {
t = next_thread(t);
if (t == signal->curr_target)
return;
}
signal->curr_target = t;
signal->curr_target 记录了上次被选中处理共享信号的线程。下次共享信号到来时,从 curr_target 的下一个线程开始搜索。这确保共享信号不会总是由同一个线程处理,实现了负载均衡。
但 wants_signal() (signal.c:946) 限制了候选线程的范围:
- 信号被阻塞的线程不参与选择。
- 正在退出的线程不参与选择。
- 被停止或被 ptrace 跟踪的线程不参与选择(SIGKILL 除外)。
- 优先选择正在运行的线程(
task_curr(p))或没有其他待处理信号的线程。
如果所有线程都不愿意接收信号,信号仍然被添加到 shared_pending 的位图和链表中。当某个线程解除阻塞或从停止状态恢复时,会在 dequeue_signal() 中从共享队列取出该信号。
14.5.8.2 pthread_kill -- tkill/tgkill
用户空间的 pthread_kill() 函数通过 tgkill() 系统调用向特定线程发送信号:
// kernel/signal.c:4165
SYSCALL_DEFINE3(tgkill, pid_t, tgid, pid_t, pid, int, sig)
{
if (pid <= 0 || tgid <= 0)
return -EINVAL;
return do_tkill(tgid, pid, sig);
}
// kernel/signal.c:4146
static int do_tkill(pid_t tgid, pid_t pid, int sig)
{
struct kernel_siginfo info;
prepare_kill_siginfo(sig, &info, PIDTYPE_PID);
return do_send_specific(tgid, pid, sig, &info);
}
pthread_kill() 发送的信号类型为 PIDTYPE_PID,进入目标线程的私有 pending 队列。si_code 设为 SI_TKILL。由于信号在私有队列中,只有目标线程会处理该信号,不会被其他线程抢先处理。
tgid 参数确保了线程标识的准确性:即使目标线程 ID(pid)被新线程复用,只要线程组 ID(tgid)不匹配,信号就不会发送到错误的线程。
14.5.8.3 pthread_sigmask -- 各线程独立 blocked 掩码
每个线程(task_struct)有独立的 blocked 和 real_blocked 信号掩码:
// include/linux/sched.h (task_struct 中的字段)
sigset_t blocked; // 当前阻塞的信号集
sigset_t real_blocked; // sigtimedwait 临时解除阻塞前的原始掩码
pthread_sigmask() 通过 rt_sigprocmask() 系统调用修改当前线程的 blocked 掩码。这意味着不同线程可以阻塞不同的信号。
信号掩码修改的关键实现在 set_current_blocked() 中:
// kernel/signal.c:2997 (在 signal.c 中的实现)
void set_current_blocked(sigset_t *newset)
{
struct task_struct *tsk = current;
spin_lock_irq(&tsk->sighand->siglock);
__set_task_blocked(tsk, newset);
spin_unlock_irq(&tsk->sighand->siglock);
}
__set_task_blocked() 更新 blocked 掩码后,检查是否有新解除阻塞的信号需要投递:
// kernel/signal.c 中
void __set_task_blocked(struct task_struct *tsk, const sigset_t *newset)
{
struct sigpending *pending = &tsk->pending;
struct sigpending *shared = &tsk->signal->shared_pending;
// 更新掩码
tsk->blocked = *newset;
// 检查是否有新解锁的信号
// 如果有,设置 TIF_SIGPENDING 使返回用户态前处理
recalc_sigpending();
}
14.5.8.4 sigaltstack -- 每线程信号栈
每个线程可以设置独立的信号备用栈(sigaltstack):
// include/linux/sched.h (task_struct 中的字段)
void __user *sas_ss_sp; // 信号栈基地址
size_t sas_ss_size; // 信号栈大小
unsigned int sas_ss_flags; // SS_ONSTACK, SS_DISABLE, SS_AUTODISARM
当信号处理器设置了 SA_ONSTACK 标志且当前不在信号栈上时,内核将使用 sas_ss_sp 和 sas_ss_size 指定的区域作为信号帧的栈。这在以下场景中特别重要:
- 栈溢出处理:当用户栈耗尽时(如无限递归),SIGSEGV 处理器需要在不同的栈上运行,否则无法执行。
- 实时信号安全:实时信号的处理函数可能需要保证不被栈空间不足影响。
SS_AUTODISARM 标志(Linux 4.11+)允许信号栈在进入信号处理器后自动解除,防止嵌套信号处理器使用同一信号栈。在 signal_delivered() (signal.c:3071) 中:
// kernel/signal.c:3071
if (current->sas_ss_flags & SS_AUTODISARM)
sas_ss_reset(current);
14.5.9 实时信号使用示例
14.5.9.1 发送实时信号
// 用户态示例 (简化)
#include <signal.h>
#include <unistd.h>
void handler(int sig, siginfo_t *si, void *ctx) {
printf("Signal %d, value: %d\n", sig, si->si_value.sival_int);
}
int main() {
struct sigaction sa = {
.sa_sigaction = handler,
.sa_flags = SA_SIGINFO, // 使用三参数处理函数
};
sigaction(SIGRTMIN, &sa, NULL);
union sigval value;
value.sival_int = 42;
sigqueue(getpid(), SIGRTMIN, value); // 发送带数据的实时信号
pause();
return 0;
}
内核路径:sigqueue() -> rt_sigqueueinfo() -> do_rt_sigqueueinfo() -> kill_proc_info() -> kill_pid_info() -> group_send_sig_info() -> do_send_sig_info() -> send_signal_locked() -> __send_signal_locked()。
在 __send_signal_locked() 中,因为 sig >= SIGRTMIN:
- legacy_queue() 返回 false,不跳过排队。
- override_rlimit 为 0,严格遵守资源限制。
- 分配 sigqueue 并通过 copy_siginfo() 复制用户提供的 siginfo(包括 si_value)。
14.5.9.2 多个实时信号的排队与投递
// 连续发送三个同编号实时信号
union sigval v1 = {.sival_int = 1};
union sigval v2 = {.sival_int = 2};
union sigval v3 = {.sival_int = 3};
sigqueue(pid, SIGRTMIN, v1);
sigqueue(pid, SIGRTMIN, v2);
sigqueue(pid, SIGRTMIN, v3);
在内核中,三次 sigqueue_alloc() 创建三个 sigqueue 节点,通过 list_add_tail() 按顺序链入 shared_pending.list。shared_pending.signal 位图中 SIGRTMIN 的位被设置。
投递时,next_signal() 找到 SIGRTMIN,collect_signal() 从链表头部取出第一个 sigqueue(值为 1)。由于链表中还有同编号的节点,不清除位图。下次 dequeue_signal() 时再次取出 SIGRTMIN 的第二个实例(值为 2),依此类推。
14.5.9.3 实时信号与标准信号的交错
如果 pending 队列中同时有 SIGTERM 和 SIGRTMIN:
next_signal()找到编号最小的 SIGTERM(15 < 34),先投递 SIGTERM。- SIGTERM 处理完成后(如果是用户处理函数),恢复上下文。
- 因为
TIF_SIGPENDING仍然被设置(SIGRTMIN 待处理),再次进入get_signal()。 dequeue_signal()取出 SIGRTMIN。
这确保了低编号信号总是先于高编号信号被处理,无论它们是标准信号还是实时信号。
14.5.10 实时信号内核实现总结
数据结构关系
task_struct
|
+-- pending (struct sigpending) // 私有信号队列
| +-- signal (sigset_t 位图) // 哪些信号待处理
| +-- list (sigqueue 链表) // FIFO 排队的 sigqueue 节点
|
+-- signal (struct signal_struct)
| +-- shared_pending (struct sigpending) // 共享信号队列
| | +-- signal (sigset_t 位图)
| | +-- list (sigqueue 链表)
| |
| +-- curr_target (task_struct *) // 轮转分发目标
| +-- flags (SIGNAL_GROUP_EXIT 等)
|
+-- blocked (sigset_t) // 阻塞掩码
+-- sas_ss_sp / sas_ss_size // 信号栈
sigqueue (slab 分配)
|
+-- list (链入 sigpending.list)
+-- flags (SIGQUEUE_PREALLOC?)
+-- info (kernel_siginfo_t)
| +-- si_signo // 信号编号
| +-- si_code // SI_USER/SI_KERNEL/SI_QUEUE/SI_TIMER/...
| +-- si_pid // 发送者 PID
| +-- si_uid // 发送者 UID
| +-- si_value // 用户自定义数据 (实时信号)
+-- ucounts (per-user 计数, 关联 RLIMIT_SIGPENDING)
发送路径总结
用户空间 sigqueue(pid, sig, value)
|
v
rt_sigqueueinfo() // signal.c:4209
| copy_siginfo_from_user()
| 安全检查: si_code >= 0 -> -EPERM
v
do_rt_sigqueueinfo() // signal.c:4190
|
v
kill_proc_info() // signal.c:1476
|
v
kill_pid_info() // signal.c:1471
| type = PIDTYPE_TGID
v
group_send_sig_info() // signal.c:1409
| check_kill_permission()
v
do_send_sig_info() // signal.c:1262
| lock_task_sighand()
v
send_signal_locked() // signal.c:1183
| PID namespace 处理
v
__send_signal_locked() // signal.c:1042
| prepare_signal() // 871
| legacy_queue() -> false (实时信号) // 1037
| override_rlimit = 0 (实时信号) // 1085
| sigqueue_alloc() // 446
| sig_get_ucounts() // 402: RLIMIT_SIGPENDING 检查
| kmem_cache_alloc()
| copy_siginfo() (包含 si_value) // 1114
| sigaddset() // 1137
| complete_signal() // 1153: 选择目标线程
| wants_signal() // 946
| signal_wake_up() // 1033: TIF_SIGPENDING + 唤醒
投递路径总结
TIF_SIGPENDING 被检测 (返回用户态前)
|
v
arch_do_signal_or_restart() // arch/x86/kernel/signal.c:333
|
v
get_signal() // kernel/signal.c:2800
| for(;;):
| dequeue_signal() // 618
| __dequeue_signal() (私有 -> 共享)
| next_signal() // 203: 位图找最低编号
| collect_signal() // 553: 链表取 sigqueue (FIFO)
| break (用户处理函数)
|
v
handle_signal() // arch/x86/kernel/signal.c:254
| setup_rt_frame() // 236
| get_sigframe() // 93: 选择信号栈
| x64_setup_rt_frame() // signal_64.c:164
| 保存 sigcontext, siginfo, ucontext
| 修改 pt_regs -> sa_handler
| signal_delivered() // 3057: 更新 blocked 掩码
实时信号机制的内核实现体现了 Linux 在性能、可靠性和 POSIX 兼容性之间的精巧平衡。sigqueue 节点的 slab 分配、层次化 ucounts 资源限制、FIFO 链表队列以及线程组轮转分发等设计,共同构建了一个既高效又可靠的信号传递系统。