/* POSIX.1-2001 に従う場合 */
#include <sys/select.h> /* 以前の規格に従う場合 */
#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 *timeout); 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 *timeout, const sigset_t *sigmask);
glibc 向けの機能検査マクロの要件 (feature_test_macros(7) 参照):
pselect(): _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600
要約すると、 select() は複数のファイルディスクリプタの監視を行うだけのもので、 それを行うための標準的な Unix コールである。
ファイルディスクリプタの配列は ファイルディスクリプタ集合 (file descriptor set) と呼ばれる。各々の集合は fd_set として宣言され、その内容は FD_CLR(), FD_ISSET(), FD_SET(), FD_ZERO() といったマクロによって変更できる。 新しいディスクリプタ集合に対しては、通常 FD_ZERO() を最初に使う。 その後で、監視対象としたいファイルディスクリプタを一つ一つ FD_SET() で追加する。 select() はこれらの集合の内容を、以降に述べる規則に従って修正する。 select() を呼んだ後、ファイルディスクリプタがまだ集合に存在しているかどうかは、 FD_ISSET() マクロによって調べることができる。 FD_ISSET() はそのディスクリプタが集合に存在していれば 0 以外の値を返し、 存在していなければ 0 を返す。 FD_CLR() は集合からのファイルディスクリプタの削除を行う。
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 */ } }
select() を使った簡単な例が select(2) のマニュアルページに書かれている。
struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 200000; /* 0.2 seconds */ select(0, NULL, NULL, NULL, &tv);
但し、これが動くと保証されているのは Unix システムに限られる。
select() がタイムアウトすると、返り値は 0 になる。 その時、ファイルディスクリプタ集合はすべて空である (しかしそうならないシステムもある)。
返り値が -1 の場合はエラーを意味し、 errno が適切にセットされる。エラーが起こった場合、 返された集合の内容や構造体 struct timeout の内容は 未定義となっており、使用すべきではない。 しかし pselect() は決して ntimeout を変更しない。
poll(2) システムコールは select() と同じ機能を持っており、 まばらなファイルディスクリプタ集合を監視する場合に いくらか効率がよい。 現在では広く利用可能であるが、以前は select() より移植性の面で劣っていた。
Linux 独自の epoll(7) API は、多数のファイルディスクリプタを監視する場合に select(2) や poll(2) よりも効率的なインタフェースを提供している。
#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); }
上記のプログラムは、ほとんどの種類の TCP 接続をフォワードする。 telnet サーバによって中継される OOB シグナルデータも扱える。 このプログラムは、データフローを双方向に同時に送るという、 ややこしい問題も処理できる。 fork(2) コールを使って、各ストリームごとに専用のスレッドを用いるほうが効率的だ、 という人もいるかもしれない。しかし、これは考えているよりずっとややこしい。 あるいは、 ioctl(2) コールを使って非ブロック I/O をセットすれば良い、というアイデアもあるだろう。 これにも実際には問題があり、タイムアウトが非効率的に起こってしまう。
このプログラムは一度にひとつ以上の同時接続を扱うことはできないが、 その様に拡張するのは簡単で、バッファのリンクリストを (接続ごとにひとつずつ) 使えばよい。 現時点のものでは、新しい接続がくると古い接続は落ちてしまう。