Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 31 (всего у книги 55 страниц)
sigpending()
Описанный ранее системный вызов sigpending()
позволяет получить набор ожидающих сигналов, т.е тех сигналов, которые появились, но еще не доставлены из-за блокировки:
#include
int sigpending(sigset_t *set);
Помимо разблокировки ожидающих сигналов, чтобы они могли быть доставлены, вы можете решить их игнорировать. Установка действия сигнала SIG_IGN
вызывает сбрасывание сигнала (даже если он был заблокирован). Сходным образом для тех сигналов, действием по умолчанию для которых является их игнорирование, установка действия в SIG_DFL
также вызывает сбрасывание таких ожидающих сигналов.
siginterrupt()
Чтобы сделать определенную функцию прерываемой или повторно запускаемой в зависимости от значения второго аргумента, в качестве удобного средства может использоваться функция siginterrupt()
. Объявление следующее:
#include
int siginterrupt(int sig, int flag);
В соответствии со стандартом POSIX поведение siginterrupt()
эквивалентно следующему коду:
int siginterrupt(int sig, int flag) {
int ret;
struct sigaction act;
(void)sigaction(sig, NULL, &act); /* Получить старые установки */
if (flag) /* Если flag равен true... */
act.sa_flags &= ~SA_RESTART; /* Запретить повторный запуск */
else /* В противном случае... */
act.sa_flags |= SA_RESTART; /* Разрешить повторный запуск */
ret = sigaction(sig, &act, NULL);
/* Поместить новые установки на место */
return ret; /* Вернуть результат */
}
В случае успеха возвращаемое значение равно 0 и -1 при ошибке.
kill()
и killpg()
Традиционная функция Unix для передачи сигналов называется kill()
. Имя несколько неправильное; все, что она делает – отправляет сигнал. (Результатом этого часто является завершение получателя сигнала, но это не обязательно верно. Однако, теперь слишком поздно менять имя.) Функция killpg()
посылает сигнал определенной группе процессов. Объявления следующие:
#include
#include
int kill(pid_t pid, int sig);
int killpg(int pgrp, int sig); /* XSI */
Аргумент sig
является либо именем сигнала, либо 0. В последнем случае сигнал не посылается, но ядро все равно осуществляет проверку ошибок. В частности, это правильный способ проверки существования данного процесса или группы, а также проверки того, что у вас есть разрешение на передачу сигналов процессу или группе процессов kill()
возвращает 0 в случае успеха и -1 при ошибке; errno
указывает на проблему.
Правила для значения pid
несколько запутаны:
pid > 0
pid
является номером процесса, и сигнал посылается этому процессу
pid = 0
Сигнал посылается каждому процессу в группе посылающего процесса.
pid = -1
Сигнал посылается каждому процессу в системе, за исключением специальных системных процессов. Применяется проверка прав доступа. На системах GNU/Linux исключается лишь процесс init
(PID 1), но у других систем могут быть другие специальные процессы.
pid < -1
Сигнал посылается группе процессов, представленной абсолютным значением pid
. Таким образом, вы можете отправить сигнал всей группе процессов, дублируя возможности killpg()
. Эта неортогональность обеспечивает историческую совместимость.
Значение pid
для kill()
сходно со значением для waitpid()
(см. раздел 9.1.6.1 «Использование функций POSIX: wait()
и waitpid()
»).
Стандартная функция С raise()
в сущности эквивалентна
int raise(int sig) {
return kill(getpid(), sig);
}
Комитет по стандартизации С выбрал имя raise()
, поскольку С должен работать также в окружениях, не относящихся к Unix, a kill()
была сочтена специфичной для Unix функцией. Представилась также возможность дать этой функции более описательное имя.
killpg()
посылает сигнал группе процессов. Пока значение pgrp
превышает 1, эта функция эквивалентна 'kill(-pgrp, sig)
'. Справочная страница GNU/Linux killpg(2) утверждает, что если pgrp
равно 0, сигнал посылается группе отправляющего процесса (Это то же самое, что и kill()
.)
Как вы могли представить, нельзя послать сигнал произвольному процессу (если вы не являетесь суперпользователем, root
). Для обычных пользователей действительный или эффективный UID отправляющего процесса должен соответствовать действительному или сохраненному set-user-ID получающего процесса. (Различные UID описаны в разделе 11.1.1 «Действительные и эффективные ID».)
Однако SIGCONT
является особым случаем: пока получающий процесс является членом того же сеанса, что и отправляющий, сигнал пройдет. (Сеансы были кратко описаны в разделе 9.2.1 «Обзор управления заданиями».) Это особое правило позволяет управляющей заданиями оболочке продолжать остановленные процессы-потомки, даже если этот остановленный процесс имеет другой ID пользователя.
System V Release 3 API был предназначен для исправления различных проблем, представленных первоначальным API сигналов V7. В частности, важной дополнительной концепцией является понятие о блокировке сигналов.
Однако, этот API оказался недостаточным, поскольку он работал лишь с одним сигналом за раз, оставляя множество широко открытых окон, через которые могли поступать нежелательные сигналы. POSIX API, работая атомарно с множеством сигналов (маской сигналов процесса, программно представленной типом sigset_t
), решает эту проблему, закрывая окна.
Первый набор функций, который мы исследовали, манипулирует значениями sigset_t
: sigfillset()
, sigemptyset()
, sigaddset()
, sigdelset()
и sigismember()
.
Следующий набор работает с маской сигналов процесса: sigprocmask()
устанавливает и получает маску сигналов процесса, sigpending()
получает набор ожидающих сигналов, a sigsuspend()
помещает процесс в состояние сна, временно заменяя маску сигналов процесса одним из своих параметров.
Функция POSIX API sigaction()
(весьма) запутана из-за необходимости обеспечить:
• обратную совместимость: SA_RESETHAND
и SA_RESTART
в поле sa_flags
;
• выбор, блокировать также полученный сигнал или нет: SA_NODEFER
для sa_flags
;
• возможность иметь два различных вида обработчиков сигналов: с одним или с тремя аргументами;
• выбор поведения для управления SIGCHLD
: SA_NOCLDSTOP
и SA_NOCLDWAIT
для sa_flags
.
Функция siginterrupt()
является удобной для разрешения или запрещения повторного запуска системных вызовов для данного сигнала.
Наконец, для посылки сигналов не только текущему, но также и другим процессам могут использоваться kill()
и killpg()
(конечно, с проверкой прав доступа).
«ЭТО УЖАСНАЯ МЫСЛЬ! СИГНАЛЫ НЕ ПРЕДНАЗНАЧЕНЫ ДЛЯ ЭТОГО! Просто скажите НЕТ».
– Джефф Колье (Geoff Collyer) -
Одним из главных механизмов межпроцессного взаимодействия (IPC) являются каналы, которые описаны в разделе 9.3 «Базовая межпроцессная коммуникация каналы и FIFO». Сигналы также можно использовать для очень простого IPC[111]111
Наша благодарность Ульриху Дрепперу (Ulrich Drepper) за помощь в разъяснении, связанных с этим проблем – Примеч. автора.
[Закрыть]. Это довольно грубо; получатель может лишь сказать, что поступил определенный сигнал. Хотя функция sigaction()
позволяет получателю узнать PID и владельца процесса, пославшего сигнал, эти сведения обычно не очень помогают.
ЗАМЕЧАНИЕ. Как указывает цитата в начале, использование сигналов для IPC почти всегда является плохой мыслью. Мы рекомендуем по возможности избегать этого. Но нашей целью является научить вас, как использовать возможности Linux/Unix, включая их отрицательные моменты, оставляя за вами принятие информированного решения, что именно использовать.
Сигналы в качестве IPC для многих программ могут быть иногда единственным выбором. В частности, каналы не являются альтернативой, если две взаимодействующие программы не запущены общим родителем, а файлы FIFO могут не быть вариантом, если одна из взаимодействующих программ работает лишь со стандартными вводом и выводом. (Примером обычного использования сигналов являются определенные системные программы демонов, таких, как xinetd
, которые принимают несколько сигналов, уведомляющих, что нужно повторно прочесть файл настроек, осуществить проверку непротиворечивости и т.д. См. xinetd(8) в системе GNU/Linux и inetd(8) в системе Unix.)
Типичная высокоуровневая структура основанного на сигналах приложения выглядит таким образом:
for(;;){
/* Ожидание сигнала */
/* Обработка сигнала */
}
Оригинальным интерфейсом V7 для ожидания сигнала является pause()
:
#include
int pause(void);
pause()
приостанавливает процесс; она возвращается лишь после того, как сигнал будет доставлен и его обработчик вернется из вызова. По определению, pause()
полезна лишь с перехваченными сигналами – игнорируемые сигналы при их появлении игнорируются, а сигналы с действием по умолчанию, завершающим процесс (с созданием файла образа или без него), продолжают действовать так же.
Проблема в только что описанной высокоуровневой структуре приложения кроется в части «Обработка сигнала». Когда этот код запускается, вы не захотите обрабатывать другой сигнал; вы хотите завершить обработку текущего сигнала до перехода к следующему. Одним из возможных решений является структурирование обработчика сигнала таким образом, что он устанавливает флаг и проверяет его в главном цикле: volatile sig_atomic_t signal_waiting = 0; /* true, если не обрабатываются сигналы */
void handler(int sig) {
signal_waiting = 1;
/* Установка других данных, указывающих вид сигнала */
В основном коде флаг проверяется:
for (;;) {
if (!signal_waiting) { /* Если возник другой сигнал, */
pause(); /* этот код пропускается */
signal_waiting = 1;
}
/* Определение поступившего сигнала */
signal_waiting = 0;
/* Обработка сигнала */
}
К сожалению, этот код изобилует условиями гонки:
for (;;) {
if (!signal_waiting) {
/* <– Сигнал может появиться здесь, после проверки условия! */
pause(); /* pause() будет вызвана в любом случае */
signal_waiting = 1;
}
/* Определение поступившего сигнала
<– Сигнал может переписать здесь глобальные данные */
signal_waiting = 0;
/* Обработка сигнала
<– То же и здесь, особенно для нескольких сигналов */
}
Решением является блокирование интересующего сигнала в любое время, кроме ожидания его появления. Например, предположим, что интересующим нас сигналом является SIGINT
:
void handler(int sig) {
/* sig автоматически блокируется функцией sigaction() */
/* Установить глобальные данные, касающиеся этого сигнала */
}
int main(int argc, char **argv) {
sigset_t set;
struct sigaction act;
/* ...обычная настройка, опции процесса и т.д. ... */
sigemptyset(&set); /* Создать пустой набор */
sigaddset(&set, SIGINT); /* Добавить в набор SIGINT */
sigprocmask(SIG_BLOCK, &set, NULL); /* Заблокировать его */
act.sa_mask = set; /* Настроить обработчик */
act.sa_handler = handler;
act.sa_flags = 0;
sigaction(sig, &act, NULL); /* Установить его */
... /* Возможно, установить отдельные обработчики */
... /* для других сигналов */
sigemptyset(&set); /* Восстановить пустой, допускает SIGINT */
for (;;) {
sigsuspend(&set); /* Ждать появления SIGINT */
/* Обработка сигнала. SIGINT здесь снова блокируется */
}
/* ...любой другой код... */
return 0;
}
Ключом к использованию этого является то, что sigsuspend()
временно заменяет маску сигналов процесса маской, переданной в аргументе. Это дает SIGINT
возможность появиться. При появлении он обрабатывается; обработчик сигнала возвращается, а вслед за ним возвращается также sigsuspend()
. Ко времени возвращения sigsuspend()
первоначальная маска процесса снова на месте.
Вы легко можете расширить этот пример для нескольких сигналов, блокируя в main()
и в обработчике все интересующие сигналы и разблокируя их лишь в вызове sigsuspended()
.
При наличии всего этого не следует в новом коде использовать pause()
. pause()
был стандартизован POSIX главным образом для поддержки старого кода. То же самое верно и для функции sigpause()
System V Release 3. Вместо этого, если нужно структурировать свое приложение с использованием сигналов для IPC, используйте исключительно функции API sigsuspend()
и sigaction()
.
10.8. Важные сигналы специального назначенияЗАМЕЧАНИЕ. Приведенный выше код предполагает, что маска сигналов процесса начинается пустой. Код изделия должен вместо этого работать с любой маской сигналов, имеющейся на момент запуска программы.
Некоторые сигналы имеют особое назначение. Здесь мы опишем наиболее важные.
sleep()
, alarm()
и SIGALARM
Часто бывает необходимо написать программу в виде
while (/* некоторое неверное условие */) {
/* подождать некоторое время */
}
Часто такая потребность возникает в сценариях оболочки, например, в ожидании регистрации определенного пользователя:
until who | grep '^arnold' > /dev/null
do
sleep 10
done
Два механизма, один низкоуровневый, другой высокоуровневый, позволяют работающему процессу узнать, когда истекло заданное количество секунд.
alarm()
и SIGALARM
Основным строительным блоком является системный вызов alarm()
:
#include
unsigned int alarm(unsigned int seconds);
После того, как alarm()
возвратится, программа продолжает работать. Однако, когда истекают seconds
секунд, ядро посылает процессу SIGALARM
. Действием по умолчанию является завершение процесса, но вы скорее всего вместо этого установите обработчик сигнала для SIGALARM
.
Возвращаемое значение либо 0, либо, если был установлен предыдущий сигнальный интервал, число секунд, остающихся до его завершения. Однако, для процесса имеется лишь один такой сигнальный интервал; предыдущий отменяется, а новый помещается на его место.
Преимуществом здесь является то, что со своим установленным обработчиком вы можете делать при поступлении сигнала все, что хотите. Недостаток же в том, что приходится быть готовым к работе в нескольких контекстах: основном контексте и контексте обработчика сигнала.
sleep()
Более легкий способ ожидания истечения фиксированного промежутка времени заключается в использовании функции sleep()
:
#include
unsigned int sleep(unsigned int seconds);
Возвращаемое значение равно 0, если процесс проспал все отведенное время. В противном случае возвращается оставшееся для сна время. Это последнее значение может возникнуть в случае, если появился сигнал, пока процесс дремал.
ЗАМЕЧАНИЕ. Функция
sleep()
часто реализуется через сочетаниеsignal()
,alarm()
иpause()
. Такой подход делает опасным смешиваниеsleep()
с вашим собственным вызовомalarm()
(или расширенной функциейsetitimer()
, описанной в разделе 14.3.3 «Интервальные таймерыsetitimer()
иgetitimer()
») Чтобы теперь узнать о функцииnanosleep()
, см. раздел 14.3.4 «Более точные паузы:nanosleep()
».
Несколько сигналов используются для реализации управления заданиями – возможностью начинать и останавливать задания и перемещать их из фонового режима на передний план и обратно. На уровне пользователя вы, несомненно, проделывали это: использовали CTRL-Z для остановки задания, bg
для помещения его в фоновый режим, а иногда использовали fg
для перемещения фонового или остановленного задания на передний план.
Секция 9.2.1 «Обзор управления заданиями» описывает в общем, как осуществляется управление заданиями. Данный раздел завершает обзор, описав сигналы управления заданиями. поскольку иногда может понадобиться перехватить их непосредственно:
SIGTSTP
Этот сигнал осуществляет «остановку терминала». Это сигнал, который ядро посылает процессу, когда пользователь за терминалом (или окном, эмулирующим терминал) набирает определенный ключ. Обычно это CTRL-Z, аналогично тому, как CTRL-C обычно посылает SIGINT
.
Действием по умолчанию для SIGTSTP
является остановка (переход в приостановленное состояние) процесса. Однако, вы можете перехватить этот сигнал, как любой другой. Хорошая мысль сделать это, если ваша программа изменяет состояние терминала. Например, рассмотрите экранные редакторы vi
или Emacs, которые переводят терминал в посимвольный режим. По получении SIGTSTP
, они должны восстановить терминал в его нормальный построчный режим, а затем приостановиться сами.
SIGSTOP
Этот сигнал также останавливает процесс, но он не может быть перехвачен, заблокирован или проигнорирован. Он может быть использован в качестве последнего средства вручную (посредством команды kill
) или программным путем. Например, только что обсужденный обработчик SIGTSTP
после восстановления состояния терминала мог бы затем использовать для остановки процесса 'raise (SIGSTOP)
'.
SIGTTIN
, SIGTTOU
Ранее эти сигналы были определены как «фоновое чтение из tty» и «фоновая запись в tty». tty является устройством терминала. В системах управления заданиями процессы, работающие в фоновом режиме, заблокированы от попыток чтения с терминала или записи в него. Когда процесс пытается осуществить любую из этих операций, ядро посылает ему соответствующий сигнал. Для обоих действием по умолчанию является остановка процесса. При желании можно перехватить эти сигналы, но для этого редко бывает необходимость.
SIGCONT
Этот сигнал вновь запускает остановленный процесс. Если процесс не остановлен, он игнорируется. При желании его можно перехватить, но опять-таки для большинства программ мало причин для осуществления этого. Продолжая наш пример, обработчик SIGCONT
для экранного редактора должен перед возвращением вернуть терминал обратно в посимвольный режим.
Когда процесс остановлен, любые другие посланные ему сигналы становятся ожидающими. Исключением является SIGKILL
, который всегда доставляется процессу и который не может быть перехвачен, заблокирован или проигнорирован. В предположении, что были посланы сигналы кроме SIGKILL
, по получении SIGCONT
ожидающие сигналы доставляются, а процесс продолжает выполнение после того, как они будут обработаны.
Как описано в разделе 9.1.1 «Создание процесса: fork()
», одним побочным эффектом вызова fork()
является создание между процессами отношений родитель-потомок. Родительский процесс может ждать завершения одного или более из своих потомков и получить статус завершения порожденного процесса посредством одного из семейства системных вызовов wait()
.
Завершившиеся порожденные процессы, которых никто не ожидал, называются зомби (zombies). Обычно каждый раз при завершении порожденного процесса ядро посылает родительскому процессу сигнал SIGCHLD
[112]112
Исторически системы BSD использовали имя SIGCHLD
, которое используется и POSIX. В System V есть сходный сигнал с именем SIGCLD
. GNU/Linux определяет последний через #define
как первый – см. табл. 10.1 – Примеч. автора.
[Закрыть]. Действием по умолчанию является игнорирование этого сигнала. В этом случае процессы зомби накапливаются до тех пор, пока родитель не вызовет wait()
или не закончится сам. В последнем случае процессы зомби получают в качестве нового родителя системный процесс init
(PID 1), который получает от них результаты как часть своей обычной работы. Сходным образом, активные потомки также получают родителем init
, и их результаты будут собраны при их завершении.
SIGCHLD
используется для большего, чем уведомление о завершении потомка. Каждый раз при остановке потомка (посредством одного из обсужденных ранее сигналов управления заданиями) родителю также посылается SIGCHLD
. Стандарт POSIX указывает, что SIGCHLD
«может быть послан» также, когда помок вновь запускается; очевидно, среди оригинальных Unix-систем имеются различия.
Сочетание флагов для поля sa_flags
в struct sigation
и использование SIG_IGN
в качестве действия для SIGCHLD
позволяет изменить способ обработки ядром остановок, возобновления или завершения потомков.
Как и с сигналами в общем, описанные здесь интерфейсы и механизмы сложны, поскольку они развивались с течением времени.
Простейшим действием, которое вы можете сделать, является изменение действия для SIGCHLD
на SIG_IGN
. В этом случае завершившиеся потомки не становятся зомби. Вместо этого статус их завершения отбрасывается, и они полностью удаляются из системы.
Другой возможностью, дающей такой же результат, является использование флага SA_NOCLDWAIТ
. В коде:
/* Старый стиль: */ /* Новый стиль: */
signal(SIGCHLD, SIG_IGN); struct sigaction sa;
sa.sa_handler = SIG_IGN;
sa.sa_flags = SA_NOCLDWAIT;
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);