From: Valentin Nechayev <netch@segfault.kiev.ua>
Newsgroups: fido7.ru.unix.prog
Subject: Правила использования сигналов в Unix
Date: Tue, 26 Aug 2003 08:07:19 +0000 (UTC)
Основной текст написал: Yar Tikhiy <yar@comp.chem.msu.su>
Если судить по исходникам популярных программ и обсуждениям Usenet
за последние лет 20, то обработка сигналов Unix -- это игра, в
которой большинство участвует, так и не удосужившись выучить правила.
Программисты привыкли, что если код не засбоит хотя бы один раз,
то он будет работать во всех случаях. Сигналы оказались для них
хорошо замаскированной ловушкой, из которой всем нам предстоит еще
долго выкарабкиваться (о последствиях неаккуратной работы с сигналами
для безопасности см. статью "Delivering Signals for Fun and Profit":
http://razor.bindview.com/publish/papers/signals.txt).
Академическое сообщество пользователей Unix всегда поощряло
"экспериментальный" стиль программирования, когда во главу угла
ставится сам факт реализации той или иной возможности, а не качество
решения. В 1986 году даже почтенный Chris Torek допускал высказывания
вроде этого: "Хотя вызывать exit(3) из обработчика сигнала не вполне
надежно, на практике это не создает никаких проблем". Впрочем, сам
мистер Torek отрекся от подобной ереси к началу 1990-х; зато
недостатка в программистах, обрабатывающих сигналы по принципу
"авось пронесет", и книгах, дающих сомнительные советы, не ощущается
до сих пор.
Что удивительно в такой ситуации, правила "игры в сигналы" на самом
деле весьма просты. Поэтому хотелось бы их четко сформулировать и
указать на основные следствия из них. Для начала я попытаюсь это
сделать для случая традиционных сигналов POSIX в однонитевой среде.
Комментарии и дополнения приветствуются.
ПРАВИЛА ИГРЫ В СИГНАЛЫ UNIX
Правило 1
Посланный процессу сигнал может быть проигнорирован, заблокирован,
или доставлен. Под доставкой понимается действие по умолчанию или
вызов назначенного обработчика. Рассмотрим эти случаи по отдельности.
1. Если сигнал игнорируется, то он не оказывает никакого воздействия
на процесс. В частности, не прерываются по EINTR прерываемые
операции.
2. Если сигнал блокируется, то его обработчик будет вызван один раз
(независимо от того, сколько раз был послан сигнал) после снятия
блокировки.
3. Если сигнал обрабатывается по умолчанию как "отвергнуть сигнал"
(discard signal), то это полностью эквивалентно его игнорированию
(см. п. 1.1). В некоторых руководствах это действие явно именуется
"ignore". Действиями по умолчанию, в зависимости от типа сигнала,
могут также быть "завершить процесс" (возможно, с записью образа
памяти), "приостановить процесс" и "продолжить процесс".
4. Если процесс назначил сигналу собственный обработчик и сигнал
должен быть доставлен, то будет вызван этот обработчик.
Если у сигнала установлен через sigaction(2) флаг SA_RESETHAND,
то эмулируется поведение SysV signal(): обработчик сигнала будет
установлен равным SIG_DFL (обработка по умолчанию) в момент
доставки, т.е. непосредственно перед вызовом текущего
обработчика.
На время вызова обработчика текущий сигнал блокируется,
если не установлен флаг SA_NODEFER (то есть, сигнал добавляется
к маске, описанной в sa_mask). Это необходимо, чтобы
избежать зацикливания обработчика.
Обработчику передается номер текущего сигнала, что позволяет
использовать один обработчик для нескольких сигналов. Возможна
передача дополнительной информации (см. SA_SIGINFO в SUS).
Между посылкой сигнала и его доставкой может пройти
неограниченное количество времени, даже если сигнал не
заблокирован. Следовательно, один и тот же сигнал может
быть послан процессу несколько раз перед тем, как он будет
доставлен. Тем не менее, обработчик будет вызван единожды,
так как система запоминает только сам факт посылки каждого
сигнала.
Правило 2
Любая операция может быть временно прервана вызовом обработчика
асинхронного сигнала, если на момент ее выполнения сигнал не
заблокирован и не игнорируется.
Сигналы посылаются и доставляются асинхронно в подавляющем
большинстве случаев. Существует, по сути, лишь один случай
синхронной посылки сигнала: когда процесс посылает сигнал
сам себе, с помощью функций abort(3) или raise(3). Согласно
C99 и SUSv3, если в ответ на raise(3) должен быть вызван
обработчик сигнала, raise(3) может вернуть управление только
после того, как это сделает обработчик.
В системах, совместимых с POSIX, вызов функции
raise(signo);
должен быть эквивалентен вызову конструкции:
kill(getpid(), signo);
Значит, в POSIX такую конструкцию можно рассматривать как
еще один способ синхронной посылки сигнала. Раздел SUSv3,
посвященный функции kill(), подробно обсуждает посылку
процессом сигнала самому себе и указывает, что в этом случае
сигнал должен быть доставлен до того, как kill() вернет
управление.
Правило 3
Существует ровно один тип статических данных, sig_atomic_t, переменную
которого может установить обработчик асинхронного сигнала. Поведение
приложения не определено, если асинхронно вызванный обработчик
обращается к статическим данным любым другим способом.
По-видимому, это весьма жесткое ограничение связано с
поддержкой архитектур, в которых обработчик асинхронного
сигнала не может напрямую обращаться к основной памяти
процесса.
Правило 4
Если обработчик асинхронного сигнала завершается возвратом, то
текущая операция может быть продолжена или прервана, в зависимости
от типа операции и значения флага SA_RESTART для этого сигнала.
Есть несколько случаев.
1. Если для текущего сигнала не установлен флаг SA_RESTART, то
некоторые системные вызовы будут прерваны с ошибкой EINTR. Если
же флаг SA_RESTART установлен, то выполнение этих вызовов будет
продолжено.
Список прерываемых вызовов может быть приведен в sigaction(2);
это вызовы ввода-вывода и wait(2). SUSv3 такого списка не
приводит, однако говорит, что сигнал без SA_RESTART прерывает
любой вызов, который может возвращать ошибку EINTR.
2. Функции семейства sleep (sleep(3), nanosleep(2) и т.п.) будут
прерваны любым доставленным сигналом, вне зависимости от наличия у
него флага SA_RESTART.
3. Вызов connect(2) в блокирующем режиме будет перван любым
доставленным сигналом, независимо от его флагов, однако сама операция
установки соединения будет продолжена в асинхронном режиме. Об
окончании операции можно узнать, передав дескриптор в poll(2) или
select(2): он будет помечен как готовый к записи.
4. Вызовы select(2), pselect(2) и poll(2) могут быть прерваны
сигналом.
Согласно SUSv3, poll(2) прерывается любым доставленным
сигналом; будут ли select(2) и pselect(2) учитывать флаг
SA_RESTART, определяет реализация.
5. Выполнение кода процесса и остальных системных вызовов будет
продолжено.
Следствия _для асинхронных сигналов_
1. [Из п. 2] Из обработчика сигнала нельзя выполнять общий c другими
частями процесса нереентерабельный участок кода, если он не защищается
всякий раз путем блокировки соответствующих сигналов.
Здесь нереентерабельность понимается в широком смысле как
зависимость по статическим данным, требующим сериализации
доступа. Таким образом, нереентерабельной может быть как
отдельная функция, так и целая библиотека. Если несколько
участков кода обращаются к общим данным, требующим сериализации,
то все они будут взаимно нереентерабельны. В англоязычной
литературе есть более точный термин "async-signal-safe", то
есть, безопасный по отношению к асинхронным сигналам.
2. [Из п. 2, с. 1] Из обработчика сигнала нельзя вызывать
нереентерабельные библиотечные функции (н.б.ф.), если только
устройством программы не гарантируется, что на момент _каждого_
вызова _любой_ н.б.ф. все сигналы, чьи обработчики содержат вызовы
_любых_ н.б.ф., не будут заблокированы.
Так как внутренние зависимости между н.б.ф. полностью зависят
от реализации, вложенный вызов _любой_ н.б.ф. может привести
к неопределенному поведению _всех_ н.б.ф. К примеру,
изрядное количество библиотечных функций явно или неявно
вызывают malloc(3); так что разрушение структур malloc(3)
в результате вложенного вызова приведет к сбою многих б.ф.
Стандарт C99 утверждает, что из обработчика асинхронного
сигнала можно вызывать только abort(3), _exit(2), _Exit(),
а также signal(3) с первым аргументом, равным номеру текущего
сигнала [C99 #7.14.1.1]. Стандарты POSIX и SUS приводят
довольно объемистый список реентерабельных функций. В
прочих системах этот список, очевидно, определяется
реализацией.
3. [Из п. 1.4, п. 2] Выполнение обработчика сигнала может быть
прервано очередным доставленным сигналом. Чтобы избежать прерывания,
необходимо заблокировать все или некоторые сигналы на время работы
чувствительного участка в обработчике. Проще всего это делать,
указав маску sa_mask в параметре sigaction(2); эта маска будет
атомарно установлена на входе в обработчик, а на выходе из него
будет восстановлено предыдущее значение маски сигналов.
4. [Из с. 1, с. 3] Если один нереентерабельный обработчик установлен
для нескольких сигналов, то на время работы нереентерабельного участка
нужно блокировать все эти сигналы.
5. [Из п. 2, п. 3] Необходимо использовать модификатор volatile,
чтобы указать компилятору на асинхронность изменения переменной типа
sig_atomic_t.
6. [Из п. 3] Не гарантируется, что обработчик сигнала может читать
статическую переменную, даже если она -- типа sig_atomic_t.
Учитывая исторически сложившуюся практику, можно считать
чтение переменной типа sig_atomic_t из обработчика сигнала
ограниченно переносимым.
7. [Из п. 2, п. 3] Можно использовать только простое присваивание
переменным sig_atomic_t. В частности, над ними не следует использовать
операции ++ и --. Это касается как обработчиков сигналов, так и
основного потока (конечно, если соответствующие сигналы не заблокированы
на момент операций с переменной типа sig_atomic_t).
В архитектуре RISC нет атомарных арифметических операций над
ОЗУ. В архитектуре CISC они есть, но компилятор не обязан их
использовать, например, из соображений оптимизации.
8. [Из п. 3] Если стандартная библиотечная функция, вызванная из
обработчика сигнала, вернула ошибку, то значение переменной errno
может быть не определено.
9. [Из п. 2] Вызов даже реентерабельной библиотечной функции из
обработчика сигнала может привести к изменению значения переменной
errno. Следовательно, если обработчик вызывает б.ф., то он должен
вначале сохранить значение errno, а перед возвратом восстановить его.
Правило 3 и следствие 8 исключают следствие 9. Проблема
здесь в том, что современные стандарты признают исторически
сложившуюся практику обращения к статическим переменным
вопреки правилу 3, но крайне не поощряют ее. Пока что это
приводит к подобным противоречиям.
10. [Из п. 2, п. 3, с. 1] "Трюк" с возвратом из обработчика сигнала
через longjmp(3), кочующий из книги в книгу (даже W.R.Stevens
приводит его), следует использовать с большой осторожностью.
Во-первых, longjmp(3) не восстановит статических переменных libc и
структуру кучи malloc(3) на момент вызова setjmp(3), а значит, он
ничем не поможет в решении проблемы реентерабельности. Во-вторых,
вызов longjmp(3) из обработчика обладает ограниченной переносимостью.
Изначально longjmp(3) из обработчика сигнала использовался,
чтобы обойти особенность 4.2BSD. Системные вызовы 4.2BSD
всегда продолжались после обработки сигнала, если обработчик
возвращал управление. Приходилось совершать longjmp(3),
чтобы прервать системный вызов.
Еще одно историческое применение longjmp(3) из обработчика
состояло в обходе изьяна pause(2). В системах без sigsuspend(2)
или sigpause(2) невозможно было гарантировать, что сигнал
не придет до вызова pause(2). Если сигнал посылался
однократно, как SIGALRM по истечению таймера, то приложение
могло "зависнуть". Для решения этой проблемы управление
из обработчика явно передавали через longjmp(3). W.R.Stevens
приводит пример использования longjmp(3) в обработчике
именно для этого случая.
Существует как минимум одна среда, претендующая на совместимость
с POSIX, в которой выход из обработчика сигнала через
longjmp(3) приводит к сбою приложения. Речь идет о Win32
и сигнале SIGINT. Этот сигнал посылается консольному
приложению при нажатии ^C и доставляется в специально
отведенную нить, даже если приложение спроектировано как
однонитевое. Конечно, этот прецедент может показаться
далеким от реалий Unix. Однако он иллюстрирует разнообразие
особенностей, с которыми можно столкнуться в POSIX-совместимых
системах.
11. [Из п. 2] Блокировка сигналов часто должна выполняться неразрывно
с другой операцией. Для этого существует ряд стандартных механизмов
и функций.
Согласно странице руководства sigaction(2) в системах на
основе BSD, несколько одновременно ожидающих доставки
сигналов будут доставлены так, что каждый последующий сигнал
прервет обработчик предыдущего перед его первой машинной
командой. Таким образом, избежать дальнейшего вложенного
вызова обработчиков можно, лишь атомарно установив маску
сигналов перед входом в текущий обработчик. Для этого служит
маска sa_mask, передаваемая как член структуры sigaction в
sigaction(2).
Чтобы приостановить выполнение программы до прихода
определенного сигнала (или любого из заданного множества
сигналов), существует системный вызов sigsuspend(2). Его
использование предполагает, что в остальное время интересующий
нас сигнал (или их множество) заблокирован, иначе можно его
упустить до вызова sigsuspend(2). Но даже если sigprocmask(2)
вызвать непосредственно перед sigsuspend(2), все равно
образуется временное окно, на протяжении которого ожидаемый
сигнал может оказаться доставлен. Поэтому sigsuspend(2)
принимает в качестве аргумента маску сигналов, которую
атомарно устанавливает на время ожидания.
Системный вызов pselect(2) фактически совмещает в себе
функциональность select(2) и sigsuspend(2). Поэтому он
также принимает в качестве одного из аргументов маску
сигналов, которую атомарно устанавливает на время ожидания
событий.
12. [Из п. 2, с. 1] Для завершения процесса из обработчика сигнала
следует использовать _exit(2) (POSIX) или _Exit() (C99), которые
не имеют побочных эффектов, в отличие от exit(3).
Побочными эффектами exit(3) являются вызов зарегистрированных
с помощью atexit(3) деструкторов, закрытие потоков stdio,
удаление временных файлов и т.п. Все это наверняка повлечет
вызов н.б.ф.
13. [Из п. 4.1] Если хотя бы у одного сигнала, для которого установлен
обработчик, нет флага SA_RESTART и этот сигнал не заблокирован, то
по EINTR может быть прервана _любая_ стандартная библиотечная функция
ввода-вывода.
Практические соображения
При планировании проекта стоит определить границы его переносимости,
так как требования разных стандартов могут заметно отличаться или
даже противоречить друг другу, что мы уже видели ранее. Если
ограничиться средой Unix, то наиболее здравым выбором, по-видимому,
будет текущая версия SUS.
Обращение из обработчика асинхронного сигнала к статическим
данным вопреки правилу 3 может считаться ограниченно
переносимым, так как работает в большинстве Unix-подобных
систем. Использовать его можно на свой страх и риск, однако
следует иметь ввиду возможные последствия для надежности и
переносимости приложения.
Не надо относиться к сигналам как к грому и молнии. В аккуратно
написанной программе всегда известно, какие сигналы в данный момент
следует принимать и обрабатывать, а какие лучше держать заблокированными
или игнорировать. Зачастую это не требует значительных усилий или
изменений в структуре программы -- достаточно четко осознавать,
почему тот или иной сигнал не повредит, скажем, при вызове функции
ввода-вывода.
Если в вашем проекте сигналы большую часть времени разблокированы
и у некоторых из них не установлен флаг SA_RESTART, то может быть
удобно сократить до минимума число вызовов функций ввода-вывода.
Нужно быть готовым перезапустить каждый такой вызов, если он вернет
EINTR или выполнит операцию не полностью. При большом количестве
вызовов (например, при посимвольном вводе-выводе посредством stdio)
это потребует соответствующего числа практически повторяющихся
"оберток", что отрицательно скажется как на производительности, так
и на удобочитаемости исходных текстов.
Наиболее простой и безопасный подход к обработке сигналов -- когда
обработчик устанавливает флаг типа sig_atomic_t, а основной поток
проверяет его и предпринимает необходимые действия. Если при этом
необходимо прерывать некоторые операции ввода-вывода, то следует или
блокировать сигналы на время остальных операций ввода-вывода, или
особо обрабатывать ошибку EINTR после _всех_ операций ввода-вывода.
В сложных случаях может оказаться удобнее эмулировать синхронные
сигналы. Для этого можно держать сигналы заблокированными большую
часть времени и разблокировать их только в специально отведенных,
реентерабельных точках программы. Особенно удобным для этой цели
является системный вызов pselect(2), который позволяет атомарно
устанавливать маску сигналов на время своего выполнения. Конечно,
это внесет задержку между отправкой сигнала и его обработкой.
В системах, где pselect(2) отсутствует, его можно частично
эмулировать, используя запись из обработчика сигнала в канал
(pipe). Для этого необходимо:
а) создать канал;
б) установить на обоих концах канала режим неблокирующего
ввода-вывода;
в) установить обработчик, который станет записывать в этот
канал 1 байт (любой, т.к. доставка через канал не будет
гарантированной) и устанавливать флаг в переменной
типа volatile sig_atomic_t;
г) включить читаемый дескриптор сигнального канала в
обработку poll(2)/select(2);
д) разблокировать сигнал;
е) вызвать poll(2)/select(2);
ж) заблокировать сигнал;
з) при наличии данных в сигнальном канале читать их оттуда
для очистки буфера;
и) если флаг установлен:
- сбросить флаг;
- предпринять необходимые действия;
к) вернуться к д).
Смысл записи в канал состоит в том, что poll(2)/select(2)
вернет управление сразу, если сигнал уже был доставлен в
обработчик. Иначе poll(2)/select(2) стал бы ждать других
событий, и реакция на сигнал произошла бы с задержкой.
Конечно, возможна потеря сигнала, если буфер сигнального
канала уже заполнен. Однако за счет установки и проверки
флага хотя бы один экземпляр сигнала будет когда-нибудь
замечен, а это вполне согласуется с моделью сигналов POSIX.
Так как сложные схемы на основе сигналов очевидно ограничены и плохо
переносимы, с сигналами всегда стоит придерживаться принципа K.I.S.S.:
"Keep it simple, stupid". Если вы выучили правила и видите, что
нетривиальное использование сигналов причиняет слишком много
неудобств, то вам пора рассмотреть другие средства IPC для вашего
проекта.
Заключение
Автор хотел бы искренне поблагодарить читателей эхи (телеконференции
FidoNet) RU.UNIX.PROG за ценные замечания и идеи для данной статьи.