UNIX: разработка сетевых приложений — страница 7 из 42

Мультиплексирование ввода-вывода: функции select и poll

6.1. Введение

В разделе 5.12 мы видели, что наш TCP-клиент обрабатывает два входных потока одновременно: стандартный поток ввода и сокет TCP. Проблема, с которой мы столкнулись, состояла в том, что пока клиент был блокирован в вызове функции

fgets
(чтение из стандартного потока ввода), процесс сервера мог быть уничтожен. TCP сервера корректно отправляет сегмент FIN протоколу TCP клиента, но поскольку процесс клиента блокирован при чтении из стандартного потока ввода, он не получит признак конца файла, пока не считает данные из сокета (возможно, значительно позже). Нам нужна возможность сообщить ядру, что мы хотим получить уведомления о том, что выполняется одно или несколько условий для ввода-вывода (например, присутствуют данные для считывания или дескриптор готов к записи новых данных). Эта возможность называется мультиплексированием (multiplexing) ввода-вывода и обеспечивается функциями
select
и
poll
. Мы рассмотрим также более новый вариант функции
select
, входящей в стандарт POSIX, называемый
pselect
.

ПРИМЕЧАНИЕ

В некоторых системах предоставляются более мощные средства ожидания событий. Одним из механизмов является устройство опроса (poll device), которое по-разному реализуется разными производителями. Этот механизм описывается в главе 14.

Мультиплексирование ввода-вывода обычно используется сетевыми приложениями в следующих случаях:

■ Когда клиент обрабатывает множество дескрипторов (обычно интерактивный ввод и сетевой сокет), должно использоваться мультиплексирование ввода- вывода. Это сценарий, который мы только что рассмотрели.

■ Возможно, хотя это и редкий случай, что клиент одновременно обрабатывает множество сокетов. Такой пример мы приведем в разделе 16.5 при использовании функции

select
в контексте веб-клиента.

■ Если сервер TCP обрабатывает и прослушиваемый сокет, и присоединенные сокеты, обычно используется мультиплексирование ввода-вывода, как это показано в разделе 6.8.

■ Если сервер работает и с TCP, и с UDP, обычно также используется мультиплексирование ввода-вывода. Такой пример мы приводим в разделе 8.15.

■ Если сервер обрабатывает несколько служб и, возможно, несколько протоколов (например, демон

inetd
, который описан в разделе 12.5), обычно используется мультиплексирование ввода-вывода.

Область применения мультиплексирования ввода-вывода не ограничивается только сетевым программированием. Любому нетривиальному приложению часто приходится использовать эту технологию.

6.2. Модели ввода-вывода

Прежде чем начать описание функций

select
и
poll
, мы должны вернуться назад и уяснить основные различия между пятью моделями ввода-вывода, доступными нам в Unix:

■ блокируемый ввод-вывод;

■ неблокируемый ввод-вывод;

■ мультиплексирование ввода-вывода (функции

select
и
poll
);

■ ввод-вывод, управляемый сигналом (сигнал

SIGIO
);

■ асинхронный ввод-вывод (функции POSIX

aio_
).

Возможно, вы захотите пропустить этот раздел при первом прочтении, а затем вернуться к нему по мере знакомства с различными моделями ввода-вывода, подробно рассматриваемыми в дальнейших главах.

Как вы увидите в примерах этого раздела, обычно различаются две фазы операции ввода:

1. Ожидание готовности данных.

2. Копирование данных от ядра процессу.

Первый шаг операции ввода на сокете обычно включает ожидание прихода данных по сети. Когда пакет приходит, он копируется в буфер внутри ядра. Второй шаг — копирование этих данных из буфера ядра в буфер приложения.

Модель блокируемого ввода-вывода

Наиболее распространенной моделью ввода-вывода является модель блокируемого ввода-вывода, которую мы использовали для всех предыдущих примеров. По умолчанию все сокеты являются блокируемыми. Используя в наших примерах сокет дейтаграмм, мы получаем сценарий, показанный на рис. 6.1.

Рис. 6.1. Модель блокируемого ввода-вывода

В этом примере вместо TCP мы используем UDP, поскольку в случае UDP признак готовности данных очень прост: получена вся дейтаграмма или нет. В случае TCP он становится сложнее, поскольку приходится учитывать дополнительные переменные, например минимальный объем данных в сокете (low water-mark).

В примерах этого раздела мы говорим о функции

recvfrom
как о системном вызове, поскольку делаем различие между нашим приложением и ядром. Вне зависимости от того, как реализована функция
recvfrom
(как системный вызов в ядре, происходящем от Беркли, или как функция, активизирующая системный вызов
getmsg
в ядре System V), она обычно выполняет переключение между работой в режиме приложения и работой в режиме ядра, за которым через определенный промежуток времени следует возвращение в режим приложения.

На рис. 6.1 процесс вызывает функцию

recvfrom
, и системный вызов не возвращает управление, пока дейтаграмма не придет и не будет скопирована в буфер приложения либо пока не произойдет ошибка. Наиболее типичная ошибка — это прерывание системного вызова сигналом, о чем рассказывалось в разделе 5.9. Процесс блокирован в течение всего времени с момента, когда он вызывает функцию
recvfrom
, до момента, когда эта функция завершается. Когда функция
recvfrom
выполняется нормально, наше приложение обрабатывает дейтаграмму.

Модель неблокируемого ввода-вывода

Когда мы определяем сокет как неблокируемый, мы тем самым сообщаем ядру следующее: «когда запрашиваемая нами операция ввода-вывода не может быть завершена без перевода процесса в состояние ожидания, следует не переводить процесс в состояние ожидания, а возвратить ошибку». Неблокируемый ввод-вывод мы описываем подробно в главе 16, а на рис. 6.2 лишь демонстрируем его свойства.

Рис. 6.2. Модель неблокируемого ввода-вывода

В первых трех случаях вызова функции

recvfrom
данных для возвращения нет, поэтому ядро немедленно возвращает ошибку
EWOULDBLOCK
. Когда мы в четвертый раз вызываем функцию
recvfrom
, дейтаграмма готова, поэтому она копируется в буфер приложения и функция
recvfrom
успешно завершается. Затем мы обрабатываем данные.

Такой процесс, когда приложение находится в цикле и вызывает функцию

recvfrom
на неблокируемом дескрипторе, называется опросом (polling). Приложение последовательно опрашивает ядро, чтобы увидеть, что какая-то операция может быть выполнена. Часто это пустая трата времени процессора, но такая модель все же иногда используется, обычно в специализированных системах.

Модель мультиплексирования ввода-вывода

В случае мультиплексирования ввода-вывода мы вызываем функцию

select
или
poll
, и блокирование происходит в одном из этих двух системных вызовов, а не в действительном системном вызове ввода-вывода. На рис. 6.3 обобщается модель мультиплексирования ввода-вывода.

Рис. 6.3. Модель мультиплексирования ввода-вывода

Процесс блокируется в вызове функции

select
, ожидая, когда дейтаграммный сокет будет готов для чтения. Когда функция
select
возвращает сообщение, что сокет готов для чтения, процесс вызывает функцию
recvfrom
, чтобы скопировать дейтаграмму в буфер приложения.

Сравнивая рис. 6.3 и 6.1, мы не найдем в модели мультиплексирования ввода- вывода каких-либо преимуществ, более того, она даже обладает незначительным недостатком, поскольку использование функции

select
требует двух системных вызовов вместо одного. Но преимущество использования функции
select
, которое мы увидим далее в этой главе, состоит в том, что мы сможем ожидать готовности не одного дескриптора, а нескольких.

ПРИМЕЧАНИЕ

Разновидностью данного способа мультиплексирования является многопоточное программирование с блокируемым вводом-выводом. Отличие состоит в том, что вместо вызова select с блокированием программа использует несколько потоков (по одному на каждый дескриптор), которые могут блокироваться в вызовах типа recvfrom.

Модель ввода-вывода, управляемого сигналом

Мы можем сообщить ядру, что необходимо уведомить процесс о готовности дескриптора с помощью сигнала

SIGIO
. Такая модель имеет название ввод-вывод, управляемый сигналом (signal-driven I/O). Она представлена в обобщенном виде на рис. 6.4.

Рис. 6.4. Модель управляемого сигналом ввода-вывода

Сначала мы включаем на сокете управляемый сигналом ввод-вывод (об этом рассказывается в разделе 22.2) и устанавливаем обработчик сигнала при помощи системного вызова

sigaction
. Возвращение из этого системного вызова происходит незамедлительно, и наш процесс продолжается (он не блокирован). Когда дейтаграмма готова для чтения, для нашего процесса генерируется сигнал
SIGIO
. Мы можем либо прочитать дейтаграмму из обработчика сигнала с помощью вызова функции
recvfrom
и затем уведомить главный цикл о том, что данные готовы для обработки (см. раздел 22.3), либо уведомить основной цикл и позволить ему прочитать дейтаграмму.

Независимо от способа обработки сигнала эта модель имеет то преимущество, что во время ожидания дейтаграммы не происходит блокирования. Основной цикл может продолжать выполнение, ожидая уведомления от обработчика сигнала о том, что данные готовы для обработки либо дейтаграмма готова для чтения.

Модель асинхронного ввода-вывода

Асинхронный ввод-вывод был введен в редакции стандарта POSIX.1g 1993 г. (расширения реального времени). Мы сообщаем ядру, что нужно начать операцию и уведомить нас о том, когда вся операция (включая копирование данных из ядра в наш буфер) завершится. Мы не обсуждаем эту модель в этой книге, поскольку она еще не получила достаточного распространения. Ее основное отличие от модели ввода-вывода, управляемого сигналом, заключается в том, что при использовании сигналов ядро сообщает нам, когда операция ввода-вывода может быть инициирована, а в случае асинхронного ввода-вывода — когда операция завершается. Пример этой модели приведен на рис. 6.5.

Рис. 6.5. Модель асинхронного ввода-вывода

Мы вызываем функцию

aio_read
(функции асинхронного ввода-вывода POSIX начинаются с
aio_
или
lio_
) и передаем ядру дескриптор, указатель на буфер, размер буфера (те же три аргумента, что и для функции read), смещение файла (аналогично функции
lseek
), а также указываем, как уведомить нас, когда операция полностью завершится. Этот системный вызов завершается немедленно, и наш процесс не блокируется в ожидании завершения ввода-вывода. В этом примере предполагается, что мы указали ядру сгенерировать некий сигнал, когда операция завершится. Сигнал не генерируется до тех пор, пока данные не скопированы в наш буфер приложения, что отличает эту модель от модели ввода-вывода, управляемого сигналом.

ПРИМЕЧАНИЕ

На момент написания книги только некоторые системы поддерживали асинхронный ввод-вывод стандарта POSIX. Например, мы не уверены, что какие-либо системы поддерживают его для сокетов. Мы используем его только как пример для сравнения с моделью управляемого сигналом ввода-вывода.

Сравнение моделей ввода-вывода

На рис. 6.6 сравнивается пять различных моделей ввода-вывода. Здесь видно главное отличие четырех первых моделей в первой фазе, поскольку вторая фаза у них одна и та же: процесс блокируется в вызове функции

recvfrom
на то время, пока данные копируются из ядра в буфер вызывающего процесса. Асинхронный ввод-вывод отличается от первых четырех моделей в обеих фазах.

Рис. 6.6. Сравнение моделей ввода-вывода


Сравнение синхронного и асинхронного ввода-вывода

POSIX дает следующие определения этих терминов:

■ Операция синхронного ввода-вывода блокирует запрашивающий процесс до тех пор, пока операция ввода-вывода не завершится.

■ Операция асинхронного ввода-вывода не вызывает блокирования запрашивающего процесса.

Используя эти определения, можно сказать, что первые четыре модели ввода- вывода — блокируемая, неблокируемая, модель мультиплексирования ввода-вывода и модель управляемого сигналом ввода-вывода — являются синхронными, поскольку фактическая операция ввода-вывода (функция

recvfrom
) блокирует процесс. Только модель асинхронного ввода-вывода соответствует определению асинхронного ввода-вывода.

6.3. Функция select

Эта функция позволяет процессу сообщить ядру, что необходимо подождать, пока не произойдет одно из некоторого множества событий, и вывести процесс из состояния ожидания, только когда произойдет одно или несколько таких событий или когда пройдет заданное количество времени.

Например, мы можем вызвать функцию

select
и сообщить ядру, что возвращать управление нужно только когда наступит любое из следующих событий:

■ любой дескриптор из набора {1, 4, 5} готов для чтения;

■ любой дескриптор из набора {2, 7} готов для записи;

■ любой дескриптор из набора {1, 4} вызывает исключение, требующее обработки;

■ истекает 10,2 с.

Таким образом, мы сообщаем ядру, какие дескрипторы нас интересуют (готовые для чтения, готовые для записи или требующие обработки исключения) и как долго нужно ждать. Интересующие нас дескрипторы не ограничиваются сокетами: любой дескриптор можно проверить с помощью функции

select
.

ПРИМЕЧАНИЕ

Беркли-реализации всегда допускали мультиплексирование ввода-вывода с любыми дескрипторами. Система SVR3 ограничивала мультиплексирование ввода-вывода дескрипторами, которые являлись устройствами STREAMS (см. главу 31), но это ограничение было снято в SVR4.

#include 

#include 


int select(int maxfdp1, fd_set *readset, fd_set *writeset,

 fd_set *exceptset, const struct timeval *timeout);

Возвращает: положительное число - счетчик готовых дескрипторов, 0 в случае тайм-аута, -1 в случае ошибки

Описание этой функции мы начнем с последнего аргумента, который сообщает ядру, сколько следует ждать, пока один из заданных дескрипторов не будет готов. Структура

timeval
задает число секунд и микросекунд:

struct timeval {

 long tv_sec;  /* секунды */

 long tv_usec; /* микросекунды */

};

С помощью этого аргумента можно реализовать три сценария:

1. Ждать вечно: завершать работу, только когда один из заданных дескрипторов готов для ввода-вывода. Для этого нужно определить аргумент

timeout
как пустой указатель.

2. Ждать в течение определенного времени: завершение будет происходить, когда один из заданных дескрипторов готов для ввода-вывода, но период ожидания ограничивается количеством секунд и микросекунд, заданным в структуре

timeval
, на которую указывает аргумент
timeout
.

3. Не ждать вообще: завершение происходит сразу же после проверки дескрипторов. Это называется опросом (polling). Аргумент

timeout
должен указывать на структуру
timeval
, а значение таймера (число секунд и микросекунд, заданных этой структурой) должно быть нулевым.

Ожидание в первых двух случаях обычно прерывается, когда процесс перехватывает сигнал и возвращается из обработчика сигнала.

ПРИМЕЧАНИЕ

Ядра реализаций, происходящих от Беркли, никогда автоматически не перезапускают функцию select [128, с. 527], в то время как ядра SVR4 перезапускают, если задан флаг SA_RESTART при установке обработчика сигнала. Это значит, что в целях переносимости мы должны быть готовы к тому, что функция select возвратит ошибку EINTR, если мы перехватываем сигналы.

Хотя структура

timeval
позволяет нам задавать значение с точностью до микросекунд, реальная точность, поддерживаемая ядром, часто значительно ниже. Например, многие ядра Unix округляют значение тайм-аута до числа, кратного 10 мс. Присутствует также и некоторая скрытая задержка: между истечением времени таймера и моментом, когда ядро запустит данный процесс, проходит некоторое время.

ПРИМЕЧАНИЕ

В некоторых системах при задании поля tv_sec более 100 млн с функция select завершается с кодом ошибки EINVAL Это, конечно, достаточно большое число (более трех лет), но факт остается фактом: структура timeval может содержать значения, не поддерживаемые функцией select.

Спецификатор

const
аргумента
timeout
означает, что данный аргумент не изменяется функцией
select
при ее возвращении. Например, если мы зададим предел времени, равный 10 с, и функция
select
возвратит управление до истечения этого времени с одним или несколькими готовыми дескрипторами или ошибкой
EINTR
, то структура
timeval
не изменится, то есть при завершении функции значение тайм-аута не станет равно числу секунд, оставшихся от исходных 10. Чтобы узнать количество неизрасходованных секунд, следует определить системное время до вызова функции
select
, а когда она завершится, определить его еще раз и вычесть первое значение из второго. Устойчивая программа должна учитывать тот факт, что системное время может периодически корректироваться администратором или демоном типа
ntpd
.

ПРИМЕЧАНИЕ

В современных системах Linux структура timeval изменяема. Следовательно, в целях переносимости будем считать, что структура timeval по возвращении становится неопределенной, и будем инициализировать ее перед каждым вызовом функции select. В POSIX указывается спецификатор const.

Три средних аргумента,

readset
,
writeset
и
exceptset
, определяют дескрипторы, которые ядро должно проверить на возможность чтения и записи и на наличие исключений (exceptions). В настоящее время поддерживается только два исключения:

1. На сокет приходят внеполосные данные. Более подробно мы опишем этот случай в главе 24.

2. Присутствие информации об управлении состоянием (control status information), которая должна быть считана с управляющего (master side) псевдотерминала, помещенного в режим пакетной обработки. Псевдотерминалы в данном томе не рассматриваются.

Проблема в том, как задать одно или несколько значений дескрипторов для каждого из трех аргументов. Функция

select
использует наборы дескрипторов, обычно это массив целых чисел, где каждый бит в каждом целом числе соответствует дескриптору. Например, при использовании 32-разрядных целых чисел первый элемент массива (целое число) соответствует дескрипторам от 0 до 31, второй элемент — дескрипторам от 32 до 63, и т.д. Детали реализации не влияют на приложение и скрыты в типе данных
fd_set
и следующих четырех макросах:

void FD_ZERO(fd_set *fdset); /* сбрасываем все биты в fdset */

void FD_SET(int fd, fd_set *fdset); /* устанавливаем бит для fd в fdset */

void FD_CLR(int fd, fd_set *fdset); /* сбрасываем бит для fd в fdset */

int FD_ISSET(int fd, fd_set *fdset); /* установлен ли бит для fd в fdset? */

Мы размещаем в памяти набор дескрипторов типа

fd_set
, с помощью этих макросов устанавливаем и проверяем биты в наборе, а также можем присвоить его (как значение) другому набору дескрипторов с помощью оператора присваивания языка С.

ПРИМЕЧАНИЕ

Описываемый нами массив целых чисел, использующий по одному биту для каждого дескриптора, — это только один из возможных способов реализации функции select. Тем не менее является обычной практикой ссылаться на отдельные дескрипторы в наборе дескрипторов как на биты, например так: «установить бит для прослушиваемого дескриптора в наборе для чтения».

В разделе 6.10 мы увидим, что функция poll использует совершенно другое представление: массив структур переменной длины, по одной структуре для каждого дескриптора.

Например, чтобы определить переменную типа

fd_set
и затем установить биты для дескрипторов 1, 4 и 5, мы пишем:

fd_set rset;


FD_ZERO(&rset); /* инициализируем набор все биты сброшены */

FD_SET(1, &rset); /* устанавливаем бит для fd 1 */

FD_SET(4, &rset); /* устанавливаем бит для fd 4 */

FD_SET(5, &rset); /* устанавливаем бит для fd 5 */

Важно инициализировать набор, так как если набор будет создан в виде автоматической переменной и не проинициализировав, результат может оказаться непредсказуемым.

Любой из трех средних аргументов функции

select
readset
,
writeset
или
exceptset
 — может быть задан как пустой указатель, если нас не интересует определяемое им условие. На самом деле, если все три указателя пустые, мы просто получаем таймер большей точности, чем обычная функция Unix
sleep
(позволяющая задавать время с точностью до секунды). Функция
poll
обеспечивает аналогичную функциональность. На рис. С.9 и С.10 [110] показана функция
sleep_us
, реализованная с помощью функций
select
и
poll
, которая позволяет устанавливать время ожидания с точностью до микросекунд.

Аргумент

maxfdp1
задает число проверяемых дескрипторов. Его значение на единицу больше максимального номера проверяемого дескриптора (поэтому мы назвали его
maxfdp1
). Проверяются дескрипторы 0, 1, 2 и далее до
maxfdp1
- 1 включительно.

Константа

FD_SETSIZE
, определяемая при подключении заголовочного файла
, является максимальным числом дескрипторов для типа данных
fd_set
. Ее значение часто равно 1024, но такое количество дескрипторов используется очень немногими программами. Аргумент
maxfdp1
заставляет нас вычислять наибольший интересующий нас дескриптор и затем сообщать ядру его значение. Например, в предыдущем коде, который включает дескрипторы 1, 4 и 5, значение аргумента
maxfdp1
равно 6. Причина, по которой это 6, а не 5, в том, что мы задаем количество дескрипторов, а не наибольшее значение, а нумерация дескрипторов начинается с нуля.

ПРИМЕЧАНИЕ

Зачем нужно было включать этот аргумент и вычислять его значение? Причина в том, что он повышает эффективность работы ядра. Хотя каждый набор типа fd_set может содержать множество дескрипторов (обычно до 1024), реальное количество дескрипторов, используемое типичным процессом, значительно меньше. Эффективность возрастает за счет того, что не копируются ненужные части набора дескрипторов между ядром и процессом и не требуется проверять биты, которые всегда являются нулевыми (см. раздел 16.13 [128]).

Функция

select
изменяет наборы дескрипторов, на которые указывают аргументы
readset
,
writeset
и
exceptset
. Эти три аргумента являются аргументами типа «значение-результат». Когда мы вызываем функцию, мы указываем интересующие нас дескрипторы, а по ее завершении результат показывает нам, какие дескрипторы готовы. Проверить определенный дескриптор из структуры
fd_set
после завершения вызова можно с помощью макроса
FD_ISSET
. Для дескриптора, не готового для чтения или записи, соответствующий бит в наборе дескрипторов будет сброшен. Поэтому мы устанавливаем все интересующие нас биты во всех наборах дескрипторов каждый раз, когда вызываем функцию
select
.

ПРИМЕЧАНИЕ

Две наиболее общих ошибки программирования при использовании функции select — это забыть добавить единицу к наибольшему номеру дескриптора и забыть, что наборы дескрипторов имеют тип «значение-результат». Вторая ошибка приводит к тому, что функция select вызывается с нулевым битом в наборе дескрипторов, когда мы думаем, что он установлен в единицу.

Возвращаемое этой функцией значение указывает общее число готовых дескрипторов во всех наборах дескрипторов. Если значение таймера истекает до того, как какой-нибудь из дескрипторов оказывается готов, возвращается нулевое значение. Возвращаемое значение -1 указывает на ошибку (которая может произойти, если, например, выполнение функции прервано перехваченным сигналом).

ПРИМЕЧАНИЕ

В ранних реализациях SVR4 функция select содержала ошибку: если один и тот же бит находился в нескольких наборах дескрипторов — допустим, дескриптор был готов и для чтения, и для записи, — он учитывался только один раз. В современных реализациях эта ошибка исправлена.

При каких условиях дескриптор становится готовым?

Мы говорили об ожидании готовности дескриптора для ввода-вывода (чтения или записи) или возникновения исключительной ситуации, требующей обработки (внеполосные данные). В то время как готовность к чтению и записи очевидна для файловых дескрипторов, в случае дескрипторов сокетов следует более внимательно изучить те условия, при которых функция

select
сообщает, что сокет готов (см. рис. 16.52 [128]).

1. Сокет готов для чтения, если выполнено хотя бы одно из следующих условий:

 1) число байтов данных в приемном буфере сокета больше или равно текущему значению минимального количества данных (low water-mark) для приемного буфера сокета. Операция считывания данных из сокета не блокируется и возвратит значение, большее нуля (то есть данные, готовые для чтения). Мы можем задать значение минимального количества данных (low-water mark) с помощью параметра сокета

SO_RCVLOWAT
. По умолчанию для сокетов TCP и UDP это значение равно 1;

 2) на противоположном конце соединение закрывается (нами получен сегмент FIN). Операция считывания данных из сокета не блокируется и возвратит нуль (то есть признак конца файла);

 3) сокет является прослушиваемым, и число установленных соединений ненулевое. Функция

accept
на прослушиваемом сокете в таком случае обычно не блокируется, хотя в разделе 16.6 мы описываем ситуацию, в которой функция accept может заблокироваться несмотря на наличие установленных соединений;

 4) ошибка сокета, ожидающая обработки. Операция чтения на сокете не блокируется и возвратит ошибку (-1) со значением переменной

errno
, указывающим на конкретное условие ошибки. Эти ошибки, ожидающие обработки, можно также получить, вызвав функцию
getsockopt
с параметром
SO_ERROR,
после чего состояние ошибки будет сброшено.

2. Сокет готов для записи, если выполнено одно из следующих условий:

 1) количество байтов доступного пространства в буфере отправки сокета больше или равно текущему значению минимального количества данных для буфера отправки сокета и либо сокет является присоединенным, либо сокету не требуется соединения (например, сокет UDP). Это значит, что если мы отключим блокировку для сокета (см. главу 16), операция записи не заблокирует процесс и возвратит положительное значение (например, число байтов, принятых транспортным уровнем). Устанавливать минимальное количество данных мы можем с помощью параметра сокета

SO_SNDLOWAT
. По умолчанию это значение равно 2048 для сокетов TCP и UDP;

 2) получатель, которому отправляются данные, закрывает соединение. Операция записи в сокет сгенерирует сигнал

SIGPIPE
(см. раздел 5.12);

 3) ошибка сокета, ожидающая обработки. Операция записи в сокет не блокируется и возвратит ошибку (-1) со значением переменной

errno
, указывающей на конкретное условие ошибки. Эти ошибки, ожидающие обработки, можно также получить и сбросить, вызвав функцию
getsockopt
с параметром сокета
SO_ERROR
.

3. Исключительная ситуация, требующая обработки, может возникнуть на сокете в том случае, если приняты внеполосные данные либо если отметка вне- полосных данных в принимаемом потоке еще не достигнута. (Внеполосные данные описываются в главе 24.)

ПРИМЕЧАНИЕ

Наши определения «готов для чтения» и «готов для записи» взяты непосредственно из макроопределений ядра soreadable и sowritable (которые описываются в [128, с. 530-531]). Аналогично, наше определение «исключительной ситуации» взято из функции soo_select, которая описана там же.

Обратите внимание, что когда происходит ошибка на сокете, функция

select
отмечает его готовым как для чтения, так и для записи.

Значения минимального количества данных (low-water mark) для приема и отправки позволяют приложению контролировать, сколько данных должно быть доступно для чтения или сколько места должно быть доступно для записи перед тем, как функция

select
сообщит, что сокет готов для чтения или записи. Например, если мы знаем, что наше приложение не может сделать ничего полезного, пока не будет получено как минимум 64 байт данных, мы можем установить значение минимального количества данных равным 64, чтобы функция
select
не вывела нас из состояния ожидания, если для чтения готово менее 64 байт.

Пока значение минимального количества данных для отправки в сокете UDP меньше, чем буфер отправки сокета (а такое отношение между ними всегда устанавливается по умолчанию), сокет UDP всегда готов для записи, поскольку соединения не требуется.

В табл. 6.1 суммируются описанные выше условия, при которых сокет становится готовым для вызова функции

select
.


Таблица 6.1. Условия, при которых функция select сообщает, что сокет готов для чтения или для записи либо, что необходима обработка исключительной ситуации

УсловиеСокет готов для чтенияСокет готов для записиИсключительная ситуация
Данные для чтения
Считывающая половина соединения закрыта
Для прослушиваемого сокета готово новое соединение
Пространство, доступное для записи
Записывающая половина соединения закрыта
Ошибка, ожидающая обработки
Внеполосные данные TCP

Максимальное число дескрипторов для функции select

Ранее мы сказали, что большинство приложений не используют много дескрипторов. Например, редко можно найти приложение, использующее сотни дескрипторов. Но такие приложения существуют, и часто они используют функцию

select
для мультиплексирования дескрипторов. Когда функция
select
была создана, операционные системы обычно имели ограничение на максимальное число дескрипторов для каждого процесса (этот предел в реализации 4.2BSD составлял 31), и функция
select
просто использовала тот же предел. Но современные версии Unix допускают неограниченное число дескрипторов для каждого процесса (часто оно ограничивается только количеством памяти и административными правилами), поэтому возникает вопрос: как же теперь работает функция
select
?

Многие реализации имеют объявления, аналогичные приведенному ниже, которое взято из заголовочного файла 4.4BSD

:

/*

  Значение FD_SETSIZE может быть определено пользователем,

  но заданное здесь по умолчанию

  является достаточным в большинстве случаев.

*/


#ifndef FD_SETSIZE

#define FD_SETSIZE 256

#endif

Исходя из этого комментария, можно подумать, что если перед подключением этого заголовочного файла присвоить

FD_SETSIZE
значение, превышающее 256, то увеличится размер наборов дескрипторов, используемых функцией
select
. К сожалению, обычно это не действует.

ПРИМЕЧАНИЕ

Чтобы понять, в чем дело, обратите внимание, что на рис. 16.53 [128] объявляются три набора дескрипторов внутри ядра, а в качестве верхнего предела используется определенное в ядре значение FD_SETSIZE. Единственный способ увеличить размер наборов дескрипторов — это увеличить значение FD_SETSIZE и затем перекомпилировать ядро. Изменения значения без перекомпиляции ядра недостаточно.

Некоторые производители изменяют свои реализации функции

select
, с тем чтобы позволить процессу задавать значение
FD_SETSIZE
, превышающее значение по умолчанию. BSD/OS также изменила реализацию ядра, чтобы допустить большие наборы дескрипторов, кроме того, в ней добавлено четыре новых макроопределения
FD_xxx
для динамического размещения больших наборов дескрипторов в памяти и для работы с ними. Однако с точки зрения переносимости не стоит злоупотреблять использованием больших наборов дескрипторов.

6.4. Функция str_cli (продолжение)

Теперь мы можем переписать нашу функцию

str_cli
, представленную в разделе 5.5 (на этот раз используя функцию
select
), таким образом, чтобы мы получали уведомление, как только завершится процесс сервера. Проблема с предыдущей версией состояла в том, что процесс мог оказаться заблокированным в вызове функции
fgets
, когда что-то происходило на сокете. Наша новая версия этой функции вместо этого блокируется в вызове функции
select
, ожидая готовности для чтения либо стандартного потока ввода, либо сокета. На рис. 6.7 показаны различные условия, обрабатываемые с помощью вызова функции
select
.

Рис. 6.7. Условия, обрабатываемые функцией select в вызове функции str_cli

Сокет обрабатывает три условия:

1. Если протокол TCP собеседника отправляет данные, сокет становится готовым для чтения, и функция

read
возвращает положительное значение (то есть число байтов данных).

2. Если протокол TCP собеседника отправляет сегмент FIN (процесс завершается), сокет становится готовым для чтения, и функция

read
возвращает нуль (признак конца файла).

3. Если TCP собеседника отправляет RST (узел вышел из строя и перезагрузился), сокет становится готовым для чтения, и функция

read
возвращает -1, а переменная
errno
содержит код соответствующей ошибки.

В листинге 6.1[1] представлен исходный код этой версии функции.

Листинг 6.1. Реализация функции str_cli с использованием функции select (усовершенствованный вариант находится в листинге 6.2)

//select/strcliselect01.c

 1 #include "unp.h"


 2 void

 3 str_cli(FILE *fp, int sockfd)

 4 {

 5  int maxfdp1;

 6  fd_set rset;

 7  char sendline[MAXLINE], recvline[MAXLINE];


 8  FD_ZERO(&rset);

 9  for (;;) {

10   FD_SET(fileno(fp), &rset);

11   FD_SET(sockfd, &rset);

12   maxfdp1 = max(fileno(fp), sockfd) + 1;

13   Select(maxfdp1, &rset, NULL, NULL, NULL);


14   if (FD_ISSET(sockfd, &rset)) { /* сокет готов для чтения */

15    if (Readline(sockfd, recvline, MAXLINE) == 0)

16     err_quit("str_cli: server terminated prematurely");

17    Fputs(recvline, stdout);

18   }

19   if (FD_ISSET(fileno(fp), &rset)) { /* входное устройство готово для

                                           чтения */

20    if (Fgets(sendline, MAXLINE, fp) == NULL)

21     return; /* все сделано */

22    Writen(sockfd, sendline, strlen(sendline));

23   }

24  }

25 }

Вызов функции select

8-13
 Нам нужен только один набор дескрипторов — для проверки готовности сокета для чтения. Этот набор дескрипторов инициализируется макросом
FD_ZERO
, после чего с помощью макроса
FD_SET
устанавливаются два бита: бит, соответствующий указателю файла
fp
стандартного потока ввода-вывода, и бит, соответствующий дескриптору сокета
sockfd
. Функция
fileno
преобразует указатель файла стандартного потока ввода-вывода в соответствующий ему дескриптор. Функция
select
(а также
poll
) работает только с дескрипторами.

Функция

select
вызывается после определения максимального из двух дескрипторов. В этом вызове указатель на набор дескрипторов для записи и указатель на набор дескрипторов с исключениями являются пустыми. Последний аргумент (ограничение по времени) также является пустым указателем, поскольку мы хотим, чтобы процесс был блокирован, пока не будут готовы данные для чтения.

Обработка сокета, готового для чтения

14-18
 Если по завершении функции
select
сокет готов для чтения, отраженная строка считывается функцией
readline
и выводится функцией
fputs
.

Обработка ввода, допускающего возможность чтения

19-23
 Если стандартный поток ввода готов для чтения, строка считывается функцией
fgets
и записывается в сокет с помощью функции
writen
.

Обратите внимание, что используются те же четыре функции ввода-вывода, что и в листинге 5.4:

fgets
,
writen
,
readline
и
fputs
, но порядок их следования внутри функции
str_cli
изменился. Раньше выполнение функции
str_cli
определялось функцией
fgets
, а теперь ее место заняла
select
. С помощью всего нескольких дополнительных строк кода (сравните листинги 6.1 и 5.4) мы значительно увеличили устойчивость клиента.

6.5. Пакетный ввод

К сожалению, наша функция

str_cli
все еще не вполне корректна. Сначала вернемся к ее исходной версии, приведенной в листинге 5.4. Эта функция работает в режиме остановки и ожидания (stop-and-wait mode), что удобно для интерактивного использования: функция отправляет строку серверу и затем ждет его ответа. Время ожидания складывается из одного периода обращения (RTT) и времени обработки сервером (которое близко к нулю в случае простого эхо-сервера). Следовательно, мы можем предположить, сколько времени займет отражение данного числа строк, если мы знаем время обращения (RTT) между клиентом и сервером.

Измерить RTT позволяет утилита

ping
. Если мы измерим с ее помощью время обращения к
connix.com
с нашего узла
solaris
, то средний период RTT после 30 измерений будет равен 175 мс. В [111, с. 89] показано, что это справедливо для дейтаграммы IP длиной 84 байт. Если мы возьмем первые 2000 строк файла
termcap
Solaris 2.5, то итоговый размер файла будет равен 98 349 байт, то есть в среднем 49 байт на строку. Если мы добавим размеры заголовка IP (20 байт) и заголовка TCP (20 байт), то средний сегмент TCP будет составлять 89 байт, почти как размер пакета утилиты
ping
. Следовательно, мы можем предположить, что общее время составит около 350 с для 2000 строк (2000×0,175 с). Если мы запустим наш эхо-клиент TCP из главы 5, действительное время получится около 354 с, что очень близко к нашей оценке.

Если считать, что сеть между клиентом и сервером является двусторонним каналом, когда запросы идут от клиента серверу, а ответы в обратном направлении, то получится изображенный на рис. 6.8 режим остановки и ожидания.

Рис. 6.8. Временная диаграмма режима остановки и ожидания: интерактивный ввод

Запрос отправляется клиентом в нулевой момент времени, и мы предполагаем, что время обращения RTT равно 8 условным единицам. Ответ, отправленный в момент времени 4, доходит до клиента в момент времени 7. Мы также считаем, что время обработки сервером нулевое и что размер запроса равен размеру ответа. Мы показываем только пакеты данных между клиентом и сервером, игнорируя подтверждения TCP, которые также передаются по сети.

Но поскольку между отправкой пакета и его приходом на другой конец канала существует задержка и канал является двусторонним, в этом примере мы используем только восьмую часть вместимости канала. Режим остановки и ожидания удобен для интерактивного ввода, но поскольку наш клиент считывает данные из стандартного потока ввода и записывает в стандартный поток вывода, а перенаправление ввода и вывода выполнить в интерпретаторе команд крайне просто, мы легко можем запустить наш клиент в пакетном режиме. Однако когда мы перенаправляем ввод и вывод, получающийся файл вывода всегда меньше файла ввода (а для эхо-сервера требуется их идентичность).

Чтобы понять происходящее, обратите внимание, что в пакетном режиме мы отправляем запросы так быстро, как их может принять сеть. Сервер обрабатывает их и отправляет обратно ответы с той же скоростью. Это приводит к тому, что в момент времени 7 канал целиком заполнен, как показано на рис. 6.9.

Рис. 6.9. Заполнение канала между клиентом и сервером: пакетный режим

Предполагается, что после отправки первого запроса мы немедленно посылаем другой запрос и т.д. Также предполагается, что мы можем отправлять запросы с той скоростью, с какой сеть способна их принимать, и обрабатывать ответы так быстро, как сеть их поставляет.

ПРИМЕЧАНИЕ

Существуют различные нюансы, имеющие отношение к передаче большого количества данных TCP (bulk data flow), которые мы здесь игнорируем. К ним относятся алгоритм медленного запуска (slow start algorithm), ограничивающий скорость, с которой данные отправляются на новое или незанятое соединение, и возвращаемые сегменты ACK. Все эти вопросы рассматриваются в главе 20 [111].

Чтобы увидеть, в чем заключается проблема с нашей функцией

str_cli
, представленной в листинге 6.1, будем считать, что файл ввода содержит только девять строк. Последняя строка отправляется в момент времени 8, как показано на рис. 6.9. Но мы не можем закрыть соединение после записи этого запроса, поскольку в канале еще есть другие запросы и ответы. Причина возникновения проблемы кроется в нашем способе обработки конца файла при вводе, когда процесс возвращается в функцию
main
, которая затем завершается. Но в пакетном режиме конец файла при вводе не означает, что мы закончили читать из сокета — в нем могут оставаться запросы к серверу или ответы от сервера.

Нам нужен способ закрыть одну половину соединения TCP. Другими словами, мы хотим отправить серверу сегмент FIN, тем самым сообщая ему, что закончили отправку данных, но оставляем дескриптор сокета открытым для чтения. Это делается с помощью функции

shutdown
, которая описывается в следующем разделе.

Вообще говоря, буферизация ввода-вывода для повышения производительности приводит к усложнению сетевых приложений (от чего пострадала и программа в листинге 6.1). Рассмотрим пример, в котором из стандартного потока ввода считывается несколько строк текста. Функция

select
передаст управление строке 20, в которой функция
fgets
считает доступные данные в буфер библиотеки
stdio
. Однако эта функция возвратит приложению только одну строку, а все остальные так и останутся в буфере. Считанная строка будет отправлена серверу, после чего будет снова вызвана функция
select
, которая будет ждать появления новых данных в стандартном потоке ввода несмотря на наличие еще не обработанных строк в буфере
stdio
. Причина в том, что
select
ничего не знает о буферах
stdio
и сообщает о доступности дескриптора для чтения с точки зрения системного вызова
read
, а не библиотечного вызова
fgets
. По этой причине использование
fgets
и
select
в одной программе считается опасным и требует особой осторожности.

Та же проблема связана с вызовом

readline
в листинге 6.1. Теперь данные скрываются от функции
select
уже не в буфере
stdio
, а в буфере
readline
. Вспомните, что в разделе 3.9 мы создали функцию, проверявшую состояние буфера
readline
. Мы могли бы воспользоваться ею перед вызовом
select
, чтобы проверить, нет ли в буфере
readline
данных, дожидающихся обработки. Наша программа усложнится еще больше, если мы допустим, что буфер
readline
может содержать лишь часть строки (то есть нам придется дожидаться считывания этой строки целиком).

Проблемы буферизации мы постараемся решить в усовершенствованной версии

str_cli
в разделе 6.7.

6.6. Функция shutdown

Обычный способ завершить сетевое соединение — вызвать функцию

close
. Но у функции
close
есть два ограничения, которых лишена функция
shutdown
:

1. Функция close последовательно уменьшает счетчик ссылок дескриптора и закрывает сокет, только если счетчик доходит до нуля. Мы рассматривали это в разделе 4.8. Используя функцию

shutdown
, мы можем инициировать обычную последовательность завершения соединения TCP (четыре сегмента, начинающихся с FIN, на рис. 2.5) независимо от значения счетчика ссылок.

2. Функция

close
завершает оба направления передачи данных — и чтение, и запись. Поскольку соединение TCP является двусторонним, возможны ситуации, когда нам понадобится сообщить другому концу соединения, что мы закончили отправку, даже если на том конце соединения имеются данные для отправки нам. Это случай, рассмотренный в предыдущем разделе при описании работы нашей функции
str_cli
в пакетном режиме. На рис. 6.10 показаны типичные вызовы функций в этом сценарии.

Рис. 6.10. Вызов функции shutdown для закрытия половины соединения TCP

#include 


int shutdown(int sockfd, int howto);

Возвращает: 0 в случае успешного выполнения, -1 в случае ошибки

Действие функции зависит от значения аргумента

howto
.

SHUT_RD
. Закрывается считывающая половина соединения: из сокета больше нельзя считывать данные, и все данные, находящиеся в данный момент в буфере приема сокета, сбрасываются. Процесс больше не может выполнять функции чтения из сокета. Любые данные для сокета TCP, полученные после вызова функции
shutdown
с этим аргументом, подтверждаются и «молча» игнорируются.

ПРИМЕЧАНИЕ

По умолчанию все, что записывается в маршрутизирующий сокет (см. главу 17), возвращается как возможный ввод на все маршрутизирующие сокеты узла. Некоторые программы вызывают функцию shutdown со вторым аргументом SHUT_RD, чтобы предотвратить получение подобной копии. Другой способ избежать копирования — отключить параметр сокета SO_USELOOPBACK.

SHUT_WR
. Закрывается записывающая половина соединения. В случае TCP это называется половинным закрытием (см. раздел 18.5 [111]). Все данные, находящиеся в данный момент в буфере отправки сокета, будут отправлены, а затем будет выполнена обычная последовательность действий по завершению соединения TCP. Как мы отмечали ранее, закрытие записывающей половины соединения выполняется независимо от того, является ли значение в счетчике ссылок дескриптора сокета положительным или нет. Процесс теряет возможность записывать данные в сокет.

SHUT_RDWR
. Закрываются и читающая, и записывающая половины соединения. Это эквивалентно двум вызовам функции
shutdown
: сначала с аргументом
SHUT_RD
, затем — с аргументом
SHUT_WR
.

В табл. 7.4 приведены все возможные сценарии, доступные процессу при вызове функций

shutdown
и
close
. Действие функции close зависит от значения параметра сокета
SO_LINGER
.

ПРИМЕЧАНИЕ

Три константы SHUT_xxx определяются в спецификации POSIX. Типичные значения аргумента howto, с которыми вы встретитесь, — это 0 (закрытие читающей половины), 1 (закрытие записывающей половины) и 2 (закрытие обеих половин).

6.7. Функция str_cli (еще раз)

В листинге 6.2 представлена наша обновленная (и корректная) функция

str_cli
. В этой версии используются функции
select
и
shutdown
. Первая уведомляет нас о том, когда сервер закрывает свой конец соединения, а вторая позволяет корректно обрабатывать пакетный ввод. Эта версия избавлена от ориентации на строки. Вместо этого она работает с буферами, что позволяет полностью избавиться от проблем, описанных в конце раздела 6.5.

Листинг 6.2. функция str_cli, использующая функцию select, которая корректно обрабатывает конец файла

//select/strcliselect02.c

 1 #include "unp.h"


 2 void

 3 str_cli(FILE *fp, int sockfd)

 4 {

 5  int maxfdp1, stdineof;

 6  fd_set rset;

 7  char buf[MAXLINE];

 8  int n;


 9  stdineof = 0;

10  FD_ZERO(&rset);

11  for (;;) {

12   if (stdineof == 0)

13    FD_SET(fileno(fp), &rset);

14   FD_SET(sockfd, &rset);

15   maxfdp1 = max(fileno(fp), sockfd) + 1;

16   Select(maxfdp1, &rset, NULL, NULL, NULL);


17   if (FD_ISSET(sockfd, &rset)) { /* сокет готов для чтения */

18    if ((n = Read(sockfd, buf, MAXLINE)) == 0) {

19     if (stdineof == 1)

20      return; /* нормальное завершение */

21     else

22      err_quit("str_cli: server terminated prematurely");

23    }


24    Write(fileno(stdout), buf, n);

25   }


26   if (FD_ISSET(fileno(fp), &rset)) { /* есть данные на входе */

27    if ((n = Read(fileno(fp), buf, MAXLINE)) == 0) {

28     stdineof = 1;

29     Shutdown(sockfd, SHUT_WR); /* отправка сегмента FIN */

30     FD_CLR(fileno(fp), &rset);

31     continue;

32    }


33    Writen(sockfd, buf, n);

34   }

35  }

36 }

5-8
stdineof
— это новый флаг, инициализируемый нулем. Пока этот флаг равен нулю, мы будем проверять готовность стандартного потока ввода к чтению с помощью функции
select
.

16-24
 Если мы считываем на сокете признак конца файла, когда нам уже встретился ранее признак конца файла в стандартном потоке ввода, это является нормальным завершением и функция возвращает управление. Но если конец файла в стандартном потоке ввода еще не встречался, это означает, что процесс сервера завершился преждевременно. В новой версии мы вызываем функции
read
и
write
и работаем с буферами, а не со строками, благодаря чему функция
select
действует именно так, как мы рассчитывали.

25-33
 Когда нам встречается признак конца файла на стандартном устройстве ввода, наш новый флаг
stdineof
устанавливается в единицу и мы вызываем функцию
shutdown
со вторым аргументом
SHUT_WR
для отправки сегмента FIN.

Если мы измерим время работы нашего клиента TCP, использующего функцию

str_cli
, показанную в листинге 6.2, с тем же файлом из 2000 строк, это время составит 12,3 с, что почти в 30 раз быстрее, чем при использовании версии этой функции, работающей в режиме остановки и ожидания.

Мы еще не завершили написание нашей функции

str_cli
: в разделе 15.2 мы разработаем ее версию с использованием неблокируемого ввода-вывода, а в разделе 23.3 — версию, работающую с программными потоками.

6.8. Эхо-сервер TCP (продолжение)

Вернемся к нашему эхо-серверу TCP из разделов 5.2 и 5.3. Перепишем сервер как одиночный процесс, который будет использовать функцию

select
для обработки любого числа клиентов, вместо того чтобы порождать с помощью функции
fork
по одному дочернему процессу для каждого клиента. Перед тем как представить этот код, взглянем на структуры данных, используемые для отслеживания клиентов. На рис. 6.11 показано состояние сервера до того, как первый клиент установил соединение.

Рис. 6.11. Сервер TCP до того, как первый клиент установил соединение

У сервера имеется одиночный прослушиваемый дескриптор, показанный на рисунке точкой.

Сервер обслуживает только набор дескрипторов для чтения, который мы показываем на рис. 6.12. Предполагается, что сервер запускается в приоритетном (foreground) режиме, а дескрипторы 0, 1 и 2 соответствуют стандартным потокам ввода, вывода и ошибок. Следовательно, первым доступным для прослушиваемого сокета дескриптором является дескриптор 3. Массив целых чисел

client
содержит дескрипторы присоединенного сокета для каждого клиента. Все элементы этого массива инициализированы значением -1.

Рис. 6.12. Структуры данных для сервера TCP с одним прослушиваемым сокетом

Единственная ненулевая запись в наборе дескрипторов — это запись для прослушиваемого сокета, и поэтому первый аргумент функции

select
будет равен 4.

Когда первый клиент устанавливает соединение с нашим сервером, прослушиваемый дескриптор становится доступным для чтения и сервер вызывает функцию

accept
. Новый присоединенный дескриптор, возвращаемый функцией
accept
, будет иметь номер 4, если выполняются приведенные выше предположения. На рис. 6.13 показано соединение клиента с сервером.

Рис. 6.13. Сервер TCP после того как первый клиент устанавливает соединение

Теперь наш сервер должен запомнить новый присоединенный сокет в своем массиве

client
, и присоединенный сокет должен быть добавлен в набор дескрипторов. Изменившиеся структуры данных показаны на рис. 6.14.

Рис. 6.14. Структуры данных после того как установлено соединение с первым клиентом

Через некоторое время второй клиент устанавливает соединение, и мы получаем сценарий, показанный на рис. 6.15.

Рис. 6.15. Сервер TCP после того как установлено соединение со вторым клиентом

Новый присоединенный сокет (который имеет номер 5) должен быть размещен в памяти, в результате чего структуры данных меняются так, как показано на рис. 6.16.

Рис. 6.16. Структуры данных после того как установлено соединение со вторым клиентом

Далее мы предположим, что первый клиент завершает свое соединение. TCP-клиент отправляет сегмент FIN, превращая тем самым дескриптор номер 4 на стороне сервера в готовый для чтения. Когда наш сервер считывает этот присоединенный сокет, функция

readline
возвращает нуль. Затем мы закрываем сокет, и соответственно изменяются наши структуры данных. Значение
client[0]
устанавливается в -1, а дескриптор 4 в наборе дескрипторов устанавливается в нуль. Это показано на рис. 6.17. Обратите внимание, что значение переменной
maxfd
не изменяется.

Рис. 6.17. Структуры данных после того как первый клиент разрывает соединение

Итак, по мере того как приходят клиенты, мы записываем дескриптор их присоединенного сокета в первый свободный элемент массива

client
(то есть в первый элемент со значением -1). Следует также добавить присоединенный сокет в набор дескрипторов для чтения. Переменная
maxi
— это наибольший используемый в данный момент индекс в массиве
client
, а переменная
maxfd
(плюс один) — это текущее значение первого аргумента функции select. Единственным ограничением на количество обслуживаемых сервером клиентов является минимальное из двух значений:
FD_SETSIZE
и максимального числа дескрипторов, которое допускается для данного процесса ядром (о чем мы говорили в конце раздела 6.3).

В листинге 6.3 показана первая половина этой версии сервера.

Листинг 6.3. Сервер TCP, использующий одиночный процесс и функцию select: инициализация

//tcpcliserv/tcpservselect01.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int i, maxi, maxfd, listenfd, connfd, sockfd;

 6  int nready, client[FD_SETSIZE],

 7  ssize_t n;

 8  fd_set rset, allset;

 9  char buf[MAXLINE];

10  socklen_t clilen;

11  struct sockaddr_in cliaddr, servaddr;


12  listenfd = Socket(AF_INET, SOCK_STREAM, 0);


13  bzero(&servaddr, sizeof(servaddr));

14  servaddr.sin_family = AF_INET;

15  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

16  servaddr.sin_port = htons(SERV_PORT);


17  Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));


18  Listen(listenfd, LISTENQ);


19  maxfd = listenfd; /* инициализация */

20  maxi = -1; /* индекс в массиве client[] */

21  for (i = 0; i < FD_SETSIZE; i++)

22   client[i] = -1; /* -1 означает свободный элемент */

23  FD_ZERO(&allset);

24  FD_SET(listenfd, &allset);

Создание прослушиваемого сокета и инициализация функции select

12-24
 Этапы создания прослушиваемого сокета те же, что и раньше: вызов функций
socket
,
bind
и
listen
. Мы инициализируем структуры данных при том условии, что единственный дескриптор, который мы с помощью функции
select
выберем, изначально является прослушиваемым сокетом.

Вторая половина функции

main
показана в листинге 6.4.

Листинг 6.4. Сервер TCP, использующей одиночный процесс и функцию select: цикл

//tcpcliserv/tcpservselect01.c

25  for (;;) {

26   rset = allset; /* присваивание значения структуре */

27   nready = Select(maxfd + 1, &rset, NULL, NULL, NULL);


28   if (FD_ISSET(listenfd, &rset)) { /* соединение с новым клиентом */

29    clilen = sizeof(cliaddr);

30    connfd = Accept(listenfd, (SA*)&cliaddr, &clilen);


31    for (i = 0; i < FD_SETSIZE; i++)

32     if (client[i] < 0) {

33      client[i] = connfd; /* сохраняем дескриптор */

34      break;

35     }

36    if (i == FD_SETSIZE)

37     err_quit("too many clients");


38    FD_SET(connfd, &allset); /* добавление нового дескриптора */

39    if (connfd > maxfd)

40     maxfd = connfd; /* для функции select */

41    if (i > maxi)

42     maxi = i; /* максимальный индекс в массиве clientf[] */


43    if (--nready <= 0)

44     continue; /* больше нет дескрипторов, готовых для чтения */

45   }

46   for (i = 0; i <= maxi; i++) { /* проверяем все клиенты на наличие

                                      данных */

47    if ((sockfd - client[i]) < 0)

48     continue;

49    if (FD_ISSET(sockfd, &rset)) {

50     if ((n = Read(sockfd, buf, MAXLINE)) == 0) {

51      /* соединение закрыто клиентом */

52      Close(sockfd);

53      FD_CLR(sockfd, &allset);

54      client[i] = -1;

55     } else

56      Writen(sockfd, line, n);


57     if (--nready <= 0)

58      break; /* больше нет дескрипторов, готовых для чтения */

59    }

60   }

61  }

62 }

Блокирование в функции select

26-27
 Функция
select
ждет, пока не будет установлено новое клиентское соединение или на существующем соединении не прибудут данные, сегмент FIN или сегмент RST.

Принятие новых соединений с помощью функции accept

28-45
 Если прослушиваемый сокет готов для чтения, новое соединение установлено. Мы вызываем функцию
accept
и соответствующим образом обновляем наши структуры данных. Для записи присоединенного сокета мы используем первый незадействованный элемент массива
client
. Число готовых дескрипторов уменьшается, и если оно равно нулю, мы можем не выполнять следующий цикл
for
. Это позволяет нам использовать значение, возвращаемое функцией
select
, чтобы избежать проверки не готовых дескрипторов.

Проверка существующих соединений

46-60
 Каждое существующее клиентское соединение проверяется на предмет того, содержится ли его дескриптор в наборе дескрипторов, возвращаемом функцией
select
. Если да, то из этого дескриптора считывается строка, присланная клиентом, и отражается обратно клиенту. Если клиент закрывает соединение, функция read возвращает нуль и мы обновляем структуры соответствующим образом.

Мы не уменьшаем значение переменной

maxi
, но могли бы проверять возможность сделать это каждый раз, когда клиент закрывает свое соединение.

Этот сервер сложнее, чем сервер, показанный в листингах 5.1 и 5.2, но он позволяет избежать затрат на создание нового процесса для каждого клиента, что является хорошим примером использования функции

select
. Тем не менее в разделе 15.6 мы опишем проблему, связанную с этим сервером, которая, однако, легко устраняется, если сделать прослушиваемый сокет неблокируемым, а затем проверить и проигнорировать несколько ошибок из функции
accept
.

Атака типа «отказ в обслуживании»

К сожалению, функционирование только что описанного сервера вызывает проблемы. Посмотрим, что произойдет, если некий клиент-злоумышленник соединится с сервером, отправит 1 байт данных (отличный от разделителя строк) и войдет в состояние ожидания. Сервер вызовет функцию

readline
, которая прочитает одиночный байт данных от клиента и заблокируется в следующем вызове функции
read
, ожидая следующих данных от клиента. Сервер блокируется (вернее, «подвешивается») этим клиентом и не может предоставить обслуживание никаким другим клиентам (ни новым клиентским соединениям, ни данным существующих клиентов), пока упомянутый клиент-злоумышленник не отправит символ перевода строки или не завершит свой процесс.

Дело в том, что обрабатывая множество клиентов, сервер никогда не должен блокироваться в вызове функции, относящейся к одному клиенту. В противном можно «подвесить» сервер, что приведет к отказу в обслуживании для всех остальных клиентов. Это называется атакой типа «отказ в обслуживании» (DoS attack — Denial of Service). Такая атака воздействует на сервер, делая невозможным обслуживание нормальных клиентов. Обезопасить себя от подобных атак позволяют следующие решения: использовать неблокируемый ввод-вывод (см. главу 16), предоставлять каждому клиенту обслуживание отдельным потоком (например, для каждого клиента порождать процесс или поток) или установить тайм-аут для ввода-вывода (см. раздел 14.2).

6.9. Функция pselect

Функция

pselect
была введена в POSIX и в настоящий момент поддерживается множеством версий Unix.

#include 

#include 

#include 


int pselect(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,

 const struct timespec *timeout, const sigset_t *sigmask);

Возвращает: количество готовых дескрипторов, 0 в случае тайм-аута, -1 в случае ошибки

Функция

pselect
имеет два отличия от обычной функции
select
:

1. Функция

pselect
использует структуру
timespec
, нововведение стандарта реального времени POSIX, вместо структуры
timeval
.

struct timespec {

 time_t tv_sec; /* секунды */

 long tv_nsec;  /* наносекунды */

};

Эти структуры отличаются вторыми элементами: элемент

tv_nsec
новой структуры задает наносекунды, в то время как элемент
tv_usec
прежней структуры задает микросекунды.

2. В функции

pselect
добавляется шестой аргумент — указатель на маску сигналов. Это позволяет программе отключить доставку ряда сигналов, проверить какие-либо глобальные переменные, установленные обработчиками этих отключенных сигналов, а затем вызвать функцию
pselect
, сообщив ей, что нужно переустановить маску сигналов.

В отношении второго пункта рассмотрим следующий пример (описанный на с. 308–309 [110]). Обработчик сигнала нашей программы для сигнала

SIGINT
просто устанавливает глобальную переменную
intr_flag
и возвращает управление. Если наш процесс блокирован в вызове функции select, возвращение из обработчика сигнала заставляет функцию завершить работу, присвоив
errno
значение
EINTR
. Код вызова
select
выглядит следующим образом:

if (intr_flag)

 handle_intr(); /* обработка этого сигнала */

if ((nready = select(...)) < 0) {

 if (errno == EINTR) {

  if (intr_flag)

   handle_intr();

 }

 ...

}

Проблема заключается в том, что если сигнал придет в промежутке между проверкой переменной

intr_flag
и вызовом функции
select
, он будет потерян в том случае, если функция
select
заблокирует процесс навсегда. С помощью функции
pselect
мы можем переписать этот пример так, чтобы он работал более надежно:

sigset_t newmask, oldmask, zeromask;


sigemptyset(&zeromask);

sigemptyset(&newmask);

sigaddset(&newmask, SIGINT);


sigprocmask(SIG_BLOCK, &newmask, &oldmask); /* блокирование сигнала SIGINT */

if (intr_flag)

 handle_intr(); /* обработка этого сигнала */

if ((nready = pselect(..., &zeromask)) < 0) {

 if (errno == EINTR) {

  if (intr_flag)

  handle_intr();

 }

 ...

}

Перед проверкой переменной

intr_flag
мы блокируем сигнал
SIGINT
. Когда вызывается функция
pselect
, она заменяет маску сигналов процесса пустым набором (
zeromask
), а затем проверяет дескрипторы, возможно, переходя в состояние ожидания. Но когда функция
pselect
возвращает управление, маске сигналов процесса присваивается то значение, которое предшествовало вызову функции
pselect
(то есть сигнал
SIGINT
блокируется).

Мы поговорим о функции

pselect
более подробно и приведем ее пример в разделе 20.5. Функцию
pselect
мы используем в листинге 20.3, а в листинге 20.4 показываем простую, хотя и не вполне корректную реализацию этой функции.

ПРИМЕЧАНИЕ

Есть одно незначительное различие между функциями select и pselect. Первый элемент структуры timeval является целым числом типа long со знаком, в то время как первый элемент структуры timspec имеет тип time_t. Число типа long со знаком в первой функции также должно было относиться к типу time_t, но мы не меняли его тип, чтобы не разрушать существующего кода. Однако в новой функции это можно было бы сделать.

6.10. Функция poll

Функция

poll
появилась впервые в SVR3, и изначально ее применение ограничивалось потоковыми устройствами (STREAMS devices) (см. главу 31). В SVR4 это ограничение было снято, что позволило функции
poll
работать с любыми дескрипторами. Функция
poll
предоставляет функциональность, аналогичную функции
select
, но позволяет получать дополнительную информацию при работе с потоковыми устройствами.

#include 


int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);

Возвращает: количество готовых дескрипторов, 0 в случае тайм-аута, -1 в случае ошибки

Первый аргумент — это указатель на первый элемент массива структур. Каждый элемент массива — это структура

pollfd
, задающая условия, проверяемые для данного дескриптора
fd
.

struct pollfd {

 int fd;        /* дескриптор, который нужно проверить */

 short events;  /* события на дескрипторе, которые нас интересуют */

 short revents; /* события, произошедшие на дескрипторе fd */

};

Проверяемые условия задаются элементом

events
, и состояние этого дескриптора функция возвращает в соответствующем элементе
revents
. (Наличие двух переменных для каждого дескриптора, одна из которых — значение, а вторая — результат, дает возможность обойтись без аргументов типа «значение-результат». Вспомните, что три средних аргумента функции
select
имеют тип «значение-результат».) Каждый из двух элементов состоит из одного или более битов, задающих определенное условие. В табл. 6.2 перечислены константы, используемые для задания флага
events
и для проверки флага
revents
.


Таблица 6.2. Различные значения флагов events и revents для функции poll

КонстантаНа входе (events)На выходе (revents)Описание
POLLINМожно считывать обычные или приоритетные данные
POLLRDNORMМожно считывать обычные данные
POLLRDBANDМожно считывать приоритетные данные
POLLPRIМожно считывать данные с высоким приоритетом
POLLOUTМожно записывать обычные данные
POLLWRNORMМожно записывать обычные данные
POLLWRBANDМожно записывать приоритетные данные
POLLERRПроизошла ошибка
POLLHUPПроизошел разрыв соединения
POLLNVALДескриптор не соответствует открытому файлу

Мы разделили эту таблицу на три части: первые четыре константы относятся ко вводу, следующие три — к выводу, а последние три — к ошибкам. Обратите внимание, что последние три константы не могут устанавливаться в элементе events, но всегда возвращаются в revents, когда выполняется соответствующее условие.

Существует три класса данных, различаемых функцией

poll
: обычные, приоритетные и данные с высоким приоритетом. Эти термины берут начало в реализациях, основанных на потоках (см. рис. 31.5).

ПРИМЕЧАНИЕ

Константа POLLIN может быть задана путем логического сложения констант POLLRDNORM и POLLRDBAND. Константа POLLIN существовала еще в реализациях SVR3, которые предшествовали полосам приоритета в SVR4, то есть эта константа существует в целях обратной совместимости. Аналогично, константа POLLOUT эквивалентна POLLWRNORM, и первая из них предшествовала второй.

Для сокетов TCP и UDP при описанных условиях функция

poll
возвращает указанный флаг
revent
. К сожалению, в определении функции
poll
стандарта POSIX имеется множество слабых мест (неоднозначностей):

■ Все регулярные данные TCP и все данные UDP считаются обычными.

■ Внеполосные данные TCP (см. главу 24) считаются приоритетными.

■ Когда считывающая половина соединения TCP закрывается (например, если получен сегмент FIN), это также считается равнозначным обычным данным, и последующая операция чтения возвратит нуль.

■ Наличие ошибки для соединения TCP может расцениваться либо как обычные данные, либо как ошибка (

POLLERR
). В любом случае последующая функция read возвращает -1, что сопровождается установкой переменной
errno
в соответствующее значение. Это происходит при получении RST или истечении таймера.

■ Информация о доступности нового соединения на прослушиваемом сокете может считаться либо обычными, либо приоритетными данными. В большинстве реализаций эта информация рассматривается как обычные данные.

Число элементов в массиве структур задается аргументом

nfds
.

ПРИМЕЧАНИЕ

Исторически этот аргумент имел тип long без знака, что является некоторым излишеством. Достаточно будет типа int без знака. В Unix 98 для этого аргумента определяется новый тип — nfds_t.

Аргумент

timeout
определяет, как долго функция находится в ожидании перед завершением. Положительным значением задается количество миллисекунд — время ожидания. В табл. 6.3 показаны возможные значения аргумента
timeout
.


Таблица 6.3. Значения аргумента timeout для функции poll

Значение аргумента timeoutОписание
INFTIMЖдать вечно
0Возвращать управление немедленно, без блокирования
>0Ждать в течение указанного числа миллисекунд

Константа

INFTIM
определена как отрицательное значение. Если таймер в данной системе не обеспечивает точность порядка миллисекунд, значение округляется в большую сторону до ближайшего поддерживаемого значения.

ПРИМЕЧАНИЕ

POSIX требует, чтобы константа INFTIM была определена в заголовочном файле , но многие системы все еще определяют ее в заголовочном файле .

Как и в случае функции select, любой тайм-аут, установленный для функции poll, ограничивается снизу разрешающей способностью часов в конкретной реализации (обычно 10 мс).

Функция

poll
возвращает -1, если произошла ошибка, 0 — если нет готовых дескрипторов до истечения времени таймера, иначе возвращается число дескрипторов с ненулевым элементом
revents
.

Если нас больше не интересует конкретный дескриптор, достаточно установить элемент

fd
структуры
pollfd
равным отрицательному значению. В этом случае элемент
events
будет проигнорирован, а элемент
revents
при возвращении функции будет сброшен в нуль.

Вспомните наши рассуждения в конце раздела 6.3 относительно константы

FD_SETSIZE
и максимального числа дескрипторов в наборе в сравнении с максимальным числом дескрипторов для процесса. У нас не возникает подобных проблем с функцией
poll
, поскольку вызывающий процесс отвечает за размещение массива структур
pollfd
в памяти и за последующее сообщение ядру числа элементов в массиве. Не существует типа данных фиксированного размера, аналогичного
fd_set
, о котором знает ядро.

ПРИМЕЧАНИЕ

POSIX требует наличия и функции select, и функции poll. Но если сравнивать их с точки зрения переносимости, то функцию select в настоящее время поддерживает больше систем, чем функцию poll. POSIX определяет также функцию pselect — усовершенствованную версию функции select, которая обеспечивает возможность блокирования сигналов и предоставляет лучшую разрешающую способность по времени, а для функции poll ничего подобного в POSIX нет.

6.11. Эхо-сервер TCP (еще раз)

Теперь мы изменим наш эхо-сервер TCP из раздела 6.8, используя вместо функции

select
функцию
poll
. В предыдущей версии сервера, работая с функцией
select
, мы должны были выделять массив
client
вместе с набором дескрипторов
rset
(см. рис. 6.12). С помощью функции
poll
мы разместим в памяти массив структур
pollfd
. В нем же мы будем хранить и информацию о клиенте, не создавая для нее другой массив. Элемент
fd
этого массива мы обрабатываем тем же способом, которым обрабатывали массив
client
(см. рис. 6.12): значение -1 говорит о том, что элемент не используется, а любое другое значение является номером дескриптора. Вспомните из предыдущего раздела, что любой элемент в массиве структур
pollfd
, передаваемый функции
poll
с отрицательным значением элемента
fd
, просто игнорируется.

В листинге 6.5 показана первая часть кода нашего сервера.

Листинг 6.5. Первая часть сервера TCP, использующего функцию poll

//tcpcliserv/tcpservpoll01.с

 1 #include "unp.h"

 2 #include <1imits.h> /* для OPEN_MAX */


 3 int

 4 main(int argc, char **argv)

 5 {

 6  int i, maxi, listenfd, connfd, sockfd;

 7  int nready;

 8  ssize_t n;

 9  char buf[MAXLINE];

10  socklen_t clilen;

11  struct pollfd client[OPEN_MAX];

12  struct sockaddr_in cliaddr, servaddr;


13  listenfd = Socket(AF_INET, SOCK_STREAM, 0);


14  bzero(&servaddr, sizeof(servaddr));

15  servaddr.sin_family = AF_INET;

16  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

17  servaddr.sin_port = htons(SERV_PORT);


18  Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));


19  Listen(listenfd, LISTENQ);


20  client[0].fd = listenfd;

21  client[0].events = POLLRDNORM;

22  for (i = 1; i < OPEN_MAX; i++)

23   client[i].fd = -1; /* -1 означает, что элемент свободен */

24  maxi = 0; /* максимальный индекс массива client[] */

Размещение массива структур pollfd в памяти

11
 Мы объявляем массив структур
pollfd
размером
OPEN_MAX
. Не существует простого способа определить максимальное число дескрипторов, которые могут быть открыты процессом. Мы снова столкнемся с этой проблемой в листинге 13.1. Один из способов ее решения — вызвать функцию POSIX
sysconf
с аргументом
_SC_OPEN_MAX
[110, с. 42-44], а затем динамически выделять в памяти место для массива соответствующего размера. Однако функция
sysconf
может возвратить некое «неопределенное» значение, и в этом случае нам придется задавать ограничение самим. Здесь мы используем только константу
OPEN_MAX
стандарта POSIX.

Инициализация

20-24
 Мы используем первый элемент в массиве
client
для прослушиваемого сокета и присваиваем дескрипторам для оставшихся элементов -1. Мы также задаем в качестве аргумента функции
poll
событие
POLLRDNORM
, чтобы получить уведомление от этой функции в том случае, когда новое соединение будет готово к приему. Переменная
maxi
содержит максимальный индекс массива
client
, используемый в настоящий момент.

Вторая часть нашей функции приведена в листинге 6.6.

Листинг 6.6. Вторая часть сервера TCP, использующего функцию poll

//tcpcliserv/tcpservpoll01.c

25  for (;;) {

26   nready = Poll(client, maxi + 1, INFTIM);


27   if (client[0].revents & POLLRDNORM) { /* новое соединение

                                              с клиентом */

28    clilen = sizeof(cliaddr);

29    connfd = Accept(listenfd. (SA*)&cliaddr, &clilen);


30    for (i = 1; i < OPEN_MAX; i++)

31     if (client[1].fd < 0) {

32      client[i].fd = connfd; /* сохраняем дескриптор */

33      break;

34     }

35    if (i == OPEN_MAX)

36     err_quit("too many clients");


37    client[i].events = POLLRDNORM;

38    if (i > maxi)

39     maxi = i; /* максимальный индекс в массиве client[] */


40    if (--nready <= 0)

41     continue; /* больше нет дескрипторов, готовых для чтения */

42   }

43   for (i = 1; i <= maxi; i++) { /* проверяем все клиенты на наличие

                                      данных */

44    if ((sockfd = client[i].fd) < 0)

45     continue;

46    if (client[i].revents & (POLLRDNORM | POLLERR)) {

47     if ((n = Read(sockfd, buf, MAXLINE)) < 0) {

48      if (errno == ECONNRESET) {

49       /* соединение переустановлено клиентом */

50       Close(sockfd);

51       client[i].fd = -1;

52      } else

53       err_sys("readline error");

54     } else if (n == 0) {

55      /* соединение закрыто клиентом */

56      Close(sockfd);

57      client[i].fd = -1;

58     } else

59      Writen(sockfd, line, n);


60     if (--nready <= 0)

61      break; /* больше нет дескрипторов, готовых для чтения */

62    }

63   }

64  }

65 }

Вызов функции poll, проверка нового соединения

26-42
 Мы вызываем функцию
poll
для ожидания нового соединения либо данных на существующем соединении. Когда новое соединение принято, мы находим первый свободный элемент в массиве
client
 — это первый элемент с отрицательным дескриптором. Обратите внимание, что мы начинаем поиск с индекса 1, поскольку элемент
client[0]
используется для прослушиваемого сокета. Когда свободный элемент найден, мы сохраняем дескриптор и устанавливаем событие
POLLRDNORM
.

Проверка данных на существующем соединении

43-63
 Два события, которые нас интересуют, — это
POLLRDNORM
и
POLLERR
. Второй флаг в элементе
event
мы не устанавливали, поскольку этот флаг возвращается всегда, если соответствующее условие выполнено. Причина, по которой мы проверяем событие
POLLERR
, в том, что некоторые реализации возвращают это событие, когда приходит сегмент RST, другие же в такой ситуации возвращают событие
POLLRDNORM
. В любом случае мы вызываем функцию
read
, и если произошла ошибка, эта функция возвратит ее. Когда существующее соединение завершается клиентом, мы просто присваиваем элементу
fd
значение -1.

6.12. Резюме

В Unix существует пять различных моделей ввода-вывода:

■ блокируемый ввод-вывод;

■ неблокируемый ввод-вывод;

■ мультиплексирование ввода-вывода;

■ управляемый сигналом ввод-вывод;

■ асинхронный ввод-вывод.

По умолчанию используется блокируемый ввод-вывод, и этот вариант встречается наиболее часто. Неблокируемый ввод-вывод и управляемый сигналом ввод-вывод мы рассмотрим в последующих главах. В этой главе мы рассмотрели мультиплексирование ввода-вывода. Асинхронный ввод-вывод определяется в стандарте POSIX, но поддерживающих его реализаций не так много.

Наиболее часто используемой функцией для мультиплексирования ввода- вывода является функция

select
. Мы сообщаем этой функции, какие дескрипторы нас интересуют (для чтения, записи или условия ошибки), а также передаем ей максимальное время ожидания и максимальное число дескрипторов (увеличенное на единицу). Большинство вызовов функции
select
определяют количество дескрипторов, готовых для чтения, и, как мы отметили, единственное условие исключения при работе с сокетами — это прибытие внеполосных данных (см. главу 21). Поскольку функция
select
позволяет ограничить время блокирования функции, мы используем это свойство в листинге 14.3 для ограничения по времени операции ввода.

Используя эхо-клиент в пакетном режиме с помощью функции

select
, мы выяснили, что даже если обнаружен признак конца файла, данные все еще могут находиться в канале на пути к серверу или от сервера. Обработка этого сценария требует применения функции
shutdown
, которая позволяет воспользоваться таким свойством TCP, как возможность половинного закрытия соединения (half-close feature).

POSIX определяет функцию

pselect
(повышающую точность таймера с микросекунд до наносекунд) которой передается новый аргумент — указатель на набор сигналов. Это позволяет избежать ситуации гонок (race condition) при перехвате сигналов, о которой мы поговорим более подробно в разделе 20.5.

Функция

poll
из System V предоставляет функциональность, аналогичную функции
select
. Кроме того, она обеспечивает дополнительную информацию при работе с потоковыми устройствами. POSIX требует наличия и функции
select
, и функции
poll
, но первая распространена шире.

Упражнения

1. Мы говорили, что набор дескрипторов можно присвоить другому набору дескрипторов, используя оператор присваивания языка С. Как это сделать, если набор дескрипторов является массивом целых чисел? (Подсказка: посмотрите на свой системный заголовочный файл

или
.)

2. Описывая в разделе 6.3 условия, при которых функция

select
сообщает, что дескриптор готов для записи, мы указали, что сокет должен быть неблокируемым, для того чтобы операция записи возвратила положительное значение. Почему?

3. Что произойдет с программой из листинга 6.1, если мы поставим слово

else
перед
if
в строке 19?

4. В листинге 6.3 добавьте необходимый код, чтобы позволить серверу использовать максимальное число дескрипторов, допустимое ядром (Подсказка: изучите функцию

setrlimit
.)

5. Посмотрите, что происходит, если в качестве второго аргумента функции

shutdown
передается
SHUT_RD
. Возьмите за основу код клиента TCP, представленный в листинге 5.3, и выполните следующие изменения: вместо номера порта
SERV_PORT
задайте порт 19 (служба
chargen
, см. табл. 2.1), а также замените вызов функции
str_cli
вызовом функции
pause
. Запустите программу, задав IP-адрес локального узла, на котором выполняется сервер
chargen
. Просмотрите пакеты с помощью такой программы, как, например,
tcpdump
(см. раздел В.5). Что происходит?

6. Почему приложение должно вызывать функцию

shutdown
с аргументом
SHUT_RDWR
, вместо того чтобы просто вызвать функцию
close
?

7. Что происходит в листинге 6.4, когда клиент отправляет RST для завершения соединения?

8. Перепишите код, показанный в листинге 6.5, чтобы вызывать функцию

sysconf
для определения максимального числа дескрипторов и размещения соответствующего массива
client
в памяти.

Глава 7