Несмотря на проблемы реентерабельности, совместное использование глобальных переменных основной программой и обработчиком сигнала может оказаться полезным. Это безопасно до тех пор, пока основная программа корректно воспринимает и обрабатывает вероятность, что обработчик может в любое время изменить значение глобальной переменной. Например, распространенной практикой является применение обработчика сигнала для выполнения единственного действия: установки глобального флага. Флаг периодически проверяется основной программой, выполняющей соответствующее действие в ответ на получение сигнала (и снимающей флаг). Если обработчик сигнала получает доступ к глобальным переменным таким образом, то мы всегда должны объявлять эти переменные с использованием ключевого слова volatile (см. раздел 6.8), дабы запретить компилятору выполнять оптимизацию, результатом которой может стать хранение переменной в регистре.
Для осуществления чтения и записи глобальной переменной может потребоваться несколько машинных кодов, а обработчик сигнала может прервать основную программу во время выполнения последовательности таких машинных кодов. (Мы говорим, что доступ к переменным является
volatile sig_atomic_t flag;
Мы приводим пример использования типа данных sig_atomic_t в листинге 22.5.
Обратите внимание, что операторы С инкремента (++) и декремента (-) не попадают в гарантию, предоставляемую типом данных sig_atomic_t. В некоторых аппаратных архитектурах эти операции могут не быть атомарным (см. раздел 30.1 для получения более подробной информации). В гарантию безопасности, предоставляемую типом данных sig_atomic_t, попадает только то, что мы можем установить переменную этого типа в обработчике сигнала и проверить ее значение в основной программе (или наоборот).
Стандарты С99 и SUSv3 устанавливают, что в каждой реализации операционной системы должны определяться две константы (в
Все рассмотренные до этого момента обработчики сигналов завершаются возвратом в основную программу. Однако простой возврат из обработчика не всегда является желательным, а в некоторых случаях может быть и бесполезным. (Такой случай мы рассмотрим при изучении аппаратно генерируемых сигналов в разделе 22.4.)
Существуют другие различные методы завершения работы обработчика сигнала.
• Вызов функции _exit() для завершения процесса. Перед этим обработчик может выполнить несколько действий для очистки. Обратите внимание, что мы не можем использовать exit() для завершения обработчика, так как эта функция не является одной из безопасных, приведенных в табл. 21.1. Эта функция небезопасна, так как сбрасывает буферы stdio перед осуществлением вызова функции _exit(), как описано в разделе 25.1.
• Вызов функции kill() или raise() для отправки сигнала, аварийно завершающего процесс (то есть сигнала, действие по умолчанию для которого — завершение процесса).
• Выполнение нелокального перехода из обработчика.
• Вызов функции abort().
Последние два метода более подробно рассматриваются в следующих подразделах.
21.2.1. Выполнение нелокального перехода из обработчика сигнала