Книга: Основы программирования в Linux

Отправка сигналов

Отправка сигналов

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

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

Функция kill посылает заданный сигнал sig процессу с идентификатором, заданным в аргументе pid. В случае успеха она возвращает 0. Для отправки сигнала посылающий процесс должен иметь право на выполнение этого действия. Обычно это означает, что у обоих процессов должен быть один и тот же идентификатор пользователя ID (т.е. вы можете отправить сигнал только одному из собственных процессов, хотя суперпользователь может отправлять сигналы любому процессу).

Функция kill завершится аварийно, вернет -1 и установит значение переменной errno, если задан неверный сигнал, (errno равна EINVAL), у процесса нет полномочий (EPERM) или заданный процесс не существует (ESRCH).

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

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

Вызов alarm намечает доставку сигнала SIGALRM через seconds секунд. В действительности сигнал будильника будет доставлен чуть позже из-за обработки задержек и учета неопределенностей. Значение 0 отменяет любой невыполненный запрос на сигнал будильника. Вызов функции alarm до получения сигнала может вызвать сброс графика доставки. У каждого процесса может быть только один невыполненный сигнал будильника. Функция alarm возвращает количество секунд, оставшихся до отправки любого невыполненного вызова, alarm, или -1 в случае аварийного завершения.

Для того чтобы увидеть как работает функция alarm, можно сымитировать ее действие, используя вызовы fork, sleep и signal (упражнение 11.8). Программа сможет запустить новый процесс с единственной целью — отправить сигнал спустя какое- то время.

Упражнение 11.8 Будильник

В программе alarm.c первая функция, ding, имитирует будильник.

#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
static int alarm_fired = 0;
void ding(int sig) {
 alarm_fired = 1;
}

В функции main вы заставляете дочерний процесс ждать пять секунд перед отправкой сигнала SIGALRM в свой родительский процесс:

int main() {
 pid_t pid;
 printf("alarm application startingn");
 pid = fork();
 switch(pid) {
 case -1:
  /* Аварийное завершение */
  perror("fork failed");
  exit(1);
 case 0:
  /* Дочерний процесс */
  sleep(5);
  kill(getppid(), SIGALRM);
  exit(0);
 }

Родительский процесс устроен так, что перехватывает сигнал SIGALRM с помощью вызова signal и затем ждет неизбежности:

 /* Если мы оказались здесь, то мы — родительский процесс */
 printf("waiting for alarm to go offn");
 (void)signal(SIGALRM, ding);
 pause();
 if (alarm_fired) printf("Ding!n");
 printf("donen");
 exit(0);
}

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

$ ./alarm
alarm application starting
waiting for alarm to go off
<5 second pause>
Ding!
done $

В этой программе вводится новая функция pause, которая просто приостанавливает выполнение программы до появления сигнала. Когда она получит сигнал, выполняется любой установленный обработчик, и выполнение продолжается как обычно. Она объявляется следующим образом:

#include <unistd.h>
int pause(void);

Функция возвращает -1 (если следующий полученный сигнал не вызвал завершения программы) с переменной errno, равной EINTR, в случае прерывания сигналом. Лучше для ожидания сигналов применять функцию sigsuspend, которую мы обсудим чуть позже в этой главе.

Как это работает

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

Применение сигналов и приостановка выполнения — важные составляющие программирования в ОС Linux. Это означает, что программа необязательно должна выполняться все время. Вместо того чтобы долго работать в цикле, проверяя, не произошло ли событие, она может ждать его наступления. Это особенно важно в многопользовательской среде, где процессы совместно используют один процессор, и такой вид деятельного ожидания оказывает большое влияние на производительность системы. Особая проблема, связанная с сигналами, заключается в том, что вы никогда не знаете наверняка, что произойдет, если сигнал появится в середине системного вызова? (Ответ весьма неудовлетворительный: все зависит от ситуации.) Вообще следует беспокоиться только о "медленных" системных вызовах, таких как считывание с терминала, когда системный вызов может вернуться с ошибкой, если сигнал появится во время его пребывания в режиме ожидания. Если вы начнете применять сигналы в своих программах, нужно учитывать, что некоторые системные вызовы могут закончиться аварийно, если сигнал создаст ошибочную ситуацию, которую вы могли не принимать во внимание до того, как добавили обработку сигналов.

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

Надежный интерфейс сигналов

Мы рассмотрели подробно возбуждение и перехват сигналов с помощью signal и родственных функций, поскольку они очень часто применяются в старых UNIX-программах. Тем не менее, стандарты X/Open и спецификации UNIX рекомендуют более современный программный интерфейс для сигналов sigaction, который более надежен.

#include <signal.h>
int sigaction<int sig, const struct sigaction *act, struct sigaction *oact);

Структура sigaction, применяемая для определения действий, предпринимаемых при получении сигнала, заданного в аргументе sig, определена в файле signal.h и как минимум включает следующие элементы:

void (*)(int)sa_handler /* функция, SIG_DFL или SIG_IGN */
sigset_t sa_mask        /* сигналы, заблокированные для sa_handler */
int sa_flags            /* модификаторы действий сигнала */

Функция sigaction задает действие, связанное с сигналом sig. Если oact не null, sigaction записывает предыдущее действие для сигнала в указанное oact место. Если act равен null, это все, что делает функция sigaction. Если указатель act не null, задается действие для указанного сигнала.

Как и функция signal, sigaction возвращает 0 в случае успешного выполнения и -1 в случае ошибки. Переменная errno получит значение EINVAL, если заданный сигнал некорректен или была предпринята попытка захватить или проигнорировать сигнал, который нельзя захватывать или игнорировать.

В структуре sigaction, на которую указывает аргумент act, sa_handler — это указатель на функцию, вызываемую при получении сигнала sig. Она очень похожа на функцию func, которая, как вы видели раньше, передавалась функции signal. Вы можете применять специальные значения SIG_IGN и SIG_DFL в поле sa_handler для обозначения того, что сигнал должен игнорироваться или должно быть восстановлено действие по умолчанию, соответственно.

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

Однако сигналы, захватываемые обработчиками, заданными в структуре sigaction, по умолчанию не восстанавливаются, и нужно задать в поле sa_flags значение SA_RESETHAND, если хотите добиться поведения, виденного вами раньше при обсуждении функции signal. Прежде чем обсуждать подробнее sigaction, давайте перепишем программу ctrlc.c, применяя sigaction вместо функции signal (упражнение 11.9).

Упражнение 11.9. Функция sigaction

Внесите приведенные далее изменения, так чтобы сигнал SIGINT перехватывался sigaction. Назовите новую программу ctrlc2.c.

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void ouch(int sig) {
 printf("OUCH! - I got signal %dn", sig);
}
int main() {
 struct sigaction act;
 act.sa_handler = ouch;
 sigemptyset(&act.sa_mask);
 act.sa_flags = 0;
 sigaction(SIGINT, &act, 0);
 while (1) {
  printf("Hello World!n");
  sleep(1);
 }
}

Когда вы выполните эту версию программы, то всегда будете получать сообщение при нажатии комбинации клавиш <Ctrl>+<C>, поскольку SIGINT обрабатывается неоднократно функцией sigaction. Для завершения программы следует нажать комбинацию клавиш <Ctrl>+<>, которая генерирует по умолчанию сигнал SIIGQUIT.

$ ./ctrlc2
Hello World!
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
^
Quit
$

Как это работает

Программа вместо функции signal вызывает sigaction для задания функции ouch как обработчика сигнала, возникающего при нажатии комбинации клавиш <Ctrl>+<C> (SIGINT). Прежде всего, она должна определить структуру sigaction, содержащую обработчик, маску сигналов и флаги, В данном случае вам не нужны никакие флаги, и создается пустая маска сигналов с помощью новой функции sigemptyset.

Примечание

После выполнения программы вы можете обнаружить дамп ядра (в файле core). Его можно безбоязненно удалить.

Оглавление книги


Генерация: 1.147. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз