Show HN: Sigwire – 一个实时TUI控制面板,用于管理Linux系统上的所有信号

Hacker News Top 工具

摘要

Sigwire 是一个实时终端仪表盘,使用 eBPF 跟踪点流式显示 Linux 系统上的所有信号,展示发送方、目标、信号类型和处理程序详情,无需 ptrace 或进程协作。

大家好,我编写了这个工具,用于检查跨进程的 Linux 信号。
查看原文
查看缓存全文

缓存时间: 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)tgkillsigqueuetimerkernelfault);然后,如果它中断了一个阻塞的系统调用,则显示 ↯ EINTR read(或当 SA_RESTART 自动恢复它时为 ↺ restarted read);然后是 处置方式 —— caught 41μs(处理程序运行了,以及耗时),default(没有处理程序,默认动作适用),或 ⊘ ignored 标记真正的致命一击(参见什么算致命)。

[!注意] ↯ EINTR 是值得关注的一个。 当一个线程停留在慢速系统调用(readpollacceptfutexnanosleep,…)中时信号到来,会将其拉出:系统调用返回 -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 readSA_RESTART 透明地恢复了它)。
  • ran —— 处理程序执行了多长时间,从传递到结束它的 rt_sigreturn(2) 计算。(那些只在 C 处理程序中设置一个标志,然后在稍后做真正工作的运行时 —— CPython、Go —— 这里显示的时间很短;那是它们自己造成的,不是 sigwire。)
  • flags —— 处理程序的 sigaction 标志(SA_RESTARTSA_SIGINFOSA_NODEFER,…)。
  • TARGET BLOCKS —— 目标在传递时刻阻塞的信号(它的 sigprocmask),直接从其 task_struct 中获取。

Esc 关闭检查器;p 恢复实时信息流。

什么算致命

☠ fatal 计数器和 行标记有意严格。因为 signal_generate生成时触发,sigwire 无法看出目标是否安装了处理程序 —— 一个 SIGTERM 可能被捕获并转化为干净关闭,或完全被忽略。因此它只在没有歧义时计数一次死亡:

  • 传递的 SIGKILL —— 无法捕获、无法忽略,始终致命;
  • 一个 产生 coredump 的信号SEGV/BUS/ABRT/ILL/FPE/TRAP/SYS/QUIT)且由内核自身触发(同步故障,不是用户空间的 kill)。

其他所有情况 —— 来自 systemdSIGTERM、来自 Ctrl-CSIGINT、运行时向其自身线程发送的 SIGPWR —— 被显示和着色,但不计为死亡,因为它可能不是死亡。

信号选择器(一个实时内核旋钮)

三个信号在任何繁忙的机器上都是纯粹的背景噪音:SIGCHLD(每个子进程的回收)、SIGURG(Go 的异步抢占心跳)和 SIGWINCH(终端大小改变,广播到每个前台进程)。默认情况下,sigwire 在内核中 静音了这三个信号,以便信息流只包含有意义的流量 —— 但哪些信号是噪音由你来决定。按 s 打开信号选择器:一个模态列表,包含每个信号,带有其实时严重性颜色和你已看到的数量,每个都可以在 shownmuted 之间切换。用方向键移动到一个信号(或输入其编号 —— 15 → 跳到 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_generatesignal:signal_generate发送者(current)+ 目标(comm/pid)、信号、si_codegroup 标志、result —— 如果信号的位在实时 mute_mask 中被置位,则在内核中丢弃
on_signal_deliversignal: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 的数据模块:将两个环形缓冲区折叠成一个滚动的信息流并带统计,将传递关联到生成,拥有静音掩码旋钮 —— 暴露 feedvisiblemuteMask 信号
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 安装它。

要从源码构建,你还需要 clangbpftool —— 但附带的静态工具链提供了它们,因此你不需要系统的 C/BPF 工具链。不需要 node/npm:esbuild 也是附带的,该项目没有第三方依赖项。

诚实的注意事项

[!注意] sigwire 是可观察性,而非强制。它向你展示什么被发起;它不会阻塞、延迟或改变任何信号。

  • 一行是一个被发起的信号。 交换机线路来自生成;目标可能捕获它、阻塞它或已经退出。处置方式/处理程序/掩码列来自传递方,并且只在内核实际传递它时才填充 —— 一个被阻塞或仍待处理的信号不显示处置方式。参见什么算致命
  • 关联是尽力而为。 生成和传递是独立的跟踪点,没有共享的 id,通过 (目标 tid, 信号) 在一个时间窗口内匹配。在同一个信号风暴发往同一个线程的情况下,配对可能模糊;在绝大多数常见情况下它是正确的。
  • 处理程序计时测量内核的框架,而非你的意图。 ran 是传递 → rt_sigreturn。一个处理程序

相似文章

Show HN:BornToBeRoot 的 NETworkManager 的免费 Linux 适配版

Hacker News Top

NMLinux 是 NETworkManager 的免费、开源 Linux 适配版,为常用网络工具提供统一的图形界面。它基于 Python 和 PySide6 构建,包含 SSH、RDP、VNC、Wi-Fi、路由追踪、网速测试等模块,面向系统管理员和高级用户。

Show HN:Epiq – 基于Git的分布式问题追踪器TUI

Hacker News Top

Epiq 是一款终端原生、基于Git的问题追踪器,具有 ASCII 看板、不可变事件日志,并支持 MCP 以实现代理工作流。它允许以本地优先的方式进行分布式协作,无需中心服务。

Show HN: Nibble

Hacker News Top

Nibble 是一种类 C 的系统编程语言,用 3000 行 C 代码实现,无需外部依赖或堆分配即可生成 LLVM IR。它支持 defer、递归、多种类型、结构体、指针,并包含图形演示。

Strace-ui、Bonsai_term 与 TUI 复兴

Hacker News Top

Jane Street 的工程师介绍了 strace-ui,这是一个用于 strace 的交互式终端 UI,通过过滤、PID 追踪和手册页集成简化了系统调用调试,并强调了由其 Bonsai 框架推动的 TUI 复兴。