Сигналы Unix
Сигнал Unix — асинхронное уведомление процессу или потоку о событии: завершении, ошибке, таймере, изменении размера терминала или запросе остановки. Это не надёжная очередь сообщений и не замена IPC. У сигнала есть номер, действие по умолчанию и состояние доставки; обычные (standard) сигналы могут сливаться в один pending-сигнал, поэтому в обработчик не приходит счётчик одинаковых событий.
Зачем это на интервью
Сигналы проверяют понимание жизненного цикла процесса в Linux и production-шutdown. Интервьюер может спросить, почему контейнер не завершился за grace period, чем SIGTERM отличается от SIGKILL, как корректно остановить Go HTTP-сервер и почему нельзя делать сложную работу в C signal handler. Хороший ответ связывает модель ядра, supervisor (systemd/Kubernetes) и контекст отмены приложения.
Минимум для E4
- Различать
SIGTERM(вежливая просьба завершиться),SIGINT(обычно Ctrl-C),SIGKILL(немедленное неперехватываемое завершение) иSIGSTOP(неперехватываемая остановка). - Знать, что
SIGKILLиSIGSTOPнельзя поймать, заблокировать или проигнорировать. - Понимать, что default action может завершить процесс, создать core dump, остановить, продолжить или игнорировать его.
- Запускать graceful shutdown по
SIGTERM: перестать принимать новую работу, отменить фоновые операции, дождаться ограниченное время и закрыть ресурсы. - Не выполнять в async signal handler небезопасные операции: выделение памяти,
printf, mutex и большинство библиотечных вызовов.
В оркестрируемом сервисе последовательность обычно такова: supervisor посылает SIGTERM, ждёт заданный deadline, затем применяет SIGKILL. SIGKILL не даёт приложению сделать cleanup, поэтому корректность должна опираться на атомарность, идемпотентность и внешние гарантии, а не только на финальный обработчик.
Углубление для E5/Senior
Генерация, pending и доставка
Сигнал можно направить процессу (kill(pid, sig)), process group, конкретному потоку (pthread_kill, Linux tgkill) или получить от ядра. Если сигнал заблокирован mask-ой, он становится pending до разблокирования. У process-directed сигнала ядро выберет подходящий поток, который не блокирует этот сигнал; thread-directed сигнал доставляется адресному потоку. В многопоточном процессе маска сигналов — свойство потока, а disposition (ignore/default/handler) — свойство процесса.
Для обычных сигналов ядро обычно хранит факт pending, а не все повторения. Десять SIGTERM, пришедших во время блокировки, могут превратиться в одну доставку. Realtime-сигналы (SIGRTMIN…SIGRTMAX) ставятся в очередь в порядке приоритета и могут нести sigqueue value, но имеют системные ограничения и редко нужны прикладному серверу.
Сигнал может прервать блокирующий системный вызов, вернув EINTR. С sigaction флаг SA_RESTART просит ядро автоматически перезапускать часть вызовов, но не все и не во всех условиях. Нельзя писать логику как «все вызовы всегда перезапускаются»: контракт конкретного syscall важнее. При уже прочитанных байтах следует обработать результат, а не безусловно повторять операцию. Связь прерываний с I/O разобрана в файловых дескрипторах.
Почему обработчик должен быть минимальным
Во время signal handler процесс может быть прерван посреди malloc, printf или захваченного mutex. Повторный вход в эти функции способен повредить состояние или навсегда заблокироваться. POSIX разрешает в async handler только небольшой набор async-signal-safe функций; на практике самый надёжный C-паттерн — установить volatile sig_atomic_t флаг либо записать байт в заранее созданный pipe, а полную работу оставить основному event loop.
Linux предоставляет альтернативы, которые упрощают серверный код:
signalfdпревращает доставку сигналов в читаемый FD; нужные сигналы надо заблокировать во всех потоках и читать из event loop.- self-pipe trick будит
poll/selectзаписью из handler, но pipe должен быть non-blocking, а переполнение обработано. pidfd_send_signalснижает риск послать сигнал уже переиспользованному PID при управлении дочерними процессами.
Выбор зависит от среды. signalfd — Linux-specific, обычный sigaction переносимее POSIX. Для маленького Go-сервиса предпочтительнее os/signal и context, а не raw handler.
Process group, session и контейнеры
Терминал отправляет Ctrl-C как SIGINT foreground process group, а не обязательно одному процессу. Shell применяет job control через process groups и SIGTSTP/SIGCONT. SIGHUP исторически означал потерю controlling terminal; daemon-ы часто используют его для reload, но это лишь соглашение, которое нужно документировать и тестировать.
PID 1 имеет особое значение в namespace контейнера: его обработка сигналов и reaping дочерних процессов влияют на весь контейнер. Если shell-скрипт является entrypoint и не делает exec приложения, сигнал может остаться у shell, а дочерний сервис не получит его. Entrypoint должен передавать сигналы и пожинать потомков либо заменяться приложением через exec; для нескольких процессов используют init/supervisor. Нельзя предполагать, что Kubernetes или systemd сами исправят неверную process tree.
Reload, observability и контракт остановки
SIGHUP для reload конфигурации безопасен только при транзакционном подходе: прочитать и валидировать новую конфигурацию отдельно, собрать новый immutable snapshot, затем атомарно заменить ссылку. Не мутируйте глобальную конфигурацию по частям из signal handler. SIGUSR1/SIGUSR2 оставляют приложению семантику, но зависят от документации команды; для административного API часто лучше аутентифицированный HTTP/RPC endpoint с аудитом.
Shutdown должен иметь бюджет: прекратить ingress, объявить readiness false, закрыть listener, дождаться in-flight запросов, остановить consumers и flush логов в пределах deadline. Метрики и логи фиксируют момент получения сигнала, число активных запросов, превышение deadline и причину forced termination. Это связывает сигналы с проектированием Go-приложений и Linux, сетями и cloud.
Ключевые понятия
Важные сигналы
| Сигнал | Обычное назначение | Можно обработать? | Практическая семантика |
|---|---|---|---|
SIGINT | прерывание с терминала | да | локально остановить процесс; в сервисе обрабатывать как shutdown |
SIGTERM | запрос завершения | да | штатное завершение от supervisor-а |
SIGQUIT | выход с диагностикой | да | default обычно core dump; не путать с SIGTERM |
SIGHUP | hangup/reload по соглашению | да | только явно реализованный reload |
SIGCHLD | изменилось состояние потомка | да | вызвать wait/waitpid, чтобы не копились zombie |
SIGPIPE | запись в закрытый pipe/socket | да | default завершает процесс; часто игнорируют и обрабатывают EPIPE |
SIGKILL | безусловно завершить | нет | последняя мера, cleanup не гарантирован |
SIGSTOP / SIGCONT | остановить / продолжить | STOP — нет, CONT — да | job control и управление процессами |
SIGPIPE — частая неожиданность в Unix-программах: запись в peer, который уже закрыл соединение, может одновременно дать EPIPE и вызвать сигнал. Политика зависит от runtime: Go для сетевых соединений обычно возвращает ошибку записи вместо внезапного завершения, но прикладной код всё равно обязан обработать эту ошибку и отмену контекста.
Go: один сигнал — одна причина отмены
func run(ctx context.Context) error {
srv := &http.Server{Addr: ":8080", Handler: handler()}
errCh := make(chan error, 1)
go func() {
errCh <- srv.ListenAndServe()
}()
select {
case err := <-errCh:
if !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
case <-ctx.Done():
timeoutCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
return srv.Shutdown(timeoutCtx)
}
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := run(ctx); err != nil {
log.Fatal(err)
}
}signal.NotifyContext отменяет context при первом указанном сигнале. Вызов stop важен: он освобождает регистрацию и возвращает последующие сигналы к обычной семантике. Нередко полезно после начала shutdown вызвать stop, чтобы второй Ctrl-C завершил процесс без ожидания; при этом политику forced exit нужно явно решить и покрыть тестами. Не вызывайте os.Exit из goroutine shutdown: deferred cleanup не выполнится, а main не сможет вернуть ошибку.
Для дочерней команды exec.CommandContext отменяет context, но конкретные последствия зависят от платформы и процесса. Если команда создаёт потомков, может потребоваться управление process group и отдельная стратегия остановки; отправка сигнала только родителю не обязательно останавливает всё дерево.
Типовые вопросы
- Почему
SIGTERMне гарантирует завершение процесса?- Его можно обработать, заблокировать или проигнорировать; процесс может зависнуть. Supervisor ограничивает ожидание и затем применяет
SIGKILL, который гарантирует прекращение, но не cleanup.
- Его можно обработать, заблокировать или проигнорировать; процесс может зависнуть. Supervisor ограничивает ожидание и затем применяет
- Почему
SIGKILLнельзя использовать для normal shutdown?- Его нельзя перехватить: не выполнятся обработчики,
defer, закрытие listener-а и flush. Данные и внешние операции должны переживать его без опоры на финализацию.
- Его нельзя перехватить: не выполнятся обработчики,
- Что происходит, если несколько
SIGTERMпришли быстро?- Для standard signal они могут coalesce в одно pending-состояние. Нельзя строить протокол «второй сигнал всегда будет получен» без явного канала/политики.
- Почему в C нельзя логировать из signal handler?
printfи логгер обычно не async-signal-safe, могут использовать lock или allocator в момент, когда прерванный поток уже держит их. Handler ставит флаг/пишет в pipe, логирование делает нормальный поток.
- Как Go HTTP-сервис должен реагировать на
SIGTERM?- Отменить root context, остановить ingress/закрыть listener, через
Server.Shutdownдождаться активных запросов до deadline, завершить workers и вернуть результат изmain.
- Отменить root context, остановить ingress/закрыть listener, через
- Чем process-directed сигнал отличается от thread-directed?
- Первый адресован процессу и доставляется одному подходящему потоку, второй — указанному потоку. Маска сигналов у каждого потока своя, disposition общая для процесса.
- Когда выбрать
signalfd?- В Linux event loop, который уже мультиплексирует FD и может централизованно блокировать сигналы во всех потоках. Для переносимого или обычного Go-кода он часто излишен.
Практика
- Проверьте graceful shutdown. Запустите Go HTTP-сервис с обработчиком, который ждёт несколько секунд, отправьте запрос и затем
SIGTERM. Критерий готовности: новый запрос после начала shutdown не принимается, начатый либо завершается до deadline, либо логируется как отменённый, а процесс возвращает контролируемый код. - Наблюдайте default actions. В отдельном тестовом процессе отправьте
SIGTERM,SIGINT,SIGSTOP/SIGCONTиSIGKILL. Критерий готовности: таблица результатов содержит состояние процесса и объясняет, почему два последних сигнала нельзя перехватить одинаково. - Разберите
EINTR. Напишите C или Go+syscall эксперимент с блокирующим вызовом и доставкой сигнала, затем изучите trace черезstrace. Критерий готовности: зафиксирован прерванный или перезапущенный вызов и указано, какая настройка/обёртка определила поведение. - Проверьте process tree. Создайте entrypoint-скрипт, сначала без
exec, затем сexec, и пошлите сигнал родительскому PID. Критерий готовности: показана разница PID/process group и сделан вывод, какой вариант пригоден для контейнера.
Частые ошибки и ловушки
- Называть сигнал «сообщением» и ожидать, что каждый повтор будет доставлен.
- Считать
SIGTERMиSIGKILLвзаимозаменяемыми или ожидать cleanup послеSIGKILL. - Делать reload, сетевые запросы или захват mutex прямо в native handler.
- Не вызывать
waitдля завершившихся дочерних процессов и оставлять zombie. - Делать shell PID 1 родителем приложения без
execили корректной signal forwarding/reaping логики. - Завершать Go-процесс через
os.Exitв shutdown-goroutine и случайно пропускатьdefer. - Полагаться только на сигнал для консистентности: любой процесс может быть убит или аварийно завершён в любой точке.
Связанные темы
База разработки и Computer Science · Процессы, потоки и goroutine · Файловые дескрипторы и stdio · Системные вызовы · Go: runtime и стандартная библиотека · Проектирование Go-приложений · Linux, сети и cloud
Источники
- POSIX.1-2024:
sigaction,sigprocmask,kill,signal,wait,forkи список async-signal-safe функций. - Linux man-pages:
signal(7),signal-safety(7),sigaction(2),signalfd(2),pidfd_send_signal(2),credentials(7). - The Linux Programming Interface, главы о сигналах, process groups, sessions и daemon-процессах.
- Документация Go: пакеты
os/signal,context,net/http,os/exec.