Usenet Replayer



koi8-r


Path:  news2.ip-mobilphone.net ! NNTPLoader.ip-mobilphone.net ! not-for-mail
From:  "RU.UNIX.PROG FAQ poster" <netch@segfault.kiev.ua>
Newsgroups:  fido7.ru.unix.prog
Subject:  Signals FAQ
Date:  Sat, 16 Aug 2014 07:38:00 +0000 (UTC)
Organization:  Dark side of coredump
Lines:  446
Sender:  fido7@ddt.demos.su
Approved:  <gateway@fido7.ru>
Message-ID:  <1187492588@segfault.kiev.ua>
NNTP-Posting-Host:  ddt.demos.su
Mime-Version:  1.0
Content-Type:  text/plain; charset=koi8-r
Content-Transfer-Encoding:  8bit
X-Trace:  ddt.demos.su 1408174680 72444 194.87.13.37 (16 Aug 2014 07:38:00 GMT)
X-Complaints-To:  gatekeeper@fido7.ru
NNTP-Posting-Date:  Sat, 16 Aug 2014 07:38:00 +0000 (UTC)
X-BeforeModerator-NNTP-Posting-Host:  localhost.segfault.kiev.ua
X-BeforeModerator-X-Trace:  segfault.kiev.ua 1408174205 60365 127.0.0.1 (16 Aug 2014 07:30:05 GMT)
X-BeforeModerator-X-Complaints-To:  usenet@segfault.kiev.ua
X-BeforeModerator-NNTP-Posting-Date:  Sat, 16 Aug 2014 07:30:05 +0000 (UTC)
X-42:  On
X-BeforeModerator-Message-ID:  <201408160730.s7G7U06d060244@segfault.kiev.ua>
X-FTN-REPLYADDR:  "RU.UNIX.PROG FAQ poster" <netch@segfault.kiev.ua>
X-Received-Body-CRC:  1806579580
X-Received-Bytes:  21893
Xref:  news2.ip-mobilphone.net fido7.ru.unix.prog:14326



Основной текст написал: Yar Tikhiy <yar at comp.chem.msu.su>

Предисловие

Если судить по исходникам популярных программ и обсуждениям Usenet
за последние лет 20, то обработка сигналов Unix -- это игра, в
которой большинство участвует, так и не удосужившись выучить правила.
Программисты привыкли, что если код не засбоит хотя бы один раз,
то он будет работать во всех случаях. Сигналы оказались для них
хорошо замаскированной ловушкой, из которой всем нам предстоит еще
долго выкарабкиваться (о последствиях неаккуратной работы с сигналами
для безопасности см. статью "Delivering Signals for Fun and Profit":
http://www.bindview.com/Services/Razor/Papers/2001/signals.cfm (Who's computer is this?) ).

Академическое сообщество пользователей Unix всегда поощряло
"экспериментальный" стиль программирования, когда во главу угла
ставится сам факт реализации той или иной возможности, а не качество
решения. В 1986 году даже почтенный Chris Torek допускал высказывания
вроде этого: "Хотя вызывать exit(3) из обработчика сигнала не вполне
надежно, на практике это не создает никаких проблем". Впрочем, сам
мистер Torek отрекся от подобной ереси к началу 1990-х; зато
недостатка в программистах, обрабатывающих сигналы по принципу
"авось пронесет", и книгах, дающих сомнительные советы, не ощущается
до сих пор.

Что удивительно в такой ситуации, правила "игры в сигналы" на самом
деле весьма просты. Поэтому хотелось бы их четко сформулировать и
указать на основные следствия из них. Для начала я попытаюсь это
сделать для случая традиционных сигналов POSIX в однонитевой среде.
Комментарии и дополнения приветствуются.



ПРАВИЛА ИГРЫ В СИГHАЛЫ UNIX

Правило 1

Посланный процессу сигнал может быть проигнорирован, заблокирован,
или доставлен. Под доставкой понимается действие по умолчанию или
вызов назначенного обработчика. Рассмотрим эти случаи по отдельности.

1. Если сигнал игнорируется, то он не оказывает никакого воздействия
на процесс. В частности, не прерываются по EINTR прерываемые
операции. (Исключение - в системах с kqueue, если установить
прослушивание сигнала (EVFILT_SIGNAL), kevent на него будет
сгенерирован даже если сигнал игнорируется.)

2. Если сигнал блокируется, то его обработчик будет вызван один раз
(независимо от того, сколько раз был послан сигнал) после снятия
блокировки.

3. Если сигнал обрабатывается по умолчанию как "отвергнуть сигнал"
(discard signal), то это полностью эквивалентно его игнорированию
(см. п. 1.1). В некоторых руководствах это действие явно именуется
"ignore". Действиями по умолчанию, в зависимости от типа сигнала,
могут также быть "завершить процесс" (возможно, с записью образа
памяти), "приостановить процесс" и "продолжить процесс".

4. Если процесс назначил сигналу собственный обработчик и сигнал
должен быть доставлен, то будет вызван этот обработчик.

Если у сигнала установлен через sigaction(2) флаг SA_RESETHAND,
то эмулируется поведение SysV signal(): обработчик сигнала будет
установлен равным SIG_DFL (обработка по умолчанию) в момент
доставки, т.е. непосредственно перед вызовом текущего
обработчика.

Hа время вызова обработчика текущий сигнал блокируется,
если не установлен флаг SA_NODEFER (то есть, сигнал добавляется
к маске, описанной в sa_mask). Это необходимо, чтобы
избежать зацикливания обработчика.

Обработчику передается номер текущего сигнала, что позволяет
использовать один обработчик для нескольких сигналов. Возможна
передача дополнительной информации (см. SA_SIGINFO в SUS).

Между посылкой сигнала и его доставкой может пройти
неограниченное количество времени, даже если сигнал не
заблокирован. Следовательно, один и тот же сигнал может
быть послан процессу несколько раз перед тем, как он будет
доставлен. Тем не менее, обработчик будет вызван единожды,
так как система запоминает только сам факт посылки каждого
сигнала.


Правило 2

Любая операция может быть временно прервана вызовом обработчика
асинхронно доставляемого сигнала, если на момент ее выполнения
сигнал не заблокирован и не игнорируется.

Сигналы посылаются и доставляются асинхронно в подавляющем
большинстве случаев. Существует, по сути, лишь один случай
синхронной посылки сигнала: когда процесс посылает сигнал
сам себе, с помощью функций abort(3) или raise(3), или через
генерацию исключительной ситуации, вызывающей SIGSEGV,
SIGBUS, SIGIOT и аналоги. Согласно C99 и SUSv3, если в ответ
на raise(3) должен быть вызван обработчик сигнала, raise(3)
может вернуть управление только после того, как это сделает
обработчик. Кроме того, важным режимом является эмуляция
синхронной доставки размаскированием сигналов только в
отведенные для этого моменты времени (см. ниже).

В системах, совместимых с POSIX, вызов функции
raise(signo);
должен быть эквивалентен вызову конструкции:
kill(getpid(), signo);
Значит, в POSIX такую конструкцию можно рассматривать как
еще один способ синхронной посылки сигнала. Раздел SUSv3,
посвященный функции kill(), подробно обсуждает посылку
процессом сигнала самому себе и указывает, что в этом случае
сигнал должен быть доставлен до того, как kill() вернет
управление.


Правило 3

Существует ровно один тип статических данных, sig_atomic_t, переменную
которого может установить обработчик асинхронного сигнала. Поведение
приложения не определено, если асинхронно вызванный обработчик
обращается к статическим данным любым другим способом.

По-видимому, это весьма жесткое ограничение связано с
поддержкой архитектур, в которых обработчик асинхронного
сигнала не может напрямую обращаться к основной памяти
процесса. Распространённые реализации на распространённых
архитектурах (x86, amd64, Alpha, Sparc...) такого ограничения
не имеют, но мне неизвестно, оговорено ли это каким-либо
явным комплектом требований или стандартом.


Правило 4

Если обработчик асинхронного сигнала завершается возвратом, то
текущая операция может быть продолжена или прервана, в зависимости
от типа операции и значения флага SA_RESTART для этого сигнала.
Есть несколько случаев.

1. Если для текущего сигнала не установлен флаг SA_RESTART, то
некоторые системные вызовы будут прерваны с ошибкой EINTR. Если
же флаг SA_RESTART установлен, то выполнение этих вызовов будет
продолжено.

Список прерываемых вызовов может быть приведен в sigaction(2);
это вызовы ввода-вывода и wait(2). SUSv3 такого списка не
приводит, однако говорит, что сигнал без SA_RESTART прерывает
любой вызов, который может возвращать ошибку EINTR. Детали
реализации (какой вызов прерывается) зависят от устройства
системы; типичные реализации могут прерывать вызовы
ввода-вывода на сокетах, терминалах, пайпах, но не на дисковых
устройствах (с возможным исключением в виде NFS и других
средств сетевого доступа к диску).

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] Hеобходимо использовать модификатор volatile,
чтобы указать компилятору на асинхронность изменения переменной типа
sig_atomic_t.

6. [Из п. 3] Hе гарантируется, что обработчик сигнала может читать
статическую переменную, даже если она -- типа sig_atomic_t.

Учитывая исторически сложившуюся практику, можно считать
чтение переменной типа sig_atomic_t из обработчика сигнала
ограниченно переносимым (для традиционных архитектур и
реализаций на них).

7. [Из п. 2, п. 3] Можно использовать только простое присваивание
переменным sig_atomic_t. В частности, над ними не следует использовать
операции ++ и --. Это касается как обработчиков сигналов, так и
основного потока (конечно, если соответствующие сигналы не заблокированы
на момент операций с переменной типа sig_atomic_t).

Атомарные арифметические и обменные операции над памятью существуют
во всех современных архитектурах (включая conditional store в
части RISC), но для них нет стандартных функций и компилятор
не обязан их использовать, например, из соображений оптимизации.

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). Hо даже если 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-подобных
систем. Использовать его можно на свой страх и риск, однако
следует иметь ввиду возможные последствия для надежности и
переносимости приложения.

Hе надо относиться к сигналам как к грому и молнии. В аккуратно
написанной программе всегда известно, какие сигналы в данный момент
следует принимать и обрабатывать, а какие лучше держать заблокированными
или игнорировать. Зачастую это не требует значительных усилий или
изменений в структуре программы -- достаточно четко осознавать,
почему тот &#