Сигналы
Что такое сигналы
В UNIX-подобных ОС для передачи информации процессам используются сигналы. Сигнал -- это число, которое ядро передаёт процессу. Определённые числа (коды сигналов) обозначают определённые события со стороны системы или пользователя. Например,
SIGSEGV
-- сигнал процессу завершиться, который обычно приводит к появлению сообщения "Segmentation fault" в терминале;SIGINT
-- прерывание с клавиатуры, посылается с помощью Ctrl+C.
Диспозиция сигналов
Диспозиция ядра по отношению к сигналу определяет, как процесс будет реагировать на этот сигнал. Для каждого сигнала определена диспозиция по умолчанию, одна из этих:
- Term -- завершать процесс.
- Ign -- игнорировать.
- Core -- Term + записать память процесса на диск.
- Stop -- приостановить процесс.
- Cont -- возобновить приостановленный процесс.
Обработчики сигналов и функция signal
На все сигналы, кроме SIGKILL
и SIGSTOP
, можно написать свой обработчик. Пример:
void sayhi(int signo) {
write(STDOUT_FILENO, "hi guys\n", 8);
}
int main() {
char c;
signal(SIGINT, sayhi);
while (read(...)) {
write(...);
}
}
Программа что-то читает и пишет, но ещё и на Ctrl+C
не завершается, а печатает приветствие.
Функция signal
принимает номер сигнала и адрес функции-обработчика:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
Вместо адреса обработчика можно передавать спец. значения:
SIG_IGN
-- игнорировать,SIG_DFL
-- вернуть диспозицию по умолчанию.
Что со стеком
Для выполнения функции sayhi
необходимо иметь стек. В любой момент времени может прийти сигнал, и нужно будет одновременно выполнять main
и sayhi
. Ядро копирует все состояние программы и запускает sayhi
на стеке ниже esp
, как обычную функцию с точки зрения копии. Однако оригинальный процесс приостанавливается, и после завершения обработчика возобновляется.
Если во время сист. вызова приходит сигнал, то сист. вызов приостанавливается, а после завершения работы обработчика возобновляется ядром - это семантика BSD.
Функция sigaction
Чтобы гибче настроить поведение при сигнале, будем использовать функцию sigaction
:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
-- код сигнала.
В struct sigaction
содержатся в том числе такие поля:
void (*sa_handler)(int)
-- указатель на обработчик, такой же, как вsignal
;int sa_flags
-- маска флагов, бывают в том числе следующие:SA_RESTART
: есть => семантика BSD, нет => надо перезапускать руками.SA_RESETHAND
: сбрасывает обработчик на дефолтный после первой обработки.
signal
<=> sigaction
с флагом SA_RESTART
. Обе эти функции изменяют диспозицию сигнала.
Пример:
void sayhi(int signo) {
write(STDOUT_FILENO, "hi guys", 7);
}
int main() {
char c;
struct sigaction sa = {
.sa_handler = sayhi,
.sa_flags = SA_RESTART,
};
sigaction(SIGINT, &sa, NULL);
while (read(...)) {
write(...);
}
}
Маски pending и blocked
В ядре для каждой пары (процесс, код сигнала) записано 2 бита: pending
и blocked
. Когда ядро получает информацию, что такому-то процессу надо выслать такой-то сигнал, оно делает так:
- Если
blocked = 1
, это значит, что обработчик этого сигнала по какой-то причине не может быть запущен прямо сейчас, поэтомуpending := 1
. - Если
blocked = 0
, это значит, что обработчик точно не выполняется и его можно запустить, поэтому он запускается, иblocked := 1
,pending := 0
. (*)
Сразу после того, как обработчик сигнала завершается:
- Ядро узнаёт об этом и ставит сигналу
blocked := 0
. - Если у этого сигнала
pending = 1
, это ситуация (*).
В частности, если сигнал приходит во время обработки такого же сигнала, пришедшего ранее, делается pending := 1
.
Проблемы синхронизации
Если используется одна и та же переменная в обработчике и в основной программе, они могут быть не синхронизированы. В результате, во время обработки сигнала может использоваться устаревшее значение переменной из программы. Чтобы избежать этой проблемы, можно использовать atomic типы, например, volatile sig_atomic_t
вместо int
.
Функция printf
работает со сложной структурой FILE
. Если одновременно использовать printf
в основной программе и в обработчике, данные этой структуры могут быть испорчены. Для предотвращения этого используется блокировка. Если вызвать printf
в обработчике до завершения printf
в основной программе, возникнет deadlock (блокировка 2 раза). Для безопасного использования функций в обработчиках можно почитать man signal-safety
или заблокировать доставку сигналов при выполнении небезопасных функций в основной программе.
Отправка сигналов
С помощью сист. вызова kill
можно отправить сигнал из процесса в процесс:
int kill(pid_t pid, int sig);
где pid
- номер процесса, которому хотим послать сигнал, sig
- номер сигнала. Если pid = -1
, то посылает сигнал всем, кому возможно.
Из командного интерпретатора:
kill -INT 1234567 # послать сигнал SIGINT процессу с pid 1234567
killall -INT executable_name # послать сигнал всем процессам с названием executable_name
SIGCHLD
SIGCHLD
- отправляется процессу, когда его дочерний процесс остановлен или завершился. Если установить ему SIG_IGN
, то потомки не превращаются в зомби, их не нужно wait
-ить.
Усовершенствованный обработчик для sigaction
sigaction
допускает установку не обычного обработчика:
void (*sa_handler)(int);
а усовершенствованного:
void (*sa_sigaction)(int, siginfo_t*, void*);
где siginfo_t
в том числе есть такие поля:
pid_t si_pid
- кто прислал сигналvoid *si_addr
- адрес, по которому произошла ошибка в случае segmentation fault.
Блокировка сигналов
Функция sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
позволяет манипулировать маской заблокированных сигналов текущего процесса. В set
лежит какая-то битмаска, как с ней работать, рассмотрим позже. Параметр how
может быть одним из трёх:
SIG_BLOCK
, тогда в ядре происходит or текущих и заданных вset
(то есть новые добавляются, уже существующие не меняются);SIG_UNBLOCK
, тогда из текущих вычитаются заданные вset
;SIG_SETMASK
, тогда маска просто заменяется наset
.
В oldset
записывается старая маска. Если set == NULL
, то в oldset
просто запишется актуальная маска.
Тип данных для маски сигналов -- sigset_t
. Чтобы задать в неё какие-нибудь типы сигналов, нужно объявить переменную такого типа, и изменять её следующими функциями:
int sigemptyset(sigset_t *set); // заполнить всеми нулями (очистить).
int sigfillset(sigset_t *set); // заполнить всеми единицами.
int sigaddset(sigset_t *set, int signum); // добавить сигнал `signum`.
int sigdelset(sigset_t *set, int signum); // убрать сигнал `signum`.
int sigismember(const sigset_t *set, int signum); // установлен ли определённый сигнал.
Системный вызов pause()
приостанавливает процесс до тех пор, пока не придёт сигнал, который не игнорируется (то есть либо прерывает процесс, либо обрабатывается).
Рассмотрим такой код:
sigprocmask(SIG_SETMASK, &mask, NULL);
pause();
В mask
установлена единица на какой-то сигнал, но который у нас написан свой обработчик. Мы хотели бы от этого кода такого поведения: если этот сигнал был заблокирован раньше, но был pending, то и запустится обработчик, и выйдет из pause()
. Но на самом деле, сначала запустится обработчик, pending
станет равен 0, и когда исполнение дойдёт до pause()
, уже не будет такого сигнала в ожидании, и pause()
будет ждать.
Поэтому на этот случай есть системный вызов, делающий эти 2 действия атомарно:
sigsuspend(&mask);
После этой строчки кода pending сигнал запустит обработчик, а потом выполнение продолжится без остановки.
Сигналы и их блокировки на практике
Обычно в большей части кода мы не хотим, чтобы его прерывали сигналами. К тому же, когда делаем fork()
или exec()
, маска заблокированных сигналов не меняется. Поэтому обычно имеет смысл на большую часть времени работы программы блокировать все сигналы, а в нужных местах их разблокировать.
signalfd
Можем создать файловый дескриптор, из которого читать информацию о поступающих сигналах: int signalfd(int fd, const sigset_t *mask, int flags);
Сигналы реального времени
Всё выше относится к POSIX-ным стандартным сигналам. Есть также сигналы реального времени (real-time signals). Их отличие в том, что если, пока сигнал заблокирован, приходит больше одного сигнала, то остальные не теряются, а сохраняются в очередь, при этом есть гарантия на порядок доставки.
У них другие номера. Номера не обозначают типы сигналов, пользователи могут сами придумывать им смысл. Номера должны лежать в диапазоне от SIGRTMIN до SIGRTMAX. Отправлять их можно только из пользовательских программ, система их отправлять не может.
Вместе с номером сигнала реального времени можно отправить max(sizeof(int), sizeof(void*))
байт.
Отправляются сигналы реального времени с помощью системного вызова sigqueue
:
int sigqueue(pid_t pid, int sig, const union sigval value);
pid
-- кому отправлять, sig
-- номер сигнала, value
-- дополнительные max(sizeof(int), sizeof(void*))
байт.
Если нет необходимости отправлять вместе с ними данные, то для работы с сигналами реального времени можно использовать механизмы обычных сигналов.