Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 30 (всего у книги 55 страниц)
Сигналы являются сложной темой, и она становится еще более сбивающей с толку. Поэтому давайте на время сделаем остановку, сделаем шаг назад и подведем итог обсужденному до сих пор:
• Сигналы являются указанием того, что произошло некоторое внешнее событие.
• raise()
является функцией ISO С для отправки сигнала текущему процессу. Как отправлять сигналы другим процессам, нам еще предстоит описать.
• signal()
контролирует диспозицию сигнала, т.е. реакцию процесса на сигнал, когда он появляется. Сигнал можно оставить системе для обработки по умолчанию, проигнорировать или перехватить.
• Когда сигнал перехватывается, вызывается функция-обработчик. Вот где сложность начинает поднимать свою безобразную голову:
• ISO С не определяет, восстанавливается ли диспозиция сигнала по умолчанию до вызова обработчика или она остается на месте. Первое является поведением V7 и современных систем System V, таких, как Solaris. Последнее является поведением BSD, используемым также в GNU/Linux. (Для форсирования поведения BSD может использоваться функция POSIX bsd_signal()
.)
• То, что случается при прерывании сигналом системного вызова, также различается в традиционной и BSD линейках. Традиционные системы возвращают -1 с errno, установленным в EINTR
. BSD системы повторно запускают системный вызов после возвращения из обработчика. Макрос GLIBC TEMP_FAILURE_RETRY()
может помочь вам написать код для обработки системных вызовов, возвращающих -1 с errno
, установленным в EINTR
.
POSIX требует, чтобы частично выполненный системный вызов возвращал успешное завершение, указав, сколько работы было выполнено. Системный вызов, который еще не начал выполняться, вызывается повторно.
• Механизм signal()
предоставляет плодотворную почву для появления условий гонки. В этой ситуации помогает тип данных ISO С sig_atomic_t
, но он не решает проблему, и определенный таким способом механизм не может обезопасить от проявления условий гонки.
• Применяется ряд дополнительных предостережений, и в частности, из обработчика сигнала безопасно может вызываться лишь часть стандартных библиотечных функций.
Несмотря на эти проблемы интерфейса signal()
для простых программ достаточно, и он все еще широко используется.
sigset()
и др.BSD 4.0 (примерно с 1980 г.) ввел дополнительные функции API для предоставления «надежных» сигналов.[109]109
Для использования API требуется компоновка с отдельной библиотекой, – ljobs
– Примеч. автора.
[Закрыть] В частности, стало возможным блокировать сигналы. Другими словами, программа могла сообщить ядру: «Зависни на этих конкретных сигналах в течении следующего небольшого промежутка времени, затем доставь их мне, когда я буду готов их принять». Большим преимуществом является то, что эта особенность упрощает обработчики сигналов, которые автоматически запускаются со своим заблокированным сигналом (чтобы избежать проблемы одновременной обработки двух сигналов) и, возможно, также и с другими заблокированными сигналами.
System V Release 3 (примерно с 1984 г.) приняла эти API и популяризовала их, в большинстве связанных с Unix документации и книгах вы, возможно, увидите, что на эти API ссылаются, как ведущие начало от System V Release 3. Эти функции следующие:
#include
int sighold(int sig); /* Добавить sig к маске сигналов процесса */
int sigrelse(int sig); /* Удалить sig из маски сигналов процесса */
int sigignore(int sig); /* Сокращение для sigset(sig, SIG_IGN) */
int sigpause(int sig);
/* Приостановить процесс, позволить появиться sig */
void (*sigset(int sig, void (*disp)(int)))(int);
/* sighandler_t sigset(int sig, sighandler_t disp); */
Стандарт POSIX для этих функций описывает их поведение в терминах маски сигналов процесса. Маска сигналов процесса отслеживает, какие сигналы (если они вообще есть) процесс заблокировал в настоящее время. Более подробно это описывается в разделе 10.6.2 «Наборы сигналов: sigset_t
и связанные функции». В API System V Release 3 нет способа получить или изменить маску сигналов процесса в целом. Функции работают следующим образом:
int sighold(int sig)
Добавляет sig
к списку заблокированных процессов (маска сигналов процесса).
int sigrelse(int sig)
Удаляет sig
из маски сигналов процесса.
int sigignore(int sig)
Игнорирует sig
. Это вспомогательная функция.
int sigpause(int sig)
Удаляет sig
из маски сигналов процесса, а затем приостанавливает процесс до появления сигнала (см. раздел 10.7 «Сигналы для межпроцессного взаимодействия»).
sighandler_t sigset(int sig, sighandler_t disp)
Это замена для signal(). (Здесь мы использовали обозначение из справочной страницы GNU/Linux, чтобы упростить восприятие объявления функции.)
Для sigset()
аргумент handler
может быть SIG_DFL
, SIG_IGN
или указатель функции, как и для signal()
. Однако, он может равняться также и SIG_HOLD
. В этом случае sig
добавляется к маске сигналов процесса, но связанное с ним действие никак не изменяется. (Другими словами, если бы у него был обработчик, он остается тем же; если было действие по умолчанию, оно не изменяется.)
Когда для установки обработчика сигнала используется sigset()
и появляется сигнал, ядро сначала добавляет сигнал к маске процессов сигнала, блокируя любой дальнейший прием этого сигнала. Запускается обработчик, а когда он возвращается, ядро восстанавливает маску сигналов процесса в то состояние, какое было до запуска обработчика. (В модели POSIX если обработчик сигнала изменяет маску сигнала, это изменение переписывается в процессе восстановления предыдущей маски, когда обработчик возвращается.)
sighold()
и sigrelse()
могут использоваться совместно для выделения так называемых критических секций кода: участков кода, который не должен прерываться определенным сигналом, чтобы структуры данных не повреждались кодом обработчика сигнала.
10.6. Сигналы POSIXЗАМЕЧАНИЕ. POSIX стандартизует эти API, поскольку главной целью POSIX является формализация существующей практики, где это возможно. Однако, функции
sigaction()
, которые вскоре будут описаны, дают вам все, что делают эти API, и даже больше. В новых программах вам не следует использовать эти API Вместо этого используйтеsigaction()
. (Мы заметили, что в справочной системе GNU/Linux нет даже страницы для sigset(2)!)
API POSIX основан на API sigvec()
из BSD 4.2 и 4.3. С небольшими изменениями этот API можно было отнести к возможностям API как V7, так и System V Release 3. POSIX сделал эти изменения и переименовал API sigaction()
. Поскольку интерфейс sigvec()
широко не использовался, мы не будем его описывать. Вместо этого в данном разделе описывается только sigaction()
, который вы и должны так или иначе использовать. (На самом деле руководства BSD 4.4 от 1994 г. помечают sigvec()
как устаревшую, указывая читателю на sigaction()
.)
Что неладно с API System V Release 3? В конце концов, они предоставляют блокирование сигналов, так, что сигналы не теряются, и любой данный сигнал может быть надежно обработан.
Ответ в том, что этот API работает лишь с одним сигналом в одно и то же время. Программы обычно обрабатывают больше одного сигнала. И когда вы находитесь в середине процесса обработки одного сигнала, вы не хотите беспокоиться по поводу обработки еще и второго. (Предположим, вы только что начали отвечать по офисному телефону, когда зазвонил ваш мобильный телефон: вы бы предпочли, чтобы телефонная система ответила вызывающему, что в данный момент вы находитесь на другой линии и что скоро освободитесь, вместо того, чтобы проделывать все это самому.)
С API sigset()
каждый обработчик сигнала должен был бы временно блокировать все другие сигналы, сделать свою работу, а затем разблокировать их. Проблема в том, что в промежутке между любыми двумя вызовами sighold()
может появиться еще не заблокированный сигнал. Сценарий, еще раз, распространенный, создающий условия гонки.
Решением является обеспечение возможности автоматической работы с группами сигналов, т.е. с помощью одного системного вызова. Вы достигаете этого, работая с наборами сигналов и маской сигналов процесса.
sigset_t
и связанные функцииМаска сигналов процесса является списком сигналов, которые процесс в настоящее время заблокировал. Сила POSIX API в том, что маской сигналов процесса можно манипулировать атомарно, как единым целым.
Маска сигналов процесса программно представляется с помощью набора сигналов. Это тип sigset_t
. Концептуально он представляет собой просто битовую маску, причем значения 0 и 1 представляют отсутствие или наличие определенного сигнала в маске.
/* Непосредственное манипулирование маской сигналов. НЕ ДЕЛАЙТЕ ЭТОГО! */
int mask = (1 << SIGHUP) | (1 << SIGINT);
/* битовая маска для SIGHUP и SIGINT */
Однако, поскольку в системе может быть больше сигналов, чем может содержаться в одной int
или long
и поскольку интенсивное использование побитовых операций тяжело для восприятия, для управления наборами сигналов существует несколько функций API.
#include
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
Эти функции следующие:
int sigemptyset(sigset_t *set)
Освобождает набор сигналов. По возвращении *set
не содержит сигналов. Возвращает 0 в случае успеха и -1 при ошибке.
int sigfillset(sigset_t *set)
Полностью заполняет набор сигналов. По возвращении *set
содержит все сигналы, определенные системой. Возвращает 0 в случае успеха и -1 при ошибке.
int sigaddset(sigset_t *set, int signum)
Добавляет signum
к маске сигналов процесса в *set
. Возвращает 0 в случае успеха и -1 при ошибке.
int sigdelset(sigset_t *set, int signum)
Удаляет signum
из маски сигналов процесса в *set
. Возвращает 0 в случае успеха и -1 при ошибке.
int sigismember(const sigset_t *set, int signum)
Возвращает true/false, если signum
присутствует или не присутствует в *set
.
Перед выполнением с переменной sigset_t
каких-то действий всегда следует вызывать одну из функций sigemptyset()
или sigfillset()
. Существуют оба интерфейса, поскольку иногда бывает нужно начать с пустого набора и работать потом лишь с одним или двумя сигналами, а в другое время бывает нужно работать со всеми сигналами, возможно, убирая один или два сигнала.
sigprocmask()
и др.Маска сигналов процесса вначале пуста – заблокированных сигналов нет. (Это упрощение; см. раздел 10.9 «Сигналы, передающиеся через fork()
и exec()
.) Три функции позволяют работать непосредственно с маской сигналов процесса:
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpending(sigset_t *set);
int sigsuspend(const sigset_t *set);
Функции следующие:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
Если oldset
не равен NULL
, получается маска сигналов текущего процесса и помещается в *oldset
. Затем маска сигналов процесса обновляется в соответствии с содержимым set
и значением how
, который должен иметь одно из следующих значений:
SIG_BLOCK
Объединить сигналы в *set
с маской сигналов текущего процесса. Новая маска является объединением текущей маски и *set
.
SIG_UNBLOCK
Удалить сигналы в *set
из маски сигналов процесса. Это не представляет проблемы, если *set
содержит сигнал, который не содержится в текущей маске сигналов процесса.
SIG_SETMASK
Заменить маску сигналов процесса содержимым *set
.
Если set
равен NULL
, a oldset
– нет, значение how
неважно. Эта комбинация получает маску сигналов текущего процесса, не меняя ее. (Это явно выражено в стандарте POSIX, но не ясно из справочной страницы GNU/Linux.)
int sigpending(sigset_t *set)
Эта функция позволяет увидеть, какие сигналы ожидают решения; т.е. *set
заполнен этими сигналами, которые были посланы, но они еще не доставлены, поскольку заблокированы.
int sigsuspend(const sigset_t *set)
Эта функция временно заменяет маску сигналов процесса содержимым *set
, а затем приостанавливает процесс, пока сигнал не будет получен. По определению, заставить функцию вернуться может только сигнал, не находящийся в *set
(см. раздел 10.7 «Сигналы для межпроцессного взаимодействия).
sigaction()
Наконец мы готовы взглянуть на функцию sigaction()
. Эта функция сложна, и мы намеренно опускаем множество деталей, которые предназначены для специального использования. Стандарт POSIX и справочная страница sigaction(2) предоставляют все подробности, хотя вы должны тщательно прочесть и то, и другое, чтобы полностью все усвоить.
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
Аргументы следующие:
int signum
Интересующий сигнал, как в случае с другими функциями обработки сигналов.
const struct sigaction *act
Определение нового обработчика для сигнала signum
.
struct sigaction *oldact
Определение текущего обработчика. Если не NULL
, система до установки *act
заполняет *oldact
. *act
может быть NULL
, в этом случае *oldact
заполняется, но больше ничего не меняется.
Таким образом, sigaction()
и устанавливает новый обработчик, и получает старый за одно действие. struct sigaction
выглядит следующим образом.
/* ПРИМЕЧАНИЕ: Порядок в структуре может варьировать. Могут быть
также и другие поля! */
struct sigaction {
sigset_t sa_mask; /* Дополнительные сигналы для блокирования */
int sa_flags; /* Контролирует поведение */
void (*sa_handler)(int);
/* Может образовать объединение с sa_sigaction */
void (*sa_sigaction)(int, siginfo_t*, void*);
/* Может образовать объединение с sa_handler */
}
Поля следующие:
sigset_t sa_mask
Набор дополнительных сигналов для блокирования при запуске функции обработчика. Таким образом, когда вызывается обработчик, общий набор заблокированных сигналов является объединением сигналов в маске процесса, сигналов в act->mask
и, если SA_NODEFER
сброшен, signum
.
int sa_flags
Флаги, контролирующие обработку сигнала ядром. См. обсуждение далее.
void (*sa_handler)(int)
Указатель на «традиционную» функцию обработчика. У нее такой же прототип (возвращаемый тип и список параметров), как у функции обработчика для signal()
, bsd_signal()
и sigset()
.
void (*sa_sigaction)(int, siginfo_t*, void*)
Указатель на функцию обработчика «нового стиля». Функция принимает три аргумента, которые вскоре будут описаны.
Которая из функций act->sa_handler
и act->sa_sigaction
используется, зависит от флага SA_SIGINFO
в act->sa_flags
. Когда имеется, используется act->sa_sigaction
; в противном случае используется act->sa_handler
. Как POSIX, так и справочная страница GNU/Linux указывают, что эти два поля могут перекрываться в памяти (т. е. быть частью union
). Таким образом, никогда не следует использовать оба поля в одной и той же struct sigaction
.
Поле sa_flags
составляется с помощью побитового ИЛИ значений одного или более флагов, перечисленных в табл. 10.3.
Таблица 10.3. Значения флагов для sa_flags
SA_NOCLDSTOP | Этот флаг имеет смысл лишь для SIGCHLD . Когда он установлен, родитель не получает сигнал при остановке порожденною процесса сигналами SIGSTOP , SIGTSTP , SIGTTIN или SIGTTOU . Эти сигналы обсуждаются позже, в разделе 10.8.2 |
SA_NOCLDWAIТ | Этот флаг имеет смысл лишь для SIGCHLD . Его поведение сложно. Мы отложим объяснение на потом, см. раздел 10.8.3 |
SA_NODEFER | Обычно данный сигнал блокируется, когда вызывается обработчик сигнала. Когда установлен один из этих флагов, данный сигнал не блокируется при запуске обработчика SA_NODEFER является официальным именем POSIX данного флага (которое и следует использовать) |
SA_NOMASK | Альтернативное имя для SA_NODEFER [110]110
Насколько мы смогли определить, имена SA_NOMASK и SA_ONESHOT являются специфическими для GNU/Linux. Если кому-нибудь известно иное, пожалуйста, сообщите нам![Закрыть] |
SA_SIGINFO | Обработчик сигнала принимает три аргумента. Как упоминалось, при данном установленном флаге должно использоваться поле sa_sigaction вместо sa_handler . |
SA_ONSTACK | Это продвинутая возможность. Обработчики сигналов могут вызываться с использованием предоставленной пользователем памяти в качестве «альтернативного стека сигнала». Эта память даётся ядру для подобного использования посредством sigaltstack() (см. sigaltstack(2)). Эта особенность больше не описывается в данной книге |
SA_RESETHAND | Этот флаг обеспечивает поведение V7: после вызова обработчика восстанавливается действие сигнала по умолчанию. Официальным именем POSIX флага (которое следует использовать) является SA_RESETHAND |
SA_ONESHOT | Альтернативное имя для SA_RESETHAND. |
SA_RESTART | Этот флаг предоставляет семантику BSD: системные вызовы, которые могут завершиться с ошибкой EINTR и которые получают этот сигнал, запускаются повторно. |
Когда в act->sa_flags
установлен флаг SA_SIGINFO
, поле act->sa_sigaction
является указателем на функцию, объявленную следующим образом:
void action_handler(int sig, siginfo_t *info, void *context) {
/* Здесь тело обработчика */
}
Структура siginfo
_t предоставляет изобилие сведений о сигнале:
/* Определение POSIX 2001. Действительное содержание может на разных системах быть разным. */
typedef struct {
int si_signo; /* номер сигнала */
int si_errno; /* значение
int si_code; /* код сигнала; см. текст */
pid_t si_pid; /* ID процесса, пославшего сигнал */
uid_t si_uid; /* настоящий UID посылающего процесса */
void *si_addr; /* адрес вызвавшей ошибку инструкции */
int si_status; /* значение завершения, может включать death-by-signal */
long si_band; /* связывающее событие для SIGPOLL/SIGIO */
union sigval si_value; /* значение сигнала (расширенное) */
} siginfo_t;
Поля si_signo
, si_code
и si_value
доступны для всех сигналов. Другие поля могут быть членами объединения, поэтому должны использоваться лишь для тех сигналов, для которых они определены. В структуре siginfo_t
могут быть также и другие поля.
Почти все поля предназначены для расширенного использования. Все подробности содержатся в стандарте POSIX и справочной странице sigaction(2). Однако, мы можем описать простое использование поля si_code
.
Для SIGBUS
, SIGCHLD
, SIGFPE
, SIGILL
, SIGPOLL
, SIGSEGV
и SIGTRAP
поле si_code может принимать любое из набора предопределенных значений, специфичных для каждого сигнала, указывая на причину появления сигнала. Откровенно говоря, детали несколько чрезмерны; повседневному коду на самом деле нет необходимости иметь с ними дела (хотя позже мы рассмотрим значения для SIGCHLD
). Для всех остальных сигналов член si_code
имеет одно из значений из табл. 10.4.
Таблица 10.4. Значения происхождения сигнала для si_code
SI_ASYNCIO | Асинхронный ввод/вывод завершен (расширенный). | |
SI_KERNEL | √ | Сигнал послан ядром. |
SI_MESGQ | Состояние очереди сообщений изменилось (расширенный.) | |
SI_QUEUE | Сигнал послан из sigqueue() (расширенный). | |
SI_SIGIO | √ | SIGIO поставлен в очередь (расширенный). |
SI_TIMER | Время таймера истекло | |
SI_USER | Сигнал послан функцией kill() . raise() и abort() также могут его вызвать, но это не обязательно. |
В особенности полезно значение SI_USER
; оно позволяет обработчику сигнала сообщить, был ли сигнал послан функциями raise()
или kill()
(описываются далее). Вы можете использовать эту информацию, чтобы избежать повторного вызова raise()
или kill()
.
Третий аргумент обработчика сигнала с тремя аргументами, void *contex
t, является расширенной возможностью, которая больше не обсуждается в данной книге.
Наконец, чтобы увидеть sigaction()
в действии, исследуйте полный исходный код обработчика сигнала для sort.c
:
2074 static void
2075 sighandler(int sig)
2076 {
2077 #ifndef SA_NOCLDSTOP /* В системе старого стиля... */
2078 signal(sig, SIG_IGN); /* – для игнорирования sig используйте signal()*/
2079 #endif – /* В противном случае sig автоматически блокируется */
2080
2081 cleanup(); /* Запуск кода очистки */
2082
2083 #ifdef SA_NOCLDSTOP /* В системе в стиле POSIX... */
2084 {
2085 struct sigaction sigact;
2086
2087 sigact.sa_handler = SIG_DFL; /* – Установить действие по умолчанию */
2088 sigemptyset(&sigact.sa_mask); /* – Нет дополнительных сигналов для блокирования */
2089 sigact.sa_flags = 0; /* – Специальные действия не предпринимаются */
2090 sigaction(sig, &sigact, NULL); /* – Поместить на место */
2091 }
2092 #else /* На системе в старом стиле... */
2093 signal(sig, SIG_DFL); /* – Установить действие по умолчанию */
2094 #endif
2095
2096 raise(sig); /* Повторно послать сигнал */
2097 }
Вот код в main()
, который помещает обработчик на свое место:
2214 #ifdef SA_NOCLDSTOP /* На системе POSIX... */
2215 {
2216 unsigned i;
2217 sigemptyset(&caught_signals);
2218 for (i = 0; i < nsigs; i++) /* – Блокировать все сигналы */
2219 sigaddset(&caught_signals, sigs[i]);
2220 newact.sa_handler = sighandler; /* – Функция обработки сигнала */
2221 newact.sa_mask = caught_signals; /* – Установить для обработчика маску сигналов процесса */
2222 newact.sa_flags =0; /* – Особых флагов нет */
2223 }
2224 #endif
2225
2226 {
2227 unsigned i;
2228 for (i = 0; i < nsigs; i++) /* Для всех сигналов... */
2229 {
2230 int sig = sigs[i];
2231 #ifdef SA_NOCLDSTOP
2232 sigaction(sig, NULL, &oldact); /* – Получить старый обработчик */
2233 if (oldact.sa_handler != SIG_IGN) /* – Если этот сигнал не игнорируется */
2234 sigaction(sig, &newact, NULL); /* – Установить наш обработчик */
2235 #else
2236 if (signal(sig, SIG_IGN) != SIG_IGN)
2237 signal(sig, sighandler); /* – Та же логика со старым API */
2238 #endif
2239 }
2240 }
Мы заметили, что строки 2216–2219 и 2221 могут быть замещены одним вызовом: sigfillset(&newact.sa_mask)
;
Мы не знаем, почему код написан именно таким способом.
Интерес представляют также строки 2233–2234 и 2236–2237, которые показывают правильный способ проверки того, игнорируется ли сигнал, и для установки обработчика лишь в том случае, если сигнал не игнорируется.
ЗАМЕЧАНИЕ. Функции API
sigaction()
иsignal()
не должны использоваться вместе для одного и того же сигнала. Хотя POSIX идет на большие длинноты, чтобы сначала сделать возможным использованиеsignal()
, получитьstruct sigaction
, представляющую диспозициюsignal()
, и восстановить ее, все равно это плохая мысль. Код будет гораздо проще читать, писать и понимать, если вы используете одну функцию или другую взаимоисключающим образам