Приложения:
Проекты:
Документация:
|
/Главная/Документация/select_tut2.html
SELECT_TUT(2) Руководство
программиста LINUX SELECT_TUT(2)
Имя
select, pselect, FD_CLR, FD_ISSET, FD_SET,
FD_ZERO - синхронное мультиплексирование ввода/вывода
Использование
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
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) - это ключевая функция
для программ на С, обрабатывающих несколько файловых дескрипторов (или
сокетов) одновременно. Три массива дескрипторов являются основными
аргументами функции: readfds, writefds, и exceptfds. Обычно select используется для ожидания
"изменения статуса" у одного или нескольких файловых дескрипторов.
"Изменение статуса" происходит когда в дескрипторе появляются данные
для чтения, или когда во внутренних буферах ядра появляется место и в
файловый дескриптор может быть произведена запись, или когда в
дескрипторе происходит ошибка (в случае сокета или канала pipe такая
ошибка возникает при закрытии другого участника соединения).
Говоря коротко, select
просто следит за множеством файловых дескрипторов, и является
стандарным для этого средством в UNIX.
Массивы файловых дескрипторов называются наборами
дескрипторов. Каждый набор обьявлен как fd_set, и его содержимое может
быть изменено с помощью макросов FD_CLR, FD_ISSET, FD_SET и FD_ZERO.
После декларирования набора дескрипторов обычно сначала вызывают
FD_ZERO. Далее, с помощью макро FD_SET можно по-одному добавлять
файловые дескрипторы. select
изменяет содержимое наборов согласно описанным ниже правилам;
после вызова select вы можете
протестировать ваш дескриптор с помошью макро FD_ISSET. FD_ISSET
возвращает ненулевой результат в случае если файловый дескриптор
существует, или нулевой результат в противном случае. Макро FD_CLR
удаляет файловый дескриптор из набора (в правильно написанной программе
надобности в этом макро быть не должно).
Аргументы
readfds
Этот набор мониторится на наличие данных для чтения в одном или
нескольких дескрипторах. После возврата из select набор readfs будет очищен от
всех дескрипторов, кроме тех, в которых есть данные, доступные для
немедленного чтения функциями recv() (для сокетов) или read() (для
каналов pipe, файлов и сокетов).
writefds
Этот набор мониторится на возможность записи данных в один или
несколько дескрипторов. После возврата из select набор readfs будет
очищен от всех дескрипторов, кроме тех, в которые можно немедленно
записать данные функциями send() (для сокетов) или write() (для каналов
pipe, файлов и сокетов).
exceptfds
Этот набор мониторится на предмет наличия ошибок или возникновения
исключительных ситуаций в одном или нескольких дескрипторах. На
практике набор exceptfds используется, в основном, в сокетах для
обработки Out-Of-Band данных (т. е. пакетов в TCP, переданных с
нарушением последовательности). Эти данные передаются с установленным
флагом MSG_OOB, поэтому набор exceptfds может использоваться только с
сокетами. Подробнее об этом смотрите в man-ах на recv(2) и send(2).
После возврата из select набор
exceptfds будет очищен от всех дескрипторов, кроме тех, из которых
могут быть немедленно прочитаны данные Out-Of-Band. Вы можете прочитать
только один байт OOB-данных (функцией recv()), запись OOB-данных может
быть произведена в любое время и не будет блокироваться. По этой
причине нет необходимости иметь четвертый набор дескрипторов для
проверки возможности записи OOB-данных.
n
Этот параметр должен превышать не единицу максимальный файловый
дескриптор в любом из наборов. Другими словами, вы должны определить
максимальное целое значение для всех ваших дескрипторов, увеличить его
на 1, и передать результат в качестве параметра n.
utimeout
Максимальное время, в течение которого select
будет ожидать смены статуса. Если этот параметр установлен в NULL, select будет заблокирован
бесконечно, ожидая событий в дескрипторах. При установке параметра в 0
секунд select возвратится
немедленно. Структура struct timeval
определена следующим образом:
struct timeval {
time_t 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 можно замаскировать сигналы,
которые нельзя получать нигде, кроме как внутри 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 (;;) { /* main loop */ for (; child_events > 0; child_events--) { /* do event work here */ } r = pselect (n, &rd, &wr, &er, 0, &orig_sigmask);
/* main body of program */ } }
Обратите внимание, что вызов 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
позволяет эффективно решить эту проблему.
Классический пример из man-а по select:
#include <stdio.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h>
int main(void) { fd_set rfds; struct timeval tv; int retval;
/* Watch stdin (fd 0) to see when it has input. */ FD_ZERO(&rfds); FD_SET(0, &rfds); /* Wait up to five seconds. */ tv.tv_sec = 5; tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv); /* Don't rely on the value of tv now! */
if (retval) printf("Data is available now.\n"); /* FD_ISSET(0, &rfds) will be true. */ else printf("No data within five seconds.\n");
exit(0); }
Пример перенаправления TCP-портов
Рассмотрим более показательный пример, иллюстрирующий удобство
использования select. Код,
приведенный далее - программа перенаправления TCP-соединения с одного
порта на другой.
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/time.h> #include <sys/types.h> #include <string.h> #include <signal.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h>
static int forward_port;
#undef max #define max(x,y) ((x) > (y) ? (x) : (y))
static int listen_socket (int listen_port) { struct sockaddr_in a; int s; int yes; if ((s = socket (AF_INET, SOCK_STREAM, 0)) < 0) { perror ("socket"); return -1; } yes = 1; if (setsockopt (s, SOL_SOCKET, SO_REUSEADDR, (char *) &yes, sizeof (yes)) < 0) { perror ("setsockopt"); close (s); return -1; } memset (&a, 0, sizeof (a)); a.sin_port = htons (listen_port); a.sin_family = AF_INET; if (bind (s, (struct sockaddr *) &a, sizeof (a)) < 0) { perror ("bind"); close (s); return -1; } printf ("accepting connections on port %d\n", (int) listen_port); listen (s, 10); return s; }
static int connect_socket (int connect_port, char *address) { struct sockaddr_in a; int s; if ((s = socket (AF_INET, SOCK_STREAM, 0)) < 0) { perror ("socket"); close (s); return -1; }
memset (&a, 0, sizeof (a)); a.sin_port = htons (connect_port); a.sin_family = AF_INET;
if (!inet_aton (address, (struct in_addr *) &a.sin_addr.s_addr)) { perror ("bad IP address format"); close (s); return -1; }
if (connect (s, (struct sockaddr *) &a, sizeof (a)) < 0) { perror ("connect()"); shutdown (s, SHUT_RDWR); close (s); return -1; } return s; }
#define SHUT_FD1 { \ if (fd1 >= 0) { \ shutdown (fd1, SHUT_RDWR); \ close (fd1); \ fd1 = -1; \ } \ }
#define SHUT_FD2 { \ if (fd2 >= 0) { \ shutdown (fd2, SHUT_RDWR); \ close (fd2); \ fd2 = -1; \ } \ }
#define BUF_SIZE 1024
int main (int argc, char **argv) { int h; int fd1 = -1, fd2 = -1; char buf1[BUF_SIZE], buf2[BUF_SIZE]; int buf1_avail, buf1_written; int buf2_avail, buf2_written;
if (argc != 4) { fprintf (stderr, "Usage\n\tfwd <listen-port> \ <forward-to-port> <forward-to-ip-address>\n"); exit (1); }
signal (SIGPIPE, SIG_IGN);
forward_port = atoi (argv[2]);
h = listen_socket (atoi (argv[1])); if (h < 0) exit (1);
for (;;) { int r, n = 0; fd_set rd, wr, er; FD_ZERO (&rd); FD_ZERO (&wr); FD_ZERO (&er); FD_SET (h, &rd); n = max (n, h); if (fd1 > 0 && buf1_avail < BUF_SIZE) { FD_SET (fd1, &rd); n = max (n, fd1); } if (fd2 > 0 && buf2_avail < BUF_SIZE) { FD_SET (fd2, &rd); n = max (n, fd2); } if (fd1 > 0 && buf2_avail - buf2_written > 0) { FD_SET (fd1, &wr); n = max (n, fd1); } if (fd2 > 0 && buf1_avail - buf1_written > 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) { SHUT_FD1; } else printf ("connect from %s\n", inet_ntoa (client_address.sin_addr)); } } /* NB: читать OOB-данные перед чтением нормальных данных */ if (fd1 > 0) if (FD_ISSET (fd1, &er)) { char c; errno = 0; r = recv (fd1, &c, 1, MSG_OOB); if (r < 1) { SHUT_FD1; } else send (fd2, &c, 1, MSG_OOB); } if (fd2 > 0) if (FD_ISSET (fd2, &er)) { char c; errno = 0; r = recv (fd2, &c, 1, MSG_OOB); if (r < 1) { SHUT_FD1; } else send (fd1, &c, 1, MSG_OOB); } if (fd1 > 0) if (FD_ISSET (fd1, &rd)) { r = read (fd1, buf1 + buf1_avail, BUF_SIZE - buf1_avail); if (r < 1) { SHUT_FD1; } else buf1_avail += r; } if (fd2 > 0) if (FD_ISSET (fd2, &rd)) { r = read (fd2, buf2 + buf2_avail, BUF_SIZE - buf2_avail); if (r < 1) { SHUT_FD2; } else buf2_avail += r; } if (fd1 > 0) if (FD_ISSET (fd1, &wr)) { r = write (fd1, buf2 + buf2_written, buf2_avail - buf2_written); if (r < 1) { SHUT_FD1; } else buf2_written += r; } if (fd2 > 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-соединений, включая сигнальные OOB-данные,
передаваемые серверами telnet.
Она решает сложную проблему поддержки двунаправленного потока данных.
Вы можете возразить, что более эффективно было-бы использовать вызов
fork() и создавать нити (threads) для каждого направления. Но такой
подход более сложен, чем кажеться. Другая идея - установить
неблокирующий режим ввода-вывода функцией ioctl(). У этого метода тоже
есть свои недостатки, так как вам в конечном итоге придется
использовать неэффективные таймауты.
Программа поддердивает только одно соединение в
каждый момент времени, хотя это может быть легко изменено
использованием связанного списка буферов - по одному на каждое
соединение. В данной версии каждое новое соединение приводит к обрыву
существующего.
Правила использования SELECT
Многие, кто пытался использовать select,
пришли к заключению, что все это с трудом поддается пониманию, а
программы получаются слабопереносимые и зависающие. Но приведенный выше
пример написан таким образом, что программа нигде не заблокируется даже
несмотря на то, что там вообще не используется неблокирующий
ввод-вывод. Довольно легко допустить ошибки, сводящие на нет
преимущества функции select,
поэтому я представил далее список важнейших правил, которых следует
придерживаться при использовании select.
1. Старайтесь всегда использовать select
без указания таймаута. Ваша программа ничего не должна делать при
отсутсвии данных. Код, зависящий от таймаутов, обычно непереносим и
труден для трассировки.
2. Значение n должно быть правильно вычислено для достижения
максимальной эффективности (см выше).
3. Не следует добавлять дескрипторы в наборы, если после вызова select вы не собираетесь проверять
дескрипторы и соответственно их обрабатывать. См. следующее правило.
4. После вызова select
необходимо проверить все дескрипторы во всех наборах. Если дескриптор
готов для записи, в него надо обязательно записать. Если дескриптор
готов для чтения, данные необходимо прочитать, и т.д,.
5. Функции read(), recv(), write(), and send() необязательно
пишут/читают то количество данных, которое вы указали. Если они
передают указанные данные полностью, то это лишь потому, что у вас
небольшой трафик и быстрый канал. Так бывает не всегда. Вам необходимо
убедится, что ваша программа работает даже если передача осуществляется
только по 1 байту.
6. Используйте однобайтовое чтение/запись только если вы абсолютно
уверены в том, что у вас немного данных. Чтение/запись малыми порциями
крайне неэффективна. В примере выше буферы имеют размер 1024 байта,
хотя они могут быть легко увеличены до максимального размера пакета в
вашей локальной сети.
7. Функции read(), recv(), write(), send() и select() могут
вернуть -1 с переменной errno установленной в EINTR или EAGAIN
(EWOULDBLOCK), что не является ошибкой. Эти ситуации должны быть
обработаны соответственно, что не сделано в прведенном выше примере.
Если ваша программа не собирается принимать сигналы, то маловероятно,
что она получит и EINTR. Если ваша программа не использует
неблокирующий режим ввода/вывода, то она никогда не получит сигнал
EAGAIN. Несмотря на это, ваша программа должна быть способна
обрабатывать и такие ситуации.
8. Никогда не вызывайте read(), recv(), write() и send() с буфером
нулевой длины.
9. Функции read(), recv(), write() или send() не возвращают значение,
меньше 1, если не возникла ошибка. Например, функция чтения read() из
канала pipe, процесс на другом конце которого завершился, возвратит 0
(дальнейший вызов read или write возвратит -1). В случае возврата 0 или
-1 при операции над дескриптором, этот дескриптор нельзя более
использовать с функцией select.
В примере выше дескриптор немедленно закрывается, после чего
устанавливается в -1 чтобы предотвратить его дальнейшее использование в
наборе дескрипторов.
10. Структура таймаута должна быть инициализирована перед каждым
вызовом select, так как
некоторые операционные системы ее модифицируют. Для функции pselect это делать необязательно, pselect не изменяет содержимое этой
структуры.
11. Библиотеки сокетов в Windows имеют проблемы при обработке
OOB-данных, кроме того select
там не работает при пустых наборах дескрипторов. Вызов 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 завершился по
таймауту, все дескрипторы в наборах должны быть пусты (что может не
выполняться на некоторых системах). Однако возращаемое значение будет в
этом случае равно 0.
При ошибке select возвращает
-1 и устанавливает соответствующее значение в errno. В случае ошибки
значения всех наборов и структур таймаутов неопределены, и не могут
быть нигде использованы. pselect
не модифицирует структуру таймаута.
Ошибки
EBADF
Неправильный дескриптор в одном из наборов. Такая ошибка чаще всего
возникает при добавлении к набору дескриптора, над которым к тому
времени была выполнена операция закрытия (close), или если до этого там
произошла какая-либо ошибка. Поэтому вам не следует добавлять к наборам
дескриптор, если в нем была ошибка чтения или записи.
EINTR
Пойман сигнал прерывания типа SIGINT, SIGCHLD и т.п. В этом случае вам
необходимо построить заново ваши наборы дескрипторов и попробовать
снова.
EINVAL
Возникает при отрицательном параметре n.
ENOMEM
Внутренняя ошибка резервирования памяти.
Замечания
Вообще говоря, все системы, поддерживающие сокеты, должны также
поддерживать и select.
Некоторые люди считают select
малоизвестной и редкоиспользуемой функцией. На самом деле, многи
программы становятся неоправданно сложными, так как не используют select. Функция select позволяет эффективно решить
многие проблемы, которые часто решаются с использованием нитей threads,
функций fork, механизмов межпроцессного взаимодействия IPC, сигналов,
разделяемой памяти и прочих сложных методов.
Вызов pselect появился
относительно недавно и используется реже.
Системный вызов poll делает то же самое, что и select, но менее элегантно. Кроме
того, эта функция менее переносима.
Соответствие стандартам
4.4BSD (функция select впервые
появилась в 4.2BSD). Функция переносима с не-BSD системами,
поддерживающими BSD-сокеты (включая различные вресии System V). Однако
следует помнить, что вариант в System V обычно устанавливает таймаут
перед выходом, в то время как варианты BSD этого не делают.
Функция pselect определена в
стандарте IEEE Std 1003.1g-2000 (POSIX.1g). Её можно найти в
glibc начиная с версии 2.1. Функция с тем же именем в glibc2.0 не
имеет параметра sigmask.
Смотрите также:
accept(2), connect(2), ioctl(2), poll(2), read(2), recv(2), select(2),
send(2), sigaddset(3),
sigdelset(3),
sigemptyset(3),
sigfillset(3),
sigismember(3),
sigprocmask(2),
write(2)
Авторы
This man page was written by Paul Sheer.
Перевод А. Максимова.
Linux 2.4 October 21, 2001 SELECT_TUT(2)
|