Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 29 (всего у книги 55 страниц)
raise()
Помимо внешнего генерирования, сигнал может быть отправлен непосредственно самой программой с использованием стандартной функции С raise()
:
#include
int raise(int sig);
Эта функция посылает сигнал sig
вызывающему процессу. (Это действие имеет свое применение; вскоре мы увидим пример.)
Поскольку raise()
определена стандартом С, для процесса это наиболее переносимый способ отправить себе сигнал. Есть другие способы, которые мы обсудим далее в главе.
Множество осложнений и отклонений обнаруживается после установки на место обработчика, после его вызова и впоследствии возвращения.
После помещения на место обработчика сигнала ваша программа развивается своим путем. Интересные вещи возникают лишь с появлением сигнала (например, пользователь нажал CTRL-C для прерывания вашей программы, или был сделан вызов raise()
).
По получении сигнала ядро останавливает процесс, где бы он ни был. Затем оно имитирует вызов процедуры обработчика сигнала, передавая ему номер сигнала в качестве ее единственного аргумента. Ядро устраивает все таким образом, что нормальный возврат из функции обработчика сигнала (либо посредством return
, либо в результате выпадения из конца функции) передает управление в ту точку программы, в которой она находилась в момент появления сигнала.
Что происходит после обработки сигнала, когда тот же самый сигнал появится в следующий раз снова? Остается ли обработчик на том же месте? Или же он сбрасывается, и для сигнала используется действие по умолчанию? Ответ, по историческим причинам, «зависит от». В частности, стандарт С оставляет это на усмотрение реализации.
На практике V7 и традиционные системы System V, такие, как Solaris, устанавливают для сигнала действие по умолчанию.
Давайте рассмотрим простой обработчик сигнала в действии под Solaris. Следующая программа, ch10-catchint.c
, перехватывает SIGINT
. Обычно вы генерируете этот сигнал, набирая на клавиатуре CTRL-C.
1 /* ch10-catchint.c – перехват SIGINT, по крайней мере, однажды. */
2
3 #include
4 #include
5 #include
6
7 /* handler – простой обработчик сигнала. */
8
9 void handler(int signum)
10 {
11 char buf[200], *cp;
12 int offset;
13
14 /* Пройти через это испытание , чтобы избежать fprintf(). */
15 strcpy(buf, "handler: caught signal ");
16 cp = buf + strlen(buf); /* cp указывает на завершающий ' ' */
17 if (signum > 100) /* маловероятно */
18 offset = 3;
19 else if (signum > 10)
20 offset = 2;
21 else
22 offset = 1;
23 cp += offset;
24
25 *cp– = ' '; /* завершить строку */
26 while (signum >0) { /* work backwards, filling in digits */
27 *cp– = (signum % 10) + '0';
28 signum /= 10;
29 }
30 strcat(buf, "n");
31 (void)write(2, buf, strlen(buf));
32 }
33
34 /* main – установить обработку сигнала и войти в бесконечный цикл */
35
36 int main(void)
37 {
38 (void)signal(SIGINT, handler);
39
40 for(;;)
41 pause(); /* ждать сигнал, см. далее в главе */
42
43 return 0;
44 }
Строки 9–22 определяют функцию обработки сигнала (остроумно названную handler()
[106]106
Handler (англ.) – обработчик – Примеч. перев.
[Закрыть]). Все, что эта функция делает, – выводит номер перехваченного сигнала и возвращается. Для вывода этого сообщения она выполняет множество ручной работы, поскольку fprintf()
не является «безопасной» для вызова из обработчика сигнала. (Вскоре это будет описано в разделе 10.4.6 «Дополнительные предостережения».)
Функция main()
устанавливает обработчик сигнала (строка 38), а затем входит в бесконечный цикл (строки 40–41). Вот что происходит при запуске:
$ ssh solaris.example.com
/* Зарегистрироваться на доступной системе Solaris */
Last login: Fri Sep 19 04:33:25 2003 from 4.3.2.1.
Sun Microsystems Inc. SunOS 5.9 Generic May 2002
$ gcc ch10-catchint.c /* Откомпилировать программу */
$ a.out /* Запустить ее */
^C handler: caught signal 2 /* Набрать ^C, вызывается обработчик */
^C /* Попробовать снова, но на этот раз... */
$ /* Программа завершается */
Поскольку V7 и другие традиционные системы восстанавливают действие сигнала по умолчанию, поэтому когда вы хотите снова получить сигнал в будущем, функция обработчика должна немедленно переустановить саму себя:
void handler(int signum) {
char buf[200], *cp;
int offset;
(void)signal(signum, handler); /* переустановить обработчик */
/* ...оставшаяся часть функции как прежде... */
}
BSD 4.2 изменила способ работы signal()
.[107]107
Изменение поведения было плохой мыслью, сильно критиковавшейся в свое время, но было слишком поздно. Изменение семантики определенного интерфейса всегда ведет к проблеме, как было в этом случае. Хотя это особенно относится к проектировщикам операционных систем, любой, кто разрабатывает библиотеки общего назначения, также должен помнить этот урок. – Примеч. автора.
[Закрыть] На системах BSD обработчик сигнала после его возвращения остается на месте. Системы GNU/Linux следуют поведению BSD. Вот что происходит под GNU/Linux:
$ ch10-catchint /* Запустить программу */
handler: caught signal 2 /* Набираем ^C, вызывается обработчик */
handler: caught signal 2 /* И снова... */
handler: caught signal 2 /* И снова! */
handler: caught signal 2 /* Помогите! */
handler: caught signal 2 /* Как нам это остановить?! */
Quit (core dumped) /* ^, генерирует SIGQUIT. Bay */
На системе BSD или GNU/Linux обработчик сигнала не должен дополнительно использовать 'signal(signum, handler)
' для переустановки обработчика. Однако, лишний вызов не причиняет никакого вреда, поэтому сохраняется статус-кво.
В действительности, POSIX предоставляет функцию bsd_signal()
, которая идентична signal()
за тем исключением, что она гарантирует, что обработчик сигнала останется установленным:
#include
void (*bsd_signal(int sig, void (*func)(int)))(int);
Это устраняет проблемы переносимости. Если вы знаете, что ваша программа будет работать лишь на системах POSIX, вы можете воспользоваться bsd_signal()
вместо signal()
.
Одно предостережение – эта функция также помечена как «устаревающая», что означает возможность отказа от нее в будущем стандарте. На практике, даже если от нее откажутся, поставщики скорее всего долгое время будут ее поддерживать. (Как мы увидим, функция API POSIX sigaction()
предоставляет достаточно возможностей для написания рабочей версии, если это вам нужно.)
Более практично, когда вызывается обработчик сигнала, это означает, что программа должна завершиться и выйти. Было бы раздражающим, если бы большинство программ по получении SIGINT
выводили бы сообщение и продолжали работу; смысл сигнала в том, что они должны остановиться!
Например, рассмотрите программу sort
. sort
, возможно, создала любое число временных файлов для использования на промежуточных этапах процесса сортировки. По получении SIGINT
, sort
должна удалить временные файлы и выйти. Вот упрощенная версия обработчика сигнала из GNU Coreutils sort.c
:
/* Обработка прерываний и отсоединений. Упрощена для представления */
static void sighandler(int sig) {
signal(sig, SIG_IGN); /* Отныне этот сигнал игнорировать */
cleanup(); /* Очистка после себя */
signal(sig, SIG_DFL); /* Восстановление действия по умолчанию */
raise(sig); /* Повторно отправить сигнал */
}
Установка действия SIG_IGN
гарантирует, что все последующие появляющиеся сигналы SIGINT
не повлияют на продолжающийся процесс очистки. Когда функция cleanup()
завершит работу, восстановление действия SIG_DFL
позволяет системе сделать снимок образа процесса, если это нужно возникшему сигналу. Вызов raise()
восстанавливает сигнал. Затем восстановленный сигнал вызывает действие по умолчанию, которое, скорее всего, завершит программу. (Далее в этой главе мы полностью покажем обработчик сигнала sort.c
.)
Значение EINTR
для errno
(см. раздел 4.3 «Определение ошибок») указывает, что системный вызов был прерван. Хотя с этим значением ошибки может завершаться большое количество системных вызовов, двумя наиболее значительными являются read()
и write()
. Рассмотрите следующий код:
void handler(int signal) { /* обработка сигналов */ }
int main(int argc, char **argv) {
signal(SIGINT, handler);
...
while ((count = read(fd, buf, sizeof buf)) > 0) {
/* Обработка буфера */
}
if (count == 0)
/* конец файла, очистка и т.п. */
else if (count == -1)
/* ошибка */
...
}
Предположим, что система успешно прочла (и заполнила) часть буфера, когда возник SIGINT
. Системный вызов read()
еще не вернулся из ядра в программу, но ядро решает, что оно может доставить сигнал. Вызывается handler()
, запускается и возвращается в середину read()
. Что возвратит read()
?
В былые времена (V7, более ранние системы System V) read()
возвратила бы -1 и установила бы errno
равным EINTR
. Не было способа сообщить, что данные были переданы. В данном случае V7 и System V действуют, как если бы ничего не случилось: не было перемещений данных в и из буфера пользователя, и смещение файла не было изменено. BSD 4.2 изменила это. Были два случая:
Медленные устройства
«Медленное устройство» является в сущности терминалом или почти всяким другим устройством, кроме обычного файла. В этом случае read()
могла завершиться с ошибкой EINTR
, лишь если не было передано никаких данных, когда появился сигнал. В противном случае системный вызов был бы запущен повторно, и read()
возвратилась бы нормально.
Обычные файлы
Системный вызов был бы запущен повторно В этом случае read()
вернулась бы нормально; возвращенное значение могло быть либо числом запрошенных байтов, либо числом действительно прочитанных байтов (как в случае чтения вблизи конца файла).
Поведение BSD несомненно полезно; вы всегда можете сказать, сколько данных было прочитано.
Поведение POSIX сходно, но не идентично первоначальному поведению BSD. POSIX указывает, что read()
[108]108
Хотя мы описываем read()
, эти правила применяются ко всем системным вызовам, которые могут завершиться с ошибкой EINTR
, как, например, семейство функций wait()
– Примеч. автора.
[Закрыть] завершается с ошибкой EINTR
лишь в случае появления сигнала до начала перемещения данных. Хотя POSIX ничего не говорит о «медленных устройствах», на практике это условие проявляется именно на них.
В противном случае, если сигнал прерывает частично выполненную read()
, возвращенное значение является числом уже прочитанных байтов. По этой причине (а также для возможности обработки коротких файлов) всегда следует проверять возвращаемое read()
значение и никогда не предполагать, что прочитано все запрошенное количество байтов. (Функция POSIX API sigaction()
, описанная позже, позволяет при желании получить поведение повторно вызываемых системных вызовов BSD.)
safe_read()
и safe_write()
Для обработки случая EINTR в традиционных системах GNU Coreutils использует две функции, safe_read()
и safe_write()
. Код несколько запутан из-за того, что один и тот же файл за счет включения #include и макросов реализует обе функции. Из файла lib/safe-read.c
в дистрибутиве Coreutils:
1 /* Интерфейс read и write для .повторных запусков после прерываний.
2 Copyright (С) 1993, 1994, 1998, 2002 Free Software Foundation, Inc.
/* ... куча шаблонного материала опущена... */
56
57 #ifdef SAFE_WRITE
58 # include "safe-write.h"
59 # define safe_rw safe_write /* Создание safe_write() */
60 # define rw write /* Использование системного вызова write() */
61 #else
62 # include "safe-read.h"
63 # define safe_rw safe_read /* Создание safe_read() */
64 # define rw read /* Использование системного вызова read() */
65 # undef const
66 # define const /* пусто */
67 #endif
68
69 /* Прочесть (записать) вплоть до COUNT байтов в BUF из(в) дескриптора FD, повторно запуская вызов при
70 прерывании. Вернуть число действительно прочитанных (записанных) байтов, 0 для EOF
71 или в случае ошибки SAFE_READ_ERROR(SAFE_WRITE_ERROR). */
72 size_t
73 safe_rw(int fd, void const *buf, size_t count)
74 {
75 ssize_t result;
76
77 /* POSIX ограничивает COUNT значением SSIZE_MAX, но мы еще больше ограничиваем его, требуя,
78 чтобы COUNT <= INT_MAX, для избежания ошибки в Tru64 5.1.
79 При уменьшении COUNT сохраняйте указатель файла выровненным по размеру блока.
80 Обратите внимание, что read (write) может быть успешным в любом случае, даже если прочитано (записано)
81 менее COUNT байтов, поэтому вызывающий должен быть готов обработать
82 частичные результаты. */
83 if (count > INT_MAX)
84 count = INT_MAX & -8191;
85
86 do
87 {
88 result = rw(fd, buf, count);
89 }
90 while (result < 0 && IS_EINTR(errno));
91
92 return (size_t) result;
93 }
Строки 57–67 обрабатывают определения, создавая соответствующим образом safe_read()
и safe_write()
(см. ниже safe_write.c
).
Строки 77–84 указывают на разновидность осложнений, возникающих при чтении. Здесь один особый вариант Unix не может обработать значения, превышающие INT_MAX
, поэтому строки 83–84 выполняют сразу две операции: уменьшают значение числа, чтобы оно не превышало INT_MAX
, и сохраняют его кратным 8192. Последняя операция служит эффективности дисковых операций: выполнение ввода/вывода с кратным основному размеру дискового блока объемом данных более эффективно, чем со случайными размерами данных. Как отмечено в комментарии, код сохраняет семантику read()
и write()
, где возвращенное число байтов может быть меньше затребованного.
Обратите внимание, что параметр count
может и в самом деле быть больше INT_MAX
, поскольку count представляет тип size_t
, который является беззнаковым (unsigned). INT_MAX
является чистым int
, который на всех современных системах является знаковым.
Строки 86–90 представляют действительный цикл, повторно осуществляющий операцию, пока она завершается ошибкой EINTR
. Макрос IS_EINTR()
не показан, но он обрабатывает случай в системах, на которых EINTR
не определен. (Должен быть по крайней мере один такой случай, иначе код не будет возиться с установкой макроса; возможно, это было сделано для эмуляции Unix или POSIX в не-Unix системе.) Вот safe_write.c
:
1 /* Интерфейс write для повторного запуска после прерываний.
2 Copyright (С) 2002 Free Software Foundation, Inc.
/* ...куча шаблонного материала опущена... */
17
18 #define SAFE_WRITE
19 #include "safe-read.с"
В строке 18 #define
определяет SAFE_WRITE
; это связано со строками 57–60 в safe_read.с
.
TEMP_FAILURE_RETRY()
Файл
#include
long int TEMP_FAILURE_RETRY(expression);
Вот определение макроса:
/* Оценить EXPRESSION и повторять, пока оно возвращает -1 с 'errno',
установленным в EINTR. */
# define TEMP_FAILURE_RETRY(expression)
(__extension__
({ long int __result;
do __result = (long int)(expression);
while (__result == -1L && errno == EINTR);
__result; }))
Макрос использует расширение GCC к языку С (как обозначено ключевым словом __extension__
), которое допускает заключенным в фигурные скобки внутри обычных скобок выражениям возвращать значение, действуя таким образом подобно простому выражению.
Используя этот макрос, мы могли бы переписать safe_read()
следующим образом:
size_t safe_read(int fd, void const *buf, size_t count) {
ssize_t result;
/* Ограничить count, как в ранее приведенном комментарии. */
if (count > INT_MAX)
count = INT_MAX & ~8191;
result = TEMP_FAILURE_RETRY(read(fd, buf, count));
return (size_t)result;
}
sig_atomic_t
(ISO C)Пока обработка одного сигнала за раз выглядит просто: установка обработчика сигнала в main()
и (не обязательная) переустановка самого себя обработчиком сигнала (или установка действия SIG_IGN
) в качестве первого действия обработчика.
Но что произойдет, если возникнут два идентичных сигнала, один за другим? В частности, что, если ваша система восстановит действие по умолчанию для вашего сигнала, а второй сигнал появится после вызова обработчика, но до того, как он себя восстановит?
Или предположим, что вы используете bsd_signal()
, так что обработчик остается установленным, но второй сигнал отличается от первого? Обычно обработчику первого сигнала нужно завершить свою работу до того, как запускается второй, а каждый обработчик сигнала не должен временно игнорировать все прочие возможные сигналы!
Оба случая относятся к состоянию гонки. Одним решением для этих проблем является как можно большее упрощение обработчиков сигналов. Это можно сделать, создав флаговые переменные, указывающие на появление сигнала. Обработчик сигнала устанавливает переменную в true и возвращается. Затем основная логика проверяет флаговую переменную в стратегических местах:
int sig_int_flag = 0; /* обработчик сигнала устанавливает в true */
void int_handler(int signum) {
sig_int_flag = 1;
}
int main(int argc, char **argv) {
bsd_signal(SIGINT, int_handler);
/* ...программа продолжается... */
if (sig_int_flag) {
/* возник SIGINT, обработать его */
}
/* ...оставшаяся логика... */
}
(Обратите внимание, что эта стратегия уменьшает окно уязвимости, но не устраняет его).
Стандарт С вводит специальный тип – sig_atomic_t
– для использования с такими флаговыми переменными. Идея, скрывающаяся за этим именем, в том, что присвоение значений переменным этого типа является атомарной операцией: т.е. они совершаются как одно делимое действие. Например, на большинстве машин присвоение значения int
осуществляется атомарно, тогда как инициализация значений в структуре осуществляется либо путем копирования всех байтов в (сгенерированном компилятором) цикле, либо с помощью инструкции «блочного копирования», которая может быть прервана. Поскольку присвоение значения sig_atomic_t
является атомарным, раз начавшись, оно завершается до того, как может появиться другой сигнал и прервать его.
Наличие особого типа является лишь частью истории. Переменные sig_atomic_t
должны быть также объявлены как volatile
:
volatile sig_atomic_t sig_int_flag = 0; /* обработчик сигнала устанавливает в true */
/* ...оставшаяся часть кода как раньше... */
Ключевое слово volatile
сообщает компилятору, что переменная может быть изменена извне, за спиной компилятора, так сказать. Это не позволяет компилятору применить оптимизацию, которая могла бы в противном случае повлиять на правильность кода
Структурирование приложения исключительно вокруг переменных sig_atomic_t
ненадежно. Правильный способ обращения с сигналами показан далее, в разделе 10.7 «Сигналы для межпроцессного взаимодействия».
Стандарт POSIX предусматривает для обработчиков сигналов несколько предостережений:
• Что случается, когда возвращаются обработчики для SIGFPE
, SIGILL
, SIGSEGV
или любых других сигналов, представляющих «вычислительные исключения», не определено.
• Если обработчик был вызван в результате вызова abort()
, raise()
или kill()
, он не может вызвать raise()
. abort()
описана в разделе 12.4 «Совершение самоубийства: abort()
», a kill()
описана далее в этой главе. (Описанная далее функция API sigaction()
с обработчиком сигнала, принимающая три аргумента, дает возможность сообщить об этом, если это имеет место.)
• Обработчики сигналов могут вызвать лишь функции из табл. 10.2. В частности, они должны избегать функций
. Проблема в том, что во время работы функции
может возникнуть прерывание, когда внутреннее состояние библиотечной функции находится в середине процесса обновления. Дальнейшие вызовы функций
могут повредить это внутреннее состояние.
Список в табл. 10.2 происходит из раздела 2.4 тома System Interfaces (Системные интерфейсы) стандарта POSIX 2001. Многие из этих функций относятся к сложному API и больше не рассматриваются в данной книге.
Таблица 10.2. Функции, которые могут быть вызваны из обработчика сигнала
_Exit() | fpathconf() | raise() | sigqueue() |
_exit() | fstat() | read() | sigset() |
accept() | fsync() | readlink() | sigsuspend() |
access() | ftruncate() | recv() | sleep() |
aio_error() | getegid() | recvfrom() | socket() |
aio_return() | geteuid() | recvmsg() | socketpair() |
aio_suspend() | getgid() | rename() | stat() |
alarm() | getgroups() | rmdir() | sysmlink() |
bind() | getpeername() | select() | sysconf() |
cfgetispeed() | getpgrp() | sem_post() | tcdrain() |
cfgetospeed() | getpid() | send() | tcflow() |
cfsetispeed() | getppid() | sendmsg() | tcflush() |
cfsetospeed() | getsockname() | sendto() | tcgetattr() |
chdir() | getsockopt() | setgid() | tcgetpgrp() |
chmod() | getuid() | setpgid() | tcsendbreak() |
chown() | kill() | setsid() | tcsetattr() |
clock_gettime() | link() | setsockopt() | tcsetpgrp() |
close() | listen() | setuid() | time() |
connect() | lseek() | shutdown() | timer_getoverrun() |
creat() | lstat() | sigaction() | timer_gettime() |
dup() | mkdir() | sigaddset() | timer_settime() |
dup2() | mkfifo() | sigdelset() | times() |
execle() | open() | sigemptyset() | umask() |
execve() | pathconf() | sigfillset() | uname() |
fchmod() | pause() | sigismember() | unlink() |
fchown() | pipe() | signal() | utime() |
fcntl() | poll() | sigpause() | wait() |
fdatasync() | posix_trace_event() | sigpending() | waitpid() |
fork() | pselect() | sigprocmask() | write() |