Операционные системы

Сигналы в 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
Сигналы в Unix

0

1

Войти