Show HN: Sigwire – 一个实时TUI控制面板,用于管理Linux系统上的所有信号
摘要
Sigwire 是一个实时终端仪表盘,使用 eBPF 跟踪点流式显示 Linux 系统上的所有信号,展示发送方、目标、信号类型和处理程序详情,无需 ptrace 或进程协作。
查看缓存全文
缓存时间: 2026/07/13 22:56
yeet-src/sigwire 项目地址:https://github.com/yeet-src/sigwire # sigwire
信号的
tail -f。 机器上任何进程发出的每一个信号 —— 谁发送的、打中了谁、哪个信号、如何触发的(kill(2)、内核、POSIX 定时器)、目标是否捕获了它以及其处理程序运行了多长时间、是否将阻塞的系统调用以EINTR撕开 —— 从内核的信号跟踪点中解码,并实时流式传输到你的终端。没有针对一个 pid 的strace -f,没有ptrace,也不需要进程的任何配合。sigwire将内核的信号机制变成一个实时配线盘:每一行是发送者 ──信号──▶ 目标,按严重性着色,标记触发方式、目标是否 捕获 了它(以及处理程序运行了多久)、是否 中断了阻塞的系统调用(↯ EINTR read),当有信号反复发送时折叠为×N,当是真正的致命一击时标记☠。 侧边栏统计正在发送的信号;暂停并选中一行可查看完整详情 —— 处置方式、处理程序地址、sigaction标志以及目标在该瞬间阻塞的信号集。因为它挂接的是内核的跟踪点,而非某个特定的进程,一次运行即可同时观察主机上的每一个信号 —— 你的应用、一个 supervisor、内核自身的故障机制 —— 而且它们完全不知道自己正在被跟踪。
[!提示] 每个信号都有两面。 sigwire 同时监控
signal:signal_generate(发送者的视角 —— 谁发起了什么,交换机的线路)和signal:signal_deliver(目标的视角 —— 是否捕获了它,使用了哪个处理程序和标志,当时阻塞了什么,以及是否中断了系统调用)。另外两个钩子 ——rt_sigreturn(2)和系统调用退出跟踪点 —— 用于计时处理程序并捕获 EINTR。所有信息都被关联到一行中。这种分离也是为什么☠ fatal计数有意保守(参见什么算致命):信号生成发生在传递之前,因此发送方无法知道信号的命运 —— 只有传递方知道,而且仅限它观察到的情况。
快速开始
curl -fsSL https://yeet.cx | sh # 安装 yeet 守护进程(一次)
yeet run github:yeet-src/sigwire # 运行仪表盘(守护进程执行特权的 BPF 加载)
手动安装指南(https://yeet.cx/docs/manual-installation)| 仅限 Linux
无需配置 —— 任何机器上的信号都是持续的背景流量,所以行会立即开始从顶部出现。想自己制造一些?kill -USR1 $$,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 色调色板:
| 严重性 | 信号 | 颜色 |
|---|---|---|
| 杀死 | SIGKILL | 热红色 |
| 致命(产生 coredump) | 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 … | 灰色 |
备注 是来源(kill(2)、tgkill、sigqueue、timer、kernel、fault);然后,如果它中断了一个阻塞的系统调用,则显示 ↯ EINTR read(或当 SA_RESTART 自动恢复它时为 ↺ restarted read);然后是 处置方式 —— caught 41μs(处理程序运行了,以及耗时),default(没有处理程序,默认动作适用),或 ⊘ ignored。☠ 标记真正的致命一击(参见什么算致命)。
[!注意]
↯ EINTR是值得关注的一个。 当一个线程停留在慢速系统调用(read、poll、accept、futex、nanosleep,…)中时信号到来,会将其拉出:系统调用返回-1/EINTR,并且除非处理程序设置了SA_RESTART,否则它不会恢复 —— 应用必须重试。忘记这一点是一个经典的、令人抓狂的、依赖时序的错误(“为什么我的read()失败了一次?”)。sigwire 实时展示它发生的过程,以及哪个系统调用被击中。按e来隐藏其他所有内容,只观察这些中断。
右侧的侧边栏是聚合视图:top 信号(按数量)、按来源的分布、以及 传递统计 —— 多少信号被捕获,多少触发了默认动作,多少被忽略。
检查一个信号
按 ↑/↓(或 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(→ 默认动作:终止 / coredump / 停止 / 忽略)或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—— 无法捕获、无法忽略,始终致命;或 - 一个 产生 coredump 的信号(
SEGV/BUS/ABRT/ILL/FPE/TRAP/SYS/QUIT)且由内核自身触发(同步故障,不是用户空间的kill)。
其他所有情况 —— 来自 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 + src/bpf/deliver.bpf.c(内核,链接成一个对象)和 src/probes/sigwire.js(用户空间)。所有内容通过 (目标 tid, 信号) 关联。
BPF 端
两个源文件链接成一个可加载对象 bin/probe.bpf.o,包含四个跟踪点程序:
| 程序 | 挂接到 | 捕获什么 |
|---|---|---|
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 | 加载 bin/probe.bpf.o 一次,绑定映射,启动程序(它们自动挂接) |
src/probes/sigwire.js | 唯一了解 BPF 的数据模块:将两个环形缓冲区折叠成一个滚动的信息流并带统计,将传递关联到生成,拥有静音掩码旋钮 —— 暴露 feed、visible、muteMask 信号 |
src/main.jsx | 组合根:输入、选择、响应式布局(窄终端上隐藏侧边栏)、mount |
src/components/feed.jsx | 交换机:发送者 ──SIG──▶ 目标、处置方式/延迟、徽章、色彩、合并 |
src/components/tally.jsx | 侧边栏 —— top 信号、按来源的分布、传递统计 |
src/components/detail.jsx | 检查器 —— 每个信号的处置方式、处理程序、标志、阻塞掩码 |
src/components/picker.jsx | 信号选择器模态框 —— 通过内核静音掩码静音/显示每个信号 |
src/components/titlebar.jsx | 品牌、实时速率、总计、☠ fatal 计数器、静音计数、实时/暂停 |
src/components/footer.jsx | 按键提示和实时过滤提示 |
src/lib/signals.js | 唯一的事实来源:名称、严重性、颜色、si_code → 来源、处置方式、标志、掩码解码、致命性 |
src/lib/format.js | 纯格式化函数 —— 填充、截断、ago()、持续时间、紧凑计数 |
src/lib/fuzzy.js | 在进程名 + pid + 信号 + 来源 + 处置方式上的子序列模糊匹配 |
模型是一个滚动的生成信号信息流,将相同的重复合并成 ×N 行。一个信号的生成行在其传递解析完成时冻结 —— 因此屏幕上已有的行永远不会改变或跳动。一个 120 毫秒的窗口定时器每帧发布一个快照,因此繁忙的环形缓冲区只导致一次重新渲染,而不是上千次。
为什么选择跟踪点,而不是 strace/ptrace
strace -f 跟踪一个进程树,并在每个事件上停止被跟踪者;ptrace 是每个目标的且具有侵入性。信号跟踪点是 内核 提升和传递信号的接缝,针对 每个 进程,无需按应用设置,也不会停止任何人。将生成 ↔ 传递 ↔ rt_sigreturn 配对,得到发送者/目标对、处置方式、每个处理程序的延迟以及 EINTR 判决,从而将信号的一生串联起来。
跨内核测试
make veristat 在 你的 内核上使用 veristat 加载 bin/probe.bpf.o —— 快速检查每个程序是否通过验证器,以及每个程序的复杂度(insns/states)。加载 BPF 需要特权,因此使用 sudo。在你的笔记本上加载的程序可能会被较旧内核的验证器拒绝。.github/workflows/kernel-matrix.yml 防止这种情况:对于矩阵中的每个内核,它构建对象,在 VM 中启动该内核(cilium 的 little-vm-helper (https://github.com/cilium/little-vm-helper),镜像来自 quay.io/lvh-images),并针对它运行附带的静态 veristat —— 如果验证器拒绝任何程序则任务失败,并将每个内核的结果透视成一个 ✅/❌ 网格。VM 内的入口是 build/verify-kernel.sh。在本地(Linux + KVM)运行相同的矩阵,使用 make veristat-matrix —— 它使用 lvh + QEMU 启动内核镜像,并打印一个 ok/FAIL 网格。使用 make veristat-matrix KERNELS="6.6 bpf-next" 选择内核。
要求
[!重要]
- 带有 BTF 的 Linux 内核(
CONFIG_DEBUG_INFO_BTF)用于 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是可观察性,而非强制。它向你展示什么被发起;它不会阻塞、延迟或改变任何信号。
- 一行是一个被发起的信号。 交换机线路来自生成;目标可能捕获它、阻塞它或已经退出。处置方式/处理程序/掩码列来自传递方,并且只在内核实际传递它时才填充 —— 一个被阻塞或仍待处理的信号不显示处置方式。参见什么算致命。
- 关联是尽力而为。 生成和传递是独立的跟踪点,没有共享的 id,通过
(目标 tid, 信号)在一个时间窗口内匹配。在同一个信号风暴发往同一个线程的情况下,配对可能模糊;在绝大多数常见情况下它是正确的。 - 处理程序计时测量内核的框架,而非你的意图。
ran是传递 →rt_sigreturn。一个处理程序
相似文章
Show HN: Mcpsnoop – MCP的Wireshark(透明代理与实时TUI)
Mcpsnoop是一款开源透明代理,带有实时终端界面,位于AI客户端与MCP服务器之间,实时显示JSON-RPC流量,用于调试工具调用、能力和性能,无需额外设置。
Show HN:BornToBeRoot 的 NETworkManager 的免费 Linux 适配版
NMLinux 是 NETworkManager 的免费、开源 Linux 适配版,为常用网络工具提供统一的图形界面。它基于 Python 和 PySide6 构建,包含 SSH、RDP、VNC、Wi-Fi、路由追踪、网速测试等模块,面向系统管理员和高级用户。
Show HN:Epiq – 基于Git的分布式问题追踪器TUI
Epiq 是一款终端原生、基于Git的问题追踪器,具有 ASCII 看板、不可变事件日志,并支持 MCP 以实现代理工作流。它允许以本地优先的方式进行分布式协作,无需中心服务。
Show HN: Nibble
Nibble 是一种类 C 的系统编程语言,用 3000 行 C 代码实现,无需外部依赖或堆分配即可生成 LLVM IR。它支持 defer、递归、多种类型、结构体、指针,并包含图形演示。
Strace-ui、Bonsai_term 与 TUI 复兴
Jane Street 的工程师介绍了 strace-ui,这是一个用于 strace 的交互式终端 UI,通过过滤、PID 追踪和手册页集成简化了系统调用调试,并强调了由其 Bonsai 框架推动的 TUI 复兴。