本页目录

信号与 ptrace

覆盖: 信号生命周期 (生成→递送→处理) → signal_struct/sighand_struct → 可靠/不可靠信号 → ptrace 机制 (PTRACE_ATTACH/PTRACE_SYSCALL/PTRACE_PEEKDATA) → seccomp 互斥 内核版本: 2.6 ~ 6.x

概述

信号是 Unix 最古老的 IPC 机制之一,也是一种统一的异常/事件通知框架。从用户敲 Ctrl-C 产生的 SIGINT,到除零触发的 SIGFPE,到子进程退出时的 SIGCHLD,都走同一套内核路径。

ptrace 则是调试器(gdb、strace)和沙箱(seccomp user notification)的基础系统调用,允许一个进程观察和控制另一个进程的执行。


信号数据结构

信号集 (sigset_t)

// include/linux/signal_types.h
// 信号集是一个位图,每个 bit 代表一个信号 (1~64)
typedef struct {
    unsigned long sig[_NSIG_WORDS];  // _NSIG=64, _NSIG_WORDS=2 (on 64-bit)
} sigset_t;

// 常用操作
sigemptyset(&set);           // 清零
sigaddset(&set, SIGINT);     // 添加
sigdelset(&set, SIGTERM);    // 删除
sigismember(&set, SIGKILL);  // 测试

每进程的信号结构

// include/linux/sched.h
struct task_struct {
    struct signal_struct    *signal;   // 线程组共享
    struct sighand_struct   *sighand;  // 线程组共享 (除非使用了 CLONE_SIGHAND)
    sigset_t                blocked;   // 每线程独立的阻塞信号集
    sigset_t                real_blocked;
    struct sigpending       pending;   // 每线程的待处理信号
};

// include/linux/sched/signal.h
struct signal_struct {
    // 线程组共享的状态
    int                     nr_threads;
    struct sigpending       shared_pending;  // 发给线程组所有线程的信号
    // fork/exec 统计
    // 资源限制
};

struct sigpending {
    struct list_head        list;       // 待处理信号的 sigqueue 链表
    sigset_t                signal;     // 位图快速查找
};

// include/linux/sched/signal.h
struct sighand_struct {
    refcount_t              count;      // 引用计数
    spinlock_t              siglock;    // 保护信号处理表
    wait_queue_head_t       signalfd_wqh;
    struct k_sigaction      action[_NSIG];  // 每个信号的处理动作
};

sigaction — 信号处理动作

// include/linux/signal_types.h
struct k_sigaction {
    struct sigaction sa;  // 包含 sa_handler / sa_sigaction / sa_mask / sa_flags
};

// sa_flags 关键位:
//   SA_NOCLDSTOP: 不接收 SIGCHLD when child stops
//   SA_NOCLDWAIT: child 退出自动回收,不产生僵尸
//   SA_SIGINFO:   使用三参数 handler (sig, siginfo, context)
//   SA_RESTART:   被信号打断的 syscall 自动重启
//   SA_NODEFER:   处理信号时不自动 block 该信号
//   SA_RESETHAND: 处理一次后恢复 SIG_DFL (一次性)

信号生命周期

生成 (generate) → 挂起 (pending) → 递送 (deliver) → 处理 (handle)

1. 生成: send_signal()

// kernel/signal.c
int send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
                enum pid_type type) {
    struct sigpending *pending;

    if (type == PIDTYPE_PID)
        pending = &t->pending;              // 发给特定线程
    else
        pending = &t->signal->shared_pending;  // 发给整个线程组

    // 检查是否已经有同信号 pending (不可靠信号去重)
    if (legacy_queue(pending, sig))
        goto ret;  // 不可靠信号: 只记位图,不排队

    // 可靠信号 (SIGRTMIN ~ SIGRTMAX): 分配 sigqueue 排队
    //   → 即使同一种信号,每次发送都入队
    //   → 不会丢失
    __sigqueue_alloc(sig, t, GFP_ATOMIC, ...);

    // 设 pending 位图 → 下次返回用户态时检查
    sigaddset(&pending->signal, sig);

    // 设 TIF_SIGPENDING → 返回用户态前触发信号处理
    set_tsk_thread_flag(t, TIF_SIGPENDING);
}

2. 不可靠信号 vs 可靠信号

传统信号 (1~31, "不可靠"):
  - 同一个信号多次发送 → pending 位图只记录 1 次
  - 如果内核发现位图已设 → 新的发送被丢弃
  - 经典问题: SIGCHLD 多个子进程同时退出 → 只收到一个通知

实时信号 (SIGRTMIN=34 ~ SIGRTMAX=64, "可靠"):
  - 每次发送都分配一个 sigqueue 排队
  - 可以携带额外数据 (siginfo.si_value)
  - 信号按编号顺序递送 (低编号优先)
  - 同编号信号先进先出

3. 递送: get_signal() → handle_signal()

// arch/x86/kernel/signal.c
// 返回用户态之前 (exit_to_user_mode_loop) 调用:
bool get_signal(struct ksignal *ksig) {
    // 1. 从 pending 中取一个未阻塞的信号
    //    先查线程 pending → 再查 shared_pending
    //    信号按编号从小到大取
    //
    // 2. 处理 ptrace 拦截 (如果有)
    //    如果处于 ptrace-stop → 检查是否被 ptracer 消费
    //
    // 3. 处理 cgroup 信号 (如果有)
    //
    // 4. 处理 SIGKILL / SIGSTOP (不能被 blocked 或被 handler 覆盖)
    //
    // 5. 检查 sa_handler:
    //    - SIG_IGN → 直接丢弃
    //    - SIG_DFL → 默认动作 (core/stop/ignore/terminate)
    //    - 自定义 handler → 设置用户态栈帧,返回 handler 地址
    //
    // 6. 设置 ksig->ka (sigaction) 和 ksig->info (siginfo)
    return true;
}

// 如果 handler != SIG_DFL/SIG_IGN:
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs) {
    // 1. 在用户态栈上创建 sigframe
    //    → 保存当前寄存器: RIP, RSP, RFLAGS, 通用寄存器
    //    → 保存 siginfo (si_signo, si_code, si_addr 等)
    //    → 保存 FPU 状态 (XSAVE)
    //
    // 2. 修改 regs:
    //    regs->rip = sa_handler    → 返回用户态时跳到信号处理函数
    //    regs->rsp = sigframe      → 栈在 signal frame 上方
    //
    // 3. 返回用户态 → 进程开始执行信号 handler
}

4. 信号返回: sigreturn()

// 信号 handler 执行完后,libc 调用 sigreturn()
// → sys_rt_sigreturn() → restore_sigcontext()
//   从 sigframe 恢复所有保存的寄存器
//   → 进程从被信号打断的地方继续执行

sigframe 是信号机制的精髓——内核在用户栈上伪造了一个"快照",handler 跑完后 sigreturn 把快照恢复,进程仿佛什么也没发生过(除了 handler 的副作用)。

5. 信号与系统调用

系统调用被信号打断的三种处理:
  SA_RESTART:   内核自动重试 (仅适用于 read/write/wait/... 等"慢"系统调用)
  无 SA_RESTART: 返回 -EINTR,由用户空间决定是否重试
  致命信号:     进程退出,不返回
  
  ERESTARTSYS / ERESTARTNOINTR / ... :
    内核内部使用的特殊错误码,在返回用户态之前被转换成:
      ERESTARTSYS   → SA_RESTART → 重试 / 否则 → EINTR
      ERESTARTNOHAND → 阻塞了但无 handler → 重试
      ERESTART_RESTARTBLOCK → 需要 restart_block 机制 (如 nanosleep)

SIGKILL 与 SIGSTOP 的不可覆盖性

// kernel/signal.c
// SIGKILL (9) 和 SIGSTOP (19) 不能被:
//   - block (sigprocmask 会忽略它们)
//   - ignore (SIG_IGN 被忽略)
//   - 被 handler 捕获 (sa_handler 被忽略)
//
// 这是设计原则: 系统永远有最后手段恢复控制
// 即使进程把所有信号都 block 了,SIGKILL 仍然生效

// 例外: 处在 TASK_UNINTERRUPTIBLE (D 状态) 的进程
//   如果进程卡在 D 状态(如等磁盘 IO),SIGKILL 无法立即终止
//   必须等 IO 完成 → 进程回到可运行状态 → 信号递送生效
//   TASK_KILLABLE (5.14+) 是改进: 允许被 SIGKILL 唤醒

ptrace

ptrace 允许一个 tracer 进程控制 tracee 进程的执行。gdb(断点/单步)、strace(syscall 跟踪)、seccomp user notification 都是基于 ptrace 构建的。

核心操作

// kernel/ptrace.c
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

// 关键请求:
PTRACE_ATTACH / PTRACE_SEIZE  // 附加到目标进程
PTRACE_DETACH                 // 分离
PTRACE_SYSCALL                // 在每次 syscall entry/exit 停止
PTRACE_SINGLESTEP             // 单步执行一条指令
PTRACE_CONT                   // 继续执行
PTRACE_PEEKDATA / PTRACE_POKEDATA  // 读写 tracee 内存
PTRACE_GETREGS / PTRACE_SETREGS    // 读写寄存器
PTRACE_O_TRACESYSGOOD         // 标记 syscall-stop 以便区分 (bit 7 of sig)
PTRACE_O_TRACEFORK / PTRACE_O_TRACEVFORK / PTRACE_O_TRACECLONE
PTRACE_O_TRACEEXIT
PTRACE_GET_SYSCALL_INFO       // 获取详细 syscall 信息 (5.3+)

内部机制

// 附加流程:
ptrace(PTRACE_ATTACH, pid, ...)ptrace_attach()
    → 设 task->ptrace = PT_PTRACED
    → 设 task->parent = tracer (真正的 parent 保存在 real_parent)
    → 发 SIGSTOP 给 tracee
    → tracee 进入 TASK_TRACED 状态
    → tracer 可以 waitpid() 获得停止通知

// 信号拦截:
// 当 tracee 收到信号,内核检查 task->ptrace:
//   → PT_PTRACED: 信号先发给 tracer (tracer 可以修改或消费)
//   → tracer 通过 waitpid() 获得信号信息
//   → tracer 决定让信号通过 (PTRACE_CONT) 或忽略

// 系统调用跟踪:
// PTRACE_SYSCALL → 设置 TIF_SYSCALL_TRACE
// → syscall entry: syscall_trace_enter() → ptrace_report_syscall()
//   → tracee 进入 TASK_TRACED,tracer 通过 waitpid 获得通知
//   → tracer 返回 → tracee 执行 syscall 本体
// → syscall exit: syscall_trace_exit() → 再次 stop
//   → tracer 可以看到 syscall 的返回值

ptrace 访问模式

// PTRACE_PEEKDATA: 通过 /proc/<pid>/mem 的底层接口
// 单次读一个 word (ptrace 传统 API, 效率低)
// 
// 更高效的方式: /proc/<pid>/mem 直接读写 (不需要 ptrace attach!)
//   → process_vm_readv() / process_vm_writev()
//   → 但只能读写,不能控制执行

// 内核实现: ptrace_access_vm() → get_user_pages_remote()
//   → 通过页表将 tracee 的虚拟地址映射到自己的地址空间
//   → 直接内存访问 (不需要 copy_from_user)

ptrace 与 seccomp 的互斥

seccomp (SECCOMP_SET_MODE_FILTER):
  一个进程要么被 ptrace,要么装 seccomp filter,不能两者兼有
  原因: 安全 — seccomp 的假设是"只有自己控制自己的 syscall 策略"
        如果被 ptrace,tracer 可以通过 POKEDATA 绕过 seccomp

  SECCOMP_RET_USER_NOTIF (seccomp user notification):
    这是"seccomp 框架下的 ptrace 替代"
    → 允许一个监督进程检查/批准被沙箱进程的 syscall
    → 不设 PT_PTRACED,可以与 seccomp 过滤器共存
    → 比 ptrace 轻量: 只拦截 syscall,不拦截信号

调试与观测

# 进程当前 blocked / pending / ignored 信号
cat /proc/<pid>/status | grep -i sig
# SigBlk: 被阻塞的信号
# SigIgn: 被忽略的信号
# SigCgt: 被捕获的信号 (有 handler)
# SigPnd: 每线程 pending
# ShdPnd: 线程组共享 pending

# 查看谁的信号 pending 最多 (可能被信号轰炸)
for pid in /proc/[0-9]*; do
    pend=$(awk '/SigPnd:/{print $2}' $pid/status 2>/dev/null)
    [ "$pend" != "0000000000000000" ] && echo "$pid: $pend"
done

# strace 看信号递送
strace -e trace=signal -p <pid>

# 发信号并看内核 trace
echo 1 > /sys/kernel/debug/tracing/events/signal/signal_generate/enable
echo 1 > /sys/kernel/debug/tracing/events/signal/signal_deliver/enable
cat /sys/kernel/debug/tracing/trace_pipe

参考与延伸

  • 内核文档⁠: Documentation/process/adding-syscalls.rst (信号与 syscall 交互), man 7 signal, man 2 ptrace
  • LWN:
    • "A new API for signal handling" (lwn.net/Articles/414618/)
    • "Seccomp user notification" (lwn.net/Articles/785746/)
  • 源码文件⁠:
    • kernel/signal.c — send_signal(), get_signal(), do_signal()
    • kernel/ptrace.c — ptrace_attach(), ptrace_resume()
    • arch/x86/kernel/signal.c — handle_signal(), setup_rt_frame()
    • arch/x86/kernel/ptrace.c — 寄存器读写

关键词: sigaction, sigpending, SIGKILL, SA_RESTART, TIF_SIGPENDING, sigframe, rt_sigreturn, PTRACE_SYSCALL, TASK_TRACED, seccomp