Skip to the content.

signalfd

基本测试

测试代码:

signalfd 可以通过 uds 传递吗?

docs/kernel/signal/uds_signalfd.c 发现 server 不能监听到:

[!NOTE] 参考神奇海螺的意见,有待验证

看一下内核 fs/signalfd.c 的实现:

  static __poll_t signalfd_poll(struct file *file, poll_table *wait)
  {
      struct signalfd_ctx *ctx = file->private_data;
      __poll_t events = 0;

      poll_wait(file, &current->sighand->signalfd_wqh, wait);

      spin_lock_irq(&current->sighand->siglock);
      if (next_signal(&current->pending, &ctx->sigmask) ||
          next_signal(&current->signal->shared_pending, &ctx->sigmask))
          events |= EPOLLIN;
      spin_unlock_irq(&current->sighand->siglock);

      return events;
  }

signalfd_dequeue 也是类似:

  spin_lock_irq(&current->sighand->siglock);
  ret = dequeue_signal(&ctx->sigmask, info, &type);

而 dequeue_signal 内部直接写死用 current:

  int dequeue_signal(sigset_t *mask, kernel_siginfo_t *info, enum pid_type *type)
  {
      struct task_struct *tsk = current;
      ...
      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);
      }
      ...
  }

也就是说,signalfd_ctx 只保存了一个 sigmask,没有任何指向创建者 task 的引用。无论这个 fd 被 dup、fork 还是通过 UDS SCM_RIGHTS 传到哪个进程,读它的时候永远看的是当前进程的 task->pending / task->signal->shared_pending。

所以 uds_signalfd.c 里:

  1. Client 创建 signalfd,阻塞 SIGINT,然后把 fd 传给 Server。
  2. Server 收到 fd 后 select/read。
  3. 你给 Client 发 SIGINT 时,信号进入 Client 的 pending 队列。
  4. Server 读这个 fd 时,内核检查的是 Server 自己的 pending 队列,里面根本没有 SIGINT。
  5. 结果 Server 一直阻塞,永远读不到信号。

简言之:signalfd 不是跨进程共享信号的事件源,它只是把当前进程的信号投递转换成 fd 可读事件。想监控哪个进程的信号,就必须在那个进程里读它自己的 signalfd。

gdb 和 signal fd 的诡异故事

使用 docs/kernel/signal/code/ 下两个小 demo :

所以,我们有两个问题:

  1. 为什么 signalfd 可以接受到信号?
  2. 为什么 STOP 可以感知到,但是 SIGINT 不会感知到?

结论 by codex

(2026-05-22 ,这个东西我不是完全确认,这个至少需要和内核中的实现对照一下)

这里不是一条“信号必然进入用户态 handler”的路径,而是三条不同路径混在一起了:

  1. 普通未阻塞的 SIGINT 在 GDB 下会先进入 ptrace 的 signal-delivery-stop,GDB 默认停住程序,但不把 SIGINT 注入给 tracee,所以用户态 handler 不执行。
  2. signalfd demo 先把 SIGINT block 住,信号不会进入普通 delivery 路径,而是留在 pending 集合中,之后由 signalfd 消费。
  3. SIGSTOP 不能被 catch、block、ignore。它不会调用用户态 handler,但会让进程停止,并可能打断正在阻塞的 syscall,例如 epoll_wait,所以恢复后可以看到 EINTR

一句话概括:

SIGINT 被 GDB 当成调试控制信号消费了;signalfd 通过 block 改变了信号路径;SIGSTOP 不走用户 handler,但会改变调度和阻塞 syscall 状态。

gdb-sigint: SIGINT handler 为什么不执行

gdb-sigint.c 注册了 SIGINT handler:

sa.sa_handler = handler;
sa.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa, NULL);

如果程序直接在 shell 中运行,按 Ctrl-C 时终端驱动会给前台进程组发送 SIGINT,进程收到后会进入普通 signal delivery,最后执行用户态 handler。

但是程序在 GDB 下运行时,SIGINT 的路径变了:

Ctrl-C
  -> SIGINT generated
  -> tracee 进入 signal-delivery-stop
  -> GDB 被 waitpid 唤醒
  -> GDB 根据自己的 signal policy 决定是否注入 SIGINT

GDB 默认对 SIGINT 的处理通常是:

stop  = yes
print = yes
pass  = no

也就是 GDB 会停住程序并打印提示,但不会把 SIGINT 继续传给被调试进程。因此:

可以在 GDB 中确认:

info signals SIGINT

如果显式允许 GDB 传递 SIGINT:

handle SIGINT pass

或者手动注入:

signal SIGINT

这时 gdb-sigint.c 的 handler 才会执行。

gdb-sigfd: signalfd 为什么可以收到 SIGINT

gdb-sigfd.c 的关键不是 signalfd 本身,而是它先把 SIGINT block 了:

sigemptyset(&mask);
sigaddset(&mask, SIGINT);
pthread_sigmask(SIG_BLOCK, &mask, NULL);

sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);

被 block 的信号不会立刻 delivery 给用户态 handler,而是先进入 pending signal 集合:

SIGINT generated
  -> SIGINT 当前被 block
  -> 不进入普通 signal delivery
  -> 不调用 handler
  -> 标记为 pending
  -> signalfd 可读
  -> read(signalfd) 取出 signalfd_siginfo

这也解释了为什么它看起来和 gdb-sigint.c 行为不同。signalfd 不是“抢赢了 GDB”,而是 pthread_sigmask(SIG_BLOCK)SIGINT 暂时不走普通 delivery 路径。信号还在进程的 pending 集合里,signalfd 只是提供了一个 fd 形式的消费接口。

ptrace 文档中也有对应语义: tracee 被 tracing 时,信号 delivery 前会先停给 tracer;但是如果信号当前被 block,signal-delivery-stop 不会发生,直到信号被 unblock。SIGSTOP 是例外,因为它不能被 block。

gdb-stop: STOP 为什么可以通过 EINTR 被感知

gdb-stop.c 阻塞在:

epoll_wait(epfd, events, MAX_EVENTS, -1);

SIGSTOP 不能被 catch、block、ignore,所以程序没有机会注册 handler 来处理它。它的效果是让整个线程组进入 stopped 状态。恢复时,Linux 上某些阻塞 syscall 会以 -1/EINTR 返回,即使没有任何用户态 handler 被调用。

路径可以理解为:

epoll_wait 正在 TASK_INTERRUPTIBLE 睡眠
  -> SIGSTOP / ptrace attach stop / GDB interrupt 让 tracee stop
  -> 内核唤醒可中断睡眠
  -> syscall 被提前结束
  -> tracee 恢复执行
  -> epoll_wait 返回 -1, errno = EINTR

这不是程序“捕获了 SIGSTOP”。程序能观察到的只是 stop/resume 对阻塞 syscall 的副作用。

man signal(7) 对 Linux 的这个行为有专门描述: 在没有 signal handler 的情况下,进程被 stop signal 停住然后由 SIGCONT 恢复后,某些阻塞接口也可能失败并返回 EINTR,其中包括 epoll_wait / epoll_pwait

ptrace 还有一个相关副作用: PTRACE_ATTACH 会给 tracee 发送 SIGSTOP。调试器通常会 suppress 这个 SIGSTOP,避免把一个假的 stop 信号真正注入给程序。但即使 signal injection 被 suppress,被中断的 syscall 仍然可能出现一次 stray EINTRptrace(2) 明确把 epoll_wait 列为这类历史问题的典型例子。

所以这类代码不能写成:

int nr = epoll_wait(epfd, events, MAX_EVENTS, -1);
assert(nr == 1);

而应该显式处理 EINTR:

int nr;

do {
	nr = epoll_wait(epfd, events, MAX_EVENTS, -1);
} while (nr < 0 && errno == EINTR);

对比

场景 信号是否 block GDB/ptrace 是否先看到 用户 handler 是否执行 用户程序能看到什么
gdb-sigint.c 默认不执行,GDB suppress 了 GDB 停住程序
gdb-sigfd.c 通常不会立刻 signal-delivery-stop 不执行 signalfd 可读
gdb-stop.c SIGSTOP 不能 block GDB/ptrace stop 或真实 stop 都会影响任务状态 不可能执行 epoll_wait 可能返回 EINTR

TODO

才知道 signalfd 是好老的功能,快 20 年了(2025-06-16)

signalfd 没有想象的那么好用的: https://news.ycombinator.com/item?id=33300856

https://news.ycombinator.com/item?id=9564975

本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。