Прежде чем обработчик сигналов возвращает управление, он увеличивает счетчик на единицу в соответствии с количеством дейтаграмм, прочитанных за один сигнал. Этот массив распечатывается программой в листинге 25.4 и представляет собой диагностическую информацию для обработки сигнала
SIGHUP
.
Последняя функция (листинг 25.4) представляет собой обработчик сигнала
SIGHUP
, который выводит массив
cntread
. Он считает количество дейтаграмм, прочитанных за один сигнал.
Листинг 25.4. Обработчик сигнала SIGHUP
//sigio/dgecho01.c
84 static void
85 sig_hup(int signo)
86 {
87 int i;
88 for (i = 0; i <= QSIZE; i++)
89 printf("cntread[%d] = %ld\n", i, cntread[i]);
90 }
Чтобы
проиллюстрировать, что сигналы не буферизуются и что в дополнение к установке флага, указывающего на управляемый сигналом ввод-вывод, необходимо перевести сокет в неблокируемый режим, запустим этот сервер с шестью клиентами одновременно. Каждый клиент посылает серверу 3645 строк (для отражения). При этом каждый клиент запускается из сценария интерпретатора в фоновом режиме, так что все клиенты стартуют приблизительно одновременно. Когда все клиенты завершены, серверу посылается сигнал
SIGHUP
, в результате чего сервер выводит получившийся массив
cntread
:
linux % udpserv01
cntread[0] = 2
cntread[1] = 21838
cntread[2] = 12
cntread[3] = 1
cntread[4] = 0
cntread[5] = 1
cntread[6] = 0
cntread[7] = 0
cntread[8] = 0
Большую часть времени обработчик сигналов читает только одну дейтаграмму, но бывает, что готово больше одной дейтаграммы. Ненулевое значение счетчика
cntread[0]
получается потому, что сигнал генерируется в процессе выполнения клиента. Мы считываем дейтаграммы в цикле обработчика сигнала. Дейтаграмма, прибывшая во время считывания других дейтаграмм, будет считана вместе с этими дейтаграммами (в том же вызове обработчика), а сигнал об ее прибытии будет отложен и доставлен процессу после завершения обработчика. Это приведет к повторному вызову обработчика, но считывать ему будет нечего (отсюда
cntread[0]>0
). Наконец, можно проверить, что взвешенная сумма элементов массива (21 838x1 + 12x2 + 1x3+1x5=21 870) равна 6x3645 (количество клиентов x количество строк клиента).
25.4. Резюме
При управляемом сигналом вводе-выводе ядро уведомляет процесс сигналом
SIGIO
, если «что-нибудь» происходит на сокете.
Для присоединенного TCP-сокета существует множество ситуаций, которые вызывают такое уведомление, что делает эту возможность практически бесполезной.
Для прослушиваемого TCP-сокета уведомление приходит процессу только в случае готовности принятия нового соединения.
Для UDP такое уведомление означает, что либо пришла дейтаграмма, либо произошла асинхронная ошибка: в обоих случаях вызывается
recvfrom
.
С помощью метода, аналогичного применяемому для сервера NTP, был изменен эхо-сервер UDP для работы с вводом-выводом, управляемым сигналом: мы стремимся выполнить чтение дейтаграммы как можно быстрее после ее прибытия, чтобы получить точную отметку времени прибытия и поставить дейтаграмму в очередь для дальнейшей обработки.
Упражнения
1. Далее приведен альтернативный вариант цикла, рассмотренного в листинге 25.2:
Согласно традиционной модели Unix, когда процессу требуется, чтобы некое действие было выполнено каким-либо другим объектом, он порождает дочерний процесс, используя функцию
fork
, и этим порожденным процессом выполняется необходимое действие. Большинство сетевых серверов под Unix устроены именно таким образом, как мы видели при рассмотрении примера параллельного (concurrent) сервера: родительский процесс осуществляет соединение с помощью функции
accept
и порождает дочерний процесс, используя функцию
fork
, а затем дочерний процесс занимается обработкой клиентского запроса.
Хотя эта концепция с успехом использовалась на протяжении многих лет, с функцией
fork
связаны определенные неудобства.
Стоимость функции
fork
довольно высока, так как при ее использовании требуется скопировать все содержимое памяти из родительского процесса в дочерний, продублировать все дескрипторы и т.д. Текущие реализации используют технологию, называемую копированием при записи( copy-on-write), при которой копирование пространства данных из родительского процесса в дочерний происходит лишь тогда, когда дочернему процессу требуется своя собственная копия. Но несмотря на эту оптимизацию, стоимость функции
fork
остается высокой.
Для передачи данных между родительским и дочерним процессами послевызова функции
fork
требуется использовать средства взаимодействия процессов (IPC). Передача информации перед вызовом
fork
не вызывает затруднений, так как при запуске дочерний процесс получает от родительского копию пространства данных и копии всех родительских дескрипторов. Но возвращение информации из дочернего процесса в родительский требует большей работы.
Обе проблемы могут быть разрешены путем использования программных потоков( threads). Программные потоки иногда называются облегченными процессами( lightweight processes), так как поток проще, чем процесс. В частности, создание потока требует в 10–100 раз меньше времени, чем создание процесса.
Все потоки одного процесса совместно используют его глобальные переменные, поэтому им легко обмениваться информацией, но это приводит к необходимости синхронизации.
Однако общими становятся не только глобальные переменные. Все потоки одного процесса разделяют:
инструкции процесса;
большую часть данных;
открытые файлы (например, дескрипторы);
обработчики сигналов и вообще настройки для работы с сигналами (действие сигнала);