Сигналы
Как и каналы, сигналы являются внешне простым и весьма употребительным средством локального межпроцессного взаимодействия, но связанные с ними идеи существенно сложнее, а понятия - многочисленнее.
Согласно стандарту 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(). (html, txt)
Сигнал задается аргументом sig, значение которого может быть нулевым; в этом случае действия функции kill() сводятся к проверке допустимости значения pid (нулевой результат - признак успешного завершения kill()).
Если pid > 0, это значение трактуется как идентификатор процесса. При нулевом значении pid сигнал посылается всем процессам из той же группы, что и вызывающий. Если значение pid равно -1, адресатами являются все процессы, которым вызывающий имеет право посылать сигналы. При прочих отрицательных значениях pid сигнал посылается группе процессов, чей идентификатор равен абсолютной величине pid.
Процесс имеет право послать сигнал адресату, заданному аргументом pid, если он (процесс) имеет соответствующие привилегии или его реальный или действующий идентификатор пользователя совпадает с реальным или сохраненным ПДП-идентификатором адресата.
У служебной программы kill имеется полезная опция -l, позволяющая увидеть соответствие между номерами сигналов и их мнемоническими именами. Результат выполнения команды kill -l может выглядеть так, как показано в листинге 8.7.
Листинг 8.7. Возможный результат выполнения команды kill -l. (html, txt)
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 "" PIPE echo "$INITLOG_ARGS -n $0 -s \"$1\" -e 1" >&21 trap - 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).