select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing
/* According to POSIX.1-2001 */
#include <sys/select.h> /* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *utimeout); void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set); #include <sys/select.h> int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *ntimeout, const sigset_t *sigmask);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
pselect(): _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600
In summary, select() just watches multiple file descriptors, and is the standard Unix call to do so.
The arrays of file descriptors are called file descriptor sets. Each set is declared as type fd_set, and its contents can be altered with the macros FD_CLR(), FD_ISSET(), FD_SET(), and FD_ZERO(). FD_ZERO() is usually the first function to be used on a newly declared set. Thereafter, the individual file descriptors that you are interested in can be added one by one with FD_SET(). select() modifies the contents of the sets according to the rules described below; after calling select() you can test if your file descriptor is still present in the set with the FD_ISSET() macro. FD_ISSET() returns non-zero if the descriptor is present and zero if it is not. FD_CLR() removes a file descriptor from the set.
struct timeval {
time_t tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
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(nfds, &rd, &wr, &er, 0, &orig_sigmask);
/* main body of program */
}
}
A simple example of the use of select() can be found in the select(2) manual page.
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 200000; /* 0.2 seconds */
select(0, NULL, NULL, NULL, &tv);
This is only guaranteed to work on Unix systems, however.
If select() timed out, then the return value will be zero. The file descriptors set should be all empty (but may not be on some systems).
A return value of -1 indicates an error, with errno being set appropriately. In the case of an error, the contents of the returned sets and the struct timeout contents are undefined and should not be used. pselect() however never modifies ntimeout.
The poll(2) system call has the same functionality as select(), and is somewhat more efficient when monitoring sparse file descriptor sets. It is nowadays widely available, but historically was less portable than select().
The Linux-specific epoll(7) API provides an interface that is more efficient than select(2) and poll(2) when monitoring large numbers of file descriptors.
#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", 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(EXIT_FAILURE);
}
signal(SIGPIPE, SIG_IGN);
forward_port = atoi(argv[2]);
h = listen_socket(atoi(argv[1]));
if (h < 0)
exit(EXIT_FAILURE);
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(EXIT_FAILURE);
}
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: read oob data before normal reads */
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;
}
/* check if write data has caught read data */
if (buf1_written == buf1_avail)
buf1_written = buf1_avail = 0;
if (buf2_written == buf2_avail)
buf2_written = buf2_avail = 0;
/* one side has closed the connection, keep
writing to the other side until empty */
if (fd1 < 0 && buf1_avail - buf1_written == 0) {
SHUT_FD2;
}
if (fd2 < 0 && buf2_avail - buf2_written == 0) {
SHUT_FD1;
}
}
exit(EXIT_SUCCESS);
}
The above program properly forwards most kinds of TCP connections including OOB signal data transmitted by telnet servers. It handles the tricky problem of having data flow in both directions simultaneously. You might think it more efficient to use a fork(2) call and devote a thread to each stream. This becomes more tricky than you might suspect. Another idea is to set non-blocking I/O using an ioctl(2) call. This also has its problems because you end up using inefficient timeouts.
The program does not handle more than one simultaneous connection at a time, although it could easily be extended to do this with a linked list of buffers --- one for each connection. At the moment, new connections cause the current connection to be dropped.
|
Закладки на сайте Проследить за страницей |
Created 1996-2025 by Maxim Chirkov Добавить, Поддержать, Вебмастеру |