Текст книги "Основы программирования в Linux"
Автор книги: Нейл Мэтью
Соавторы: Ричард Стоунс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 44 (всего у книги 67 страниц)
Процесс может отправить сигнал другому процессу, включая себя самого, с помощью вызова функции kill
. Вызов завершится аварийно, если у программы нет полномочий на отправку сигнала, часто потому что процесс-получатель принадлежит другому пользователю. Эта функция эквивалентна команде оболочки с тем же именем.
#include
#include
int kill(pid_t pid, int sig);
Функция kill
посылает заданный сигнал sig
процессу с идентификатором, заданным в аргументе pid
. В случае успеха она возвращает 0. Для отправки сигнала посылающий процесс должен иметь право на выполнение этого действия. Обычно это означает, что у обоих процессов должен быть один и тот же идентификатор пользователя ID (т.е. вы можете отправить сигнал только одному из собственных процессов, хотя суперпользователь может отправлять сигналы любому процессу).
Функция kill
завершится аварийно, вернет -1 и установит значение переменной errno
, если задан неверный сигнал, (errno
равна EINVAL
), у процесса нет полномочий (EPERM
) или заданный процесс не существует (ESRCH
).
Сигналы предоставляют полезное средство, именуемое будильником или сигналом тревоги. Вызов функции alarm
может применяться для формирования сигнала SIGALRM
в определенное время в будущем.
#include
unsigned int alarm(unsigned int seconds);
Вызов alarm
намечает доставку сигнала SIGALRM
через seconds
секунд. В действительности сигнал будильника будет доставлен чуть позже из-за обработки задержек и учета неопределенностей. Значение 0 отменяет любой невыполненный запрос на сигнал будильника. Вызов функции alarm
до получения сигнала может вызвать сброс графика доставки. У каждого процесса может быть только один невыполненный сигнал будильника. Функция alarm
возвращает количество секунд, оставшихся до отправки любого невыполненного вызова, alarm
, или -1 в случае аварийного завершения.
Для того чтобы увидеть как работает функция alarm
, можно сымитировать ее действие, используя вызовы fork
, sleep
и signal
(упражнение 11.8). Программа сможет запустить новый процесс с единственной целью – отправить сигнал спустя какое– то время.
Упражнение 11.8 Будильник
В программе alarm.c первая функция, ding
, имитирует будильник.
#include
#include
#include
#include
#include
static int alarm_fired = 0;
void ding(int sig) {
alarm_fired = 1;
}
В функции main
вы заставляете дочерний процесс ждать пять секунд перед отправкой сигнала SIGALRM
в свой родительский процесс:
int main() {
pid_t pid;
printf(«alarm application startingn»);
pid = fork();
switch(pid) {
case -1:
/* Аварийное завершение */
perror(«fork failed»);
exit(1);
case 0:
/* Дочерний процесс */
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}
Родительский процесс устроен так, что перехватывает сигнал SIGALRM
с помощью вызова signal
и затем ждет неизбежности:
/* Если мы оказались здесь, то мы – родительский процесс */
printf(«waiting for alarm to go offn»);
(void)signal(SIGALRM, ding);
pause();
if (alarm_fired) printf(«Ding!n»);
printf(«donen»);
exit(0);
}
Когда вы выполните программу, то увидите, что она делает паузу на пять секунд, в течение которых ждет имитации будильника:
$ ./alarm
alarm application starting
waiting for alarm to go off
<5 second pause>
Ding!
done $
В этой программе вводится новая функция pause
, которая просто приостанавливает выполнение программы до появления сигнала. Когда она получит сигнал, выполняется любой установленный обработчик, и выполнение продолжается как обычно. Она объявляется следующим образом:
#include
int pause(void);
Функция возвращает -1 (если следующий полученный сигнал не вызвал завершения программы) с переменной errno
, равной EINTR
, в случае прерывания сигналом. Лучше для ожидания сигналов применять функцию sigsuspend
, которую мы обсудим чуть позже в этой главе.
Как это работает
Программа имитации будильника запускает новый процесс вызовом fork
. Этот дочерний процесс ожидает пять секунд и затем посылает сигнал SIGALRM
своему родителю. Родитель подготавливается к получению сигнала SIGALRM
и затем делает паузу до тех пор, пока не будет получен сигнал. Функция printf
не вызывается непосредственно в обработчике, вместо этого вы устанавливаете флаг, который проверяете позже.
Применение сигналов и приостановка выполнения – важные составляющие программирования в ОС Linux. Это означает, что программа необязательно должна выполняться все время. Вместо того чтобы долго работать в цикле, проверяя, не произошло ли событие, она может ждать его наступления. Это особенно важно в многопользовательской среде, где процессы совместно используют один процессор, и такой вид деятельного ожидания оказывает большое влияние на производительность системы. Особая проблема, связанная с сигналами, заключается в том, что вы никогда не знаете наверняка, что произойдет, если сигнал появится в середине системного вызова? (Ответ весьма неудовлетворительный: все зависит от ситуации.) Вообще следует беспокоиться только о "медленных" системных вызовах, таких как считывание с терминала, когда системный вызов может вернуться с ошибкой, если сигнал появится во время его пребывания в режиме ожидания. Если вы начнете применять сигналы в своих программах, нужно учитывать, что некоторые системные вызовы могут закончиться аварийно, если сигнал создаст ошибочную ситуацию, которую вы могли не принимать во внимание до того, как добавили обработку сигналов.
Нужно тщательно программировать сигналы, потому что существует ряд "состояний гонок", возникающих в программах, применяющих сигналы. Например, если вы намерены вызвать pause для ожидания сигнала и этот сигнал возникнет до вызова pause, ваша программа может ждать неопределенно долго события, которое не произойдет. Новоиспеченный программист сталкивается с множеством таких состояний гонок, важных проблем синхронизации или согласования времени. Всегда очень внимательно проверяйте программный код, использующий сигналы.
Надежный интерфейс сигналов
Мы рассмотрели подробно возбуждение и перехват сигналов с помощью signal
и родственных функций, поскольку они очень часто применяются в старых UNIX-программах. Тем не менее, стандарты X/Open и спецификации UNIX рекомендуют более современный программный интерфейс для сигналов sigaction
, который более надежен.
#include
int sigaction
Структура sigaction
, применяемая для определения действий, предпринимаемых при получении сигнала, заданного в аргументе sig
, определена в файле signal.h и как минимум включает следующие элементы:
void (*)(int)sa_handler /* функция, SIG_DFL или SIG_IGN */
sigset_t sa_mask /* сигналы, заблокированные для sa_handler */
int sa_flags /* модификаторы действий сигнала */
Функция sigaction
задает действие, связанное с сигналом sig
. Если oact
не null
, sigaction
записывает предыдущее действие для сигнала в указанное oact
место. Если act
равен null
, это все, что делает функция sigaction
. Если указатель act
не null
, задается действие для указанного сигнала.
Как и функция signal
, sigaction
возвращает 0 в случае успешного выполнения и -1 в случае ошибки. Переменная errno
получит значение EINVAL
, если заданный сигнал некорректен или была предпринята попытка захватить или проигнорировать сигнал, который нельзя захватывать или игнорировать.
В структуре sigaction
, на которую указывает аргумент act
, sa_handler
– это указатель на функцию, вызываемую при получении сигнала sig
. Она очень похожа на функцию func
, которая, как вы видели раньше, передавалась функции signal
. Вы можете применять специальные значения SIG_IGN
и SIG_DFL
в поле sa_handler
для обозначения того, что сигнал должен игнорироваться или должно быть восстановлено действие по умолчанию, соответственно.
Поле sa_mask
описывает множество сигналов, которые будут добавлены в маску сигналов процесса перед вызовом функции sa_handler
. Это множество сигналов, которые блокируются и не должны доставляться процессу. Такое поведение мешает возникновению ситуации, описанной ранее, в которой сигнал был получен до того, как его обработчик дошел до завершения. Применение поля sa_mask
может устранить это состояние гонок.
Однако сигналы, захватываемые обработчиками, заданными в структуре sigaction
, по умолчанию не восстанавливаются, и нужно задать в поле sa_flags
значение SA_RESETHAND
, если хотите добиться поведения, виденного вами раньше при обсуждении функции signal
. Прежде чем обсуждать подробнее sigaction
, давайте перепишем программу ctrlc.c, применяя sigaction
вместо функции signal
(упражнение 11.9).
Упражнение 11.9. Функция sigaction
Внесите приведенные далее изменения, так чтобы сигнал SIGINT
перехватывался sigaction
. Назовите новую программу ctrlc2.c.
#include
#include
#include
void ouch(int sig) {
printf(«OUCH! – I got signal %dn», sig);
}
int main() {
struct sigaction act;
act.sa_handler = ouch;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, 0);
while (1) {
printf(«Hello World!n»);
sleep(1);
}
}
Когда вы выполните эту версию программы, то всегда будете получать сообщение при нажатии комбинации клавиш SIGINT
обрабатывается неоднократно функцией sigaction
. Для завершения программы следует нажать комбинацию клавиш SIIGQUIT
.
$ ./ctrlc2
Hello World!
Hello World!
Hello World!
^C
OUCH! – I got signal 2
Hello World!
Hello World!
^C
OUCH! – I got signal 2
Hello World!
Hello World!
^
Quit
$
Как это работает
Программа вместо функции signal
вызывает sigaction
для задания функции ouch
как обработчика сигнала, возникающего при нажатии комбинации клавиш SIGINT
). Прежде всего, она должна определить структуру sigaction
, содержащую обработчик, маску сигналов и флаги, В данном случае вам не нужны никакие флаги, и создается пустая маска сигналов с помощью новой функции sigemptyset
.
Множества сигналовПримечание
После выполнения программы вы можете обнаружить дамп ядра (в файле core). Его можно безбоязненно удалить.
В заголовочном файле signal.h определены тип sigset_t
и функции, применяемые для манипулирования множествами сигналов. Эти множества используются в sigaction
и других функциях для изменения поведения процесса при получении сигналов.
#include
int sigaddset(sigset_t *set, int signo);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigdelset(sigset_t *set, int signo);
Приведенные функции выполняют операции, соответствующие их названиям, sigemptyset
инициализирует пустое множество сигналов. Функция sigfillset
инициализирует множество сигналов, заполняя его всеми заданными сигналами, sigaddset
и sigdelset
добавляют заданный сигнал (signo
) в множество сигналов и удаляют его из множества. Они все возвращают 0 в случае успешного завершения и -1 в случае ошибки, заданной в переменной errno
. Единственная определенная ошибка EINVAL
описывает сигнал как некорректный.
Функция sigismember
определяет, включен ли заданный сигнал в множество сигналов. Она возвращает 1, если сигнал является элементом множества, 0, если нет и -1 с errno
, равной EINVAL
, если сигнал неверный.
#include
int sigismember(sigset_t *set, int signo);
Маска сигналов процесса задается и просматривается с помощью функции sigprocmask
. Маска сигналов – это множество сигналов, которые заблокированы в данный момент и не будут приниматься текущим процессом.
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
Функция sigprocmask
может изменять маску сигналов процесса разными способами в соответствии с аргументом how
. Новые значения маски сигналов передаются в аргументе set
, если он не равен null
, а предыдущая маска сигналов будет записана в множество сигналов oset
.
Аргумент how
может принимать одно из следующих значений:
□ SIG_BLOCK
– сигналы аргумента set
добавляются к маске сигналов;
□ SIG_SETMASK
—маска сигналов задается аргументом set
;
□ SIG_UNBLOCK
– сигналы в аргументе set
удаляются из маски сигналов.
Если аргумент set
равен null
, значение how
не используется и единственная цель вызова – перенести значение текущей маски сигналов в аргумент oset
.
Если функция sigprocmask
завершается успешно, она возвращает 0. Функция вернет -1, если параметр how
неверен, в этом случае переменная errno
будет равна EINVAL
.
Если сигнал заблокирован процессом, он не будет доставлен, но останется ждать обработки. Программа может определить с помощью функции sigpending
, какие из заблокированных ею сигналов ждут обработки.
#include
int sigpending(sigset_t *set);
Она записывает множество сигналов, заблокированных от доставки и ждущих обработки, в множество сигналов, на которое указывает аргумент set
. Функция возвращает 0 при успешном завершении и -1 в противном случае с переменной errno
, содержащей ошибку. Данная функция может пригодиться, когда программе потребуется обрабатывать сигналы и управлять моментом вызова функции обработки.
С помощью функции sigsuspend
процесс может приостановить выполнение, пока не будет доставлен один сигнал из множества сигналов. Это более общая форма функции pause
, с которой вы уже встречались.
#include
int sigsuspend(const sigset_t *sigmask);
Функция sigsuspend
замещает маску сигналов процесса множеством сигналов, заданным в аргументе sigmask
, и затем приостанавливает выполнение. Оно будет возобновлено после выполнения функции обработки сигнала. Если полученный сигнал завершает программу, sigsuspend
никогда не вернет ей управление. Если полученный сигнал не завершает программу, sigsuspend
вернет с переменной errno
, равной EINTR
.
Флаги sigaction
Поле sa_flags
структуры sigaction
, применяемой в функции sigaction
, может содержать значения, изменяющие поведение сигнала (табл. 11.5).
Таблица 11.5
SA_NOCLDSTOP | Не генерируется SIGCHLD , когда дочерние процессы остановлены |
SA_RESETHAND | Восстанавливает при получении действие, соответствующее значению SIG_DFL |
SA_RESTART | Перезапускает прерванные функции вместо ошибки EINTR |
SA_NODEFER | При перехвате сигнала не добавляет его а маску сигналов |
Флаг SA_RESETHAND
может применяться для автоматической очистки функции сигнала при захвате сигнала, как мы видели раньше.
Многие системные вызовы, которые использует программа, прерываемые, т.е. при получении сигнала они вернутся с ошибкой и переменная errno
получит значение EINTR
, чтобы указать, что функция вернула управление в результате получения сигнала. Поведение требует повышенного внимания со стороны приложения, использующего сигналы. Если в поле sa_flags
функции sigaction
установлен флаг SA_RESTART
, функция, которая в противном случае могла быть прервана сигналом, вместо этого будет возобновлена, как только выполнится функция обработки сигнала.
Обычно, когда функция обработки сигнала выполняется, полученный сигнал добавляется в маску сигналов процесса во время работы функции обработки. Это препятствует последующему появлению того же сигнала, заставляющему функцию обработки сигнала выполняться снова. Если функция не реентерабельная, вызов ее другим экземпляром сигнала до того, как она завершит обработку первого сигнала, может создать проблемы. Но если установлен флаг SA_NODEFER
, маска сигнала не меняется при получении этого сигнала.
Функция обработки сигнала может быть прервана в середине и вызвана снова чем-нибудь еще. Когда вы возвращаетесь к первому вызову функции, крайне важно, чтобы она все еще действовала корректно. Она должна быть не просто рекурсивной (вызывающей саму себя), а реентерабельной (в нее можно войти и выполнить ее снова). Подпрограммы ядра, обслуживающие прерывания и имеющие дело с несколькими устройствами одновременно, должны быть реентерабельными, поскольку высокоприоритетное прерывание может "войти" в тот код, который выполняется.
Функции, которые безопасно вызываются в обработчике сигнала и в стандарте X/Open гарантированно описанные либо как реентерабельные, либо как самостоятельно не возбуждающие сигналов, перечислены в табл. 11.6.
Все функции, не включенные в табл. 11.6, следует считать небезопасными в том, что касается сигналов.
Таблица 11.6
access | alarm | cfgetispeed | cfgetospeed |
cfsetispeed | cfsetospeed | chdir | chmod |
chown | close | creat | dup2 |
dup | execle | execve | exit |
fcntl | fork | fstat | getegid |
geteuid | getgid | getgroups | getpgrp |
getpid | getppid | getuid | kill |
link | lseek | mkdir | mkfifo |
open | pathconf | pause | pipe |
read | rename | rmdir | setgid |
setpgid | setsid | setuid | sigaction |
sigaddset | sigdelset | sigemptyset | sigfillset |
sigismember | signal | sigpending | sigprocmask |
sigsuspend | sleep | stat | sysconf |
tcdrain | tcflow | tcflush | tcgetattr |
tcgetpgrp | tcsendbreak | tcsetattr | tcsetpgrp |
time | times | umask | uname |
unlink | utime | wait | waitpid |
write |
Общая сводка сигналов
В этом разделе мы перечисляем сигналы, в которых нуждаются программы Linux и UNIX для обеспечения стандартных реакций.
Стандартное действие для сигналов, перечисленных в табл. 11.7, – аварийное завершение процесса со всеми последствиями вызова функции _exit
(которая похожа на exit
, но не выполняет никакой очистки перед возвратом управления ядру). Тем не менее, состояние становится доступным функции wait
, а функция waitpid
указывает на аварийное завершение, вызванное описанным сигналом.
Таблица 11.7
SIGALRM | Генерируется таймером, установленным функцией alarm |
SIGHUP | Посылается управляющему процессу отключающимся терминалом или управляющим процессом во время завершения каждому процессу с высоким приоритетом |
SIGINT | Обычно возбуждается с терминала при нажатии комбинации клавиш |
SIGKILL | Обычно используется из командной оболочки для принудительного завершения процесса с ошибкой, т.к. этот сигнал не может быть перехвачен или проигнорирован |
SIGPIPE | Генерируется при попытке записи в канал при отсутствии связанного с ним считывателя |
SIGTERM | Отправляется процессу как требование завершиться. Применяется UNIX при выключении для запроса остановки системных сервисов. Это сигнал, по умолчанию посылаемый командой kill |
SIGUSR1 , SIGUSR2 | Может использоваться процессами для взаимодействия друг с другом, возможно, чтобы заставить их сообщить информацию о состоянии |
По умолчанию сигналы, перечисленные в табл. 11.8, также вызывают преждевременное завершение. Кроме того, могут выполняться действия, зависящие от реализации, например, создание файла core.
Таблица 11.8
SIGFPE | Генерируется исключительной ситуацией во время операций с плавающей точкой |
SIGILL | Процессор выполнил недопустимую команду. Обычно возбуждается испорченной программой или некорректным модулем совместно используемой памяти |
SIGQUIT | Обычно возбуждается с терминала при нажатии комбинации клавиш |
SIGSEGV | Нарушение сегментации, обычно возбуждается при чтении из некорректного участка памяти или записи в него, а также выход за границы массива или разыменование неверного указателя. Перезапись локального массива и повреждение стека могут вызвать сигнал SIGSEGV при возврате функции по неверному адресу |
При получении одного из сигналов, приведенных в табл. 11.9, по умолчанию процесс приостанавливается.
Таблица 11.9
SIGSTOP | Останавливает выполнение (не может быть захвачен или проигнорирован) |
SIGTSTP | Сигнал останова терминала часто возбуждается нажатием комбинации клавиш |
SIGTTIN , SIGTTOU | Применяются командной оболочкой для обозначения того, что фоновые задания остановлены, т.к. им необходимо прочесть данные с терминала или выполнить вывод |
Сигнал SIGCONT
возобновляет остановленный процесс и игнорируется при получении неостановленным процессом. Сигнал SIGCHLD
по умолчанию игнорируется (табл. 11.10).
Таблица 11.10
SIGCONT | Продолжает выполнение, если процесс остановлен |
SIGCHLD | Возбуждается, когда останавливается или завершается дочерний процесс |
Резюме
В этой главе вы убедились, что процессы – это основной компонент операционной системы Linux. Вы узнали, как они могут запускаться, завершаться и просматриваться и как вы можете применять их для решения задач программирования. Вы также познакомились с сигналами, которые могут использоваться для управления действиями выполняющихся программ. Вы убедились, что все процессы Linux, вплоть до init
включительно, используют одни и те же системные вызовы, доступные любому программисту.