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 <defu do_exit
Теперь дочерний процесс находится в состоянии
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) — это два сигнала, игнорируемых по умолчанию, с которыми мы встретимся в тексте.
Функция signal
Согласно 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