Сигналы

Каналы

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

К числу наиболее простых и в то же время самых употребительных средств межпроцессного взаимодействия принадлежат каналы, представляемые файлами соответствующего типа. Стандарт POSIX-2001 различает именованные и безымянные каналы. Напомним, что первые создаются функцией mkfifo() и одноименной служебной программой, а вторые - функцией pipe(). Именованным каналам соответствуют элементы файловой системы, ко вторым можно обращаться только посредством файловых дескрипторов. В остальном эти разновидности каналов эквивалентны.

Взаимодействие между процессами через канал может быть установлено следующим образом: один из процессов создает канал и передает другому соответствующий открытый файловый дескриптор. После этого процессы обмениваются данными через канал при помощи функций read() и write(). Примером подобного взаимодействия служит программа, показанная в листинге 8.1.

#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <sys/wait.h> /* Программа копирует строки со стандартного ввода на стандартный вывод, *//* "прокачивая" их через канал. *//* Используются функции ввода/вывода нижнего уровня */ #define MY_PROMPT "Вводите строки\n"#define MY_MSG "Вы ввели: " int main (void) { int fd [2]; char buf [1]; int new_line = 1; /* Признак того, что надо выдать сообщение MY_MSG */ /* перед отображением очередной строки */ /* Создадим безымянный канал */ if (pipe (fd) < 0) { perror ("PIPE"); exit (1); } switch (fork ()) { case -1: perror ("FORK"); exit (2); case 0: /* Чтение из канала и выдачу на стандартный вывод */ /* реализуем в порожденном процессе. */ /* Необходимо закрыть дескриптор, предназначенный */ /* для записи в канал, иначе чтение не завершится */ /* по концу файла */ close (fd [1]); while (read (fd [0], buf, 1) == 1) { if (write (1, buf, 1)!= 1) { perror ("WRITE TO STDOUT"); break; } } exit (0); } /* Чтение со стандартного ввода и запись в канал */ /* возложим на родительский процесс. */ /* Из соображений симметрии закроем дескриптор, */ /* предназначенный для чтения из канала */ close (fd [0]); if (write (fd [1], MY_PROMPT, sizeof (MY_PROMPT) - 1)!= sizeof (MY_PROMPT) - 1) { perror ("WRITE TO PIPE-1"); } while (read (0, buf, 1) == 1) { /* Перед отображением очередной строки */ /* нужно выдать сообщение MY_MSG */ if (new_line) { if (write (fd [1], MY_MSG, sizeof (MY_MSG) - 1)!= sizeof (MY_MSG) - 1) { perror ("WRITE TO PIPE-2"); break; } } if (write (fd [1], buf, 1)!= 1) { perror ("WRITE TO PIPE-3"); break; } new_line = (buf [0] == '\n'); } close (fd [1]); (void) wait (NULL); return (0);}

Листинг 8.1. Пример взаимодействия между процессами через канал с помощью функций ввода/вывода нижнего уровня.

Решение той же задачи, но с использованием функций буферизованного ввода/вывода, показано в листинге 8.2.

#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <limits.h>#include <sys/wait.h>#include <assert.h> /* Программа копирует строки со стандартного ввода на стандартный вывод, *//* "прокачивая" их через канал. *//* Используются функции буферизованного ввода/вывода */ int main (void) { int fd [2]; FILE *fp [2]; char line [LINE_MAX]; /* Создадим безымянный канал */ if (pipe (fd) < 0) { perror ("PIPE"); exit (1); } /* Сформируем потоки по файловым дескрипторам канала */ assert ((fp [0] = fdopen (fd [0], "r"))!= NULL); assert ((fp [1] = fdopen (fd [1], "w"))!= NULL); /* Отменим буферизацию вывода */ setbuf (stdout, NULL); setbuf (fp [1], NULL); switch (fork ()) { case -1: perror ("FORK"); exit (2); case 0: /* Чтение из канала и выдачу на стандартный вывод */ /* реализуем в порожденном процессе. */ /* Необходимо закрыть поток, предназначенный для */ /* записи в канал, иначе чтение не завершится */ /* по концу файла */ fclose (fp [1]); while (fgets (line, sizeof (line), fp [0])!= NULL) { if (fputs (line, stdout) == EOF) { break; } } exit (0); } /* Чтение со стандартного ввода и запись в канал */ /* возложим на родительский процесс. */ /* Из соображений симметрии закроем поток, */ /* предназначенный для чтения из канала */ fclose (fp [0]); fputs ("Вводите строки\n", fp [1]); while (fgets (line, sizeof (line), stdin)!= NULL) { if ((fputs ("Вы ввели: ", fp [1]) == EOF) || (fputs (line, fp [1]) == EOF)) { break; } } fclose (fp [1]); (void) wait (NULL); return (0);}

Листинг 8.2. Пример взаимодействия между процессами через канал с помощью функций буферизованного ввода/вывода.

Если не указано противное, обмен данными через канал происходит в синхронном режиме: процесс, пытающийся читать из пустого канала, открытого кем-либо на запись, приостанавливается до тех пор, пока данные не будут в него записаны; с другой стороны, запись в полный канал задерживается до освобождения необходимого для записи места. Чтобы отменить подобный режим взаимодействия, надо связать с дескрипторами канала флаг статуса O_NONBLOCK (это может быть сделано при помощи функции fcntl()). В таком случае чтение или запись, которые невозможно выполнить немедленно, завершаются неудачей.

Подчеркнем, что при попытке чтения из пустого канала результат равен 0 (как признак конца файла), только если канал не открыт кем-либо на запись. Под "кем-либо" понимается и сам читающий процесс; по этой причине в приведенной выше программе потребовалось закрыть все экземпляры файлового дескриптора fd [1], возвращенного функцией pipe() как дескриптор для записи в канал.

Функция popen(), описанная выше, при рассмотрении командного интерпретатора, является более высокоуровневой по сравнению с pipe(). Она делает сразу несколько вещей: порождает процесс, обеспечивает выполнение в его рамках заданной команды, организует канал между вызывающим и порожденным процессами и формирует необходимые потоки для этого канала. Если при обращении к popen() задан режим " w ", то стандартный ввод команды, выполняющейся в рамках порожденного процесса, перенаправляется на конец канала, предназначенный для чтения; если задан режим " r ", то в канал перенаправляется стандартный вывод.

После вызова popen() процесс может писать в канал или читать из него посредством функций буферизованного ввода/вывода, используя сформированный поток. Канал остается открытым до момента вызова функции pclose() (см. листинг 8.3).

#include <stdio.h>int pclose (FILE *stream);

Листинг 8.3. Описание функции pclose().

Функция pclose() не только закрывает поток, сформированный popen(), но и дожидается завершения порожденного процесса, возвращая его статус.

Типичное применение popen() - организация канала для выдачи динамически порождаемых данных на устройство печати командой lp (см. листинг 8.4).

#include <stdio.h>/* Программа печатает несколько первых строк треугольника Паскаля */#define T_SIZE 16int main (void) { FILE *outptr; long tp [T_SIZE]; /* Массив для хранения текущей строки треугольника */ int i, j; /* Инициализируем массив, чтобы далее все элементы */ /* можно было считать и выводить единообразно */ tp [0] = 1; for (i = 1; i < T_SIZE; i++) { tp [i] = 0; } /* Создадим канал с командой */ if ((outptr = popen ("lp", "w")) == NULL) { perror ("POPEN"); return (-1); } (void) fprintf (outptr, "\nТреугольник Паскаля:\n"); for (i = 0; i < T_SIZE; i++) { /* Элементы очередной строки нужно считать от конца к началу */ /* Элемент tp [0] пересчитывать не нужно */ for (j = i; j > 0; j--) { tp [j] += tp [j - 1]; } /* Вывод строки треугольника в канал */ for (j = 0; j <= i; j++) { (void) fprintf (outptr, " %ld", tp [j]); } (void) fprintf (outptr, "\n"); } return (pclose (outptr));}

Листинг 8.4. Пример создания и использования канала для вывода данных.

Сходным образом можно организовать канал для чтения результатов выполнения команды (см. листинг 8.5).

#include <stdio.h>#include <limits.h>#include <assert.h> #define MY_CMD "ls -l *.c" int main (void) { FILE *inptr; char line [LINE_MAX]; assert ((inptr = popen (MY_CMD, "r"))!= NULL); while (fgets (line, sizeof (line), inptr)!= NULL) { fputs (line, stdout); } return (pclose (inptr));}

Листинг 8.5. Пример создания и использования канала для ввода данных.

Сигналы

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

Согласно стандарту POSIX-2001, под сигналом понимается механизм, с помощью которого процесс или поток управления уведомляют о некотором событии, произошедшем в системе, или подвергают воздействию этого события. Примерами подобных событий могут служить аппаратные исключительные ситуации и специфические действия процессов. Термин "сигнал" используется также для обозначения самого события.

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

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

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

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

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

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

SIG_DFL

Подразумеваемые действия, зависящие от сигнала. Они описаны в заголовочном файле <signal.h>.

SIG_IGN

Игнорировать сигнал. Доставка сигнала не оказывает воздействия на процесс.

указатель на функцию

Обработать сигнал, выполнив при его доставке заданную функцию. После завершения функции обработки процесс возобновляет выполнение с точки прерывания. Обычно функция обработки вызывается в соответствии со следующим C-заголовком: void func (int signo); где signo - номер доставленного сигнала.

Первоначально, до входа в функцию main(), реакция на все сигналы установлена как SIG_DFL или SIG_IGN.

Функция называется асинхронно-сигнально-безопасной (АСБ), если ее можно вызывать без каких-либо ограничений при обработке сигналов. В стандарте POSIX-2001 имеется список функций, которые должны быть либо повторно входимыми, либо непрерываемыми сигналами, что превращает их в АСБ-функции. В этот список включены 117 функций, в том числе почти все из рассматриваемых нами.

Если сигнал доставляется потоку, а реакция заключается в завершении, остановке или продолжении, весь процесс должен завершиться, остановиться или продолжиться.

Перейдем к изложению возможностей по генерации сигналов. Выше была кратко рассмотрена служебная программа kill как средство терминирования процессов извне. На самом деле она посылает заданный сигнал; то же делает и одноименная функция (см. листинг 8.6).

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

Листинг 8.6. Описание функции kill().

Сигнал задается аргументом sig, значение которого может быть нулевым; в этом случае действия функции kill() сводятся к проверке допустимости значения pid (нулевой результат - признак успешного завершения kill()).

Если pid > 0, это значение трактуется как идентификатор процесса. При нулевом значении pid сигнал посылается всем процессам из той же группы, что и вызывающий. Если значение pid равно -1, адресатами являются все процессы, которым вызывающий имеет право посылать сигналы. При прочих отрицательных значениях pid сигнал посылается группе процессов, чей идентификатор равен абсолютной величине pid.

Процесс имеет право послать сигнал адресату, заданному аргументом pid, если он (процесс) имеет соответствующие привилегии или его реальный или действующий идентификатор пользователя совпадает с реальным или сохраненным ПДП-идентификатором адресата.

У служебной программы kill имеется полезная опция -l, позволяющая увидеть соответствие между номерами сигналов и их мнемоническими именами. Результат выполнения команды kill -l может выглядеть так, как показано в листинге 8.7.

1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR213) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO30) SIGPWR 31) SIGSYS 32) SIGRTMIN 33) SIGRTMIN+134) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+538) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+942) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+1346) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-1450) SIGRTMAX-13 51) SIGRTMAX-12 52) SIGRTMAX-11 53) SIGRTMAX-1054) SIGRTMAX-9 55) SIGRTMAX-8 56) SIGRTMAX-7 57) SIGRTMAX-658) SIGRTMAX-5 59) SIGRTMAX-4 60) SIGRTMAX-3 61) SIGRTMAX-262) SIGRTMAX-1 63) SIGRTMAX

Листинг 8.7. Возможный результат выполнения команды kill -l.

Мы не будем пояснять назначение всех представленных в листинге сигналов, ограничившись кратким описанием тех, что фигурируют в стандарте POSIX-2001 как обязательные для реализации. Попутно отметим, что, согласно стандарту языка C, должны быть определены имена всего шести сигналов: SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV и SIGTERM.

SIGABRT

Сигнал аварийного завершения процесса. Подразумеваемая реакция предусматривает, помимо аварийного завершения, создание файла с образом памяти процесса.

SIGALRM

Срабатывание будильника. Подразумеваемая реакция - аварийное завершение процесса.

SIGBUS

Ошибка системной шины как следствие обращения к неопределенной области памяти. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGCHLD

Завершение, остановка или продолжение порожденного процесса. Подразумеваемая реакция - игнорирование.

SIGCONT

Продолжение процесса, если он был остановлен. Подразумеваемая реакция - продолжение выполнения или игнорирование (если процесс не был остановлен).

SIGFPE

Некорректная арифметическая операция. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGHUP

Сигнал разъединения. Подразумеваемая реакция - аварийное завершение процесса.

SIGILL

Некорректная команда. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGINT

Сигнал прерывания, поступивший с терминала. Подразумеваемая реакция - аварийное завершение процесса.

SIGKILL

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

SIGPIPE

Попытка записи в канал, из которого никто не читает. Подразумеваемая реакция - аварийное завершение процесса.

SIGQUIT

Сигнал выхода, поступивший с терминала. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGSEGV

Некорректное обращение к памяти. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGSTOP

Остановка выполнения (этот сигнал нельзя перехватить для обработки или проигнорировать). Подразумеваемая реакция - остановка процесса.

SIGTERM

Сигнал терминирования. Подразумеваемая реакция - аварийное завершение процесса.

SIGTSTP

Сигнал остановки, поступивший с терминала. Подразумеваемая реакция - остановка процесса.

SIGTTIN

Попытка чтения из фонового процесса. Подразумеваемая реакция - остановка процесса.

SIGTTOU

Попытка записи из фонового процесса. Подразумеваемая реакция - остановка процесса.

SIGUSR1, SIGUSR2

Определяемые пользователем сигналы. Подразумеваемая реакция - аварийное завершение процесса.

SIGPOLL

Опрашиваемое событие. Подразумеваемая реакция - аварийное завершение процесса.

SIGPROF

Срабатывание таймера профилирования. Подразумеваемая реакция - аварийное завершение процесса.

SIGSYS

Некорректный системный вызов. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGTRAP

Попадание в точку трассировки/прерывания. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGURG

Высокоскоростное поступление данных в сокет. Подразумеваемая реакция - игнорирование.

SIGVTALRM

Срабатывание виртуального таймера. Подразумеваемая реакция - аварийное завершение процесса.

SIGXCPU

Исчерпан лимит процессорного времени. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGXFSZ

Превышено ограничение на размер файлов. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

Процесс (поток управления) может послать сигнал самому себе с помощью функции raise() (см. листинг 8.8). Для процесса вызов raise() эквивалентен kill (getpid(), sig);

#include <signal.h>int raise (int sig);

Листинг 8.8. Описание функции raise().

Посылка сигнала самому себе использована в функции abort() (см. листинг 8.9), вызывающей аварийное завершение процесса. (Заметим, что этого не произойдет, если функция обработки сигнала SIGABRT не возвращает управления. С другой стороны, abort() отменяет блокирование или игнорирование SIGABRT.)

#include <stdlib.h>void abort (void);

Листинг 8.9. Описание функции abort().

Опросить и изменить способ обработки сигналов позволяет функция sigaction() (см. листинг 8.10).

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

Листинг 8.10. Описание функции sigaction().

Для описания способа обработки сигнала используется структура sigaction, которая должна содержать по крайней мере следующие поля:

void (*sa_handler) (int); /* Указатель на функцию обработки сигнала *//* или один из макросов SIG_DFL или SIG_IGN */sigset_t sa_mask; /* Дополнительный набор сигналов, блокируемых *//* на время выполнения функции обработки */int sa_flags; /* Флаги, влияющие на поведение сигнала */void (*sa_sigaction) (int, siginfo_t *, void *);/* Указатель на функцию обработки сигнала */

Приложение, соответствующее стандарту, не должно одновременно использовать поля обработчиков sa_handler и sa_sigaction.

Тип sigset_t может быть целочисленным или структурным и представлять набор сигналов (см. далее).

Тип siginfo_t должен быть структурным по крайней мере со следующими полями:

int si_signo; /* Номер сигнала */int si_errno; /* Значение переменной errno, ассоциированное с данным сигналом */int si_code; /* Код, идентифицирующий причину сигнала */pid_t si_pid; /* Идентификатор процесса, пославшего сигнал */uid_t si_uid; /* Реальный идентификатор пользователя процесса, пославшего сигнал */void *si_addr; /* Адрес, вызвавший генерацию сигнала */int si_status; /* Статус завершения порожденного процесса */long si_band; /* Событие, связанное с сигналом SIGPOLL */

В заголовочном файле <signal.h> определены именованные константы, предназначенные для работы с полем si_code, значения которого могут быть как специфичными для конкретного сигнала, так и универсальными. К числу универсальных кодов относятся:

SI_USER

Сигнал послан функцией kill().

SI_QUEUE

Сигнал послан функцией sigqueue().

SI_TIMER

Сигнал сгенерирован в результате срабатывания таймера, установленного функцией timer_settime().

SI_ASYNCIO

Сигнал вызван завершением асинхронной операции ввода/вывода.

SI_MESGQ

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

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

ILL_ILLOPC

Некорректный код операции.

ILL_COPROC

Ошибка сопроцессора.

FPE_INTDIV

Целочисленное деление на нуль.

FPE_FLTOVF

Переполнение при выполнении операции вещественной арифметики.

FPE_FLTSUB

Индекс вне диапазона.

SEGV_MAPERR

Адрес не отображен на объект.

BUS_ADRALN

Некорректное выравнивание адреса.

BUS_ADRERR

Несуществующий физический адрес.

TRAP_BRKPT

Процесс достиг точки прерывания.

TRAP_TRACE

Срабатывание трассировки процесса.

CLD_EXITED

Завершение порожденного процесса.

CLD_STOPPED

Остановка порожденного процесса.

POLL_PRI

Поступили высокоприоритетные данные.

Вернемся непосредственно к описанию функции sigaction(). Если аргумент act отличен от NULL, он указывает на структуру, специфицирующую действия, которые будут ассоциированы с сигналом sig. По адресу oact (если он не NULL) возвращаются сведения о прежних действиях. Если значение act есть NULL, обработка сигнала остается неизменной; подобный вызов можно использовать для опроса способа обработки сигналов.

Следующие флаги в поле sa_flags влияют на поведение сигнала sig.

SA_NOCLDSTOP

Не генерировать сигнал SIGCHLD при остановке или продолжении порожденного процесса (значение аргумента sig должно равняться SIGCHLD).

SA_RESETHAND

При входе в функцию обработки сигнала sig установить подразумеваемую реакцию SIG_DFL и очистить флаг SA_SIGINFO (см. далее).

SA_SIGINFO

Если этот флаг не установлен и определена функция обработки сигнала sig, она вызывается с одним целочисленным аргументом - номером сигнала. Соответственно, в приложении следует использовать поле sa_handler структуры sigaction. При установленном флаге SA_SIGINFO функция обработки вызывается с двумя дополнительными аргументами, как void func (int sig, siginfo_t *info, void *context); второй аргумент указывает на данные, поясняющие причину генерации сигнала, а третий может быть преобразован к указателю на тип ucontext_t - контекст процесса, прерванного доставкой сигнала. В этом случае приложение должно использовать поле sa_sigaction и поля структуры типа siginfo_t. В частности, если значение si_code неположительно, сигнал был сгенерирован процессом с идентификатором si_pid и реальным идентификатором пользователя si_uid.

SA_NODEFER

По умолчанию обрабатываемый сигнал добавляется к маске сигналов процесса при входе в функцию обработки; флаг SA_NODEFER предписывает не делать этого, если только sig не фигурирует явным образом в sa_mask.

Опросить и изменить способ обработки сигналов можно и на уровне командного интерпретатора, посредством специальной встроенной команды trap:

trap [действие условие...]

Аргумент "условие" может задаваться как EXIT (завершение командного интерпретатора) или как имя доставленного сигнала (без префикса SIG). При задании аргумента "действие" минус обозначает подразумеваемую реакцию, пустая цепочка ("") - игнорирование. Если в качестве действия задана команда, то при наступлении условия она обрабатывается как eval действие.

Команда trap без аргументов выдает на стандартный вывод список команд, ассоциированных с каждым из условий. Выдача имеет формат, пригодный для восстановления способа обработки сигналов (см. листинг 8.11).

save_traps=$(trap)...eval "$save_traps"

Листинг 8.11. Пример сохранения и восстановления способа обработки сигналов посредством специальной встроенной команды trap.

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

trap '$HOME/logout' EXIT

Листинг 8.12. Пример использования специальной встроенной команды trap.

При перенаправлении вывода в файл приходится считаться с возможностью возникновения ошибок, специфичных для каналов. Чтобы защитить от них процедуры начальной загрузки, в ОС Lunix применяются связки из игнорирования и последующего восстановления подразумеваемой реакции на сигнал SIGPIPE (см. листинг 8.13).

trap "" PIPEecho "$INITLOG_ARGS -n $0 -s \"$1\" -e 1" >&21trap - PIPE

Листинг 8.13. Пример использования специальной встроенной команды trap для защиты от ошибок, специфичных для каналов.

К техническим аспектам можно отнести работу с наборами сигналов, которая выполняется посредством функций, показанных в листинге 8.14. Функции sigemptyset() и sigfillset() инициализируют набор, делая его, соответственно, пустым или "полным". Функция sigaddset() добавляет сигнал signo к набору set, sigdelset() удаляет сигнал, а sigismember() проверяет вхождение в набор. Обычно признаком завершения является нулевой результат, в случае ошибки возвращается -1. Только sigismember() выдает 1, если сигнал signo входит в набор set.

#include <signal.h>int sigemptyset (sigset_t *set);int sigfillset (sigset_t *set);int sigaddset (sigset_t *set, int signo);int sigdelset (sigset_t *set, int signo);int sigismember (const sigset_t *set, int signo);

Листинг 8.14. Описание функций для работы с наборами сигналов.

Функция sigprocmask() (см. листинг 8.15) предназначена для опроса и/или изменения маски сигналов процесса, определяющей набор блокируемых сигналов.

#include <signal.h>int sigprocmask (int how, const sigset_t *restrict set, sigset_t *restrict oset);

Листинг 8.15. Описание функции sigprocmask().

Если аргумент set отличен от NULL, он указывает на набор, используемый для изменения текущей маски сигналов. Аргумент how определяет способ изменения; он может принимать одно из трех значений: SIG_BLOCK (результирующая маска получается при объединении текущей и заданной аргументом set), SIG_SETMASK (результирующая маска устанавливается равной set) и SIG_UNBLOCK (маска set вычитается из текущей).

По адресу oset (если он не NULL) возвращается прежняя маска. Если значение set есть NULL, набор блокируемых сигналов остается неизменным; подобный вызов можно использовать для опроса текущей маски сигналов процесса.

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

Нельзя блокировать сигналы, не допускающие игнорирования.

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

#include <signal.h>int sigpending (sigset_t *set);

Листинг 8.16. Описание функции sigpending().

#include <signal.h>int sigwait (const sigset_t *restrict set, int *restrict sig);

Листинг 8.17. Описание функции sigwait().

Функция sigwait() выбирает ждущий сигнал из заданного набора (он должен включать только блокированные сигналы), удаляет его из системного набора ждущих сигналов и помещает его номер по адресу, заданному аргументом sig. Если в момент вызова sigwait() нужного сигнала нет, процесс (поток управления) приостанавливается до появления такового.

Отметим, что стандарт POSIX-2001 не специфицирует воздействие функции sigwait() на обработку сигналов, включенных в набор set. Чтобы дождаться доставки обрабатываемого или терминирующего процесс сигнала, можно воспользоваться функцией pause() (см. листинг 8.18).

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

Листинг 8.18. Описание функции pause().

Функция pause() может ждать доставки сигнала неопределенно долго. Возврат из pause() осуществляется после возврата из функции обработки сигнала (результат при этом равен -1). Если прием сигнала вызывает завершение процесса, возврата из функции pause(), естественно, не происходит.

Несмотря на внешнюю простоту, использование функции pause() сопряжено с рядом тонкостей. При наивном подходе сначала проверяют некоторое условие, связанное с сигналом, и, если оно не выполнено (сигнал отсутствует), вызывают pause(). К сожалению, сигнал может быть доставлен в промежутке между проверкой и вызовом pause(), что нарушает логику работы процесса и способно привести к его зависанию. Решить подобную проблему позволяет функция sigsuspend() (см. листинг 8.19) в сочетании с рассмотренной выше функцией sigprocmask().

#include <signal.h>int sigsuspend (const sigset_t *sigmask);

Листинг 8.19. Описание функции sigsuspend().

Функция sigsuspend() заменяет текущую маску сигналов вызывающего процесса на набор, заданный аргументом sigmask, а затем переходит в состояние ожидания, аналогичное функции pause(). После возврата из sigsuspend() (если таковой произойдет) восстанавливается прежняя маска сигналов.

Обычно парой функций sigprocmask() и sigsuspend() обрамляют критические интервалы. Перед входом в критический интервал посредством sigprocmask() блокируют некоторые сигналы, а на выходе вызывают sigsuspend() с маской, которую возвратила sigprocmask(), восстанавливая тем самым набор блокированных сигналов и дожидаясь их доставки.

В качестве примера использования описанных выше функций работы с сигналами рассмотрим упрощенную реализацию функции abort() (см. листинг 8.20).

#include <unistd.h>#include <signal.h>#include <stdio.h> void abort (void) { struct sigaction sact; sigset_t sset; /* Вытолкнем буфера */ (void) fflush (NULL); /* Снимем блокировку сигнала SIGABRT */ if ((sigemptyset (&sset) == 0) && (sigaddset (&sset, SIGABRT) == 0)) { (void) sigprocmask (SIG_UNBLOCK, &sset, (sigset_t *) NULL); } /* Пошлем себе сигнал SIGABRT. */ /* Возможно, его перехватит функция обработки, */ /* и тогда вызывающий процесс может не завершиться */ raise (SIGABRT); /* Установим подразумеваемую реакцию на сигнал SIGABRT */ sact.sa_handler = SIG_DFL; sigfillset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIGABRT, &sact, NULL); /* Снова пошлем себе сигнал SIGABRT */ raise (SIGABRT); /* Если сигнал снова не помог, попробуем еще одно средство завершения */ _exit (127);} int main (void) { printf ("Перед вызовом abort()\n"); abort (); printf ("После вызова abort()\n"); return 0;}

Листинг 8.20. Упрощенная реализация функции abort() как пример использования функций работы с сигналами.

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

Еще одним примером использования механизма сигналов может служить приведенная в листинге 8.21 упрощенная реализация функции sleep(), предназначенной для "засыпания" на заданное число секунд. (Можно надеяться, что не описанные пока средства работы с временем интуитивно понятны.)

#include <unistd.h>#include <stdio.h>#include <signal.h>#include <time.h> /* Функция обработки сигнала SIGALRM. *//* Она ничего не делает, но игнорировать сигнал нельзя */static void signal_handler (int sig) {/* В демонстрационных целях распечатаем номер обрабатываемого сигнала */ printf ("Принят сигнал %d\n", sig);} /* Функция для "засыпания" на заданное число секунд *//* Результат равен разности между заказанной и фактической *//* продолжительностью "сна" */unsigned int sleep (unsigned int seconds) { time_t before, after; unsigned int slept; sigset_t set, oset; struct sigaction act, oact; if (seconds == 0) { return 0; } /* Установим будильник на заданное время, */ /* но перед этим блокируем сигнал SIGALRM */ /* и зададим свою функцию обработки для него */ if ((sigemptyset (&set) < 0) || (sigaddset (&set, SIGALRM) < 0) || sigprocmask (SIG_BLOCK, &set, &oset)) { return seconds; } act.sa_handler = signal_handler; act.sa_flags = 0; act.sa_mask = oset; if (sigaction (SIGALRM, &act, &oact) < 0) { return seconds; } before = time ((time_t *) NULL); (void) alarm (seconds); /* Как атомарное действие восстановим старую маску сигналов */ /* (в надежде, что она не блокирует SIGALRM) */ /* и станем ждать доставки обрабатываемого сигнала */ (void) sigsuspend (&oset); /* сигнал доставлен и обработан */ after = time ((time_t *) NULL); /* Восстановим прежний способ обработки сигнала SIGALRM */ (void) sigaction (SIGALRM, &oact, (struct sigaction *) NULL); /* Восстановим первоначальную маску сигналов */ (void) sigprocmask (SIG_SETMASK, &oset, (sigset_t *) NULL); return ((slept = after - before) > seconds? 0: (seconds - slept));} int main (void) { struct sigaction act; /* В демонстрационных целях установим обработку прерывания с клавиатуры */ act.sa_handler = signal_handler; (void) sigemptyset (&act.sa_mask); act.sa_flags = 0; (void) sigaction (SIGINT, &act, (struct sigaction *) NULL); printf ("Заснем на 10 секунд\n"); printf ("Проснулись, не доспав %d секунд\n", sleep (10)); return (0);}

Листинг 8.21. Упрощенная реализация функции sleep() как пример использования механизма сигналов.

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

Вызвать "недосыпание" приведенной программы можно, послав ей сигнал SIGALRM (например, посредством команды kill -s SIGALRM идентификатор_процесса) или SIGINT (путем нажатия на клавиатуре терминала комбинации клавиш CTRL+C).


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: