Ориентация главы
Модуль signal объединяет POSIX-сигналы и консольные события Windows в модель наблюдателя сигналов: дескриптор xsignalwatch плюс семейство регистрации xrtSignalOn/Once/OnOwned/OnceOwned подписывает на сигналы; события доставляются как асинхронные уведомления (не стеснённая среда сигнальных обработчиков); семантика once подходит сигналам класса SIGINT «обработать один раз»; автозачистка предотвращает висячие срабатывания на путях выхода. Три высокочастотных сигнала — SIGINT (Ctrl+C), SIGTERM (команда остановки системы оркестрации) и SIGHUP (традиционный сигнал перезагрузки конфигурации) — три главных входа управления жизненным циклом службы.
49.1引入
Писать сигнальные обработчики вручную — знаменитое минное поле C: внутри обработчика сигнала безопасна лишь горстка функций (printf/malloc среди них нет — лог или распределение памяти в обработчике есть неопределённое поведение); кроссплатформенные различия (в Windows нет настоящих POSIX-сигналов, Ctrl+C — консольное событие); связь обработчика с главным циклом — только через ограниченные механизмы вроде volatile sig_atomic_t. Каждый пункт — уже исхоженная кем-то яма.
Решение XRT — наблюдатели + асинхронная доставка: при регистрации наблюдатель объявляет интересующие сигналы; при наступлении сигнала модуль доставляет событие вашему обратному вызову в безопасном контексте (не в исходном обработчике) — в обратном вызове весь API библиотеки доступен как обычно; xsignalevent несёт имя сигнала и счётчик (многократные приходы одного сигнала сливаются в счёт). Проблема стеснённой среды обходится структурно, а не зубрёжкой списков сигнал-безопасных функций.
49.2概念
Модель наблюдателя
Семантику счётчика стоит отметить: обратный вызов может получить слитые приходы (событие несёт count) — «Ctrl+C нажали трижды» это одно событие с count=3, а не три вызова: намерение быстро нажимающего пользователя (принудительный выход) не будет проглочено задержкой позапросной обработки.
Семантика once и owned-on
Образец signal_tour разворачивает две продвинутые семантики: once — наблюдатель после одного срабатывания отписывается сам (сценарий SIGINT «первый Ctrl+C — грациозный выход»: второй Ctrl+C, означающий «убей уже», перехватывать больше не нужно); owned-on — жизненный цикл наблюдателя привязан к владельцу (счётчик автоуничтожения проверяет destroyed=1), и путь выхода не повисает. Две семантики вместе закрывают традиционно трудное «завершение сигнальной обработки».
Три входных сигнала
| Сигнал | Триггер | Стандартный ответ |
|---|---|---|
| SIGINT | Ctrl+C (интерактив) | грациозный выход: перестать принимать, довести находящееся в работе, принудительный выход по таймеру |
| SIGTERM | остановка оркестратора (k8s/docker stop) | то же - главный канал выхода в контейнерной среде |
| SIGHUP | отключение терминала / традиционный сигнал перезагрузки | перезагрузка конфигурации: перечитать, атомарно переключить (глава 46) |
Три входа сходятся на шаблоне грациозный выход/перезагрузка: получили сигнал → выставили флаг остановки/перезагрузки → главный цикл проверяет флаг → идём путём завершения (Flush журналов, закрытие дескрипторов, ожидание запросов в полёте) — сигнальный обратный вызов только ставит флажок, тяжёлая работа остаётся главному циклу (тот же корень, что дисциплина лёгких обратных вызовов главы 48).
Разделение с процессами главы 53
Модуль signal этой главы ведает приёмом (как мой процесс отвечает на сигналы); модуль process главы 53 — отправкой (как я подаю сигнал дочернему процессу — лестница SIGTERM→SIGKILL при убийстве по тайм-ауту). Обе стороны делят имена сигналов (xrtSignalName), но API у каждого своё — та же логика разделения глав, что у файлового семейства.
49.3示例
Полная программа: наблюдение сигналов и счётчик
Из образца репозитория examples/process/signal/main.c:
#include <stdio.h> #include <xrt.h> /* 示例回调在普通 XRT 线程上下文记录信号。 */ static void exampleSignal( xsignalwatch* pWatch, const xsignalevent* pEvent, ptr pData ) { xatomic32* pReceived = (xatomic32*)pData; (void)pWatch; printf("signal=%s count=%u total=%llu\n", pEvent->Name, (unsigned)pEvent->Count, (unsigned long long)pEvent->Total); xrtAtomic32Store(pReceived, 1u, XMEMORY_RELEASE); } */ /* 订阅本进程中断信号并演示可自动回归的投递路径。 */ int main(void) { xatomic32 Received = XRT_ATOMIC32_INIT(0u); xsignalwatch* pWatch = xrtSignalOn( XSIGNAL_INT, exampleSignal, &Received ); double iDeadline = xrtTimer() + 3; if ( (pWatch == NULL) || !xrtSignalRaise(XSIGNAL_INT) ) { xrtSignalFree(pWatch); return 1; } while ( xrtAtomic32Load(&Received, XMEMORY_ACQUIRE) == 0u ) { if ( xrtTimer() >= iDeadline ) { xrtSignalFree(pWatch); return 2; } xrtSleep(1u); } xrtSignalFree(pWatch); return xrtSignalShutdown() ? 0 : 3; }
Что только что произошло. (1) Наблюдатель регистрирует набор сигналов и обратный вызов — подписку хранит дескриптор xsignalwatch. (2) После прихода сигнала (Ctrl+C) событие доставляется в обратный вызов — xsignalevent несёт signal=INT (имя) и count=1 (число слитых приходов); total — собственный накопительный счёт образца. (3) Обратный вызов исполняется в нормальной среде — printf употреблён напрямую (в исходном обработчике сигнала это неопределённое поведение; модель наблюдателя делает это легальным). (4) Событие несёт и Total (накопительный итог), и Name (строковое имя) — отсюда три поля signal=INT count=1 total=1. Программа после сигнала выходит упорядоченно, зачистка наблюдателя выполняется автоматически.
Полная программа: обзор всех интерфейсов
Из examples/process/signal_tour/main.c — запросы поддержки, owned-on, once и управление счётчиком:
*/ #include <stdio.h> #include <xrt.h> #include <math.h> static inline double exampleTimerLimit(int64 Timeout) { return Timeout == XRT_WAIT_FOREVER ? INFINITY : xrtTimer() + (double)Timeout / 1000.0; } static inline bool exampleTimerExpired(double Limit) { return xrtTimer() >= Limit; } static inline int64 exampleTimerRemaining(double Limit) { double Ms; if (Limit == INFINITY) return XRT_WAIT_FOREVER; Ms = ceil((Limit - xrtTimer()) * 1000.0); return Ms <= 0 ? 0 : Ms >= 0x1p63 ? INT64_MAX : (int64)Ms; } static volatile int g_Destroyed = 0; static volatile int g_Fired = 0; static void exampleFree(ptr pData) { (void)pData; g_Destroyed = g_Destroyed + 1; } static void exampleOnCallback(xsignalwatch* pWatch, const xsignalevent* pEvent, ptr pData) { (void)pWatch; (void)pData; (void)pEvent; g_Fired = g_Fired + 1; } static void exampleOnceCallback(xsignalwatch* pWatch, const xsignalevent* pEvent, ptr pData) { (void)pWatch; (void)pData; (void)pEvent; g_Fired = g_Fired + 1; } static bool exampleSpinFlag(volatile int* pFlag, int iExpect) { double iDeadline = exampleTimerLimit(INT64_C(3000)); while ( *pFlag < iExpect ) { if ( exampleTimerExpired(iDeadline) ) { return false; } xrtThreadYield(); } return true; } int main(void) { xsignalwatch* pOwned = NULL; xsignalwatch* pOnce = NULL; xsignalwatch* pOnceOwned = NULL; xsignalwatch* pRef = NULL; int iResult = 1; /* ---- 元数据:Supported 正反 + Name + Healthy ---- */ if ( !xrtSignalSupported(XSIGNAL_INT) || !xrtSignalSupported(XSIGNAL_TERM) || /* Windows 无 TERM */ (xrtSignalName(XSIGNAL_INT) == NULL) || (xrtSignalName(XSIGNAL_NONE) == NULL) || !xrtSignalHealthy() ) { goto Cleanup; } printf("signal: supported=1/1 name=%s healthy=1\n", xrtSignalName(XSIGNAL_INT)); /* ---- OnOwned:投递一次并等回调 + 析构恰好一次 ---- */ g_Fired = 0; g_Destroyed = 0; pOwned = xrtSignalOnOwned(XSIGNAL_INT, exampleOnCallback, NULL, exampleFree); if ( (pOwned == NULL) || !xrtSignalActive(pOwned) || (xrtSignalCode(pOwned) != XSIGNAL_INT) ) { goto Cleanup; } /* Ref:增引用(Off 后再 Free 一次配平)。 */ pRef = xrtSignalRef(pOwned); if ( (pRef != pOwned) || !xrtSignalActive(pRef) ) { goto Cleanup; } if ( !xrtSignalRaise(XSIGNAL_INT) || !exampleSpinFlag(&g_Fired, 1) ) { goto Cleanup; } if ( !xrtSignalOff(pOwned) || xrtSignalActive(pOwned) ) { goto Cleanup; } * 再释放 Ref 那份(最后一个 → 恰好析构一次)。 */ xrtSignalFree(pOwned); pOwned = NULL; if ( g_Destroyed != 0 ) { goto Cleanup; } xrtSignalFree(pRef); pRef = NULL; if ( g_Destroyed != 1 ) { goto Cleanup; } printf("signal: owned-on fired=%d destroyed=%d\n", g_Fired, g_Destroyed); /* ---- Once:第一次投递触发后自动注销 ---- */ g_Fired = 0; pOnce = xrtSignalOnce(XSIGNAL_INT, exampleOnceCallback, NULL); if ( (pOnce == NULL) || !xrtSignalActive(pOnce) || !xrtSignalRaise(XSIGNAL_INT) || !exampleSpinFlag(&g_Fired, 1) ) { goto Cleanup; } xrtSignalFree(pOnce); pOnce = NULL; /* OnceOwned:一次性 + 析构器(fired 从上一段的 1 续计)。 */ g_Destroyed = 0; pOnceOwned = xrtSignalOnceOwned(XSIGNAL_INT, exampleOnceCallback, NULL, exampleFree); if ( (pOnceOwned == NULL) || !xrtSignalRaise(XSIGNAL_INT) || !exampleSpinFlag(&g_Fired, 2) ) { /* 上一 Once 累计 */ goto Cleanup; } /* Once 在首次调度时已自动注销:无需再 Off,直接释放。 */ xrtSignalFree(pOnceOwned); pOnceOwned = NULL; if ( g_Destroyed != 1 ) { goto Cleanup; } printf("signal: once fired=%d destroyed=%d\n", g_Fired, g_Destroyed); /* ---- Count / Received / Clear ---- */ { uint64 iBefore = xrtSignalCount(XSIGNAL_INT); if ( !xrtSignalReceived(XSIGNAL_INT) || (xrtSignalCount(XSIGNAL_INT) < 2u) || (iBefore < 2u) ) { goto Cleanup; } if ( !xrtSignalClear(XSIGNAL_INT) || xrtSignalReceived(XSIGNAL_INT) || (xrtSignalCount(XSIGNAL_INT) != 0u) ) { goto Cleanup; } } printf("signal: count/received/clear ok\n"); /* ---- Ignore / Restore / RestoreAll ---- */ if ( !xrtSignalIgnore(XSIGNAL_INT) || !xrtSignalRestore(XSIGNAL_INT) || !xrtSignalIgnore(XSIGNAL_INT) || !xrtSignalRestoreAll() ) { goto Cleanup; } printf("signal: ignore/restore/all ok\n"); iResult = 0; Cleanup: xrtSignalFree(pOnceOwned); xrtSignalFree(pOnce); xrtSignalFree(pRef); xrtSignalFree(pOwned); return iResult; }
Что только что произошло. (1) Запросы поддержки (supported=1/1) и преобразование имён (name=INT) — сигнальные возможности зондируются по платформе (в портативном коде сперва спросите, потом регистрируйте). (2) owned-on: срабатывание в области действия владельца (fired=1), авточистка при уничтожении владельца (destroyed=1) — привязка жизненного цикла предотвращает висячесть. (3) once: два срабатывания (fired=2), затем саморазрушение однократного наблюдателя (destroyed=1) — механизм «первый перехватил, остальные пропустил». (4) Счёт, выборка и очистка (count/received/clear ok) — вход потребления в стиле опроса. (5) Игнорирование, восстановление и пакетная зачистка (ignore/restore/all ok) — управление сигнальной маской: временно заглушить сигнал и вернуть как было, пакетная отписка напоследок.
49.4契约
- Безопасная среда: обратные вызовы исполняются в нормальном контексте — весь API библиотеки доступен; стеснённость исходного обработчика переваривается внутри модуля.
- Слитый счёт: многократные приходы одного сигнала сливаются в одно событие (поле count); быстрые повторные нажатия не теряют намерения пользователя.
- once/owned: семантика once (самоуничтожение после срабатывания) и привязка к владельцу (зачистка вместе с ним) — пути завершения не висят.
- Лёгкие обратные вызовы: обратный вызов только ставит флажок или ставит в очередь; тяжёлая работа — главному циклу (дисциплина главы 48 в сигнальной редакции).
- Разделение приёма и отправки: здесь приём; подача сигналов дочерним процессам — модуль process главы 53.
- Зонд возможностей: поддержка сигналов запрашивается по платформе — портативный код сперва зондирует, потом регистрирует.
От примеров к инженерии: три хозяина сигнальной обработки
Жизненный цикл службы (арена трёх входов): на старте регистрируются once-наблюдатели SIGINT/SIGTERM → первый сигнал ставит флаг остановки → главный цикл завершает (прекратить приём, ожидание находящегося в работе с лимитом, Flush журналов, закрытие дескрипторов) → выход; после отписки once второй сигнал идёт по умолчанию — завершение — выход принудительный.Горячая перезагрузка конфигурации: SIGHUP запускает конвейер перезагрузки (конфигурация главы 43 → diff → атомарное переключение) — сигнал только триггер, вся тяжёлая работа на проторенных путях главного цикла.Надзор за потомками: в сцепке с главой 52 — события класса SIGCHLD у родителя входят через наблюдателя в поток жнеца (сбор ресурсов дочерних процессов); подающая сторона — в модуле process. Сигнальные обратные вызовы всех трёх хозяев одной формы: поставить флажок или поставить в очередь, несколько строк — и готово — полное согласие с дисциплиной лёгкой работы главы 48.
Растворение исторического багажа: почему сигнальный API таков
Чтобы понять дизайн API этой главы, нужно знать, что он растворяет. Обработчик POSIX-сигнала исполняется «в произвольно вытесненном контексте» — ваш главный поток может быть прерван сигналом, держа внутренний замок malloc, и вызов malloc в обработчике входит в тот же замок повторно — deadlock. Поэтому классическая обработка сигналов допускает лишь async-signal-safe функции (очень короткий список), и даже printf запрещён. Модель наблюдателя XRT делает в низкоуровневом обработчике минимальный захват и переносит доставку в безопасный контекст — поэтому ваш обратный вызов «выглядит как обычная функция». Это не прятанье сложности, а перенос сложности в слой, где ей место: дисциплину стеснённой среды несёт модуль, бизнес-обратные вызовы сохраняют непосредственность обычного кода. Та же мысль встретится вам в главе 47 (системные обратные вызовы → пул задач) и в предпросмотре главы 63 (уведомления о завершении IO) — «минимизировать на границе, нормализовать в бизнесе» есть универсальная философия асинхронного дизайна XRT.
Полный список: сигналы и грациозный выход
Грациозный выход — кульминационный сценарий сигнальной обработки; полный список завершает главу: регистрация — once-наблюдатели SIGINT/SIGTERM (первый перехват); флажок — атомарный бит остановки, единственное действие обратного вызова; порядок завершения — прекратить приём (закрыть слушателей) → ожидание находящегося в работе (с дедлайном) → Flush (журналы/файлы) → закрытие дескрипторов (в порядке обратном зависимостям) → код выхода (сознательно различить грациозный и по тайм-ауту); выход по тайм-ауту — срок истёк или второй сигнал — немедленный принудительный выход, без бесконечного ожидания; тест — сигнальный путь репетируется (CI посылает себе сигнал и проверяет полноту завершения). Шесть пунктов списка — критерии оценки упражнения-вызова и одновременно приёмочный лист «действий остановки» каждой выходящей в продакшен службы.
Типичные ошибки
Симптом: изредка deadlock или крушение во время обработки сигнала — Flush журнальной системы в обратном вызове сталкивается с тем же Sink главного потока; блокировка аллокатора в обратном вызове сталкивается с тем же замком, который держал прерванный поток.
Причина: сигналы приходят когда угодно — обратный вызов исполняется параллельно с любым кодом главного потока; прикосновение к нереентерабельным ресурсам, которые главный поток использует (журналы, аллокатор, контейнеры), это гонка.
static void onSignal(const xsignalevent* pEvent, ptr pData) { StopAllWorkers(); /* тяжело: остановить пулы потоков, Flush журналов - коллекция гонок */ ExitProcess(0); }
static volatile bool gStop = false; /* флажок (или атомарный) */ static void onSignal(const xsignalevent* pEvent, ptr pData) { gStop = true; /* легко: только выставить флажок */ } /* главный цикл проверяет gStop -> идёт путём завершения (остановка пулов/Flush/закрытие - безопасный контекст) */
Симптом: служба застревает на пути завершения — пользователь жмёт Ctrl+C без ответа, остаётся kill -9; какое-то ожидание в коде завершения никогда не истекает.
Причина: первый сигнал запустил грациозный выход, но путь завершения без тайм-аута; последующие Ctrl+C тем же наблюдателем продолжают «грациозно проглатываться» — намерение пользователя «принудительно выйти» игнорируется.
xrtSignalOn(SIGINT, gracefulSignal, NULL); /* постоянно: каждый сигнал грациозен - выхода нет */ static void gracefulSignal(...) { gStop = true; } /* главный цикл: while(!gStop) ... завершение WaitAll() может ждать вечно */
xrtSignalOnce(SIGINT, firstSignal, NULL); /* семантика once: перехватить только первый */ static void firstSignal(...) { gStop = true; } /* завершение с лимитом времени (дедлайн главы 42); тайм-аут или второй сигнал - немедленный выход */ /* после отписки once второй Ctrl+C идёт по поведению по умолчанию (завершение) - принудительный выход */
Практические задания
Зарегистрируйте наблюдателей SIGINT/SIGTERM/SIGHUP, выставляющих флажки остановки и перезагрузки; главный цикл проверяет раз в секунду и печатает изменения состояния (имитация двух путей: грациозный выход и горячая перезагрузка).
Реализуйте полный поток выхода: первый сигнал → прекратить приём → ожидание находящегося в работе (лимит 5 секунд) → Flush → выход; уложились — вывести «graceful», по тайм-ауту — принудительный выход с выводом «forced». Двумя потоками сымитируйте работу в полёте и проверьте семантику ожидания.
SIGHUP запускает перезагрузку: перечитать трёхслойную конфигурацию главы 43 → diff старого и нового деревьев значений (обход главы 31) → печать изменившихся полей и применение (уровень журнала — немедленно; адрес прослушивания — отказ и откат). Критерии приёмки: перезагрузка без прерывания службы; нелегальная конфигурация отвергнута, старая продолжает обслуживать; список изменений полон и пригоден для аудита.
Итоги
| Тема | Кратко |
|---|---|
| Семейство регистрации | xrtSignalOn/Once/OnOwned/OnceOwned; отписка xrtSignalOff/xrtSignalFree |
| Безопасная среда | обратные вызовы в нормальном контексте - printf/журналы/контейнеры доступны (стеснённость переварена модулем) |
| Поля события | Name имя / Count слито на этот раз / Total накопительно; потребление опросом - count/received/clear |
| once/owned | самоуничтожение после одного срабатывания / зачистка вместе с владельцем - завершения не висят |
| Три входа | SIGINT интерактивный выход / SIGTERM остановка оркестратора / SIGHUP перезагрузка конфигурации |
| Дисциплина лёгкой работы | обратный вызов ставит флажок, главный цикл завершает - сигнальная редакция закона асинхронной границы |
| Управление маской | игнорирование/восстановление/пакетная отписка - временно заглушить сигнал и вернуть; разделение приёма и подачи: подача - глава 53 |
📖 Справочники API в этой главе:signal (26 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки