sigwire
tail -f用于信号。 系统上每个进程引发的每个信号——谁发送的、谁接收的、哪个信号、如何引发的(kill(2)、内核、POSIX 定时器)、目标是否捕获了它以及其处理程序运行了多长时间、它是否用EINTR撕开了阻塞的系统调用——从内核的信号跟踪点解码并实时流式传输到您的终端。无需对单一 pid 使用strace -f,无需ptrace,无需被涉及进程的配合。
sigwire 将内核的信号机制转变为实时配线架:每一行是 sender ──SIGNAL──▶ target,按严重性着色,标记引发方式,目标是否捕获了它(以及其处理程序运行了多长时间),是否中断了阻塞的系统调用**(↯ EINTR read),当某个信号大量出现时折叠为 ×N,并且当它是真正的致命一击时标记 ☠。** 侧边栏统计着传输中的信号;暂停并选择一行以查看完整画面——信号处置、处理程序地址、sigaction 标志以及目标当时正在阻塞的信号。
因为它挂钩内核的跟踪点,而不是任何单一进程,一次运行即可同时监控主机上的每个信号——您的应用程序、监督程序、内核自身的故障机制——而它们都不会意识到自己正在被跟踪。
[!TIP] 每个信号的两个方面。 sigwire 同时监控
signal:signal_generate(发送者的视角——谁引发了什么,配线架线路)和signal:signal_deliver(目标的视角——是否捕获了它,使用哪个处理程序和标志,当时阻塞了什么,以及是否中断了系统调用)。另外两个钩子——rt_sigreturn(2)和系统调用退出跟踪点——对处理程序进行计时并捕获 EINTR。所有这些都被关联回同一行。这种分离也是为什么☠ fatal计数故意保守的原因(参见什么算作致命):信号的产生发生在传递之前,因此发送方无法知道信号的命运——只有传递方可以,而且仅限于它观察到的情况。
curl -fsSL https://yeet.cx | sh # install the yeet daemon (one time) yeet run github:yeet-src/sigwire # run the dashboard (the daemon does the privileged BPF load)
[手动安装指南](https://yeet.cx/docs/manual-installation) | 仅限 Linux
无需配置 — 信号在任何机器上都是持续的背景流量,因此行会立即从顶部开始出现。想自己生成一些?`kill -USR1 <pid>`、对前台作业按 `Ctrl-C`,或者启动任何托管运行时并观察其 GC/调度器 ping 自己的线程(`↯ EINTR futex` 滚动而过)。
## 控制
信号流默认跟随最新信号;选择一行或暂停后,它会保持静止,同时数据在下方继续流动。
| 键 | 操作 |
| --- | ------ |
| `p` · `Space` | 暂停/恢复信号流(冻结以阅读) |
| `↑`/`↓`, `k`/`j` | 暂停并检查一行 — 打开详情面板 |
| `/` | 模糊过滤 — 匹配进程、PID、信号、来源和处置方式;匹配字符实时高亮 |
| `e` | 过滤至**仅中断的系统调用**(`↯ EINTR` / `↺ restarted`) |
| `s` | 打开**信号选择器** — 实时静音或显示任何信号 |
| `Esc` | 退出一层 — 清除过滤/关闭选择器/取消选择,然后退出 |
| `q` | 退出 |
## 您正在查看的内容
每一行是一个生成的信号,最新的在最顶部:```
WHEN SENDER SIGNAL TARGET NOTE
now bash·4402──SIGINT───▶ node·8813 kill(2) ↯ EINTR read caught 41µs
1.2s systemd·1──────SIGTERM──▶ nginx·1291 kill(2) caught 1.2ms
3.4s kernel·8813──SIGSEGV──▶ chrome·8813 fault default ☠
4.1s postgres·507──SIGUSR1───▶ postgres·509 ×6 kill(2) caught 9µs
每一行都是一个块:发送方 → 目标 为 comm·pid(发送方是发起信号的一方,current;目标是信号指向的进程),中间的导线承载着根据严重性着色的信号名称,×N 将相同信号的一次突发合并为一行,右侧的注释给出来源,然后是任何系统调用中断,最后是处置方式。
每一行在交付解析完成时冻结,此后永远不会再改变——因此一次突发会作为一条稳定的日志滚动显示,而不是闪烁的聚合。
导线根据严重性着色,采用与 UI 其余部分相同的 256 色调色板:
注释 是来源(kill(2)、tgkill、sigqueue、timer、kernel、fault);然后,如果它中断了阻塞的系统调用,会显示 ↯ EINTR read(或 ↺ restarted read,当 SA_RESTART 自动恢复它时);最后是处置方式——caught 41µs(处理程序运行了,以及耗时多久)、default(没有处理程序,采用了默认行为)或 ⊘ ignored。☠ 标记真正的致命一击(参见什么算致命)。
[!NOTE]
↯ EINTR是需要关注的重点。 当一个信号到达时,线程正阻塞在慢速系统调用(read、poll、accept、futex、nanosleep……)中,该线程会被强制唤醒:系统调用返回-1/EINTR,并且除非处理程序设置了SA_RESTART,否则它不会恢复——应用程序必须重试。忘记这一点是一个经典的、令人抓狂的、与时机相关的 bug(“为什么我的read()失败了一次?”)。sigwire 实时显示这种情况的发生,以及是哪个系统调用受到了影响。按e键隐藏所有其他内容,只观察这些中断。
右侧的轨道是聚合视图:按数量排列的顶部信号、按来源的分类,以及交付统计——捕获了多少信号、默认处理了多少、忽略了多少。
按 ↑/↓(或 p)冻结流并选择一行;轨道变为一个详细信息面板,显示关于该确切信号的交付侧所知的一切:```
SIGNAL
SIGUSR1 (10) user
from ctarget·3980913
to ctarget·3980913
RAISED
via tgkill
code SI_TKILL
scope thread
result delivered
DELIVERY
handled caught
syscall EINTR ← read
handler 0x55f0a1c3
ran 3.0ms
flags SA_SIGINFO
TARGET BLOCKS
SIGINT SIGQUIT SIGTERM
- **handled** — `caught`(运行了用户空间处理程序)、`default`(→ 默认动作:终止/核心转储/停止/忽略)或 `ignored`。
- **syscall** — 如果该信号中断了一个阻塞的系统调用:`EINTR ← read`(用户空间看到 `EINTR`)或 `restarted read`(`SA_RESTART` 透明地恢复它)。
- **ran** — 处理程序执行的时间,从递送到结束它的 `rt_sigreturn(2)` 计算。(只会在 C 处理程序中设置标志并在以后执行实际工作的运行时——CPython、Go——在这里显示一个很小的时间;那是它们的问题,不是 sigwire 的。)
- **flags** — 处理程序上的 `sigaction` 标志(`SA_RESTART`、`SA_SIGINFO`、`SA_NODEFER`……)。
- **TARGET BLOCKS** — 目标在递送时刻已阻塞的信号(其 `sigprocmask`),直接来自其 `task_struct`。
`Esc` 关闭检查器;`p` 恢复实时显示。
## 什么算作致命
`☠ fatal` 计数器和 `☠` 行徽章故意严格。因为 `signal_generate` 在*产生*时触发,sigwire 无法看到目标是否安装了处理程序——`SIGTERM` 可能被捕获并转换为干净关闭,或者完全被忽略。因此,只有当死亡是明确无误时,它才会计数:
- **`SIGKILL` 递送**——不可捕获、不可忽略、始终致命;**或**
- **内核自身引发的**(同步故障,而非用户空间的 `kill`)一个**核心转储信号**(`SEGV`/`BUS`/`ABRT`/`ILL`/`FPE`/`TRAP`/`SYS`/`QUIT`)。
其他所有信号——来自 `systemd` 的 `SIGTERM`、来自你的 `Ctrl-C` 的 `SIGINT`、运行时发给自己线程的 `SIGPWR`——都会被显示和着色,但不计为死亡,因为很可能不是。
## 信号选择器(一个实时的内核旋钮)
在任何繁忙的机器上,三个信号都是纯背景噪音:`SIGCHLD`(每次子进程回收)、`SIGURG`(Go 的异步抢占心跳)和 `SIGWINCH`(终端大小变化,广播给所有前台进程)。sigwire 默认在**内核内**静音这三个信号,以便显示感兴趣流量——但哪些信号是噪音取决于你。
按下 `s` 打开**信号选择器**:一个模态列表,包含每个信号及其实时严重性颜色和你已经看到的数量,每个信号可在 `shown`(显示)和 `muted`(静音)之间切换。用箭头移动到某个信号(或**输入其数字**——`1`、`5` → 跳到 15),然后按 `space`,该信号立即切换。`a` 一次性切换**所有**信号。标题栏中的 `muted` 计数跟踪当前隐藏的数量。
这是演示的双向部分:静音掩码是正在运行的 BPF 程序的 `.data` 部分中的一个 `__u64` 全局变量,切换一行会通过 `DataSec.patch()` 修补匹配的位,而程序持续运行。内核在静音信号到达环形缓冲区之前就丢弃它们,因此静音不会消耗任何成本——而且取消静音会在流式过程中恢复信号,无需重新加载。
## 工作原理
核心是 [`src/bpf/sigwire.bpf.c`](https://github.com/yeet-src/sigwire/blob/HEAD/src/bpf/sigwire.bpf.c) + [`src/bpf/deliver.bpf.c`](https://github.com/yeet-src/sigwire/blob/HEAD/src/bpf/deliver.bpf.c)(内核,链接成一个对象)和 [`src/probes/sigwire.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/probes/sigwire.js)(用户空间)。所有内容通过 `(target tid, signal)` 关联。
### BPF 侧
两个源文件链接成一个可加载对象 `bin/probe.bpf.o`,包含四个 tracepoint 程序:
| 程序 | 附加到 | 捕获内容 |
|---|---|---|
| `on_signal_generate` | `signal:signal_generate` | 发送者(`current`)+ 目标(`comm`/`pid`)、信号、`si_code`、`group` 标志、`result`——如果该信号的位在实时 `mute_mask` 中设置,则在内核中丢弃 |
| `on_signal_deliver` | `signal:signal_deliver` | 目标的处置(`sa_handler`)、`sa_flags`,以及来自 `task_struct` 的 `blocked` 信号集;为处理程序计时标记递送 |
| (rt_sigreturn) | `syscalls:sys_enter_rt_sigreturn` | 与标记的递送进行差异比较,得到处理程序的运行时间 |
| (sys_exit) | `raw_syscalls:sys_exit` | 记录罕见的 `-ERESTART*` 返回值,以便下一个 `signal_deliver` 将其解析为 `EINTR`/`restarted` 以及被中断的系统调用号 |
映射连接内核和用户空间:
- `events` — `RINGBUF`,每次产生一个 `signal_event`。
- `dispatch` — `RINGBUF`,每次递送/处理程序返回一个 `dispatch_event`。
- `mute_mask` — `.data` 部分中的一个 `__u64` 全局变量;选择器通过修补单个位在内核中丢弃信号。
- `handler_start` / `restart_pending` — `HASH`,以 tid 为键,每个线程的临时空间,用于将一次递送与其 `rt_sigreturn` 配对,以及将系统调用的 `-ERESTART*` 退出与后续递送配对。
### JS 侧
| 文件 | 职责 |
|---|---|
| [`src/probes/probe.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/probes/probe.js) | 加载 `bin/probe.bpf.o` 一次,绑定映射,启动程序(它们自动附加) |
| [`src/probes/sigwire.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/probes/sigwire.js) | 唯一的 BPF 感知数据模块:将两个环形缓冲区折叠成一个滚动 feed 并带有统计,将递送关联到产生,拥有静音掩码旋钮——暴露 `feed`、`visible`、`muteMask` 信号 |
| [`src/main.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/main.jsx) | 组合根:输入、选择、响应式布局(窄终端上隐藏侧栏)、`mount` |
| [`src/components/feed.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/feed.jsx) | 交换机板:`发送者 ──SIG──▶ 目标`、处置/延迟、徽章、色调、合并 |
| [`src/components/tally.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/tally.jsx) | 侧栏——顶部信号、按来源分解、递送统计 |
| [`src/components/detail.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/detail.jsx) | 检查器——每个信号的处置、处理程序、标志、阻塞掩码 |
| [`src/components/picker.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/picker.jsx) | 信号选择器模态框——通过内核静音掩码静音/显示每个信号 |
| [`src/components/titlebar.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/titlebar.jsx) | 品牌、实时速率、总计、`☠ fatal` 计数器、静音计数、实时/暂停 |
| [`src/components/footer.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/footer.jsx) | 按键提示和实时过滤提示 |
| [`src/lib/signals.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/lib/signals.js) | 唯一真相来源:名称、严重性、颜色、`si_code` → 来源、处置、标志、掩码解码、致命性 |
| [`src/lib/format.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/lib/format.js) | 纯格式化器——填充、截断、`ago()`、持续时间、紧凑计数 |
| [`src/lib/fuzzy.js`](https://github.com/yeet-src/sigwire/blob/HEAD/src/lib/fuzzy.js) | 对进程+pid+信号+来源+处置的子序列模糊匹配 |
该模型是一个滚动的**产生信号 feed**,将相同的重复合并成 `×N` 行。一个信号的产生行在其递送解析后立即冻结——因此屏幕上已存在的行永远不会更改或跳跃。一个 120 毫秒的窗口计时器每帧发布一个快照,因此繁忙的环形缓冲区只需一次重新渲染,而不是数千次。
### 为什么使用 tracepoint,而不是 `strace`/`ptrace`
`strace -f` 跟踪一个进程树并在每次事件时停止跟踪对象;`ptrace` 针对每个目标且具有侵入性。信号 tracepoint 是*内核*对所有进程引发和递送信号的接缝,无需每个应用设置,且不会停止任何人。将产生 ↔ 递送 ↔ `rt_sigreturn` 配对,可以得到发送者/目标对、处置、每个处理程序的延迟以及 EINTR 判定,从而将信号的整个生命周期串联起来。
## 跨内核测试
`make veristat` 在**你的**内核上使用 veristat 加载 `bin/probe.bpf.o`——快速检查每个程序是否通过验证器,以及每个程序的复杂度(insns/states)。加载 BPF 需要权限,因此使用 `sudo`。
在你的笔记本上加载的程序可能会被较旧内核的验证器拒绝。[`.github/workflows/kernel-matrix.yml`](https://github.com/yeet-src/sigwire/blob/HEAD/.github/workflows/kernel-matrix.yml) 可以防止这种情况:对于矩阵中的每个内核,它构建对象,在虚拟机([cilium's little-vm-helper](https://github.com/cilium/little-vm-helper),使用 `quay.io/lvh-images` 的镜像)中启动该内核,并针对它运行供应商提供的静态 **veristat**——如果验证器拒绝任何程序则作业失败,并将每个内核的结果透视成一个 ✅/❌ 网格。虚拟机中的门控脚本是 [`build/verify-kernel.sh`](https://github.com/yeet-src/sigwire/blob/HEAD/build/verify-kernel.sh)。
使用 `make veristat-matrix` 在本地(Linux + KVM)运行相同的矩阵——它使用 `lvh` + QEMU 启动内核映像,并打印一个 `ok`/`FAIL` 网格。使用 `make veristat-matrix KERNELS="6.6 bpf-next"` 选择内核。
## 要求
> [!重要]
> - **一个带有 BTF**(`CONFIG_DEBUG_INFO_BTF`)的 Linux 内核,用于 CO-RE——`bpftool` 从它生成 `src/bpf/include/vmlinux.h`。在当前的 Arch、Fedora、Ubuntu 和 Debian(自 ~5.4 以来的每个主流发行版内核)上默认启用。
> - **yeet 守护进程**,执行特权的 BPF 加载。BPF 能力被委托给一个守护进程,因此 `sigwire` 本身以无特权方式运行。`curl -fsSL https://yeet.cx | sh` 安装它。
>
> 要从源代码构建,你还需要 `clang` 和 `bpftool`——但供应商提供的静态工具链提供了它们,因此你不需要系统的 C/BPF 工具链。不需要 node/npm:esbuild 也是供应商提供的,并且该项目没有第三方依赖项。
## 诚实的注意事项
> [!注意]
> `sigwire` 是观测,而非强制执行。它显示被引发的内容;它不会阻塞、延迟或改变任何信号。
- **一行是一个*被引发的*信号。** 交换机板行来自产生阶段;目标可能捕获它、阻塞它或已经退出。处置/处理程序/掩码列来自*递送*侧,并且只有在内核实际递送时才填充——一个被阻塞或仍处于待决状态的信号不会显示处置。参见[什么算作致命](#什么算作致命)。
- **关联是尽力而为的。** 产生和递送是分开的 tracepoint,没有共享 ID,通过 `(目标 tid, 信号)` 在一个时间窗口内匹配。在同一线程的相同信号风暴下,配对可能会模糊;但在绝大多数常见情况下是正确的。
- **处理程序计时测量内核框架,而非你的意图。** `ran` 是从递送到 `rt_sigreturn`。一个只设置标志的处理程序(CPython、Go 的运行时代码)在微秒内返回,即使“真正”的工作稍后发生在事件循环中——准确,但可能不是你期望的。
- **EINTR 检测监视每个系统调用退出。** 捕获被中断的系统调用需要附加到 `raw_syscalls:sys_exit`,它在系统范围内的*每个*系统调用返回时触发(处理程序立即放弃所有除罕见 `-ERESTART*` 代码外的其他代码,因此额外成本是每个系统调用几条指令——但它并非为零)。系统调用*名称*是 x86-64 表;其他架构显示原始系统调用编号。
- **内核信号的发送者是 `current`。** 对于同步故障(来自错误访问的 `SIGSEGV`),那是故障任务本身——正确且有用。对于异步内核信号,`current` 是内核引发信号时正在运行的任何任务,这是一个提示,而非绝对真理。
- **实时信号编号是名义上的。** `SIGRTMIN+n` 通过原始偏移量显示;库保留低几位供自身使用。
- **`comm` 是 16 字节。** 长进程名会被内核截断,而非 sigwire。
## 社区问题
**它会拖慢被跟踪的进程吗?**
没有明显的开销。tracepoint 程序是被动的;成本是在每次信号时进行有界环形缓冲区写入(以及每个系统调用退出时用于 EINTR 检测的几条指令),如果用户空间落后,环形缓冲区会丢弃而不是阻塞。
**它会显示针对在我启动时已经运行的进程的信号吗?**
会。从 sigwire 附加的那一刻起,tracepoint 为每个信号触发,无论发送者或目标何时启动——没有遗漏的每个进程状态。
**它适用于任何进程,还是仅一个?**
主机上的任何进程,同时全部覆盖——发送者/目标区分栏可以区分它们。这是整个机器的信号流量,而非单个 pid。
**我可以导出 feed 吗?**
没有内建功能。`probes/sigwire.js` 中的 `RingBuf.subscribe` 回调保存了每个解码的记录,因此 JSON/HTTP/Kafka 接收器是那里的一条分支。要设置托管管道,[联系我们](https://yeet.cx/)。
## 从源代码构建```sh
make # clang + bpftool → bin/probe.bpf.o ; esbuild → src/index.jsx
make bpf # just the BPF object
make bundle # just the JS bundle
make clean # remove build artifacts
然后 yeet run . 会运行本地构建。make 运行两个独立的编译器:clang + bpftool 将 src/bpf/*.bpf.c 链接成可加载对象 bin/probe.bpf.o;esbuild 将 src/main.jsx 打包成 src/index.jsx,通过 tsconfig 的 paths 解析 @/(源根目录)和 #/(项目根目录)这两个构建时别名,并将 yeet:* 内置模块保留为外部依赖。两个编译器都来自供应商提供的静态工具链,因此构建过程不需要系统 C/BPF 工具链,也不需要 node/npm。生成的 vmlinux.h、src/index.jsx 和 bin/*.bpf.o 是构建产物。
由于别名仅在构建时有效,运行时通过 import.meta.dirname 而非别名来定位 BPF 对象。参见 AGENTS.md(即 CLAUDE.md)了解 yeet 仪表板编写指南。
双许可证 BSD/GPL。BPF 程序在 src/bpf/sigwire.bpf.c 中声明了 char LICENSE[] SEC("license") = "Dual BSD/GPL",内核要求其使用的辅助函数必须包含此声明。
| 严重性 | 信号 | 颜色 |
|---|
| 致命 | SIGKILL | 热红 |
| 致命(生成 core 文件) | SEGV BUS ABRT ILL FPE TRAP SYS QUIT | 红色 |
| 终止 | TERM INT HUP PIPE ALRM … | 琥珀色 |
| 作业控制 | STOP TSTP TTIN TTOU | 黄色 |
| 继续 | CONT | 绿色 |
| 用户 | USR1 USR2 | 青色 |
| 实时 | SIGRTMIN+n | 紫色 |
| 内务 | CHLD URG WINCH … | 灰色 |