Операционные системы
Сигналы в Unix
Каждый Ctrl+C для остановки программы, каждый graceful shutdown сервера по SIGTERM, каждый "Segmentation fault" - за всем этим стоят сигналы. Это один из древнейших механизмов Unix (появился ещё в 1970-х), но до сих пор критичен для системного программирования. Понимание сигналов отличает джуниора от senior backend engineer.
- **Graceful shutdown сервисов.** Когда systemd перезапускает nginx, он отправляет SIGTERM. Nginx перехватывает сигнал, завершает текущие HTTP запросы, закрывает соединения, сохраняет логи и корректно завершается. Без обработки SIGTERM активные соединения оборвались бы ошибкой 502.
- **Мониторинг и health checks.** Kubernetes отправляет SIGTERM под'у при scale-down с grace period 30 секунд. Если процесс не завершился, следует SIGKILL. Production-ready приложения обрабатывают SIGTERM для корректного завершения долгих операций (DB transactions, network requests).
- **Debugging и profiling.** GDB использует SIGTRAP для breakpoints. Profilers (perf, gperftools) используют SIGPROF для sampling. strace перехватывает syscalls через ptrace + SIGTRAP. Понимание сигналов критично для debugging production issues.
Цели урока
- Знать ключевые сигналы: SIGTERM, SIGINT, SIGKILL, SIGSTOP, SIGSEGV, SIGCHLD
- Различать catchable (TERM, INT) и uncatchable (KILL, STOP)
- Понимать signal-safe functions: список async-signal-safe из man 7 signal-safety
- Применять sigaction (а не signal), маскировать сигналы в обработчике
- Real-time signals (SIGRTMIN-SIGRTMAX): очередь, payload, гарантия доставки
Основы сигналов
**Сигнал** - это программное прерывание, отправленное процессу операционной системой или другим процессом. Это механизм асинхронного уведомления о событиях: от пользовательских команд (Ctrl+C) до критических ошибок (segmentation fault).
Сигнал как телеграмма
Аналогия: процесс как офисный работник. Обычные данные поступают через почту (pipes, sockets) - их читают по расписанию. **Сигнал** - это курьер, который врывается прямо в офис с критическим сообщением: "ПОЖАР!", "НАЧАЛЬНИК ТРЕБУЕТ ОТЧЁТ!", "ОБЕДЕННЫЙ ПЕРЕРЫВ!". Работник **обязан** прервать текущую задачу и отреагировать.
В Unix существует около 30 стандартных сигналов. Каждый имеет номер и символическое имя. Самые важные: • **SIGINT (2)** - Ctrl+C, прерывание с клавиатуры • **SIGTERM (15)** - вежливая просьба завершиться • **SIGKILL (9)** - немедленное убийство (нельзя перехватить) • **SIGSEGV (11)** - segmentation fault • **SIGCHLD (17)** - дочерний процесс завершился
**Ключевое свойство сигналов - асинхронность.** Сигнал может прийти в ЛЮБОЙ момент выполнения программы: посреди функции, во время системного вызова, даже между инструкциями процессора. Это создаёт уникальные проблемы безопасности.
Каждый сигнал имеет **действие по умолчанию**: 1. **Term** - завершить процесс 2. **Ign** - игнорировать 3. **Core** - завершить + создать core dump (снимок памяти) 4. **Stop** - приостановить процесс 5. **Cont** - возобновить приостановленный процесс
Как работает команда kill
Команда `kill` НЕ убивает процессы - она просто отправляет сигналы! По умолчанию отправляет SIGTERM (15), который процесс может перехватить и игнорировать. SIGKILL (9) - единственный сигнал, который гарантированно убивает, потому что его **нельзя** перехватить или игнорировать. Ядро убивает процесс принудительно.
Почему SIGKILL гарантированно убивает процесс, а SIGTERM - нет?
Обработка сигналов
Процесс может изменить поведение при получении сигнала, установив **signal handler** - функцию, которая вызывается при получении сигнала. Это основа graceful shutdown, обработки Ctrl+C, реакции на таймауты.
**Проблема signal():** устаревший интерфейс с неопределённым поведением на разных системах. Современный код использует **sigaction()** - более мощный и предсказуемый API.
**Флаги sigaction (sa_flags):** • **SA_SIGINFO** - использовать расширенный handler с параметрами siginfo_t • **SA_RESTART** - автоматически перезапускать прерванные syscalls (read, write) • **SA_NODEFER** - не блокировать сигнал во время его обработки • **SA_RESETHAND** - сбросить handler к default после первого вызова
Graceful shutdown в production
При вызове `systemctl restart nginx` systemd отправляет SIGTERM процессу nginx. Nginx перехватывает сигнал, **не принимает новые соединения**, завершает обработку текущих запросов, закрывает log файлы, освобождает ресурсы и **только потом** завершается. Без graceful shutdown активные соединения оборвались бы ошибкой.
**Маскирование сигналов** - временная блокировка сигналов во время выполнения критической секции. Заблокированные сигналы становятся **pending** (ожидающими) и доставляются после разблокировки.
Пример: перезагрузка конфигурации
Nginx использует сигнал **SIGHUP** для перезагрузки конфигурации без остановки сервера. Handler читает новый конфиг, создаёт новые worker процессы с новой конфигурацией, а старые workers завершаются после обработки текущих запросов. Zero-downtime перезагрузка!
Что произойдёт, если заблокировать SIGTERM через sigprocmask(), а затем отправить процессу kill -TERM?
Signal-Safe Programming
**Async-signal-safe** - главная сложность программирования с сигналами. Signal handler может прервать программу в ЛЮБОЙ момент, даже посреди вызова malloc() или printf(). Это создаёт race conditions и приводит к deadlocks.
Почему printf() опасен в signal handler
Сценарий: основная программа вызывает printf(), который внутри берёт мьютекс на stdout. В этот момент приходит сигнал, вызывается handler, который ТОЖЕ пытается взять мьютекс на stdout через printf(). **Deadlock!** Handler ждёт мьютекс, который держит основная программа, но она приостановлена до завершения handler. Вечное ожидание.
**Async-signal-safe функции (разрешены в handler):** • **I/O:** read(), write(), close(), pipe() • **Process:** _exit(), fork(), kill(), sigaction() • **Memory:** mmap(), munmap() (НО НЕ malloc/free!) • **Sync:** sem_post() (НО НЕ mutex!) ПОЛНЫЙ список: `man 7 signal-safety` **Запрещено:** printf, malloc, free, pthread_mutex_*, fopen, exit() и большинство библиотечных функций
**Классический паттерн: self-pipe trick.** Handler не делает тяжёлую работу, а только **сигнализирует** основному циклу через pipe. Основной цикл обрабатывает сигнал в безопасном контексте (не в handler).
Linux signalfd - современный подход
Linux предлагает **signalfd()** - конвертирует сигналы в файловый дескриптор! Можно использовать сигналы в event loop (epoll, select) вместе с сокетами и таймерами. Никаких handler - просто читаешь сигналы как данные из файла.
**Реентерабельность (reentrancy)** - ключевое требование к async-signal-safe функциям. Функция должна корректно работать, даже если вызывается сама из себя (через прерывание сигналом).
Почему malloc() и free() запрещены в signal handler?
Real-Time Сигналы
**Real-time сигналы (POSIX.1b)** - расширение стандартных сигналов с гарантированной доставкой и очерёдностью. Стандартные сигналы (SIGINT, SIGTERM) имеют недостатки: если отправить 3 SIGTERM подряд, процесс получит только 1 (сигналы не queued). Real-time сигналы решают эту проблему.
**Отличия real-time сигналов от стандартных:** • **Guaranteed delivery** - каждый отправленный сигнал будет доставлен (в пределах лимита очереди) • **Ordering** - сигналы доставляются в порядке приоритета и времени отправки • **Payload** - можно передать данные (int или pointer) вместе с сигналом • **Range:** SIGRTMIN (34) до SIGRTMAX (64) - 30+ дополнительных сигналов
Use case: асинхронный I/O
Linux AIO (Asynchronous I/O) использует real-time сигналы для уведомлений. Когда диск завершает операцию чтения, kernel отправляет SIGRTMIN с payload - указателем на структуру aiocb. Программа получает уведомление о завершении без polling и blocking.
**Приоритеты real-time сигналов:** Lower number = higher priority. SIGRTMIN имеет наивысший приоритет, SIGRTMAX - низший. Если pending несколько сигналов, сначала доставляются высокоприоритетные.
**Ограничения real-time сигналов:** Kernel имеет лимит на количество queued сигналов на пользователя (обычно RLIMIT_SIGPENDING ~ 16000). Если очередь переполнена, sigqueue() вернёт EAGAIN. Проверяй лимиты через `ulimit -i`.
Real-time vs стандартные сигналы
**Стандартные:** Отправишь 100 SIGUSR1 подряд - процесс получит 1 или 2 (сигналы сливаются). Используй для событий, где количество не важно: "перезагрузить конфиг". **Real-time:** Отправишь 100 SIGRTMIN - процесс получит все 100 в порядке отправки. Используй для задач, где важна каждая: "обработать транзакцию 42", "завершить запрос 101".
Сигналы - это просто функции, которые вызываются при событии, работают как обычные callbacks
Сигналы - асинхронные прерывания, которые могут прийти в ЛЮБОЙ момент и прервать программу между любыми двумя инструкциями, создавая уникальные проблемы безопасности
Обычный callback вызывается в определённой точке программы (event loop, после завершения операции). Signal handler вызывается асинхронно: может прервать программу посреди malloc(), во время удержания мьютекса, между чтением и записью глобальной переменной. Это приводит к race conditions, deadlocks и corruption данных. Именно поэтому существует строгий список async-signal-safe функций и запрещены printf/malloc/mutex в handlers. Сигналы - низкоуровневый механизм IPC, требующий глубокого понимания kernel и concurrency.
В чём ключевое преимущество real-time сигналов (sigqueue) над стандартными (kill)?
Ключевые идеи
- **Сигналы - асинхронные программные прерывания.** Могут прийти в любой момент выполнения программы. Kernel доставляет сигналы при переходе из kernel mode в user mode. SIGKILL и SIGSTOP нельзя перехватить - kernel убивает процесс принудительно.
- **Обработка сигналов: signal() устарел, используй sigaction().** Современный API с предсказуемым поведением, поддержкой флагов (SA_RESTART, SA_SIGINFO), маскированием сигналов. Graceful shutdown через обработку SIGTERM/SIGHUP - стандартная практика production систем.
- **Async-signal-safety - главная сложность.** В signal handler разрешены только async-signal-safe функции (write, _exit, sigaction). Запрещены: printf, malloc, mutex. Self-pipe trick или signalfd() для безопасной обработки в main loop.
- **Real-time сигналы - guaranteed delivery + payload.** Стандартные сигналы сливаются (pending бит), real-time queued. Можно передать данные (int/pointer), есть приоритеты. Используются в AIO, task scheduling, приоритетных очередях.
Связанные темы
Сигналы - фундаментальный IPC механизм, связанный с процессами, синхронизацией и системным программированием:
- Процессы и IPC — Сигналы - один из механизмов IPC. Другие: pipes, shared memory, message queues. Сигналы - единственный асинхронный механизм доставки событий
- Системные вызовы — kill(), sigaction(), sigprocmask(), signalfd() - системные вызовы для работы с сигналами. Понимание syscalls критично для эффективного использования сигналов
- Concurrency и синхронизация — Signal handlers - форма асинхронного выполнения кода, аналог interrupt handlers. Те же проблемы: race conditions, reentrancy, atomicity
- Файловые дескрипторы и I/O — signalfd() конвертирует сигналы в FD, интегрируя их в event-driven архитектуры (epoll/select). Self-pipe trick использует pipe() для безопасной обработки
Вопросы для размышления
- Почему современные асинхронные фреймворки (Node.js, tokio, asyncio) редко используют сигналы напрямую, предпочитая signalfd/event loops?
- Как спроектировать graceful shutdown для микросервиса с активными HTTP соединениями, DB транзакциями и background jobs? Какие сигналы использовать?
- В чём фундаментальная разница между сигналами (push notification от kernel) и polling (программа сама проверяет флаги)? Когда polling предпочтительнее?
Связанные уроки
- os-13-ipc — Сигналы - один из механизмов IPC, легковесная асинхронная нотификация
- os-02-processes — Нужно понимать жизненный цикл процессов для работы с сигналами
- net-14-udp — UDP и сигналы: fire-and-forget, без гарантии доставки
- os-15-syscalls — Сигналы обрабатываются через системные вызовы (sigaction)
- rt-18