signalfd
基本测试
测试代码:
- code/src/c/signal/signalfd-iouring.c
- code/src/c/signal/signalfd-select.c
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, ¤t->sighand->signalfd_wqh, wait);
spin_lock_irq(¤t->sighand->siglock);
if (next_signal(¤t->pending, &ctx->sigmask) ||
next_signal(¤t->signal->shared_pending, &ctx->sigmask))
events |= EPOLLIN;
spin_unlock_irq(¤t->sighand->siglock);
return events;
}
signalfd_dequeue 也是类似:
spin_lock_irq(¤t->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 里:
- Client 创建 signalfd,阻塞 SIGINT,然后把 fd 传给 Server。
- Server 收到 fd 后 select/read。
- 你给 Client 发 SIGINT 时,信号进入 Client 的 pending 队列。
- Server 读这个 fd 时,内核检查的是 Server 自己的 pending 队列,里面根本没有 SIGINT。
- 结果 Server 一直阻塞,永远读不到信号。
简言之:signalfd 不是跨进程共享信号的事件源,它只是把当前进程的信号投递转换成 fd 可读事件。想监控哪个进程的信号,就必须在那个进程里读它自己的 signalfd。
gdb 和 signal fd 的诡异故事
使用 docs/kernel/signal/code/ 下两个小 demo :
- gdb-sigint.c : 注册了 signal handler ,但是不会执行,程序不会接受到任何的消息
- gdb-sigfd.c : gdb 中 ctrl-c ,会监听到信号
- gdb-stop.c : 发现,如果 gdb 中 ctrl-c ,然后恢复,epoll 会返回为 EINTR
所以,我们有两个问题:
- 为什么 signalfd 可以接受到信号?
- 为什么 STOP 可以感知到,但是 SIGINT 不会感知到?
结论 by codex
(2026-05-22 ,这个东西我不是完全确认,这个至少需要和内核中的实现对照一下)
这里不是一条“信号必然进入用户态 handler”的路径,而是三条不同路径混在一起了:
- 普通未阻塞的
SIGINT在 GDB 下会先进入 ptrace 的 signal-delivery-stop,GDB 默认停住程序,但不把SIGINT注入给 tracee,所以用户态 handler 不执行。 signalfddemo 先把SIGINTblock 住,信号不会进入普通 delivery 路径,而是留在 pending 集合中,之后由signalfd消费。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 继续传给被调试进程。因此:
- handler 不会执行
- 如果没有注册 handler,程序也不会因为默认
SIGINT动作退出 SA_RESTART在这里不是关键,因为根本没有执行用户态 handler
可以在 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 EINTR。ptrace(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 将按侵权追究法律责任,其它情况随意。