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

Дополнительные функции ввода-вывода

14.1. Введение

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

read
и
write
:
recv
и
send
, допускающие четвертый аргумент, содержащий флаги, передаваемые от процесса к ядру;
readv
и
writev
, позволяющие нам задавать массив буферов для ввода или вывода;
recvmsg
и
sendmsg
, объединяющие все свойства других функций ввода-вывода и обладающие новой возможностью получения и отправки вспомогательных данных.

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

14.2. Тайм-ауты сокета

Существует три способа установки тайм-аута для операции ввода-вывода через сокет.

1. Вызов функции

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

2. Блокирование при ожидании ввода-вывода в функции

select
, имеющей встроенное ограничение времени, вместо блокирования в вызове функции
read
или
write
.

3. Использование более новых параметров сокета —

SO_RCVTIMEO
и
SO_SNDTIMEO
. Проблема при использовании этого подхода заключается в том, что не все реализации поддерживают новые параметры сокетов.

Все три технологии работают с функциями ввода и вывода (такими как

read
,
write
и их вариациями, например
recvfrom
и
sendto
), но нам также хотелось бы иметь технологию, работающую с функцией
connect
, поскольку процесс соединения TCP может занять длительное время (обычно 75 с). Функцию
select
можно использовать для установки тайм-аута функции
connect
, только когда сокет находится в неблокируемом режиме (который мы рассматриваем в разделе 16.3), а параметры сокетов, устанавливающие тайм-аут, не работают с функцией
connect
. Мы также должны отметить, что первые две технологии работают с любым дескриптором, в то время как третья технология только с дескрипторами сокетов.

Теперь мы представим примеры применения всех трех технологий.

Тайм-аут для функции connect (сигнал SIGALRM)

В листинге 14.1[1] показана наша функция

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

Листинг 14.1. Функция connect с тайм-аутом

//lib/connect_timeo.c

 1 #include "unp.h"


 2 static void connect_alarm(int);


 3 int

 4 connect_timeo(int sockfd, const SA *saptr, socklen_t salen, int nsec)

 5 {

 6  Sigfunc *sigfunc;

 7  int n;


 8  sigfunc = Signal(SIGALRM, connect_alarm);

 9  if (alarm(nsec) != 0)

10   err_msg("connect_timeo: alarm was already set");


11  if ((n = connect(sockfd, saptr, salen)) < 0) {

12   close(sockfd);

13   if (errno == EINTR)

14    errno = ETIMEDOUT;

15  }

16  alarm(0); /* отключение alarm */

17  Signal(SIGALRM, sigfunc); /* восстанавливаем прежний обработчик

                                 сигнала */

18  return (n);

19 }


20 static void

21 connect_alarm(int signo)

22 {

23  return; /* просто прерываем connect() */

24 }

Установка обработчика сигналов

8
 Для
SIGALRM
устанавливается обработчик сигнала. Текущий обработчик сигнала (если таковой имеется) сохраняется, и таким образом мы можем восстановить его в конце функции.

Установка таймера

9-10
 Таймер для процесса устанавливается на время (число секунд), заданное вызывающим процессом. Возвращаемое значение функции
alarm
— это число секунд, остающихся в таймере для процесса (если он уже установлен для процесса) в настоящий момент или 0 (если таймер не был установлен прежде). В первом случае мы выводим сообщение с предупреждением, поскольку мы стираем предыдущую установку таймера (см. упражнение 14.2).

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

11-15
 Вызывается функция
connect
, и если функция прерывается (
EINTR
), мы присваиваем переменной errno значение
ETIMEDOUT
. Сокет закрывается, чтобы не допустить продолжения трехэтапного рукопожатия.

Выключение таймера и восстановление предыдущего обработчика сигнала

16-18
 Таймер при обнулении выключается, и восстанавливается предыдущий обработчик сигналов (если таковой имеется).

Обработка сигнала SIGALRM

20-24
 Обработчик сигнала просто возвращает управление. Предполагается, что это прервет ожидание функции
connect
, заставив ее возвратить ошибку
EINTR
. Вспомните нашу функцию
signal
(см. листинг 5.5), которая не устанавливает флага
SA_RESTART
, когда перехватываемый сигнал — это сигнал
SIGALRM
.

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

connect
, используя эту технологию, но мы не можем увеличить период, заданный для ядра. В Беркли-ядре тайм-аут для функции
connect
обычно равен 75 с. Мы можем задать меньшее значение для нашей функции, допустим 10, но если мы задаем большее значение, скажем 80, тайм- аут самой функции
connect
все равно составит 75 с.

Другой важный момент в данном примере — то, что мы используем возможность прерывания системного вызова (

connect
) для того, чтобы возвратить управление, прежде чем истечет время ожидания ядра. Такой подход допустим, когда мы выполняем системный вызов и можем обработать возвращение ошибки
EINTR
. Но в разделе 29.7 мы встретимся с библиотечной функцией, выполняющей системный вызов, которая сама выполняет заново системный вызов при возвращении ошибки
EINTR
. Мы можем продолжать работать с сигналом
SIGALRM
и в этом случае, но в листинге 29.6 мы увидим, что нам придется воспользоваться функциями
sigsetjmp
и
siglongjmp
, поскольку библиотечная функция игнорирует ошибку
EINTR
.

Тайм-аут для функции recvfrom (сигнал SIGALRM)

В листинге 14.2 показана новая версия функции

dg_cli
, приведенной в листинге 8.4, в которую добавлен вызов функции
alarm
для прерывания функции recvfrom при отсутствии ответа в течение 5 с.

Листинг 14.2. Функция dg_cli, в которой при установке тайм-аута для функции recvfrom используется функция alarm

//advio/dgclitimeo3.c

 1 #include "unp.h"


 2 static void signalrm(int);


 3 void

 4 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

 5 {

 6  int n;

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


 8  Signal(SIGALRM, signalrm);


 9  while (Fgets(sendline, MAXLINE, fp) != NULL) {


10   Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);


11   alarm(5);

12   if ((n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL)) < 0) {

13    if (errno == EINTR)

14     fprintf(stderr, "socket timeout\n");

15    else

16     err_sys("recvfrom error");

17   } else {

18    alarm(0);

19    recvline[n] = 0; /* завершающий нуль */

20    Fputs(recvline, stdout);

21   }

22  }

23 }


24 static void

25 sig_alrm(int signo)

26 {

27  return; /* просто прерываем recvfrom() */

28 }

Обработка тайм-аута из функции recvfrom

8-22
 Мы устанавливаем обработчик для сигнала
SIGALRM
и затем вызываем функцию
alarm
для 5-секундного тайм-аута при каждом вызове функции
recvfrom
. Если функция
recvfrom
прерывается нашим обработчиком сигнала, мы выводим сообщение об ошибке и продолжаем работу. Если получена строка от сервера, мы отключаем функцию alarm и выводим ответ.

Обработчик сигнала SIGALRM

24-28
 Наш обработчик сигналов возвращает управление, прерывая блокированную функцию
recvfrom
.

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

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

Тайм-аут для функции recvfrom (функция select)

Мы демонстрируем вторую технологию для установки тайм-аута (использование функции

select
) в листинге 14.3. Здесь показана наша функция
readable_timeo
, которая ждет, когда дескриптор станет готов для чтения, но не более заданного числа секунд.

Листинг 14.3. Функция readable_timeo: ожидание, когда дескриптор станет готов для чтения

//lib/readable_timео.c

 1 #include "unp.h"


 2 int

 3 readable_timeo(int fd, int sec)

 4 {

 5  fd_set rset;

 6  struct timeval tv;


 7  FD_ZERO(&rset);

 8  FD_SET(fd, &rset);


 9  tv.tv_sec = sec;

10  tv.tv_usec = 0;


11  return (select(fd + 1, &rset, NULL, NULL, &tv));

12  /* > если дескриптор готов для чтения */

13 }

Подготовка аргументов для функции select

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

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

11-12
 Функция
select
ждет, когда дескриптор станет готов для чтения или истечет заданное время ожидания. Возвращаемое значение этой функции — это возвращаемое значение функции
select
: -1 в случае ошибки, 0, если истекло время ожидания, и положительное значение, задающее число готовых дескрипторов, если таковые появились.

Эта функция не выполняет операции чтения — она просто ждет, когда дескриптор будет готов к чтению. Следовательно, эту функцию можно использовать с любым типом сокета — TCP или UDP.

Создание аналогичной функции, называемой

writable_timeo
, тривиально. Эта функция ждет, когда дескриптор будет готов для записи.

Мы используем эту функцию в листинге 14.4, где показана еще одна версия нашей функции

dg_cli
, приведенной в листинге 8.4. Эта новая версия вызывает функцию
recvfrom
, только когда наша функция
readable_timeo
возвращает положительное значение.

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

recvfrom
, пока функция
readable_timeo
не сообщит нам, что дескриптор готов для чтения. Тем самым мы гарантируем, что функция
recvfrom
не заблокируется.

Листинг 14.4. Функция dg_cli, вызывающая функцию readable_timeo для установки тайм-аута

//advio/dgclitimeo1.c

 1 #include "unp.h"


 2 void

 3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

 4 {

 5  int n;

 6  char sendline[MAXLINE], recvline[MAXLINE + 1];


 7  while (Fgets(sendline, MAXLINE, fp) != NULL) {


 8   Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);


 9   if (Readable_timeo(sockfd, 5) == 0) {

10    fprintf(stderr, "socket timeout\n");

11   } else {

12    n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);

13    recvline[n] = 0; /* завершающий нуль */

14    Fputs(recvline, stdout);

15   }

16  }

17 }

Тайм-аут для функции recvfrom (параметр сокета SO_RCVTIMEO)

В нашем последнем примере демонстрируется применение параметра сокета

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

Листинг 14.5. Функция dg_cli, использующая параметр сокета SO_RCVTIMEO для установки тайм-аута

//advio/dgclitimeo2.c

 1 #include "unp.h"


 2 void

 3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

 4 {

 5  int n;

 6  char sendline[MAXLINE], recvline[MAXLINE + 1];

 7  struct timeval tv;


 8  tv.tv_sec = 5;

 9  tv.tv_usec = 0;

10  Setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));


11  while (Fgets(sendline, MAXLINE, fp) != NULL) {


12    Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);


13    n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);

14    if (n < 0) {

15     if (errno == EWOULDBLOCK) {

16     fprintf(stderr, "socket timeout\n");

17     continue;

18    } else

19     err_sys("recvfrom error");

20   }

21   recvline[n] = 0; /* завершающий нуль */

22   Fputs(recvline, stdout);

23  }

24 }

Установка параметра сокета

8-10
 Четвертый аргумент функции
setsockopt
— это указатель на структуру
timeval
, в которую записывается желательное значение тайм-аута.

Проверка тайм-аута

15-17
 Если тайм-аут операции ввода-вывода истекает, функция (в данном случае
recvfrom
) возвращает ошибку
EWOULDBLOCK
.

14.3. Функции recv и send

Эти две функции аналогичны стандартным функциям

read
и
write
, но для них требуется дополнительный аргумент.

#include 


ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);

ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);

Обе функции возвращают: количество прочитанных или записанных байтов в случае успешного выполнения, -1 в случае ошибки

Первые три аргумента функций

recv
и
send
совпадают с тремя первыми аргументами функций read и write. Аргумент
flags
либо имеет нулевое значение, либо формируется в результате применения операции логического ИЛИ к константам, представленным в табл. 14.1.


Таблица 14.1. Аргумент flags для функций ввода-вывода

flagsОписаниеrecvsend
MSG_DONTROUTEHe искать в таблице маршрутизации
MSG_DONTWAITТолько эта операция является неблокируемой
MSG_OOBОтправка или получение внеполосных данных
MSG_PEEKПросмотр приходящих сообщений
MSG_WAITALLОжидание всех данных

MSG_DONTROUTE
. Этот флаг сообщает ядру, что получатель находится в нашей сети, и поэтому не нужно выполнять поиск в таблице маршрутизации. Дополнительную информацию об этом свойстве мы приводим при описании параметра сокета
SO_DONTROUTE
(см. раздел 7.5). Это свойство можно включить для одной операции вывода с флагом
MSG_DONTROUTE
или для всех операций вывода данного сокета, используя указанный параметр сокета.

MSG_DONTWAIT
. Этот флаг указывает, что отдельная операция ввода-вывода является неблокируемой. Таким образом, отпадает необходимость включать флаг отсутствия блокировки для сокета, выполнять операцию ввода-вывода и затем выключать флаг отсутствия блокировки. Неблокируемый ввод-вывод мы опишем в главе 15 вместе с включением и выключением флага отсутствия блокировки для всех операций ввода-вывода через сокет.

ПРИМЕЧАНИЕ

Этот флаг введен в Net/3 и может не поддерживаться в некоторых системах.

MSG_OOB
. С функцией
send
этот флаг указывает, что отправляются внеполосные данные. В случае TCP в качестве внеполосных данных должен быть отправлен только 1 байт, как показано в главе 21. С функцией
recv
этот флаг указывает на то, что вместо обычных данных должны читаться внеполосные данные.

MSG_PEEK
. Этот флаг позволяет нам просмотреть пришедшие данные, готовые для чтения, при этом после выполнения функции
recv
или
recvfrom
данные не сбрасываются (при повторном вызове этих функций снова возвращаются уже просмотренные данные). Подробнее мы поговорим об этом в разделе 14.7.

MSG_WAITALL
. Этот флаг был впервые введен в 4.3BSD Reno. Он сообщает ядру, что операция чтения должна выполняться до тех пор, пока не будет прочитано запрашиваемое количество байтов. Если система поддерживает этот флаг, мы можем опустить функцию
readn
(см. листинг 3.9) и заменить ее макроопределением

#define readn(fd, ptr, n) recv(fd, ptr, n, MSG_WAITALL)

Даже если мы задаем флаг

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

Существуют дополнительные флаги, используемые протоколами, отличными от TCP/IP. Например, транспортный уровень OSI основан на записях (а не на потоке байтов, как TCP), и для операций вывода поддерживает флаг

MSG_EOR
, задающий конец логической записи.

С аргументом

flags
связана одна фундаментальная проблема: он передается по значению и не является аргументом типа «значение-результат». Следовательно, он может использоваться только для передачи флагов от процесса к ядру. Ядро не может передать флаги обратно процессу. Это не представляет проблемы с TCP/IP, поскольку очень редко бывает необходимо передавать флаги обратно процессу от ядра. Но когда к 4.3BSD Reno были добавлены протоколы OSI, появилась необходимость возвращать процессу флаг
MSG_EOR
при операции ввода. В 4.3BSD Reno было принято решение оставить аргументы для общеупотребительных функций (
recv
и
recvfrom
) как есть и изменить структуру
msghdr
, которая используется с функциями
recvmsg
и
sendmsg
. В разделе 14.5 мы увидим, что в эту структуру был добавлен целочисленный элемент
msg_flags
, и поскольку структура передается по ссылке, ядро может изменить флаги, содержащиеся в этом элементе, по завершении функции. Это значит также, что если процессу необходимо, чтобы флаги изменялись ядром, процесс должен вызвать функцию
recvmsg
вместо вызова функции
recv
или
recvfrom
.

14.4. Функции readv и writev

Эти две функции аналогичны функциям

read
и
write
, но
readv
и
writev
позволяют использовать для чтения или записи один или более буферов с помощью одного вызова функции. Эти операции называются операциями распределяющего чтения (scatter read) (поскольку вводимые данные распределяются по нескольким буферам приложения) и объединяющей записи (gather write) (поскольку данные из нескольких буферов объединяется для одной операции вывода).

#include 


ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);

ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);

Обе функции возвращают: количество считанных или записанных байтов, -1 в случае ошибки

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

:

struct iovec {

 void *iov_base; /* начальный адрес буфера */

 size_t iov_len; /* размер буфера */

};

ПРИМЕЧАНИЕ

Типы данных элементов структуры iovec определяются POSIX. Вам могут встретиться реализации, определяющие iov_base как char*, a iov_len как int.

Существует некоторый предел числа элементов в массиве структур iovec, зависящий от реализации. Linux позволяет использовать до 1024 элементов, а HP-UD — до 2100. POSIX требует, чтобы константа

IOV_MAX
определялась включением заголовочного файла
и чтобы ее значение было не менее 16.

Функции

readv
и
writev
могут использоваться с любым дескриптором, а не только с сокетами. Кроме того,
writev
является атомарной операцией. Для протокола, основанного на записях, такого как UDP, один вызов функции
writev
генерирует одну дейтаграмму UDP.

Мы отметили одно использование функции

writev
с параметром сокета
TCP_NODELAY
в разделе 7.9. Мы сказали, что при записи с помощью функции
write
4 байт и затем 396 байт может активизироваться алгоритм Нагла, и предпочтительное решение в данном случае заключается в вызове функции
writev
для двух буферов.

14.5. Функции recvmsg и sendmsg

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

read
,
readv
,
recv
и
recvfrom
вызовами функции
recvmsg
. Аналогично, все вызовы различных функций вывода можно заменить вызовами функции
sendmsg
.

#include 


ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);

Обе функции возвращают: количество прочитанных или записанных байтов в случае успешного выполнения, -1 в случае ошибки

Большинство аргументов обеих функций скрыто в структуре

msghdr
:

struct msghdr {

 void         *msg_name;     /* адрес протокола */

 socklen_t    msg_namelen;   /* размер адреса протокола */

 struct iovec *msg_iov;      /* массив буферов */

 size_t       msg_iovlen;    /* количество элементов в массиве msg_iov */

 void         *msg_control;  /* вспомогательные данные: должны быть

                                выровнены для структуры cmsghdr */

 socklen_t    msg_controllen; /* размер вспомогательных данных */

 int          msg_flags;      /* флаги, возвращенные функцией recvmsg() */

};

ПРИМЕЧАНИЕ

Показанная нами структура msghdr восходит к 4.3BSD Reno и определяется POSIX. Некоторые системы (например, Solaris 2.5) используют более раннюю структуру msghdr, которая появилась в 4.2BSD. У более ранней структуры нет элемента msg_flags, а элементы msg_control и msg_controllen называются msg_accrights и msg_accrightslen. В этой системе поддерживается только одна форма вспомогательных данных — передача дескрипторов файлов (так называемые права доступа). При появлении протоколов OSI в 4.3BSD Reno были добавлены новые формы вспомогательных данных, вследствие чего были обобщены имена элементов структуры.

Элементы

msg_name
и
msg_namelen
используются, когда сокет не является присоединенным (например, неприсоединенный сокет UDP). Они аналогичны пятому и шестому аргументам функций
recvfrom
и
sendto
:
msg_name
указывает на структуру адреса сокета, в которой вызывающий процесс хранит адрес протокола получателя для функции
sendmsg
или функция
recvmsg
хранит адрес протокола отправителя. Если нет необходимости задавать адрес протокола (например, сокет TCP или присоединенный сокет UDP), элемент
msg
_name должен быть пустым указателем. Элемент
msg_namelen
является аргументом типа «значение» для функции
sendmsg
, но для функции
recvmsg
это аргумент типа «значение-результат».

Элементы

msg_iov
и
msg_iovlen
задают массив буферов ввода и вывода (массив структур
iovec
), аналогичный второму и третьему аргументам функций
readv
и
writev
.

Элементы

msg_control
и
msg_controllen
задают расположение и размер необязательных вспомогательных данных. Элемент
msg_controllen
— это аргумент типа «значение-результат» функции
recvmsg
. Вспомогательные данные мы рассматриваем в разделе 14.6.

Работая с функциями

recvmsg
и
sendmsg
, следует учитывать различие между двумя флаговыми переменными: это аргумент
flags
, который передается по значению, и элемент
msg_flags
структуры
msghdr
, который передается по ссылке (поскольку функции передается адрес этой структуры).

■ Элемент

msg_flags
используется только функцией
recvmsg
. Когда вызывается функция
recvmsg
, аргумент
flags
копируется в элемент
msg_flags
[128, с. 502], и это значение используется ядром для управления приемом данных. Затем это значение обновляется в зависимости от результата функции
recvmsg
.

■ Элемент

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

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

msg_flags
может возвращать функция
recvmsg
. Для элемента
sendmsg.msg_flags
нет колонки, потому что, как мы отмечали, он не используется.


Таблица 14.2. Флаги для различных функций ввода-вывода

ФлагПроверяются функциями send flags sendto flags sendmsg flagsПроверяются функциями recv flags recvfrom flags recvmsg flagsВозвращаются функцией recvmsg msg_flags
MSG_DONTROUTE
MSG_DONTWAIT
MSG_PEEK
MSG_WAITALL
MSG_EOR
MSG_OOB
MSG_BCAST
MSG_MCAST
MSG_TRUNC
MSG_CTRUNC

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

recvmsg
.

MSG_BCAST
. Этот флаг введен в BSD/OS и возвращается, если дейтаграмма была получена как широковещательная дейтаграмма канального уровня или если ее IP-адрес получателя является широковещательным адресом. Этот флаг предоставляет более удачную возможность определить, что дейтаграмма UDP была отправлена на широковещательный адрес, чем параметр сокета
IP_RECVDSTADDR
.

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

MSG_TRUNC
. Этот флаг возвращается, если дейтаграмма была усечена: у ядра имеется больше данных для возвращения, чем позволяет пространство в памяти, выделенное для них процессом (сумма всех элементов
iov_len
). Более подробно мы рассмотрим это в разделе 22.3.

MSG_CTRUNC
. Этот флаг возвращается, если были усечены вспомогательные данные: у ядра имеется больше вспомогательных данных для возвращения, чем позволяет выделенное для них процессом пространство в памяти (
msg_controllen
).

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

MSG_OOB
. Этот флаг никогда не возвращается для внеполосных данных TCP. Он возвращается другими наборами протоколов (например, протоколами OSI).

Реализации могут возвращать некоторые из входных аргументов

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

На рис. 14.1 представлена структура

msghdr
и информация, на которую она указывает. На этом рисунке отражена ситуация, предшествующая вызову функции
recvmsg
для сокета UDP.

Рис. 14.1. Структуры данных в тот момент, когда функция recvmsg вызывается для сокета UDP

Для адреса протокола в памяти выделяется 16 байт, а для вспомогательных данных — 20 байт. Инициализируется массив из трех структур iovec: первая задает 100-байтовый буфер, вторая — 60-байтовый буфер, третья — 80-байтовый буфер. Мы также предполагаем, что был установлен параметр сокета

IP_RECVDSTADDR
для получения IP-адреса получателя из дейтаграммы UDP.

Затем будем считать, что с адреса 198.6.38.100, порт 2000, приходит 170-байтовая дейтаграмма UDP, предназначенная для нашего сокета UDP с IP-адресом получателя 206.168.112.96. На рис. 14.2 показана вся информация, содержащаяся в структуре

msghdr
в момент завершения функции
recvmsg
.

Рис. 14.2. Изменение рис. 14.1 при завершении функции

Затемненными показаны поля, изменяемые функцией

recvmsg
. По сравнению с рис. 14.1 на рис. 14.2 изменяется следующее:

■ В буфер, на который указывает элемент

msg_name
, записывается структура адреса сокета Интернета, содержащая IP-адрес и UDP-порт отправителя, определенные по полученной дейтаграмме.

■ Обновляется аргумент

msg_namelen
, имеющий тип «значение-результат». Его новым значением становится количество данных, хранящихся в
msg_name
. Но на самом деле его значение как перед вызовом функции
recvmsg
, так и при ее завершении равно 16.

■ Первые 100 байт данных записываются в первый буфер, следующие 60 байт — во второй буфер и последние 10 байт — в третий буфер. Последние 70 байт третьего буфера не изменяются. Возвращаемое значение функции

recvmsg
— это размер дейтаграммы (170).

■ Буфер, на который указывает

msg_control
, заполняется как структура
cmsghdr
. (Более подробно о вспомогательных данных мы поговорим в разделе 14.6, а об этом параметре сокета — в разделе 22.2.) Значение
cmsg_len
равно 16,
cmsg_level
IPPROTO_IP
,
cmsg_type
IP_RECVDSTADDR
, а следующие 4 байта 20-байтового буфера содержат IP-адрес получателя из полученной дейтаграммы UDP. Последние 4 байта 20-байтового буфера, которые мы предоставили для хранения вспомогательных данных, не изменяются.

■ Обновляется элемент

msg_controllen
— его новым значением становится фактический размер записанных вспомогательных данных. Этот аргумент также является аргументом типа «значение-результат», и его результат по завершении функции равен 16.

■ Элемент

msg_flags
изменяется функцией
recvmsg
, но процессу никакие флаги не возвращаются.

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


Таблица 14.3. Сравнение пяти групп функций ввода-вывода

ФункцияПроизвольный дескрипторТолько дескриптор сокетаОдин буфер для чтения и записиРаспределяющее чтение, объединяющая записьНаличие флаговУказание адреса собеседникаУправляющая информация
read, write
readv, writev
recv, send
recvfrom, sendto
recvmsg, sendsg

14.6. Вспомогательные данные

Вспомогательные данные (ancillary data) можно отправлять и получать, используя элементы

msg_control
и
msg_controllen
структуры
msghdr
с функциями
sendmsg
и
recvmsg
. Другой термин, используемый для обозначения вспомогательных данных, — управляющая информация (control information). В этом разделе мы рассматриваем данное понятие и показываем структуру и макросы, используемые для создания и обработки вспомогательных данных. Примеры программ мы откладываем до следующих глав, в которых рассказывается о применении вспомогательных данных.

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


Таблица 14.4. Использование вспомогательных данных

Протоколcmsg_levelcmsg_typeОписание
IPv4IPPROTO_IPIP_RECVDSTADDRПолучает адрес получателя с дейтаграммой UDP
IP_RECVIFПолучает индекс интерфейса с дейтаграммой UDP
IPv6IPPROTO_IPV6IPV6_DSTOPTSЗадает/получает параметры получателя
IPV6_HOPLIMITЗадает/получает предел количества транзитных узлов
IPV6_HOPOPTSЗадает/получает параметры для транзитных узлов
IPV6_NEXTHOPЗадает следующий транзитный адрес
IPV6_PKTINFOЗадает/получает информацию о пакете
IPV6_RTHDRЗадает/получает информацию о пакете
Домен UnixSOL_SOCKETSCM_RIGHTSПосылает/получает дескрипторы
SCM_CREDSПосылает/получает данные, идентифицирующие пользователя

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

Вспомогательные данные состоят из одного или более объектов вспомогательных данных (ancillary data objects), каждый из которых начинается со структуры

cmsghdr
, определяемой подключением заголовочного файла
:

struct cmsghdr {

 socklen_t cmsg_len;   /* длина структуры в байтах */

 int       cmsg_level; /* исходящий протокол */

 int       cmsg_type;  /* тип данных, специфичный для протокола */

 /* далее следует массив символов без знака cmsg_data[] */

};

Мы уже видели эту структуру на рис. 14.2, когда она использовалась с параметром сокета

IP_RECVDSTADDR
для возвращения IP-адреса получателя полученной дейтаграммы UDP. Вспомогательные данные, на которые указывает элемент
msg_control
, должны быть соответствующим образом выровнены для структуры
cmsghdr
. Один из способов выравнивания мы показываем в листинге 15.7.

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

Рис. 14.3. Два объекта вспомогательных данных

Элемент

msg_control
указывает на первый объект вспомогательных данных, а общая длина вспомогательных данных задается элементом
msg_controllen
. Каждому объекту предшествует структура
cmsghdr
, которая описывает объект. Между элементом
cmsg_type
и фактическими данными может существовать заполнение, а также заполнение может быть в конце данных, перед следующим объектом вспомогательных данных. Пять макросов
CMSG_xxx
, которые мы описываем далее, учитывают это возможное заполнение.

ПРИМЕЧАНИЕ

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


На рис. 14.4 приводится формат структуры

cmsghdr
при ее использовании с доменным сокетом Unix для передачи дескрипторов (см. раздел 15.7) или передачи данных, идентифицирующих пользователя (см. раздел 15.8).

Рис. 14.4. Структура cmsghdr при использовании с доменными сокетами Unix

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

cmsghdr
занимает 4 байта и между структурой
cmsghdr
и данными нет заполнения. При передаче дескрипторов содержимое массива
cmsg_data
— это фактические значения дескрипторов. На этом рисунке мы показываем только один передаваемый дескриптор, но в общем может передаваться и более одного дескриптора (тогда значение элемента
cmsg_len
будет равно 12 плюс число дескрипторов, умноженное на 4, если считать, что каждый дескриптор занимает 4 байта).

Вспомогательные данные, возвращаемые функцией

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

#include 

#include  /* для макроса ALIGN во многих реализациях */


struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mhdrptr);

Возвращает: указатель на первую структуру cmsghdr или NULL, если нет вспомогательных данных


struct cmsghdr *CMSG_NXTHDR(struct msghdr *mhdrptr, struct cmsghdr *cmsgptr);

Возвращает: указатель на структуру cmsghdr или NULL, если нет больше объектов вспомогательных данных


unsigned char *CMSG_DATA(struct cmsghdr *cmsgptr);

Возвращает: указатель на первый байт данных, связанных со структурой cmsghdr


unsigned int CMSG_LEN(unsigned int length);

Возвращает: значение, которое записывается в cmsg_len


unsigned int CMSG_SPACE(unsigned int length);

Возвращает: общий размер объекта вспомогательных данных

ПРИМЕЧАНИЕ

В POSIX определены первые пять макросов, а в [113] определены последние два.

Эти макросы могли бы быть использованы в следующем псевдокоде:

struct msghdr msg;

struct cmsghdr *cmsgptr;


/* заполнение структуры msg */


/* вызов recvmsg() */


for (cmsgptr = CMSG_FIRSTHDR(&msg); cmsgptr != NULL;

 cmsgptr = CMSG_NXTHDR(&msg, cmsgptr)) {

 if (cmsgptr->cmsg_level == ... &&

  cmsgptr->cmsg_type == ...) {

  u_char *ptr;


  ptr = CMSG_DATA(cmsgptr);

  /* обработка данных, на которые указывает ptr */

 }

}

Макрос

CMSG_FIRSTHDR
возвращает указатель на первый объект вспомогательных данных или пустой указатель, если в структуре
msghdr
нет вспомогательных данных (или
msg_control
является пустым указателем, или
cmsg_len
меньше размера структуры
cmsghdr
). Макрос
CMSG_NXTHDR
возвращает пустой указатель, когда в буфере управления нет другого объекта вспомогательных данных.

ПРИМЕЧАНИЕ

Многие существующие реализации макроса CMSG_FIRSTHRD никогда не используют элемент msg_controllen и просто возвращают значение cmsg_control. В листинге 22.2 мы проверяем значение msg_controllen перед вызовом макроопределения.

Разница между макросами

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

14.7. Сколько данных находится в очереди?

Иногда требуется узнать, сколько данных находится в очереди для чтения данного сокета, не считывая эти данные. Для этого имеется три способа.

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

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

MSG_PEEK
(см. табл. 14.1). Если мы не уверены, что какие-либо данные готовы для чтения, мы можем объединить этот флаг с отключением блокировки для сокета или с флагом
MSG_DONTWAIT
.

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

recv
. Например, предположим, что мы вызываем recv для сокета TCP, задавая буфер длиной 1024 и флаг
MSG_PEEK
, и возвращаемое значение равно 100. Если затем мы снова вызовем функцию recv, возможно, возвратится более 100 байт (мы задаем длину буфера больше 100), поскольку в промежутке между двумя нашими вызовами
recv
могли быть получены дополнительные данные.

А что произойдет в случае сокета UDP, когда в приемном буфере имеется дейтаграмма? При вызове

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

3. Некоторые реализации поддерживают команду

FIONREAD
функции
ioctl
. Третий аргумент функции
ioctl
— это указатель на целое число, а возвращаемое в этом целом числе значение — это текущее число байтов в приемном буфере сокета [128, с. 553]. Это значение является общим числом установленных в очередь байтов, которое для сокета UDP включает все дейтаграммы, установленные в очередь. Также помните о том, что значение, возвращаемое для сокета UDP, в Беркли-реализациях включает пространство, требуемое для структуры адреса сокета, содержащей IP-адрес отправителя и порт для каждой дейтаграммы (16 байт для IP4, 24 байта для IP6).

14.8. Сокеты и стандартный ввод-вывод

Во всех наших примерах мы применяли то, что иногда называется вводом-выводом Unix, вызывали функции

read
и
write
и их разновидности (
recv
,
send
и т.д.). Эти функции работают с дескрипторами и обычно реализуются как системные вызовы внутри ядра Unix.

Другой метод выполнения ввода-вывода заключается в использовании стандартной библиотеки ввода-вывода. Она задается стандартом ANSI С и была задумана как библиотека, совместимая с не-Unix системами, поддерживающими ANSI С. Стандартная библиотека ввода-вывода обрабатывает некоторые моменты, о которых мы должны заботиться сами при использовании функций ввода- вывода Unix, таких как автоматическая буферизация потоков ввода и вывода. К сожалению, ее обработка буферизации потока может представить новый ряд проблем, о которых следует помнить. Глава 5 [110] подробно описывает стандартную библиотеку ввода-вывода, а в [92] представлена полная реализация стандартной библиотеки ввода-вывода и ее обсуждение.

ПРИМЕЧАНИЕ

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

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

■ Стандартный поток ввода-вывода может быть создан из любого дескриптора при помощи вызова функции

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

■ Сокеты TCP и UDP являются двусторонними. Стандартные потоки ввода- вывода также могут быть двусторонними: мы просто открываем поток типа

r+
, что означает чтение-запись. Но в таком потоке за функцией вывода не может следовать функция ввода, если между ними нет вызова функции
fflush
,
fseek
,
fsetpots
или
rewind
. Аналогично, за функцией вывода не может следовать функция ввода, если между ними нет вызова функции
fseek
,
fsetpots
,
rewind
, в том случае, когда при вводе не получен признак конца файла. Проблема с последними тремя функциями состоит в том, что все они вызывают функцию
lseek
, которая не работает с сокетами.

■ Простейший способ обработки подобной проблемы чтения-записи — это открытие двух стандартных потоков ввода-вывода для данного сокета: одного для чтения и другого для записи.

Пример: функция str_echo, использующая стандартный ввод-вывод

Сейчас мы модифицируем наш эхо-сервер TCP (см. листинг 5.2) для использования стандартного ввода-вывода вместо функций

readline
и
writen
. В листинге 14.6 представлена версия нашей функции
str_echo
, использующая стандартный ввод-вывод. (С этой версией связана проблема, которую мы вскоре опишем.)

Листинг 14.6. Функция str_echo, переписанная с использованием стандартного ввода-вывода

//advio/str_echo_stdiо02.с

 1 #include "unp.h"


 2 void

 3 str_echo(int sockfd)

 4 {

 5  char line[MAXLINE];

 6  FILE *fpin, *fpout;


 7  fpin = Fdopen(sockfd, "r");

 8  fpout = Fdopen(sockfd, "w");


 9  while (Fgets(line, MAXLINE, fpin) != NULL)

10  Fputs(line, fpout);

11 }

Преобразование дескриптора в поток ввода и поток вывода

7-10
 Функцией
fdopen
создаются два стандартных потока ввода-вывода: один для ввода и другой для вывода. Вызовы функций
readline
и
writen
заменены вызовами функций
fgets
и
fputs
.

Если мы запустим наш сервер с этой версией функции

str_echo
и затем запустим наш клиент, мы увидим следующее:

hpux % tcpcli02 206.168.112.96

hello, worldмы набираем эту строку, но не получаем отражения

and hiи на эту строку нет ответа

hello??и на эту строку нет ответа

^Dнаш символ конца файла

hello, world затем выводятся три отраженные строки

and hi

hello??

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

■ Мы набираем первую строку ввода, и она отправляется серверу.

■ Сервер читает строку с помощью функции

fgets
и отражает ее с помощью функции
fputs
.

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

■ Мы набираем вторую строку ввода, и она отправляется серверу.

■ Сервер читает строку с помощью функции

fgets
и отражает ее с помощью функции
fputs
.

■ Снова стандартная библиотека ввода-вывода сервера только копирует строку в свой буфер, но не выдает содержимое буфера в дескриптор, поскольку он не заполнен.

■ По тому же сценарию вводится третья строка.

■ Мы набираем наш символ конца файла, и функция

str_cli
(см. листинг 6.2) вызывает функцию
shutdown
, посылая серверу сегмент FIN.

■ TCP сервера получает сегмент FIN, который читает функция

fgets
, в результате чего функция
fgets
возвращает пустой указатель.

■ Функция

str_echo
возвращает серверу функцию
main
(см. листинг 5.9), и дочерний процесс завершается при вызове функции
exit
.

■ Библиотечная функция

exit
языка С вызывает стандартную функцию очистки ввода-вывода [110, с. 162-164], и буфер вывода, который был частично заполнен нашими вызовами функции
fputs
, теперь выводит скопившиеся в нем данные.

■ Дочерний процесс сервера завершается, в результате чего закрывается его присоединенный сокет, клиенту отсылается сегмент FIN и заканчивается последовательность завершения соединения TCP.

■ Наша функция

str_cli
получает и выводит три отраженных строки.

■ Затем функция

str_cli
получает символ конца файла на своем сокете, и клиент завершает свою работу.

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

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

fflush
или процесс завершается посредством вызова функции
exit
. Обычный размер стандартного буфера ввода-вывода — 8192 байта.

2. Буферизация по строкам (line buffered) означает, что ввод-вывод имеет место, только когда встречается символ перевода строки, процесс вызывает функцию

fflush
или процесс завершается вызовом функции
exit
.

3. Отсутствие буферизации (unbuffered) означает, что ввод-вывод имеет место каждый раз, когда вызывается функция стандартного ввода-вывода.

Большинство реализаций Unix стандартной библиотеки ввода-вывода используют следующие правила:

■ Стандартный поток ошибок никогда не буферизуется.

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

■ Все остальные потоки тоже буферизованы полностью, если они не подключены к терминалу, в случае чего они буферизованы по строкам.

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

str_echo
в листинге 14.6, заключается в том, что поток вывода (
fpot
) полностью буферизован. Есть два решения: мы можем сделать поток вывода буферизованным по строкам при помощи вызова функции
setvbuf
либо заставить каждую отраженную строку выводиться при помощи вызова функции
fflush
после каждого вызова функции
fputs
. Применение любого из этих изменений скорректирует поведение нашей функции
str_echo
. На практике оба варианта чреваты ошибками и могут плохо взаимодействовать с алгоритмом Нагла. В большинстве случаев оптимальным решением будет отказаться от использования стандартной библиотеки ввода-вывода для сокетов и работать с буферами, а не со строками (см. раздел 3.9). Использование стандартных функций ввода-вывода имеет смысл в тех случаях, когда потенциальный выигрыш перевешивает затруднения.

ПРИМЕЧАНИЕ

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

14.9. Расширенный опрос

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

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

Интерфейс /dev/poll

В Solaris имеется специальный файл

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

После открытия

/dev/poll
программа должна инициализировать массив структур
pollfd
(тех же, которые используются функцией
poll
, но в этом случае поле
revents
не используется). Затем массив передается ядру вызовом
write
(структура записывается непосредственно в
/dev/poll
). После этого программа может вызывать
ioctl DP_POLL
и ждать событий. При вызове
ioctl
передается следующая структура:

struct dvpoll {

 struct pollfd* dp_fds;

 int            dp_nfds;

 int            dp_timeout;

};

Поле

dp_fds
указывает на буфер, используемый для хранения массива структур
pollfd
, возвращаемых вызовом
ioctl
. Поле
dp_nfds
задает размер буфера. Вызов
ioctl
блокируется до появления интересующих программу событий на любом из опрашиваемых дескрипторов, или до прохождения
dp_timeout
миллисекунд. При нулевом значении тайм-аута функция
ioctl
возвращается немедленно (то есть данный способ может использоваться для реализации неблокируемых сокетов). Тайм-аут, равный -1, означает неопределенно долгое ожидание.

Измененный код функции

str_cli
, переписанной из листинга 6.2 с использованием
/dev/poll
, приведен в листинге 14.7.

Листинг 14.7. Функция str_cli, использующая /dev/poll

//advio/str_cli_poll03.c

 1 #include "unp.h"

 2 #include 


 3 void

 4 str_cli(FILE *fp, int sockfd)

 5 {

 6  int stdineof;

 7  char buf[MAXLINE];

 8  int n;

 9  int wfd;

10  struct pollfd pollfd[2];

11  struct dvpoll dopoll;

12  int i;

13  int result;


14  wfd = Open("/dev/poll", O_RDWR, 0);


15  pollfd[0].fd = fileno(fp);

16  pollfd[0].events = POLLIN;

17  pollfd[0].revents = 0;


18  pollfd[1].fd = sockfd;

19  pollfd[1].events = POLLIN;

20  pollfd[1].revents = 0;


21  Write(wfd, pollfd, sizeof(struct pollfd) * 2);


22  stdineof = 0;

23  for (;;) {

24   /* блокирование до готовности сокета */

25   dopoll.dp_timeout = -1;

26   dopoll.dp_nfds = 2;

27   dopoll.dp_fds = pollfd;

28   result = Ioctl(wfd, DP_POLL, &dopoll);


29   /* цикл по готовым дескрипторам */

30   for (i = 0; i < result; i++) {

31    if (dopoll.dp_fds[i].fd == sockfd) {

32     /* сокет готов к чтению */

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

34      if (stdineof == 1)

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

36      else

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

38     }


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

40    } else {

41     /* дескриптор готов к чтению */

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

43      stdineof = 1;

44      Shutdown(sockfd, SHUT_WR); /* отправка FIN */

45      continue;

46     }


47     Writen(sockfd, buf, n);

48    }

49   }

50  }

51 }

Составление списка дескрипторов для /dev/poll

14-21
 Заполнив массив структур
pollfd
, мы передаем его в
/dev/poll
. В нашем примере используются только два файловых дескриптора, так что мы помещаем их в статический массив. На практике программы, использующие
/dev/poll
, обычно следят за сотнями или даже тысячами дескрипторов одновременно, поэтому массив выделяется динамически.

Ожидание данных

24-28
 Программа не вызывает
select
, а блокируется в вызове
ioctl
в ожидании поступления данных. Возвращаемое значение представляет собой количество готовых к чтению дескрипторов файлов.

Цикл по дескрипторам

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

Интерфейс kqueue

Система FreeBSD версии 4.1 предложила сетевым программистам новый интерфейс, получивший название

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

#include 

#include 

#include 


int kqueue(void);

int kevent(int kq, const struct kevent *changelist, int nchanges,

 struct kevent *eventlist, int nevents, const struct timespec *timeout);

void EV_SET(struct kevent *kev, uintptr_t ident, short filter,

 u_short flags, u_int fflags, intptr_t data, void *udata);

Функция

kqueue
возвращает новый дескриптор
kqueue
, который может использоваться в последующих вызовах
kevent
. Функция
kevent
применяется для регистрации интересующих событий, а также для получения уведомлений об этих событиях. Параметры
changelist
и
nchanges
описывают изменения в предыдущем варианте списка событий. Если
nchanges
отлично от нуля, выполняются все запрошенные в структуре
changelist
изменения. Функция
kevent
возвращает количество событий или нуль, если произошел выход по тайм-ауту. В аргументе
timeout
хранится значение тайм-аута, обрабатываемое подобно тому, как при вызове
select
(
NULL
для блокирования, ненулевое значение для задания конкретного тайм- аута, а нулевое значение трактуется как необходимость неблокирующего вызова). Обратите внимание, что параметр
timeout
имеет тип
struct timespec
, отличающийся от
struct timeval
в вызове
select
тем, что первый имеет наносекундное разрешение, а второй — микросекундное.

Структура

kevent
определяется в заголовочном файле
:

struct kevent {

 uintptr_t ident;  /* идентификатор (например, дескриптор файла) */

 short     filter; /* тип фильтра (например, EVFILT_READ) */

 u_short   flags;  /* флаги действий (например, EV_ADD); */

 u_int     fflags; /* флаги, относящиеся к конкретным фильтрам */

 intptr_t  data;   /* данные, относящиеся к конкретным фильтрам */

 void      uidata; /* непрозрачные пользовательские данные */

};

Действия по смене фильтра и флаговые возвращаемые значения приведены в табл. 14.5.


Таблица 14.5. Флаги для операций kevent

Значение flagsОписаниеИзменяетсяВозвращается
EV_ADDДобавить новое событие, подразумевается по умолчанию, если не указан флаг EV_DISABLE
EV_CLEARСброс состояния события после считывания его пользователем
EV_DELETEУдаление события из фильтра
EV_DISABLEОтключение события без удаления его из фильтра
EV_ENABLEВключение отключенного перед этим события
EV_ONESHOTУдаление события после его однократного срабатывания
EV_EOFДостигнут конец файла
EV_ERRORПроизошла ошибка, код errno записан в поле data

Типы фильтров приведены в табл. 14.6.


Таблица 14.6. Типы фильтров

Значение filterОписание
EVFILT_AIOСобытия асинхронного ввода-вывода
EVFILT_PROCСобытия exit, fork, exec для процесса
EVFILT_READДескриптор готов для чтения (аналогично select)
EVFILT_SIGNALОписание сигнала
EVFILT_TIMERПериодические или одноразовые таймеры
EVFILT_VNODEИзменение и удаление файлов
EVFILT_WRITEДескриптор готов для записи (аналогично select)

Перепишем функцию

str_cli
из листинга 6.2 так, чтобы она использовала
kqueue
. Результат представлен в листинге 14.8.

Листинг 14.8. Функция str_cli, использующая kqueue

//advio/str_cli_kqueue04.c

 1 #include "unp.h"


 2 void

 3 str_cli(FILE *fp, int sockfd)

 4 {

 5  int kq, i, n, nev, stdineof = 0, isfile;

 6  char buf[MAXLINE];

 7  struct kevent kev[2];

 8  struct timespec ts;

 9  struct stat st;


10  isfile = ((fstat(fileno(fp), &st) 0) &&

11   (st.st_mode & S_IFMT) == S_IFREG);


12  EV_SET(&kev[0], fileno(fp), EVFILT_READ, EV_ADD, 0, 0, NULL);

13  EV_SET(&kev[1], sockfd, EVFILT_READ, EV_ADD, 0, 0, NULL);


14  kq = Kqueue();

15  ts.tv_sec = ts.tv_nsec = 0;

16  Kevent(kq, kev, 2, NULL, 0, &ts);


17  for (;;) {

18   nev = Kevent(kq, NULL, 0, kev, 2, NULL);


19   for (i = 0; i < nev; i++) {

20    if (kev[i].ident == sockfd) { /* сокет готов для чтения */

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

22      if (stdineof == 1)

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

24      else

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

26     }


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

28    }


29    if (kev[i].ident == fileno(fp)) { /* входной поток готов к чтению */

30     n = Read(fileno(fp), buf, MAXLINE);

31     if (n > 0)

32      Writen(sockfd, buf, n);


33     if (n == 0 || (isfile && n == kev[i].data)) {

34      stdineof = 1;

35      Shutdown(sockfd, SHUT_WR); /* отправка FIN */

36      kev[i].flags = EV_DELETE;

37      Kevent(kq, &kev[i], 1, NULL, 0, &ts); /* удаление

                                                 kevent */

38      continue;

39     }

40    }

41   }

42  }

43 }

Проверка, указывает ли дескриптор на файл

10-11
 Поведение
kqueue
при достижении конца файла зависит от того, связан ли данный дескриптор с файлом, каналом или терминалом, поэтому мы вызываем
fstat
, чтобы убедиться, что мы работаем с файлом. Эти сведения понадобятся позже.

Настройка структур kevent для kqueue

12-13
 При помощи макроса
EV_SET
мы настраиваем две структуры
kevent
. Обе содержат фильтр событий готовности к чтению (
EVFILT_READ
) и запрос на добавление этого события к фильтру (
EV_ADD
).

Создание kqueue и добавление фильтров

14-16
 Мы вызываем
kqueue
, чтобы получить дескриптор
kqueue
, устанавливаем тайм- аут равным нулю, чтобы сделать вызов
kevent
неблокируемым, и наконец, вызываем
kevent
с массивом
kevent
на месте соответствующего аргумента.

Бесконечный цикл с блокированием в kevent

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

Перебор возвращаемых событий в цикле

19
 Мы проверяем все возвращаемые события и обрабатываем их последовательно.

Сокет готов для чтения

20-28
 Эта часть кода ничем не отличается от листинга 6.2.

Вход готов для чтения

29-40
 Код практически аналогичен листингу 6.2 за тем отличием, что нам приходится обрабатывать конец файла, возвращаемый
kqueue
. Для каналов и терминалов
kqueue
возвращает событие готовности дескриптора к чтению, подобно
select
, так что мы можем считать из этого дескриптора символ конца файла. Для файлов
kqueue
возвращает количество байтов, оставшихся в поле
data
структуры
struct kevent
и предполагает, что приложение само определит, когда оно доберется до конца этих данных. Поэтому мы переписываем цикл таким образом, чтобы отправлять данные по сети, если они были считаны из дескриптора. Затем проверяется достижение конца файла: если мы считали нуль байтов или если мы считали все оставшиеся байты из дескриптора файла, значит, это и есть
EOF
. Еще одно изменение состоит в том, что вместо
FD_CLR
для удаления дескриптора из набора файлов мы используем флаг
EV_DELETE
и вызываем
kevent
для удаления события из фильтра в ядре.

Рекомендации

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

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

14.10. Резюме

Существует три способа установить ограничение времени для операции с сокетом:

■ Использовать функцию

alarm
и сигнал
SIGALRM
.

■ Задать предел времени в функции

select
.

■ Использовать более новые параметры сокета

SO_RCVTIMEO
и
SO_SNDTIMEO
.

Первый способ легко использовать, но он включает обработку сигналов и, как показано в разделе 20.5, может привести к ситуации гонок. Использование функции

select
означает, что блокирование происходит в этой функции (с заданным в ней пределом времени) вместо блокирования в вызове функции
read
,
write
или
connect
. Другая альтернатива — использование новых параметров сокета — также проста в использовании, но предоставляется не всеми реализациями.

Функции

recvmsg
и
sendmsg
являются наиболее общими из пяти групп предоставляемых функций ввода-вывода. Они объединяют целый ряд возможностей, свойственных других функциям ввода-вывода, позволяя задавать флаг
MSG_xxx
(как функции
recv
и
send
), возвращать или задавать адрес протокола собеседника (как функции
recvfrom
и
sendto
), использовать множество буферов (как функции
readv
и
writev
). Кроме того, они обеспечивают две новых возможности: возвращение флагов приложению и получение или отправку вспомогательных данных.

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

cmsghdr
, задающая его длину, уровень протокола и тип данных. Пять макросов, начинающихся с префикса
CMSG_
, используются для создания и анализа вспомогательных данных.

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

Многие производители предоставляют усовершенствованные средства опроса событий без накладных расходов на

select
и
poll
. Не стоит увлекаться написанием непереносимого кода, однако иногда преимущества перевешивают риск непереносимости.

Упражнения

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

SIGALRM
?

2. В листинге 14.1 мы выводим предупреждение, если у процесса уже установлен таймер

alarm
. Измените эту функцию так, чтобы новое значение
alarm
для процесса задавалось после выполнения connect до завершения функции.

3. Измените листинг 11.5 следующим образом: перед вызовом функции

read
вызовите функцию
recv
с флагом
MSG_PEEK
. Когда она завершится, вызовите функцию
ioctl
с командой
FIONREAD
и выведите число байтов, установленных в очередь в буфере приема сокета. Затем вызовите функцию
read
для фактического чтения данных.

4. Что происходит с оставшимися в стандартном буфере ввода-вывода данными, если процесс, дойдя до конца функции

main
, не обнаруживает там функции
exit
?

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

Глава 15