Документация по LinuxLinuxDoc.Ru 🔍

select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing

НАЗВАНИЕ
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - syn-
chronous I/O multiplexing

СИНТАКСИС
#include
#include
#include

int select(int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *utimeout);

int pselect(int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *ntimeout,
sigset_t *sigmask);

FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);

ОПИСАНИЕ
Функция select (или pselect) является основной функцией
большинства программ на языке C, эффективно обрабатывающих
одновременно более одного файловового дескриптора (или
сокета). Ее аргументами являются три массива файловых
дескрипторов: readfds, writefds и exceptfds. Как правило,
при использовании select программа ожидает "изменения
состояния" одного или более файловых дескрипторов. Под
"изменением состояния" понимается появление новых символов
в потоке, с которым связан файловый дескриптор, или
появление во внутренних буферах ядра места для записи в
поток, или возникновение ошибки, связанной с файловым
дескриптором (в случае сокета или канала это происходит,
когда другая сторона закрывает соединение).

Суммируя вышесказанное, select просто следит за
несколькими файловыми дескрипторами и является стандартным
вызовом Unix для этих целей.

Массивы файловых дескрипторов называются наборами файловых
дескрипторов. Каждый набор объявлен как тип fd_set и его
содержимое может быть изменено макросами FD_CLR, FD_ISSET,
FD_SET и FD_ZERO. Обычно FD_ZERO является первой
функцией, используемой со свежеобъявленным набором. После
этого, отдельные файловые дескрипторы могут быть
по-очереди добавлены с помощью FD_SET. select изменяет
содержимое наборов в соответсвие с правилами, описанными
ниже; после вызова select вы можете проверить, находится
ли ваш файловый дескриптор все еще в наборе с помощью
макроса FD_ISSET, возвращающей ноль, если дескриптор
присутствует в наборе, и отличное от нуля значение, если
не присутствует. FD_CLR удаляет файловый дескриптор из
набора, хотя практическая ценность этого в хорошей
программе сомнительна.


АРГУМЕНТЫ
readfds
Этот набор служит для слежения за операциями
чтения. После возврата из select readfds очищается
от всех дескрипторов файлов, за исключением тех,
для которых возможно немедленное чтение функциями
recv() (для сокетов) или read() (для каналов,
файлов и сокетов).

writefds
Этот набор служит для слежения за появлением места
для записи данных в любой из файловых дескрипторов
набора. После возврата из select writefds очищатся
от всех файловых дескрипторов, за исключением тех,
для которых возможна немедленная запись функциями
send() (для сокетов) или write() (для каналов,
файлов и сокетов).

exceptfds
Этот набор служит для слежения за исключениями или
ошибками, связанными с любым из файловых
дескрипторов набора. На самом деле слежение
производится за появлением внепоточных (Out of
Bounds - OOB) данных. Внепоточные данные посылаются
через сокет с помощью флага MSG_OOB и, в
действительности, exceptfds работает только для
сокетов. Более подробно об этом написанов recv(2) и
send(2). После возврата из select exceptfds
очищается от всех файловых дескрипторов, кроме тех,
для которых доступны внепоточные данные. Прочитать
можно лишь один байт внепоточных данных (это
делается с помощью recv()). Записать внепоточные
данные можно в любой момент. Эта операция является
неблокируемой. Поэтому нет необходимости в
четвертом наборе, который мог бы служить для
слежения за возможностью записи внепоточных данных
в сокет.

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

utimeout
Этот аргумент задает наибольшее время, которое
функция select будет ожидать изменения состояния
дескрипторов. Если за это время ничего не
произойдет, то функция возвратит управление
вызвавшей программе. Если значение этого аргумента
равно NULL, то select будет ожидать бесконечно.
utimeout может быть установлен в ноль секунд; в
этом случае select возвратит управление немедленно.
Структура struct timeval определена как

struct timeval {
long tv_sec; /* секунды */
long tv_usec; /* микросекунды */
};

ntimeout
Этот аргумент имеет то же значение, что и utimeout,
но структура struct timespec позволяет указывать
время с точностью до наносекунд:

struct timespec {
long tv_sec; /* секунды */
long tv_nsec; /* наносекунды */
};

sigmask
Этот аргумент содержит набор сигналов, которые
разрешены во время вызова pselect (см. sigaddset(3)
и sigprocmask(2)). В качестве аргумента может быть
передан NULL; в этом случае при входе в функцию и
выходе из нее набор разрешенных сигналов не
меняется. В этом случае функция ведет себя как
select.


ОБЪЕДИНЕНИЕ СИГНАЛОВ И СОБЫТИЙ
pselect должен использоваться как в случае если вы
ожидаете сигнала, так и в случае, если вы ожидаете данных
из файлового дескриптора. Программы, обрабатывающие
сигналы, как правило лишь выставляют в обработчике сигнала
глобальный флаг, который означает, что событие должно быть
обработано в главно цикле программы. Появление сигнала
заставит select (или pselect) вернуть управление вызвавшей
программе; при этом errno будет установлен в EINTR. Это
поведение продиктовано необходимостью обработки сигналов
программой (ее главным циклом) во избежание бесконечной
блокировки select. В главном цикле программы должно быть
условие, проверяющее глобальный флаг. Возникает вопрос: а
что если сигнал придет после проверки этого условия, но до
вызова select? В этом случае select навсегда
заблокируется, хотя и есть ожидающее событие. Для
разрешения этой проблемы существует функция pselect. Эта
функция может быть использована для маскировки сигналов,
которые не должны быть приняты нигде, кроме как внутри
pselect. Например, предположим что интересующее нас
событие - это завершение дочернего процесса. Перед
запуском главного цикла мы должны заблокировать SIGCHLD с
помощью sigprocmask. Наш вызов pselect разрешит SIGCHLD
указав изначальную маску сигналов. Программ будет
выглядеть так:

int child_events = 0;

void child_sig_handler (int x) {
child_events++;
signal (SIGCHLD, child_sig_handler);
}

int main (int argc, char **argv) {
sigset_t sigmask, orig_sigmask;

sigemptyset (&sigmask);
sigaddset (&sigmask, SIGCHLD);
sigprocmask (SIG_BLOCK, &sigmask,
&orig_sigmask);

signal (SIGCHLD, child_sig_handler);

for (;;) { /* главный цикл */
for (; child_events > 0; child_events--) {
/* здесь обработка событий */
}
r = pselect (n, &rd, &wr, &er, 0, &orig_sigmask);

/* главная часть программы */
}
}

Обратите внимание, что вышеуказанный вызов pselect может
быть заменен:

sigprocmask (SIG_BLOCK, &orig_sigmask, 0);
r = select (n, &rd, &wr, &er, 0);
sigprocmask (SIG_BLOCK, &sigmask, 0);

но в этом случае все равно существует вероятность того,
что сигнал будет получен после первого вызова sigprocmask,
но до вызова select. Если вы все же решите сделать так, то
разумно, как минимум, установить конечное время ожидания,
чтобы процесс не блокировался. В настоящее время glibc
работает таким образом. Ядро Linux не имеет встроенного
вызова pselect.


ПРАКТИКУМ

Итак, какой прок от использования select? Разве нельзя
просто считывать и записывать данные в файловые
дескрипторы когда того захочется? Смысл использования
select в том, что он следит за несколькими дескрипторами
одновременно и корректно переводит процесс в режим
ожидания, когда активности не наблюдается. Таким образом
он позволяет вам одновременно обрабатывать несколько
каналов и сокетов. Программисты Unix часто попадают в
ситуацию, когда необходимо обработать ввод-вывод с более
чем одного файловго дескриптора в то время как поток
данных может быть неравномерным. Если вы создатите
последовательность вызовов read и write, то вы можете
попасть в ситуацию, когда один из вызовов будет ожидать
данные из/в файлового дескриптора, в то время как другой
будет простаивать, хотя данные для него уже появились.
select позволяет эффективно справиться с такой ситуацией.

Классический пример использования select приведен на
странице man select:


int
main(void) {
fd_set rfds;
struct timeval tv;
int retval;

/* Следим ввели ли что-либо в stdin (fd 0). */
FD_ZERO(&rfds);
FD_SET(0, &rfds);
/* Ждем до 5 секунд. */
tv.tv_sec = 5;
tv.tv_usec = 0;

retval = select(1, &rfds, NULL, NULL, &tv);
/* На значение tv в данный момент полагаться нельзя! */

if (retval)
printf("Данные доступны.\n");
/* FD_ISSET(0, &rfds) will be true. */
else
printf("Нет данных в течение 5 секунд.\n");

exit(0);
}



ПРИМЕР ПЕРЕНАПРАВЛЕНИЯ ПОРТА
Пример ниже лучше демонстрирует возможности select.
Программа осуществляет перенаправление одного порта TCP на
другой.


static int forward_port;


static int listen_socket (int listen_port) {
struct sockaddr_in a;
int s;
int yes;
if ((s = socket (AF_INET, SOCK_STREAM, 0)) 0) {
FD_SET (fd2, &wr);
n = max (n, fd2);
}
if (fd1 > 0) {
FD_SET (fd1, &er);
n = max (n, fd1);
}
if (fd2 > 0) {
FD_SET (fd2, &er);
n = max (n, fd2);
}

r = select (n + 1, &rd, &wr, &er, NULL);

if (r == -1 && errno == EINTR)
continue;
if (r < 0) {
perror ("select()");
exit (1);
}
if (FD_ISSET (h, &rd)) {
unsigned int l;
struct sockaddr_in client_address;
memset (&client_address, 0, l =
sizeof (client_address));
r = accept (h, (struct sockaddr *)
&client_address, &l);
if (r < 0) {
perror ("accept()");
} else {
SHUT_FD1;
SHUT_FD2;
buf1_avail = buf1_written = 0;
buf2_avail = buf2_written = 0;
fd1 = r;
fd2 =
connect_socket (forward_port,
argv[3]);
if (fd2 0)
if (FD_ISSET (fd1, &er)) {
char c;
errno = 0;
r = recv (fd1, &c, 1, MSG_OOB);
if (r 0)
if (FD_ISSET (fd2, &er)) {
char c;
errno = 0;
r = recv (fd2, &c, 1, MSG_OOB);
if (r 0)
if (FD_ISSET (fd1, &rd)) {
r =
read (fd1, buf1 + buf1_avail,
BUF_SIZE - buf1_avail);
if (r 0)
if (FD_ISSET (fd2, &rd)) {
r =
read (fd2, buf2 + buf2_avail,
BUF_SIZE - buf2_avail);
if (r 0)
if (FD_ISSET (fd1, &wr)) {
r =
write (fd1,
buf2 + buf2_written,
buf2_avail -
buf2_written);
if (r 0)
if (FD_ISSET (fd2, &wr)) {
r =
write (fd2,
buf1 + buf1_written,
buf1_avail -
buf1_written);
if (r < 1) {
SHUT_FD2;
} else
buf1_written += r;
}
/* проверить, что запись данных получила считанные данные */
if (buf1_written == buf1_avail)
buf1_written = buf1_avail = 0;
if (buf2_written == buf2_avail)
buf2_written = buf2_avail = 0;
/* одна из сторон закрыла соединение, продолжать
записывать, пока другая сторона не закончит */
if (fd1 < 0
&& buf1_avail - buf1_written == 0) {
SHUT_FD2;
}
if (fd2 < 0
&& buf2_avail - buf2_written == 0) {
SHUT_FD1;
}
}
return 0;
}

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

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


ЗАКОН SELECT
Многие из тех, кто пытался использовать select,
сталкивались с поведением, которое трудно понять, и
которое приводила к непереносимым или просто плохим
результатам. Например, вышеприведенная программа тщательно
спланирована так, чтобы ни в каком случае не
блокироваться, хотя для ее файловых дескрипторов не
установлен неблокирующий режим (см. ioctl(2)). Несложно
перечислить неочевидные ошибки, которые лишат всех
преимуществ использования select, поэтому я приведу список
основных моментов, на которые нужно обращать внимание при
использовании select.


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

2. Для повышения эффективности значение n должно быть
правильно вычислено как указано выше.

3. Файловые дескрипторы не должны добавляться в
наборы, если вы не планируете после вызова select
проверить результат и соответсвующим образом
отреагировать.См. следующее правило.

4. После возврата из select должны быть проверены все
файловые дескрипторы во всех наборах. В каждый
дескриптор, готовый к записи, должны быть записаны
данные, и из каждого дескриптора, готового к
чтению, данные должны быть прочитаны, и т.д.

5. Функции read(), recv(), write() и send() не
обязательно считывают/записывают данные в полном
объеме. Такое, конечно, возможно при низком
траффике или быстром потоке, однако происходит
далеко не всегда. Вы должны рассчитывать, что ваши
функции получают/отправляют только один байт за
раз.

6. Никогда не считывайте/записывайте побайтно, если
только вы не асболютно уверены в том, что нужно
обработать небольшой объем данных. Крайне
неэффективно считывать/записывать меньшее
количество байт, чем вы можете поместь в буфер за
один раз. Буферы в вышеприведенном примере имеют
размер 1024 байта, однако могут быть легко
увеличены до максимального размера пакета в вашей
локальной сети.

7. Функции read(), recv(), write() и send(), также как
и select() могут возвратить -1 с errno
установленным в EINTR или EAGAIN (EWOULDBLOCK), что
не является ошибкой. Такие ситуации должны быть
правильно обработаны (в вышеприведенной программе
этого не сделано). Если ваша программа не
собирается принимать сигналы, то маловероятно, что
вы получите EINTR. Если ваша программа не
использует неблокирующий ввод-вывод, то вы не
получите EAGAIN. В любом случае, вы должны
обрабатывать эти ошибки для полноты.

8. Кроме случаев, описанных в 7., функции read(),
recv(), write() и send() никогда не возвращают
значение меньшее единицы, если не произошла ошибка.
Например, read() при работе с каналом, на котором
противоположная сторона завершила работу,
возвращает ноль, но возвращает ноль только один
раз. Если хотя бы одна из этих функций вернула 0
или -1, то вы
не должны больше использовать этот дескриптор. В
примере выше я немедленно закрываю дескриптор и
устанавливаю его в -1 для предотвращения его
включения в набор.

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

10. Я слышал, что сокетный уровень в Windows не
обрабатывает правильно внепоточные данные. Кроме
того, он неправильно работает с select при
отсутствии файловых дескрипторов. Отсутствие
файловых дескрипторов - это полезный способ
перевести процесс в режим ожидания на период меньше
секунды.


ИМИТАЦИЯ USLEEP
В системах, не имеющих функции usleep, вы можете
использовать select с конечной задержкой и без файловых
дескрипторов следующим образом:

struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 200000; /* 0.2 секунды */
select (0, NULL, NULL, NULL, &tv);

Это гарантированно работает только на системах Unix.


ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ

При удачно завершении select возвращает общее число
дескрипторов, которые еще присутствкют в наборах.

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

Значение -1 сообщает об ошибке, при этом errno
устанавливается соответствующим образом. В случае ошибки
содержимое наборов и структуры времени ожидания не
определено и не должно быть использовано. pselect никогда
не изменяет ntimeout.


КОДЫ ОШИБОК
EBADF Набор содержит неправильный дескриптор файла. Эта
ошибка возвращается, если вы включили в набор
файловый дескриптор, уже закрытый функцией close,
или если с файловым дескриптором произошла
какая-либо ошибка. Вы не должны добавлять в наборы
файловые дескрипторы, вернувшие ошибку при чтении
или записи.

EINTR Был получен сигнал, такой как SIGINT или SIGCHLD
или другой. В этом случае необходимо пересоздать
наборы и попробовать еще раз.

EINVAL Значение n отрицательно.

ENOMEM Внутренняя ошибка выделения памяти.


ЗАМЕЧАНИЯ
В общем случае, все операционные системы, поддерживающие
сокеты, поддерживают также и select. Некоторые считают
select экзотической и редко используемой функцией. На
самом деле многие программы без нее становятся чрезвычайно
сложными. select может быть использована для решения задач
переносимым и эффективным способом, вместо которого многие
программисты пытаются использовать подпроцессы, ветвления
процессов, IPC, сигналы, разделение памяти и другие
грязные методы. pselect - это более новая функция,
используемая не так часто.


СООТВЕТСТВИЕ СТАНДАРТАМ

4.4BSD (функция select впервые появилась в 4.2BSD). В
общем случае переносима на/с несовместимые с BSD системы,
поддерживающие сокеты BSD (включая варианты System V).
Однако стоит обратить внимание на то, что варианты в Sys-
tem V обычно меняют переменную времени ожидания перед
выходом, а варианты в BSD этого не делают.

Функция pselect описана в IEEE Std 1003.1g-2000
(POSIX.1g). Она есть в glibc2.1 и более поздних версиях.
В glibc2.0 есть функция с таким именем, но она не имеет
аргумента sigmask.

АВТОРЫ

Эта страница руководства была написана Полом Широм (Paul
Sheer).
Читать новости Linux в Telegram
Linux - select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing
Мы в соцсетях ✉