
Инструмент для наблюдения за сигналами ядра в реальном времени, использующий точки трассировки eBPF для потоковой передачи каждого сигнала, поданного на хосте Linux, с отображением отправителя, цели, статуса, задержки обработчика и прерываний системных вызовов.
sigwire
tail -fдля сигналов. Каждый сигнал, который генерирует любой процесс в системе — кто отправил, кому предназначен, какой сигнал, как он был вызван (kill(2), ядро, POSIX-таймер), перехватил ли его цель и как долго выполнялся обработчик, вырвал ли он заблокированный системный вызов с помощьюEINTR— декодируется из трассировочных точек ядра по сигналам и транслируется в реальном времени в ваш терминал. Безstrace -fдля одного PID, безptrace, без участия отслеживаемых процессов.
sigwire превращает механизм сигналов ядра в живую патч‑панель: каждая строка — это отправитель ──СИГНАЛ──▶ цель, окрашена по степени важности, помечена способом вызова, тем, перехватила ли цель сигнал (и как долго выполнялся обработчик), прервал ли он заблокированный системный вызов (↯ EINTR read), сворачивается в ×N при массовой отправке и отмечается ☠ когда это настоящее смертельное попадание. Боковая панель показывает статистику проходящих сигналов; поставьте на паузу и выберите строку, чтобы увидеть полную картину — диспозицию, адрес обработчика, флаги sigaction и сигналы, которые цель блокировала в этот момент.
Поскольку sigwire перехватывает трассировочные точки ядра, а не какой-то один процесс, один запуск отслеживает все сигналы на хосте одновременно — ваше приложение, супервизор, собственный механизм ошибок ядра — и ни один из них не знает, что за ним следят.
[!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` фонового задания или запустите любое управляемое окружение и наблюдайте, как его сборщик мусора/планировщик отправляет сигналы собственным потокам (`↯ 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 сворачивает пачку одинаковых сигналов в одну строку, а примечание справа указывает источник, затем — прерывание системного вызова, затем — диспозицию.
Каждая строка застывает в момент разрешения доставки и больше никогда не изменяется — поэтому пачка прокручивается как стабильный журнал, а не мерцающий агрегат.
Провод окрашен по степени серьёзности по той же 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, он не возобновляется — приложение должно повторить вызов. Забыть об этом — классическая, сводящая с ума, зависящая от времени ошибка («почему мой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)`, который её завершает. (Если обработчик только устанавливает флаг, а остальную работу делает позже — CPython, Go — здесь будет крошечное время; это их особенность, не sigwire.)
- **flags** — флаги `sigaction` для обработчика (`SA_RESTART`, `SA_SIGINFO`, `SA_NODEFER`, …).
- **TARGET BLOCKS** — сигналы, которые цель заблокировала (своим `sigprocmask`) в момент доставки, взятые напрямую из её `task_struct`.
`Esc` закрывает инспектор; `p` возобновляет прямую трансляцию.
## Что считается фатальным
Счётчик `☠ fatal` и значок `☠` на строках намеренно строги. Поскольку `signal_generate` срабатывает при *генерации*, sigwire не может узнать, установила ли цель обработчик — `SIGTERM` может быть перехвачен и превращён в корректное завершение, или полностью проигнорирован. Поэтому фатальным считается только однозначный случай:
- **Доставлен `SIGKILL`** — неперехватываемый, неигнорируемый, всегда фатальный; **или**
- **Сигнал с дампом ядра** (`SEGV`/`BUS`/`ABRT`/`ILL`/`FPE`/`TRAP`/`SYS`/`QUIT`), который **подняло само ядро** (синхронный сбой, а не пользовательский `kill`).
Всё остальное — `SIGTERM` от `systemd`, `SIGINT` от вашего `Ctrl-C`, `SIGPWR` от рантайма к своим потокам — показывается и раскрашивается, но не считается смертью, потому что ей, скорее всего, не было.
## Выбор сигналов (живой тумблер ядра)
Три сигнала — это просто фоновый шум на любой загруженной машине: `SIGCHLD` (каждый завершённый потомок), `SIGURG` (сердцебиение асинхронного вытеснения в Go) и `SIGWINCH` (изменение размера терминала, рассылается всем процессам переднего плана). sigwire по умолчанию **отключает** эти три сигналы **в ядре**, чтобы канал показывал только интересный трафик — но какие сигналы считать шумом, решаете вы.
Нажмите `s`, чтобы открыть **выбор сигналов**: модальный список всех сигналов с их живым цветом серьёзности и количеством замеченных, каждый можно переключить между `показывать` и `отключить`. Стрелками переходите к сигналу (или **введите его номер** — `1`, `5` → перейти к 15) и нажмите `space`, чтобы мгновенно переключить. `a` переключает **все** сразу. В строке заголовка счётчик `muted` показывает, сколько скрыто.
Это двусторонняя часть демо: маска отключения — это глобальная переменная `__u64` в секции `.data` работающей BPF-программы, и переключение строки патчит соответствующий бит через `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) (пользовательское пространство). Всё соотносится по паре `(tid цели, сигнал)`.
### Сторона 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` — глобальная `__u64` в секции `.data`; picker изменяет отдельные биты, чтобы отбрасывать сигналы в ядре.
- `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`, `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`, количество отключённых, live/paused |
| [`src/components/footer.jsx`](https://github.com/yeet-src/sigwire/blob/HEAD/src/components/footer.jsx) | подсказки по клавишам и строка ввода фильтра live |
| [`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 + сигналу + источнику + диспозиции |
Модель — это скользящая **лента сгенерированных сигналов**, объединяющая одинаковые повторения в строки с `×N`. Строка генерации сигнала фиксируется в момент, когда приходит её доставка — то есть уже отображаемая строка никогда не меняется и не прыгает. Таймер окна в 120 мс публикует один снимок за кадр, так что даже при загруженном кольцевом буфере происходит только один рендер, а не тысячи.
### Почему tracepoints, а не `strace`/`ptrace`
`strace -f` следует за одним деревом процессов и останавливает трассируемого при каждом событии; `ptrace` привязан к цели и интрузивен. Трассировочные точки сигналов — это стык, где *ядро* поднимает и доставляет сигнал для *всех* процессов, без настройки для каждого приложения и без остановки кого-либо. Сопоставление генерации ↔ доставки ↔ `rt_sigreturn` даёт пару отправитель/цель, диспозицию, задержку обработчика и вердикт EINTR, связывающие всю жизнь сигнала.
## Тестирование на разных ядрах
`make veristat` загружает `bin/probe.bpf.o` с veristat на **вашем** ядре — быстрая проверка, что все программы проходят верификацию, плюс сложность каждой программы (insns/states). Загрузка BPF требует привилегий, поэтому используйте `sudo`.
Программа, загружаемая на вашем ноутбуке, может быть отклонена верификатором более старого ядра. От этого защищает [`.github/workflows/kernel-matrix.yml`](https://github.com/yeet-src/sigwire/blob/HEAD/.github/workflows/kernel-matrix.yml): для каждого ядра в своей матрице он собирает объект, загружает это ядро в виртуальной машине ([cilium/little-vm-helper](https://github.com/cilium/little-vm-helper), образы из `quay.io/lvh-images`) и запускает встроенный статический **veristat** — если верификатор отклоняет любую программу, задача помечается как неуспешная, и результаты по каждому ядру сворачиваются в таблицу ✅/❌. Шлюз внутри VM — [`build/verify-kernel.sh`](https://github.com/yeet-src/sigwire/blob/HEAD/build/verify-kernel.sh).
Запустите ту же матрицу локально (Linux + KVM) с помощью `make veristat-matrix` — она загружает образы ядра через `lvh` + QEMU и выводит таблицу `ok`/`FAIL`. Выберите ядра с помощью `make veristat-matrix KERNELS="6.6 bpf-next"`.
## Требования
> [!IMPORTANT]
> - **Ядро Linux с BTF** (`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 также поставляется, и у проекта нет сторонних зависимостей.
## Честные оговорки
> [!NOTE]
> `sigwire` — это наблюдение, а не управление. Он показывает, что было поднято; он не блокирует, не задерживает и не изменяет ни один сигнал.
- **Строка соответствует *поднятому* сигналу.** Строка в коммутаторе приходит из генерации; цель может перехватить его, заблокировать или уже завершиться. Колонки диспозиции/обработчика/маски берутся со стороны *доставки* и заполняются только после того, как ядро действительно доставит сигнал — для заблокированного или ещё ожидающего сигнала диспозиция не показывается. См. [Что считается фатальным](#что-считается-фатальным).
- **Сопоставление выполняется по возможности.** Генерация и доставка — разные трассировочные точки без общего идентификатора, сопоставляются по `(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, независимо от того, когда были запущены отправитель или цель — нет пропущенного состояния для каждого процесса.
**Работает ли это для одного процесса или для всех?**
Для любого процесса на хосте, все одновременно — поле отправитель/цель их различает. Это трафик сигналов всей машины, а не одного pid.
**Можно ли экспортировать ленту?**
Встроенной возможности нет. Callback'и `RingBuf.subscribe` в `probes/sigwire.js` содержат каждую декодированную запись, так что реализация вывода в 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, разрешая псевдонимы времени сборки @/ (корень исходников) и #/ (корень проекта) через параметр paths в tsconfig и оставляя встроенные модули yeet:* внешними. Оба компилятора берутся из поставляемой статической тулчейна, поэтому для сборки не требуется системный C/BPF тулчейн и не нужны node/npm. Сгенерированные vmlinux.h, src/index.jsx и bin/*.bpf.o являются артефактами сборки.
Поскольку псевдонимы существуют только на этапе сборки, среда выполнения находит BPF-объект через import.meta.dirname, а не через псевдоним. Смотрите AGENTS.md (также известный как CLAUDE.md) в руководстве по созданию панели управления yeet.
Двойная лицензия BSD/GPL. BPF-программа объявляет char LICENSE[] SEC("license") = "Dual BSD/GPL" в файле src/bpf/sigwire.bpf.c, что требуется ядру для используемых ей хелперов.
Создано с помощью yeet, среды выполнения JavaScript для написания eBPF-программ на Linux. Присоединяйтесь к нам на Discord.
| серьёзность | сигналы | цвет |
|---|
| kill | SIGKILL | ярко-красный |
| фатальные (с дампом ядра) | 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 … | серый |