int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *utimeout);
int pselect(int nfds, 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 просто следит за несколькими файловыми дескрипторами и является стандартным вызовом Unix для этих целей.
Массивы файловых дескрипторов называются наборами файловых дескрипторов. Каждый набор объявлен, как тип fd_set и его содержимое может быть изменено макросами FD_CLR, FD_ISSET, FD_SET и FD_ZERO. Обычно FD_ZERO является первой функцией, используемой со свежеобъявленным набором. После этого, отдельные файловые дескрипторы могут быть по-очереди добавлены с помощью FD_SET. select изменяет содержимое наборов в соответсвие с правилами, описанными ниже; после вызова select вы можете проверить, находится ли ваш файловый дескриптор все еще в наборе с помощью макроса FD_ISSET, возвращающей ненулевое значение, если дескриптор присутствует в наборе, и ноль, если не присутствует. FD_CLR удаляет файловый дескриптор из набора, хотя практическая ценность этого в хорошей программе сомнительна.
struct timeval { time_t tv_sec; /* секунды */ long tv_usec; /* микросекунды */ };
struct timespec { long tv_sec; /* секунды */ long tv_nsec; /* наносекунды */ };
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 (nfds, &rd, &wr, &er, 0, &orig_sigmask); /* главная часть программы */ } }
Обратите внимание, что вышеуказанный вызов pselect может быть заменен:
sigprocmask (SIG_BLOCK, &orig_sigmask, 0); r = select (nfds, &rd, &wr, &er, 0); sigprocmask (SIG_BLOCK, &sigmask, 0);
но в этом случае все равно существует вероятность того, что сигнал будет получен после первого вызова sigprocmask, но до вызова select. Если вы все же решите сделать так, то разумно, как минимум, установить конечное время ожидания, чтобы процесс не блокировался. В настоящее время glibc работает таким образом. Ядро Linux не имеет встроенного вызова pselect.
Итак, какой прок от использования select? Разве нельзя просто считывать и записывать данные в файловые дескрипторы когда того захочется? Смысл использования select в том, что он следит за несколькими дескрипторами одновременно и корректно переводит процесс в режим ожидания, когда активности не наблюдается. Таким образом он позволяет вам одновременно обрабатывать несколько каналов и сокетов. Программисты Unix часто попадают в ситуацию, когда необходимо обработать ввод-вывод с более чем одного файловго дескриптора в то время как поток данных может быть неравномерным. Если вы создатите последовательность вызовов read и write, то вы можете попасть в ситуацию, когда один из вызовов будет ожидать данные из/в файлового дескриптора, в то время как другой будет простаивать, хотя данные для него уже появились. select позволяет эффективно справиться с такой ситуацией.
Классический пример использования 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; /* Следим ввели ли что-либо в 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 на другой.
#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 ("ожидание соединений на порту %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 ("неправильный формат адрес IP"); 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, "Использование\n\tfwd <слушаемый-порт> \ <порт-куда-перенаправлять> <IP-адрес-куда-перенаправлять>\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, nfds = 0; fd_set rd, wr, er; FD_ZERO (&rd); FD_ZERO (&wr); FD_ZERO (&er); FD_SET (h, &rd); nfds = max (nfds, h); if (fd1 > 0 && buf1_avail < BUF_SIZE) { FD_SET (fd1, &rd); nfds = max (nfds, fd1); } if (fd2 > 0 && buf2_avail < BUF_SIZE) { FD_SET (fd2, &rd); nfds = max (nfds, fd2); } if (fd1 > 0 && buf2_avail - buf2_written > 0) { FD_SET (fd1, &wr); nfds = max (nfds, fd1); } if (fd2 > 0 && buf1_avail - buf1_written > 0) { FD_SET (fd2, &wr); nfds = max (nfds, fd2); } if (fd1 > 0) { FD_SET (fd1, &er); nfds = max (nfds, fd1); } if (fd2 > 0) { FD_SET (fd2, &er); nfds = max (nfds, fd2); } r = select (nfds + 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: считать внепоточные данные перед обычными */ 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, включая внепоточные данные, передаваемые серверами telnet. Она справляется со сложной проблемой поддержания одновременного двустороннего обмена данными. Возможно, вы решите, что эффективнее использовать fork() и выделить отдельный подпроцесс для каждого потока. На самом деле это сложнее, чем кажется. Другой идеей может быть использование неблокирующего ввода-вывода с помощью ioctl(). Это также может вызвать проблемы из за того, что придется использовать неэффективные таймауты.
Программа не обрабатывает более одного соединения, однако она может быть легко добработана для этого с путем добавления связанного списка буферов - по одному на каждое соединение. В данный момент новые соединения приводят к закрытию текущего.
Многие из тех, кто пытался использовать select, сталкивались с поведением, которое трудно понять, и которое приводила к непереносимым или просто плохим результатам. Например, вышеприведенная программа тщательно спланирована так, чтобы ни в каком случае не блокироваться, хотя для ее файловых дескрипторов не установлен неблокирующий режим (см. ioctl(2)). Несложно перечислить неочевидные ошибки, которые лишат всех преимуществ использования select, поэтому я приведу список основных моментов, на которые нужно обращать внимание при использовании 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.
Системный вызов poll(2) имеет такую же функциональность, как и select, но с более грубым поведением. Он менее портируем, чем select.
Функция pselect описана в IEEE Std 1003.1g-2000 (POSIX.1g). Она есть в glibc2.1 и более поздних версиях. В glibc2.0 есть функция с таким именем, но она не имеет аргумента sigmask.
Закладки на сайте Проследить за страницей |
Created 1996-2024 by Maxim Chirkov Добавить, Поддержать, Вебмастеру |