Между временем, когда сигнал отправлен и тем, когда он вызывает какое-то действие, его называют ожидающим (pending). Это значит, что ядро знает, что сигнал должен быть обработан, но пока не имеет возможности сделать это. Как только сигнал поступает в процесс назначения, он называется доставленным. Если доставленный сигнал вызывает выполнение специального фрагмента кода (имеется в виду обработчик сигнала), то такой сигнал считается перехваченным. Есть разные способы, которыми процесс может предотвратить асинхронную доставку сигнала, но все же обработать его (например, с помощью системного вызова sigwait()
). Когда такое случается, сигнал называют принятым.
Чтобы облегчить понимание, мы будем использовать эту терминологию на протяжении всей книги[52].
12.1.2. Простые сигналы
Изначально обработка сигналов была проста. Системный вызов signal()
использовался для того, чтобы сообщить ядру, как доставить процессу определенный сигнал.
#include
void * signal(int signum, void *handler);
Здесь signum
— это сигнал, который нужно обработать, a handler
определяет действия, которое должно быть выполнено при доставке сигнала. Обычно handler
— это указатель на функцию-обработчик сигнала, которая не принимает параметров и не возвращает значения. Когда сигнал доставлен процессу, ядро как можно скорее запускает функцию-обработчик. Когда функция возвращает управление, ядро возобновляет выполнение процесса с того места, где он был прерван. Системные инженеры распознают в этом механизме обработки сигналов аналог доставки аппаратных прерываний. Прерывания и сигналы очень похожи и у них возникают сходные проблемы.
Доступно множество номеров сигналов. В табл. 12.1 перечислены все сигналы, поддерживаемые в настоящее время Linux, за исключением сигналов реального времени. Они имеют символические имена, начинающиеся с SIG
, и мы будем использовать SIG
, говоря о каком-то из них.
Параметр handler может иметь два специальных значения — SIG_IGN
и SIG_DFL
(оба определены в
). Если указано SIG_IGN
, сигнал игнорируется, SIG_DFL
сообщает ядру, что нужно выполнить действие по умолчанию, как правило, уничтожив процесс либо проигнорировав сигнал. Два сигнала — SIGKILL
и SIGSTOP
— не могут быть перехвачены. Ядро всегда выполняет действие по умолчанию для этих сигналов, соответственно, уничтожая процесс и приостанавливая его.
Функция signal()
возвращает предыдущий обработчик сигнала (который мог быть SIG_IGN
или SIG_DFL
). Обработчики сигналов резервируются при создании новых процессов вызовом fork()
, и все сигналы, которые установлены в SIG_IGN
, игнорируются и после вызова exec()
[53]. Все не игнорируемые сигналы после exec()
устанавливаются в SIG_DFL
.
Все это выглядит достаточно простым, пока вы не спросите себя: что произойдет, если сигнал SIG
будет отправлен процессу, который уже исполняет обработчик сигнала для SIG
.
Очевидно, что должно сделать ядро — так это прервать процесс и запустить обработчик сигнала сначала. Это порождает две проблемы. Первая — обработчик сигнала должен работать правильно, если он вызван тогда, когда уже сам работает. Хотя само по себе это и не сложно, но обработчики сигналов, которые манипулируют общепрограммными ресурсами, такими как глобальные структуры данных или файлы, должны быть написаны очень аккуратно. Функции, которые ведут себя правильно, когда вызваны подобным образом, называются реентерабельными функциями[54].
Простая техника блокировки, которая достаточна для координации доступа к данным между конкурирующими процессами, не обеспечивает реентерабельности. Например, техника блокировки файлов, представленная в главе 13, не может использоваться для того, чтобы позволить обработчику сигналов, манипулирующему файлами данных, быть реентерабельным. Когда обработчик сигналов вызывается первый раз, он может просто изумительно заблокировать файл данных и начать запись в него. Если же этот обработчик будет прерван другим сигналом, в то время пока он удерживает блокировку, второй вызов обработчика не сможет блокировать файл, поскольку его блокировал первый вызов. К сожалению, вызов, который удерживает блокировку, приостанавливается до тех пор, пока второй вызов, который будет ожидать разблокировки, завершит работу.
Сложность написания реентерабельных обработчиков сигналов — это главная причина того, почему ядро не доставляет сигнал процессу, который уже его обрабатывает. Такая модель также затрудняет процессам возможность обрабатывать большое количество сигналов, поступающих ему слишком быстро. Как только сигнал осуществляет новый вызов обработчика, стек процесса растет безо всякого предела, пренебрегая правильным поведением самой программы.