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;
    ...
}

五点机制:

  1. SA_RESTORER 强制(:171):pretcode 必须指向 glibc 提供的蹦床——sa_restorer 内是 mov $SYS_rt_sigreturn, %rax; syscall。早期内核曾用栈上可执行蹦床(restore_rt 代码直接写在栈上),W^X 时代全部改为调用用户态固定蹦床;
  2. unsafe_put_user 批量写:user_access_begin/end 包裹的 SMAP 屏障区(1.6 节 STAC/CLAC),一次性写完帧免得反复开关;
  3. handler 的参数即帧:si=&frame->info、dx=&frame->uc——SA_SIGINFO handler 的两参就是用户栈上帧内的地址,非 SIGINFO handler 也能从栈上"意外"读到它们(注释原文的兼容说明);
  4. 寄存器改造:ip=handler, sp=frame——返回用户态的最后一刻,被打断的现场整体压进 uc_mcontext,CPU 视角下"handler 是被 sigreturn 之前的某个神秘调用者调用的";
  5. 影子栈联动(: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 再回到断点,每一步都有明确的内核物主。