Текст книги "UNIX: разработка сетевых приложений"
Автор книги: Уильям Ричард Стивенс
Соавторы: Эндрю М. Рудофф,Билл Феннер
Жанр:
ОС и Сети
сообщить о нарушении
Текущая страница: 13 (всего у книги 88 страниц) [доступный отрывок для чтения: 32 страниц]
В листинге 5.3 показана функция main
TCP-клиента.
Листинг 5.3. Эхо-клиент TCP
//tcpcliserv/tcpcli01.c
1 #include "unp.h"
2 int
3 main(int argc, char **argv)
4 {
5 int sockfd;
6 struct sockaddr_in servaddr;
7 if (argc != 2)
8 err_quit("usage: tcpcli
9 sockfd = Socket(AF_INET, SOCK_STREAM, 0);
10 bzero(&servaddr. sizeof(servaddr));
11 servaddr.sin_family = AF_INET;
12 servaddr.sin_port = htons(SERV_PORT);
13 Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
14 Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));
15 str_cli(stdin, sockfd); /* эта функция выполняет все необходимые
действия со стороны клиента */
16 exit(0);
17 }
Создание сокета, заполнение структуры его адреса
9-13
Создается сокет TCP и структура адреса сокета заполняется IP-адресом сервера и номером порта. IP-адрес сервера мы берем из командной строки, а известный номер порта сервера ( SERV_PORT
) – из нашего заголовочного файла unp.h
.
Соединение с сервером
14-15
Функция connect
устанавливает соединение с сервером. Затем функция str_cli
(см. листинг 5.4) выполняет все необходимые действия со стороны клиента.
Эта функция, показанная в листинге 5.4, обеспечивает отправку запроса клиента и прием ответа сервера в цикле. Функция считывает строку текста из стандартного потока ввода, отправляет ее серверу и считывает отраженный ответ сервера, после чего помещает отраженную строку в стандартный поток вывода.
Листинг 5.4. Функция str_cli: цикл формирования запроса клиента
//lib/str_cli.c
1 #include "unp.h"
2 void
3 str_cli(FILE *fp, int sockfd)
4 {
5 char sendline[MAXLINE], recvline[MAXLINE];
6 while (Fgets(sendline, MAXLINE, fp) != NULL) {
7 Writen(sockfd,. sendline, strlen(sendline));
8 if (Readline(sockfd, recvline, MAXLINE) == 0)
9 err_quit("str_cli: server terminated prematurely");
10 Fputs(recvline, stdout);
11 }
12 }
Считывание строки, отправка серверу
6-7
Функция fgets
считывает строку текста, а функция writen
отправляет эту строку серверу.
Считывание отраженной сервером строки, запись в стандартный поток вывода
8-10
Функция readline
принимает отраженную сервером строку, а функция fputs
записывает ее в стандартный поток вывода.
Возврат в функцию main
11-12
Цикл завершается, когда функция fgets
возвращает пустой указатель, что означает достижение конца файла или обнаружение ошибки. Наша функция-обертка Fgets
проверяет наличие ошибки, и если ошибка действительно произошла, прерывает выполнение программы. Таким образом, функция Fgets
возвращает пустой указатель только при достижении конца файла.
Наш небольшой пример использования TCP (около 150 строк кода для двух функций main
, str_echo
, str_cli
, readline
и writen
) позволяет понять, как запускаются и завершаются клиент и сервер и, что наиболее важно, как развиваются события, если произошел сбой на узле клиента или в клиентском процессе, потеряна связь в сети и т.д. Только при понимании этих «граничных условий» и их взаимодействия с протоколами TCP/IP мы сможем обеспечить устойчивость клиентов и серверов, которые смогут справляться с подобными ситуациями.
Сначала мы запускаем сервер в фоновом режиме на узле linux
.
linux % tcpserv01 &
[1] 17870
Когда сервер запускается, он вызывает функции socket
, bind
, listen
и accept
, а затем блокируется в вызове функции accept
. (Мы еще не запустили клиент.) Перед тем, как запустить клиент, мы запускаем программу netstat
, чтобы проверить состояние прослушиваемого сокета сервера.
linux % netstat -a
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 *:9877 *:* LISTEN
Здесь мы показываем только первую строку вывода и интересующую нас строку. Эта команда показывает состояние всехсокетов в системе, поэтому вывод может быть большим. Для просмотра прослушиваемых сокетов следует указать параметр -a
.
Результат совпадает с нашими ожиданиями. Сокет находится в состоянии LISTEN, локальный IP-адрес задан с помощью символа подстановки (то есть является универсальным) и указан локальный порт 9877. Функция netstat
выводит звездочку для нулевого IP-адреса ( INADDR_ANY
, универсальный адрес) или для нулевого порта.
Затем на том же узле мы запускаем клиент, задав IP-адрес сервера 127.0.0.1. Мы могли бы задать здесь и нормальный адрес сервера (его IP-адрес в сети).
linux % tcpcli01 127.0.0.1
Клиент вызывает функции socket
и connect
, последняя осуществляет трехэтапное рукопожатие TCP. Когда рукопожатие TCP завершается, функция connect возвращает управление процессу-клиенту, а функция accept
– процессу-серверу. Соединение установлено. Затем выполняются следующие шаги:
1. Клиент вызывает функцию str_cli
, которая блокируется в вызове функции fgets
, поскольку мы еще ничего не ввели.
2. Когда функция accept
возвращает управление процессу-серверу, последний вызывает функцию fork
, а дочерний процесс вызывает функцию str_echo
. Та вызывает функцию read
, блокируемую в ожидании получения данных от клиента.
3. Родительский процесс сервера снова вызывает функцию accept
и блокируется в ожидании подключения следующего клиента.
У нас имеется три процесса, и все они находятся в состоянии ожидания (блокированы): клиент, родительский процесс сервера и дочерний процесс сервера.
ПРИМЕЧАНИЕ
Мы специально поставили первым пунктом (после завершения трехэтапного рукопожатия) вызов функции str_cli, происходящий на стороне клиента, а затем уже перечислили действия на стороне сервера. Причину объясняет рис. 2.5: функция connect возвращает управление, когда клиент получает второй сегмент рукопожатия. Однако функция accept не возвращает управление до тех пор, пока сервер не получит третий сегмент рукопожатия, то есть пока не пройдет половина периода RTT после завершения функции connect.
Мы намеренно запускаем и клиент, и сервер на одном узле – так проще всего экспериментировать с клиент-серверными приложениями. Поскольку клиент и сервер запущены на одном узле, функция netstat
отображает теперь две дополнительные строки вывода, соответствующие соединению TCP:
l inux % netstat -a
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 localhost:9877 localhost:42758 ESTABLISHED
tcp 0 0 localhost:42758 localhost:42758 ESTABLISHED
tcp 0 0 *:9877 *:* LISTEN
Первая из строк состояния ESTABLISHED
соответствует дочернему сокету сервера, поскольку локальным портом является порт 9877. Вторая строка ESTABLISHED
– это клиентский сокет, поскольку локальный порт – порт 42 758. Если мы запускаем клиент и сервер на разных узлах, на узле клиента будет отображаться только клиентский сокет, а на узле сервера – два серверных сокета.
Для проверки состояний процессов и отношений между ними можно также использовать команду ps
:
linux % ps -t pts/6 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
22038 22036 pts/6 S -bash wait4
17870 22038 pts/6 S ./tcpserv01 wait_for_connect
19315 17870 pts/6 S ./tcpserv01 tcp_data_wait
19314 22038 pts/6 S ./tcpcli01 127.0.0.1 read_chan
Мы вызвали ps
с несколько необычным набором аргументов для того, чтобы получить всю необходимую для дальнейшего обсуждения информацию. Мы запустили клиент и сервер из одного окна ( pts/6
, что означает псевдотерминал 6). В колонках PID
и PPID
показаны отношения между родительским и дочерним процессами. Можно точно сказать, что первая строка tcpserv01
соответствует родительскому процессу, а вторая строка tcpserv01
– дочернему, поскольку PPID дочернего процесса – это PID родительского. Кроме того, PPID родительского процесса совпадает с PID интерпретатора команд ( bash
).
Колонка STAT
для всех трех сетевых процессов отмечена символом S
. Это означает, что процессы находятся в состоянии ожидания (sleeping). Если процесс находится в состоянии ожидания, колонка WCHAN
сообщит нам о том, чем он занят. В Linux значение wait_for_connect
выводится, если процесс блокируется функцией accept
или connect
, значение tcp_data_wait
– если процесс блокируется при вводе или выводе через сокет, a read_chan
– если процесс блокируется при терминальном вводе-выводе. Так что для наших трех сетевых процессов значения WCHAN
выглядят вполне осмысленно.
На этом этапе соединение установлено, и все, что бы мы ни вводили на стороне клиента, отражается обратно.
linux % tcpcli01 127.0.0.1 эту строку мы показывали раньше
hello, world наш ввод
hello, world отраженная сервером строка
good bye
good bye
^D Ctrl+D – наш завершающий символ для обозначения конца файла
Мы вводим две строки, каждая из них отражается, затем мы вводим символ конца файла (EOF) Ctrl+D
, который завершает работу клиента. Если мы сразу же выполним команду netstat
, то увидим следующее:
linux % netstat -а | grep 9877
tcp 0 0 *:9877 *:*
tcp 0 0 local host:42758 localhost:9877
Клиентская часть соединения (локальный порт 42 758) входит в состояние TIME_WAIT (см. раздел 2.6), и прослушивающий сервер все еще ждет подключения другого клиента. (В этот раз мы передаем вывод netstat
программе grep
, чтобы вывести только строки с заранее известным портом нашего сервера. Но при этом также удаляется строка заголовка.)
Перечислим этапы нормального завершения работы нашего клиента и сервера.
1. Когда мы набираем символ EOF, функция fgets
возвращает пустой указатель, и функция str_cli
возвращает управление (см. листинг 5.4).
2. Когда функция str_cli
возвращает управление клиентской функции main
(см. листинг 5.3), последняя завершает работу, вызывая функцию exit
.
3. При завершении процесса выполняется закрытие всех открытых дескрипторов, так что клиентский сокет закрывается ядром. При этом серверу посылается сегмент FIN, на который TCP сервера отвечает сегментом ACK. Это первая половина последовательности завершения работы соединения TCP. На этом этапе сокет сервера находится в состоянии CLOSE_WAIT, а клиентский сокет – в состоянии FIN_WAIT_2 (см. рис. 2.4 и 2.5).
4. Когда TCP сервера получает сегмент FIN, дочерний процесс сервера находится в состоянии ожидания в вызове функции read
(см. листинг 5.2), а затем функция read
возвращает нуль. Это заставляет функцию str_echo
вернуть управление функции main
дочернего процесса сервера.
5. Дочерний процесс сервера завершается с помощью вызова функции exit
(см. листинг 5.1).
6. Все открытые дескрипторы в дочернем процессе сервера закрываются. Закрытие присоединенного сокета дочерним процессом вызывает отправку двух последних сегментов завершения соединения TCP: FIN от сервера клиенту и ACK от клиента (см. рис. 2.5). На этом этапе соединение полностью завершается. Клиентский сокет входит в состояние TIME_WAIT.
7. Другая часть завершения процесса относится к сигналу SIGCHLD
. Он отправляется родительскому процессу, когда завершается дочерний процесс. Это происходит и в нашем примере, но мы не перехватываем данный сигнал в коде, и по умолчанию он игнорируется. Дочерний процесс входит в состояние зомби (zombie). Мы можем проверить это с помощью команды ps
.
linux % ps -t pts/6 -o pid,ppid,tty,stat,args,wchan
PID PPID TT STAT COMMAND WCHAN
22038 22036 pts/6 S -bash read_chan
17870 22038 pts/6 S ./tcpserv01 wait_for_connect
19315 17870 pts/6 Z [tcpserv01
Теперь дочерний процесс находится в состоянии Z
(зомби).
Процессы-зомби нужно своевременно удалять, а это требует работы с сигналами Unix. Поэтому в следующем разделе мы сделаем обзор управления сигналами, а затем продолжим рассмотрение нашего примера.
5.8. Обработка сигналов POSIXСигнал– это уведомление процесса о том, что произошло некое событие. Иногда сигналы называют программными прерываниями( software interrupts). Подразумевается, что процесс не знает заранее о том, когда придет сигнал.
Сигналы могут посылаться в следующих направлениях:
■ одним процессом другому процессу (или самому себе);
■ ядром процессу.
Сигнал SIGCHLD
, упомянутый в конце предыдущего раздела, ядро посылает родительскому процессу при завершении дочернего.
Для каждого сигнала существует определенное действие( actionили disposition– характер). Действие, соответствующее сигналу, задается с помощью вызова функции sigaction
(ее описание следует далее) и может быть выбрано тремя способами:
1. Мы можем предоставить функцию, которая вызывается при перехвате определенного сигнала. Эта функция называется обработчиком сигнала( signal handler), а действие называется перехватыванием сигнала( catching). Сигналы SIGKILL
и SIGSTOP
перехватить нельзя. Наша функция вызывается с одним целочисленным аргументом, который является номером сигнала, и ничего не возвращает. Следовательно, прототип этой функции имеет вид:
void handler(int signo);
Для большинства сигналов вызов функции sigaction
и задание функции, вызываемой при получении сигнала, – это все, что требуется для обработки сигнала. Но дальше вы увидите, что для перехватывания некоторых сигналов, в частности SIGIO
, SIGPOLL
и SIGURG
, требуются дополнительные действия со стороны процесса.
2. Мы можем игнорироватьсигнал, если действие задать как SIG_IGN
. Сигналы SIGKILL
и SIGSTOP
не могут быть проигнорированы.
3. Мы можем установить действие для сигнала по умолчанию, задав его как SIG_DFL
. Действие сигнала по умолчанию обычно заключается в завершении процесса по получении сигнала, а некоторые сигналы генерируют копию области памяти процесса в его текущем каталоге (так называемый дамп– core dump). Есть несколько сигналов, для которых действием по умолчанию является игнорирование. Например, SIGCHLD
и SIGURG
(посылается по получении внеполосных данных, см. главу 24) – это два сигнала, игнорируемых по умолчанию, с которыми мы встретимся в тексте.
Согласно POSIX, чтобы определить действие для сигнала, нужно вызвать функцию sigaction
. Однако это достаточно сложно, поскольку один аргумент этой функции – это структура, для которой необходимо выделение памяти и заполнение. Поэтому проще задать действие сигнала с помощью функции signal
. Первый ее аргумент – это имя сигнала, а второй – либо указатель на функцию, либо одна из констант SIG_IGN
и SIG_DFL
. Но функция signal
существовала еще до появления POSIX.1, и ее различные реализации имеют разную семантику сигналов с целью обеспечения обратной совместимости. В то же время POSIX четко диктует семантику при вызове функции sigaction
. Это обеспечивает простой интерфейс с соблюдением семантики POSIX. Мы включили эту функцию в нашу собственную библиотеку вместе функциями err_ XXX
и функциями-обертками, которые мы используем для построения всех наших программ. Она представлена в листинге 5.5. Функция-обертка Signal
здесь не показана, потому что ее вид не зависит от того, какую именно функцию signal
она должна вызывать.
Листинг 5.5. Функция signal, вызывающая функцию POSIX sigaction
//lib/signal.c
1 #include "unp.h"
2 Sigfunc*
3 signal(int signo, Sigfunc *func)
4 {
5 struct sigaction act, oact;
6 act.sa_handler = func;
7 sigemptyset(&act.sa_mask);
8 act.sa_flags = 0;
9 if (signo == SIGALRM) {
10 #ifdef SA_INTERRUPT
11 act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
12 #endif
13 } else {
14 #ifdef SA_RESTART
15 act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
16 #endif
17 }
18 if (sigaction(signo, &act, &oact) < 0)
19 return (SIG_ERR);
20 return (oact.sa_handler);
21 }
Упрощение прототипа функции при использовании typedef
2-3
Обычный прототип для функции signal
усложняется наличием вложенных скобок:
void (*signal(int signo, void (* func)(int)))(int);
Чтобы упростить эту запись, мы определяем тип Sigfunc
в нашем заголовочном файле unp.h
следующим образом:
typedef void Sigfunc(int);
указывая тем самым, что обработчики сигналов – это функции с целочисленным аргументом, ничего не возвращающие ( void
). Тогда прототип функции выглядит следующим образом:
Sigfunc *signal(int signo, Sigfunc * func);
Указатель на функцию, являющуюся обработчиком сигнала, – это второй аргумент функции и в то же время возвращаемое функцией значение.
Установка обработчика
6
Элемент sa_handler
структуры sigaction
устанавливается равным аргументу func
функции signal
.
Установка маски сигнала для обработчика
7
POSIX позволяет нам задавать набор сигналов, которые будут блокированыпри вызове обработчика сигналов. Любой блокируемый сигнал не может быть доставлен процессу. Мы устанавливаем элемент sa_mask
равным пустому набору. Это означает, что во время работы обработчика дополнительные сигналы не блокируются. POSIX гарантирует, что перехватываемый сигнал всегда блокирован, пока выполняется его обработчик.
Установка флага SA_RESTART
8-17
Флаг SA_RESTART
не является обязательным, и если он установлен, то системный вызов, прерываемый этим сигналом, будет автоматически снова выполнен ядром. (В продолжении нашего примера мы более подробно поговорим о прерванных системных вызовах.) Если перехватываемый сигнал не является сигналом SIGALRM
, мы задаем флаг SA_RESTART
, если таковой определен. (Причина, по которой сигнал SIGALRM
обрабатывается отдельно, состоит в том, что обычно цель его генерации – ввести ограничение по времени в операцию ввода-вывода, как показано в листинге 14.2. В этом случае мы хотим, чтобы блокированный системный вызов был прерван сигналом.) Более ранние системы, особенно SunOS 4.x, автоматически перезапускают прерванный системный вызов по умолчанию и затем определяют флаг SA_INTERRUPT
. Если этот флаг задан, мы устанавливаем его при перехвате сигнала SIGALRM
.
Вызов функции sigaction
18-20
Мы вызываем функцию sigaction
, а затем возвращаем старое действие сигнала как результат функции signal
.
В книге мы везде используем функцию signal
из листинга 5.5.
Сведем воедино следующие моменты, относящиеся к обработке сигналов в системе, совместимой с POSIX.
■ Однажды установленный обработчик сигналов остается установленным (в более ранних системах обработчик сигналов удалялся каждый раз по выполнении).
■ На время выполнения функции – обработчика сигнала доставляемый сигнал блокируется. Более того, любые дополнительные сигналы, заданные в наборе сигналов sa_mask
, переданном функции sigaction
при установке обработчика, также блокируются. В листинге 5.5 мы устанавливаем sa_mask
равным пустому набору, что означает, что никакие сигналы, кроме перехватываемого, не блокируются.
■ Если сигнал генерируется один или несколько раз, пока он блокирован, то обычно после разблокирования он доставляется только один раз, то есть по умолчанию сигналы Unix не устанавливаются в очередь. Пример мы рассмотрим в следующем разделе. Стандарт POSIX реального времени 1003.1b определяет набор надежныхсигналов, которые помещаются в очередь, но в этой книге мы их не используем.
■ Существует возможность выборочного блокирования и разблокирования набора сигналов с помощью функции sigprocmask
. Это позволяет нам защитить критическую область кода, не допуская перехватывания определенных сигналов во время ее выполнения.
Назначение состояния зомби – сохранить информацию о дочернем процессе, чтобы родительский процесс мог ее впоследствии получить. Эта информация включает идентификатор дочернего процесса, статус завершения и данные об использовании ресурсов (время процессора, память и т.д.). Если у завершающегося процесса есть дочерний процесс в зомбированном состоянии, идентификатору родительского процесса всех зомбированных дочерних процессов присваивается значение 1 (процесс init
), что позволяет унаследовать дочерние процессы и сбросить их (то есть процесс init
будет ждать ( wait
) их завершения, благодаря чему будут удалены зомби). Некоторые системы Unix в столбце COMMAND
выводят для зомбированных процессов значение
.
Очевидно, что нам не хотелось бы оставлять процессы в виде зомби. Они занимают место в ядре, и в конце концов у нас может не остаться идентификаторов для нормальных процессов. Когда мы выполняем функцию fork
для дочерних процессов, необходимо с помощью функции wait
дождаться их завершения, чтобы они не превратились в зомби. Для этого мы устанавливаем обработчик сигналов для перехватывания сигнала SIGCHLD
и внутри обработчика вызываем функцию wait
. (Функции wait
и waitpid
мы опишем в разделе 5.10.) Обработчик сигналов мы устанавливаем с помощью вызова функции
Signal(SIGCHLD, sig_chld);
в листинге 5.1, после вызова функции listen
. (Необходимо сделать это до вызова функции fork
для первого дочернего процесса, причем только один раз.) Затем мы определяем обработчик сигнала – функцию sig_chld
, представленную в листинге 5.6.
Листинг 5.6. Версия обработчика сигнала SIGCHLD, вызывающая функцию wait (усовершенствованная версия находится в листинге 5.8)
//tcpcliserv/sigchldwait.с
1 #include "unp.h"
2 void
3 sig_chld(int signo)
4 {
5 pid_t pid;
6 int stat;
7 pid = wait(&stat);
8 printf("child terrmnatedn", pid);
9 return;
10 }
ВНИМАНИЕ
В обработчике сигналов не рекомендуется вызов стандартных функций ввода-вывода, таких как printf, по причинам, изложенным в разделе 11.18. В данном случае мы вызываем функцию printf как средство диагностики, чтобы увидеть, когда завершается дочерний процесс.
В системах System V и Unix 98 дочерний процесс не становится зомби, если процесс задает действие SIG_IGN для SIGCHLD. К сожалению, это верно только для System V и Unix 98. В POSIX прямо сказано, что такое поведение этим стандартом не предусмотрено. Переносимый способ обработки зомби состоит в том, чтобы перехватывать сигнал SIGCHLD и вызывать функцию wait или waitpid.
Если мы откомпилируем в Solaris 9 программу, представленную в листинге 5.1, вызывая функцию Signal
с нашим обработчиком sig_chld
, и будем использовать функцию signal
из системной библиотеки (вместо нашей версии, показанной в листинге 5.5), то получим следующее:
solaris % tcpserv02 & запускаем сервер в фоновом режиме
[2] 16939
solaris % tcpcli01 127.0.0.1 затем клиент
hi there набираем эту строку
hi there и она отражается сервером
^D вводим символ конца файла
child 16942 terminated функция printf из обработчика сигнала выводит эту строку
accept error: Interrupted system call но функция main преждевременно прекращает выполнение
Последовательность шагов в этом примере такова:
1. Мы завершаем работу клиента, вводя символ EOF. TCP клиента посылает сегмент FIN серверу, и сервер отвечает сегментом ACK.
2. Получение сегмента FIN доставляет EOF ожидающей функции readline
дочернего процесса. Дочерний процесс завершается.
3. Родительский процесс блокирован в вызове функции accept
, когда доставляется сигнал SIGCHLD
. Функция sig_chld
(наш обработчик сигнала) выполняется, функция wait
получает PID дочернего процесса и статус завершения, после чего из обработчика сигнала вызывается функция printf
. Обработчик сигнала возвращает управление.
4. Поскольку сигнал был перехвачен родительским процессом, в то время как родительский процесс был блокирован в медленном(см. ниже) системном вызове (функция accept
), ядро заставляет функцию accept
возвратить ошибку EINTR
(прерванный системный вызов). Родительский процесс не обрабатывает эту ошибку корректно (см. листинг 5.1), поэтому функция main
преждевременно завершается.
Цель данного примера – показать, что при написании сетевых программ, перехватывающих сигналы, необходимо получать информацию о прерванных системных вызовах и обрабатывать их. В этом специфичном для Solaris 2.5 примере функция signal
из стандартной библиотеки С не осуществляет автоматический перезапуск прерванного вызова, то есть флаг SA_RESTART
, установленный нами в листинге 5.5, не устанавливается функцией signal из системной библиотеки. Некоторые другие системы автоматически перезапускают прерванный системный вызов. Если мы запустим тот же пример в 4.4BSD, используя ее библиотечную версию функции signal
, ядро перезапустит прерванный системный вызов и функция accept
не возвратит ошибки. Одна из причин, по которой мы определяем нашу собственную версию функции signal
и используем ее далее, – решение этой потенциальной проблемы, возникающей в различных операционных системах (см. листинг 5.5).
Кроме того, мы всегда программируем явную функцию return
для наших обработчиков сигналов (см. листинг 5.6), даже если функция ничего не возвращает ( void
), чтобы этот оператор напоминал нам о возможности прерывания системного вызова при возврате из обработчика.