Работа с сигналами в Linux

Часть 1. Основы работы с сигналами

Comments

Серия контента:

Этот контент является частью # из серии # статей: Работа с сигналами в Linux

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Работа с сигналами в Linux

Следите за выходом новых статей этой серии.

В современных операционных системах существует понятие межпроцессного взаимодействия (Inter-Process Communication – IPC) – это набор способов обмена данными между процессами и/или потоками. Одним из таких способов обмена служат сигналы. Концепцию сигналов поддерживает большинство операционных систем, но, например, Windows, не имеет их полноценной поддержки для использования в качестве одного из способов IPC – в подобных операционных системах сигналы лишь реализованы в стандартной библиотеке языка C.

Концепция сигналов

Сигналы способны в случайное время (асинхронно) прерывать процесс для обработки какого-либо события. Процесс может быть прерван сигналом по инициативе другого процесса или ядра. Ядро использует сигналы для извещения процессов о различных событиях, например о завершении дочернего процесса.

Сигналы имеют определенный жизненный цикл. Вначале сигнал создается – высылается процессом или генерируется ядром. Затем сигнал ожидает доставки в процесс-приемник. Принято считать, что период ожидания сигнала равен промежутку времени между созданием сигнала и действием, которое этот сигнал высылает. В конце жизненного цикла сигнала происходит его перехват (прием) процессом и выполнение связанных с сигналом действий.

Простые сигналы и надежные сигналы

Существует деление сигналов на простые и надежные.

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

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

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

В процессе решения проблемы были разработаны надежные сигналы, которые стандартизованы в POSIX и используются по сей день. Далее в статье рассматриваются именно надежные сигналы.

Сигналы и системные вызовы

Сигнал может прийти к процессу в тот момент, когда процесс находится внутри какого-нибудь системного вызова, например, ожидает ввода данных в read(). В этом случае развитие событий может пойти по следующему пути: приложение не пытается перехватить сигнал и прерывается ядром (например, приход сигнала SIGTERM) – тогда терминал остается в нестандартной конфигурации, что может затруднить работу пользователя. Конечно, можно перехватить сигнал, очистить терминал в обработчике сигнала, а затем выйти, но достаточно сложно написать такой обработчик сигнала, который бы знал, что делала программа в момент прерывания, чтобы решить, следует ли выполнять очистку терминала, или нет.

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

Современная реализация сигналов заставляет медленные системные вызовы возвращать код ошибки EINTR, когда они прерываются приходящим сигналом. Быстрые системные вызовы должны завершаться перед тем, как сигнал будет доставлен. Медленный системный вызов – системный вызов, требующий неопределенного количества времени для своего завершения, например read(), wait(), write(). Все системные вызовы, зависящие от непредсказуемых ресурсов, таких как действия человека, сетевые данные и т.п., являются медленными. Естественно, все остальные системные вызовы – быстрые.

Обычно программа обрабатывает код ошибки EINTR и, если не произошло ничего фатального, перезапускает системный вызов. Большинство Unix-like операционных систем в настоящее время делает это по умолчанию – нужно лишь обработать сигнал в обработчике, а системный вызов будет перезапущен автоматически. В Linux по умолчанию системные вызовы не перезапускаются, но для каждого сигнала процесс может установить флаг, указывающий системе о необходимости перезапуска медленных системных вызовов, прерванных этим сигналом.

Посылка сигналов

Посылка сигналов от одного процесса к другому обычно осуществляется при помощи системного вызова kill(). Его первый параметр – PID процесса, которому посылается сигнал; второй параметр – номер сигнала. Если мы хотим послать сигнал SIGTERM процессу с PID 6666, то используем системный вызов kill() так:

 kill(6666, SIGTERM);

Вместо положительного значения PID можно передать вызову равное по модулю, но отрицательное значение. Тогда сигнал будет послан всем процессам из группы с номером, равным модулю переданного PID'а. Если PID равен 0, то сигнал посылается всем процессам из группы, к которой относится и текущий процесс. Эти возможности используются в основном оболочками для управления заданиями.

Если в качестве PID передать в системный вызов kill() значение -1, то сигнал будет послан всем процессам за исключением init'а. Такая возможность применяется для завершения работы системы.

Более подробная информация по системному вызову kill() приведена в man 2 kill.

Послать сигнал самому себе процесс может при помощи системного вызова raise(), который принимает один параметр – номер сигнала. Пример:

 raise(SIGTERM);

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

Перехват сигналов

Все программы, подчиняющиеся стандарту POSIX, регистрируют свои обработчики сигналов при помощи системного вызова sigaction(). Этот системный вызов имеет три параметра: первый – int signum – номер перехватываемого сигнала. Второй – struct sigaction * act – указатель на структуру, описывающую правила установки обработчика. Третий параметр – struct sigaction * oact – принимает уже установленные правила обработчика сигнала. Либо второй, либо третий (но не оба сразу!) параметр можно установить в NULL при необходимости.

Структура struct sigaction имеет следующее описание:

 struct sigaction {  
     __sighandler_t sa_handler;  
     sigset_t sa_mask;  
     int sa_flags;  
 };

sa_handler – указатель на обработчик сигнала, причем обработчик должен быть объявлен следующим образом:

 void signal_handler(int signo);

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

sa_mask – набор сигналов, которые должны блокироваться при вызове заданного в этой же структуре обработчика сигнала. Как их устанавливать, рассматривается далее.

Параметр sa_flags позволяет процессу модифицировать поведение сигнала. Параметр может принимать всего четыре значения, которые, впрочем, можно объединять при помощи битовой операции "ИЛИ":

  1. SA_NOCLDSTOP – отсылать сигнал SIGCHLD только в случае прерывания дочернего процесса. Приостановка дочернего процесса не вызывает посылки сигнала.
  2. SA_NODEFER – эмуляция простых (ненадежных) сигналов.
  3. SA_RESTHAND – после прихода сигнала его обработчик сбрасывается в SIG_DFL.
  4. SA_RESTART – перезапуск системного вызова после возврата из обработчика сигнала. Если флаг не установлен, то системный вызов возвращает ошибку EINTR.

Как обычно, при успешном выполнении sigaction() возвращается 0, а в случае ошибки – отрицательное значение.

Маска сигналов процесса

Добавление сигналов в структуру sigset_t sa_mask, ее очистка и т.п. осуществляются при помощи набора функций sigemptyset(), sigfillset(), sigaddset(), sigdelset(). Первые две функции принимают один параметр – указатель на структуру sigset_t. Эти функции очищают и заполняют всеми возможными сигналами структуру sigset_t соответственно.

Последние две функции, соответственно, добавляют и удаляют один определенный сигнал из структуры и имеют по два параметра. Их первый параметр – указатель на структуру sigset_t, а второй – номер сигнала.

Все рассмотренные выше функции возвращают 0 при успешном завершении и число, не равное нулю, – при ошибке.

Кроме того, существует еще одна функция, проверяющая, находится ли указанный сигнал в указанном наборе – sigismember(). Ее параметры совпадают с параметрами sigaddset(). Функция возвращает 1, если сигнал находится в наборе, 0 – если не находится, и отрицательное число – при возникшей ошибке.

Помимо всего прочего, мы можем задать список сигналов, доставка которых процессу будет заблокирована. Это выполняется при помощи функции sigprocmask(int how, const sigset_t * set, sigset_t * oldset).

Первый ее параметр описывает то, что должно выполняться:

  1. SIG_BLOCK – сигналы из набора set блокируются;
  2. SIG_UNBLOCK – сигналы из набора set разблокируются;
  3. SIG_SETMASK – сигналы из набора set блокируются, остальные разблокируются.

Второй параметр является указателем на тот самый набор, сигналы из которого блокируются/разблокируются. Если он равен NULL, то значение первого параметра игнорируется системным вызовом.

Третий параметр – указатель на уже используемую маску сигналов; его можно поставить в NULL, если эти данные не нужны.

Для получения списка ожидающих сигналов можно использовать функцию sigpending(), которая принимает единственный параметр – указатель на структуру sigset_t, куда будет записан набор ожидающих сигналов.

Принципы написания обработчиков сигналов

Одно из самых главных правил написания обработчиков сигналов – обработчик должен быть реентерабельным, т.е. он должен допускать свой повторный вызов, когда процесс уже находится в обработчике. Нужно заботиться о том, чтобы обработчик сигналов не использовал глобальные структуры данных или медленные системные вызовы. Если избежать этого, увы, невозможно, то стоит позаботиться о защите от повторного вызова обработчика во время работы со структурой данных или с системным вызовом. Добиться этого можно, заблокировав на время доставку сигнала, обработчик которого сейчас работает, при помощи системного вызова sigprocmask(). Например, мы имеем обработчик сигнала SIGCHLD, выполняем в нем блокировку так:

 void chld_handler(int signum) {  
     sigset_t set;  
   
     if (sigemptyset(&set)) {  
         return;  
     )  
     if (sigaddset(&set, SIGCHLD)) {  
         return;  
     }  
   
     if (sigprocmask(SIG_BLOCK, &set, NULL)) {  
         return;  
     }  
   
     /* делаем здесь что-то важное */  
   
     if (sigprocmask(SIG_UNBLOCK, &set, NULL)) {  
         return;  
     }  
   
     return;  
 }

Кроме всего перечисленного выше, считается, что обработчик должен быть максимально простым – в идеале он должен выставлять некий флаг и завершаться, а все остальное должна выполнять основная часть программы.

Заключение

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

В завершение цикла будет рассказано о том, как получить (и отправить) дополнительные данные о сигнале, если вам не хватает обычной информации о том, что сигнал откуда-то пришел в ваш процесс.


Ресурсы для скачивания


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=495997
ArticleTitle=Работа с сигналами в Linux: Часть 1. Основы работы с сигналами
publish-date=06152010