Linux内核分析之进程间通信-01
This language version is unavailable; showing the other language.
27.1 信号处理全链路
信号最具欺骗性的地方在于:kill() 返回时什么都没发生,handler 却"稍后"执行了。本节沿时间轴把这个"稍后"拆开——信号从挂起集到 handler 入口要经过返回用户态边界这道唯一的闸门,而系统调用重启语义是这道闸门上最精巧的协商机制。
27.1.1 时间轴:为什么不立即投递
线程时间轴与投递时机:
kill() 执行 (另一个线程/CPU)
└─ __send_signal: 目标 pending 挂起集置位
+ set_tsk_thread_flag(t, TIF_SIGPENDING) (14.3 节)
[此刻目标线程可能: 在跑/在睡/在内核临界区]
目标线程的下一个"返回用户态边界":
系统调用返回 / 异常返回 / 中断返回
→ exit_to_user_mode_loop 检查 thread_info 标志
→ 见 _TIF_SIGPENDING → 投递
为什么必须等边界:内核态代码持锁、操作临界结构,若在任意点被"劫持"进 handler,handler 的返回路径(sigreturn 恢复寄存器)会摧毁内核现场。边界投递把异步事件转换成"内核与用户态的唯一接口处"的同步检查——代价是延迟不确定(目标线程长时间不回用户态则信号延迟),收益是内核态绝对安全。睡眠中的线程被信号唤醒(signal_wake_up),也是先退出睡眠回到边界再投递。
27.1.2 闸门:exit_to_user_mode_loop
// kernel/entry/common.c:63-64
if (ti_work & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
arch_do_signal_or_restart(regs);
// arch/x86/kernel/signal.c:333-340(架构入口)
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;
}
...
}
_TIF_SIGPENDING(经典信号)与 _TIF_NOTIFY_SIGNAL(任务工作等借道通知,32 章关联)共享这条分支。get_signal()(kernel/signal.c:2799)在 sighand->siglock 保护下 dequeue_signal()(:618)——从私有挂起集与共享实时队列按编号序取出一个(14.5 节的可靠信号语义),返回非零表示"有 handler 或默认动作待执行"。
一个边界只投递一个信号:handle_signal 完成后直接返回用户态(执行 handler)——挂起集里剩下的信号等 handler 结束、sigreturn、再下一次边界。这是"信号处理期间又来信号"时序的根源(14 章语义面的机制基础)。
27.1.3 系统调用重启:与信号的协商
被信号打断的慢速系统调用(read/write/poll 睡在等待队列上)返回什么?这由一组内部错误码与 SA_RESTART 标志协商:
// arch/x86/kernel/signal.c:262-283(handle_signal 内, 有 handler 时)
/* 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; /* 永不重启: 报 EINTR */
break;
case -ERESTARTSYS:
if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {
regs->ax = -EINTR; /* 无 SA_RESTART: EINTR */
break;
}
fallthrough;
case -ERESTARTNOINTR:
regs->ax = regs->orig_ax; /* 恢复调用号 */
regs->ip -= 2; /* 回退到 syscall 指令 */
break;
}
}
// arch/x86/kernel/signal.c:341-357(无 handler 时, arch_do_signal 内)
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;
}
}
regs->ip -= 2 是全部魔法的所在:x86_64 的 syscall 指令 2 字节长,回退 IP 让 CPU 返回用户态后重新执行同一系统调用——硬件视角下这次调用"从未发生"。四种错误码的语义矩阵:
| 错误码 | 有 handler | 无 handler / SA_RESTART |
|---|---|---|
ERESTARTNOINTR |
自动重启 | 自动重启("中断不报告") |
ERESTARTSYS |
SA_RESTART→重启;否则 EINTR |
自动重启 |
ERESTARTNOHAND |
EINTR | 自动重启("仅无 handler 时重启") |
ERESTART_RESTARTBLOCK |
EINTR | 换成 restart_syscall 重启(带剩余时间重算——nanosleep 的实现) |
用户可见的结论:"write 返回 EINTR"意味着 handler 未注册 SA_RESTART;glibc 把多数 syscall 包装成"EINTR 自动重试"(TEMP_FAILURE_RETRY)来抹平这一层。信号与系统调用的所有坑——半读数据、部分写、EINTR 重试、nanosleep 剩余时间——都收敛在这四行 switch 上。
27.1.4 handler 注册的检查与无 handler 的默认路径
get_signal() 内(signal.c:2799 起)的裁决序:
dequeue_signal 取出 signr
│
├─ ptrace 停留检查 (被跟踪则先停给调试器看)
├─ sigaction[signr] == SIG_IGN? → 丢弃, 继续 deque 下一个
├─ sigaction[signr] == SIG_DFL?
│ 默认动作分四类:
│ 终止(TERM/CORE): do_group_exit — 14.1 节 do_exit 链
│ 停止(STOP/TSTP): do_signal_stop — 全线程组停车
│ 继续(CONT): prepare_signal 唤醒停车者
│ 忽略(CHLD): 丢弃 (SIGCHLD 默认忽略)
└─ 有 handler: 填充 ksignal{sig, info, ka}, 返回 true
→ arch_do_signal_or_restart 走 handle_signal (27.2 节搭帧)
SIGCHLD 默认忽略的特殊性:它不是 SIG_IGN 而是默认行为忽略——区别在"投递后丢弃"与"根本不入队"。zombie 的收割责任由此落在 wait 族而非信号(14.1 节 do_exit 的 EXIT_ZOMBIE 呼应)。
27.1.5 同步信号:进程自己制造的异常
以上时间轴假设信号来自他者;SIGSEGV/SIGFPE/SIGILL 来自当前指令的异常——它们的投递路径更直接:异常处理程序(如 do_invalid_op)调 force_sig_fault() 挂起后直接走最近的返回用户态边界。这带来一个不可协商的约束:handler 执行时"肇事指令不会重试"——除非 handler 自己修复现场并返回(SIGSEGV handler 里改 ucontext 的 ip,JVM/游戏引擎的热补丁技巧)。si_addr/si_code 由异常处理器填好放进 ksig->info(27.2 节随帧写入用户栈)。
小结
信号全链路的骨架是"一次挂起 + 一个边界":发送只做挂起与标志置位,真正的裁决与搭建被强制推迟到返回用户态的 exit_to_user_mode_loop 分支,get_signal 出队、handle_signal 搭帧后直接进入 handler——一个边界只投一个信号。系统调用重启用 regs->ip -= 2 的"时间回退"实现四种 ERESTART 语义与 SA_RESTART 的协商,所有 EINTR 类用户态坑都在这个 switch 上收敛。下一节进入 handler 落地的那一帧内存——rt_sigframe 的布局与栈切换细节。
27.2 信号帧与栈切换
handler 能像普通函数一样被调用、还能 sigreturn 回到断点,依赖内核在用户栈上伪造的一整块现场——rt_sigframe。本节逐字节拆解这块内存的布局、栈指针的选址算法(红区、备用栈、FPU 区)、返回蹦床(SA_RESTORER)、以及 CET 影子栈对这一"凭空制造调用"的校验配合。
27.2.1 rt_sigframe 布局
// arch/x86/include/asm/sigframe.h:59-65
struct rt_sigframe {
char __user *pretcode; /* 返回蹦床地址 */
struct ucontext uc; /* ucontext: 寄存器现场+掩码 */
struct siginfo info; /* si_* 结构 (SA_SIGINFO) */
/* fp state follows here */ /* XSAVE 区跟在后面 */
};
用户栈上的 rt_sigframe (handler 执行时的内存视图, 地址向下增长):
更早的栈内容 (被信号打断前的现场)
┌──────────────────────────┐ ← sp 原值
│ FPU/XSAVE 区 (fpstate) │ 27.2.3 节选址算法放在最上
├──────────────────────────┤
│ struct siginfo info │ si_signo/si_code/si_addr...
│ ucontext uc: │
│ uc_flags / uc_link │
│ uc_stack (信号处理期间的栈描述)
│ uc_mcontext (sigcontext: 全部通用寄存器+RIP+RFLAGS)
│ uc_sigmask │ handler 期间的信号掩码
├──────────────────────────┤
│ pretcode → sa_restorer │ 栈顶: 返回地址位置
└──────────────────────────┘ ← regs->sp 新值
[ handler 从这里开始执行 ]
读取现场的三方: handler 第二三参数 = &frame->info, &frame->uc
sigreturn 时内核从 uc.uc_mcontext 恢复寄存器
ucontext 是 glibc getcontext/ucontext_t 的内核侧孪生——handler 里 (ucontext_t*)uctx->uc_mcontext.gregs[REG_RIP] 改写返回点(25.1.6 节 SIGSEGV 热修复技巧)操作的就是这块内存。
27.2.2 选址:get_sigframe 的栈算法
// arch/x86/kernel/signal.c:94-140(节选)
get_sigframe(struct ksignal *ksig, struct pt_regs *regs, size_t frame_size,
void __user **fpstate)
{
struct k_sigaction *ka = &ksig->ka;
/* Default to using normal stack */
bool nested_altstack = on_sig_stack(regs->sp);
bool entering_altstack = false;
unsigned long sp = regs->sp;
...
/* redzone */
if (!ia32_frame)
sp -= 128; /* :104 AMD64 ABI 红区 */
/* This is the X/Open sanctioned signal stack switching. */
if (ka->sa.sa_flags & SA_ONSTACK) { /* :107 */
if (sas_ss_flags(sp) == 0) {
sp = current->sas_ss_sp + current->sas_ss_size;
entering_altstack = true; /* 切到 sigaltstack */
}
} else if (ia32_frame && ...) {
/* legacy signal stack switching (i386 时代 restorer 段) */
}
sp = fpu__alloc_mathframe(sp, ia32_frame, &buf_fx, &math_size);
*fpstate = (void __user *)sp; /* :125 FPU 区顶部 */
sp -= frame_size; /* :128 再让出 rt_sigframe */
...
/* i386 ABI 对齐 / 64 位 16 字节对齐 */
}
选址的四步让位序列:红区 128 字节(AMD64 ABI 允许 leaf 函数在 sp 之下无告知写 128B——信号帧必须避开,否则踩坏被打断函数的红区)→ SA_ONSTACK 切换(sas_ss_flags 判定当前 sp 是否已在备用栈内——嵌套信号处理时不再切,SS_AUTODISARM 由 sigaltstack 的标志位支持)→ FPU 区(XSAVE 状态放在帧尾上方,sigreturn 时 xrstor 恢复——AVX-512 时代这块可超 2KB,27.2.4 节动态 MINSIGSTKSZ 的动因)→ 帧本体。栈溢出保护(access_ok 检查、失败走 signal_fault,signal.c:366-383 强杀 SIGSEGV)保证"栈已爆"时信号自身不再二次爆栈。
27.2.3 帧填充与寄存器改造:x64_setup_rt_frame
// arch/x86/kernel/signal_64.c:164-230(节选)
int x64_setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs)
{
sigset_t *set = sigmask_to_save();
struct rt_sigframe __user *frame;
...
/* x86-64 should always use SA_RESTORER. */
if (!(ksig->ka.sa.sa_flags & SA_RESTORER))
return -EFAULT; /* :171 蹦床必须存在 */
frame = get_sigframe(ksig, regs, sizeof(struct rt_sigframe), &fp);
...
/* 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);
/* Set up to return from userspace. If provided, use a stub
already in userspace. */
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;
}
if (setup_signal_shadow_stack(ksig)) /* :221 CET */
return -EFAULT;
/* Set up registers for signal handler */
regs->di = ksig->sig; /* 第 1 参: 信号号 */
/* In case the signal handler was declared without prototypes */
regs->ax = 0;
/* This also works for non SA_SIGINFO handlers because they expect
the next argument after the signal number on the stack. */
regs->si = (unsigned long)&frame->info; /* 第 2 参: siginfo */
regs->dx = (unsigned long)&frame->uc; /* 第 3 参: ucontext */
regs->ip = (unsigned long) ksig->ka.sa.sa_handler;
regs->sp = (unsigned long)frame;
...
}
五点机制:
- SA_RESTORER 强制(:171):
pretcode必须指向 glibc 提供的蹦床——sa_restorer内是mov $SYS_rt_sigreturn, %rax; syscall。早期内核曾用栈上可执行蹦床(restore_rt代码直接写在栈上),W^X 时代全部改为调用用户态固定蹦床; - unsafe_put_user 批量写:
user_access_begin/end包裹的 SMAP 屏障区(1.6 节 STAC/CLAC),一次性写完帧免得反复开关; - handler 的参数即帧:
si=&frame->info、dx=&frame->uc——SA_SIGINFOhandler 的两参就是用户栈上帧内的地址,非 SIGINFO handler 也能从栈上"意外"读到它们(注释原文的兼容说明); - 寄存器改造:
ip=handler, sp=frame——返回用户态的最后一刻,被打断的现场整体压进uc_mcontext,CPU 视角下"handler 是被 sigreturn 之前的某个神秘调用者调用的"; - 影子栈联动(:221):CET shadow stack 要求 call/ret 配对,信号这种"跳转不走 call"的机制会触发影子栈违例——
setup_signal_shadow_stack在影子栈上同步压入pretcode,sigreturn时再弹出,让硬件视角的调用链自洽(1.6 节 CET 加固的信号适配层)。
27.2.4 sigreturn:现场的回滚
handler 返回 → ret 到 pretcode (sa_restorer)
→ mov $rt_sigreturn 号, %rax; syscall
→ 内核 sys_rt_sigreturn:
1. 取用户 sp, 定位 rt_sigframe (sp+8 跳过 ret 槽)
2. restore_sigcontext: 从 uc.uc_mcontext 恢复全部寄存器
(含 RIP — 回到被打断的那条指令)
3. 恢复信号掩码 = uc.uc_sigmask
4. xrstor FPU 区 (fpstate)
5. 影子栈弹出 (与 setup 对称)
→ 一切如信号从未发生 (挂起的其余信号等下一个边界, 27.1.2)
安全要点:sigreturn 是"用户指着一坨用户内存说'把这里当现场恢复'"——restore_sigcontext 严格校验保留位与 CS/SS 合法性(27.1.3 节 UC_STRICT_RESTORE_SS 标志的用途),坏帧由 signal_fault 报告并 SIGSEGV。SROP(Sigreturn-Oriented Programming)攻击正是滥用这一机制:伪造 rt_sigframe 后调 sigreturn 即可"一次性改写全部寄存器"——ROP 链的终极原语,防御依赖帧完整性无法静态保证的现实,转向 CET/影子栈的运行期约束。
27.2.5 动态 MINSIGSTKSZ:FPU 增长的代价
// arch/x86/kernel/signal.c:386-430(要点)
#ifdef CONFIG_DYNAMIC_SIGFRAME
#ifdef CONFIG_STRICT_SIGALTSTACK_SIZE
static bool strict_sigaltstack_size __ro_after_init = true;
...
* MINSIGSTKSZ is 2048 and can't be changed despite the fact that AVX512
* exceeds that size already...
POSIX 常量 MINSIGSTKSZ=2048 在 AVX-512(XSAVE 区可超 2.5KB)时代成了谎言——旧程序按常量分配 sigaltstack 会被帧填充溢出。CONFIG_DYNAMIC_SIGFRAME 让 sigaltstack_size_required() 按本机 CPU 的 XSAVE 特性动态计算,glibc 经 getauxval(AT_MINSIGSTKSZ) 获取真值;STRICT_SIGALTSTACK_SIZE 启动参数可切换回严格校验。这是"硬件特性增长迫使 ABI 常量活体化"的教科书案例——27.2.2 节的 FPU 区选址正是它的消费端。
小结
rt_sigframe 是内核在用户栈上伪造的"完整函数调用现场":pretcode 蹦床 + ucontext(寄存器/掩码/备用栈描述)+ siginfo + FPU 区。get_sigframe 以红区→备用栈→FPU→帧的四步让位选址,x64_setup_rt_frame 以 SMAP 屏障批量填充并把寄存器改造成"handler 即将执行"的假象,影子栈同步压栈维持 CET 的调用链一致性;sigreturn 从 ucontext 整体回滚现场,SROP 风险与动态 MINSIGSTKSZ 是这一机制的攻防与演进注脚。信号的机制面至此闭合——从 kill 到 handler 再回到断点,每一步都有明确的内核物主。