select_tut(2) - Linux 手册页

名称

select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - 同步 I/O 多路复用

概要

/* 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);
glibc 的功能测试宏要求(参见 feature_test_macros(7))
pselect(): _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600

描述

select()(或 pselect())用于高效监控多个文件描述符,查看其中是否有描述符处于“就绪”状态;即查看是否有 I/O 变为可能,或者描述符上是否发生了“异常情况”。

其主要参数是三个文件描述符“集合”:readfdswritefdsexceptfds。每个集合都声明为 fd_set 类型,其内容可以使用宏 FD_CLR()、FD_ISSET()、FD_SET() 和 FD_ZERO() 进行操作。新声明的集合应首先使用 FD_ZERO() 清空。select() 会根据下述规则修改集合的内容;调用 select() 后,可以使用 FD_ISSET() 宏测试文件描述符是否仍在集合中。如果指定的文件描述符存在于集合中,FD_ISSET() 返回非零值,否则返回零。FD_CLR() 用于从集合中移除文件描述符。

参数

readfds
监控此集合以查看其任何文件描述符是否有数据可供读取。select() 返回后,readfds 中将清除除那些立即可供读取的文件描述符之外的所有文件描述符。
writefds
监控此集合以查看其任何文件描述符是否有空间写入数据。select() 返回后,writefds 中将清除除那些立即可供写入的文件描述符之外的所有文件描述符。
exceptfds
监控此集合是否有“异常情况”。实际上,只有一种常见的异常情况:TCP 套接字上有可供读取的带外 (OOB) 数据。有关 OOB 数据的更多详细信息,请参阅 recv(2)、send(2) 和 tcp(7)。(select(2) 指示异常情况的另一种不太常见的情况发生在处于数据包模式的伪终端中;请参阅 tty_ioctl(4)。)select() 返回后,exceptfds 中将清除除那些发生了异常情况的文件描述符之外的所有文件描述符。
nfds

这是一个整数,其值为所有集合中任何文件描述符的最大值加一。换句话说,在向每个集合添加文件描述符时,必须计算所有描述符中的最大整数值,然后将其加一,并将此值作为 nfds 传递。

utimeout
这是 select() 在返回之前可能等待的最长时间,即使没有发生任何有趣的事情。如果此值作为 NULL 传递,则 select() 将无限期阻塞,直到有文件描述符就绪。utimeout 可以设置为零秒,这将导致 select() 立即返回,并提供调用时文件描述符的就绪状态信息。结构体 struct timeval 的定义如下
struct timeval {
    time_t tv_sec;    /* seconds */
    long tv_usec;     /* microseconds */
};
ntimeout
pselect() 的此参数与 utimeout 具有相同的含义,但 struct timespec 具有纳秒精度,如下所示
struct timespec {
    long tv_sec;    /* seconds */
    long tv_nsec;   /* nanoseconds */
};
sigmask
此参数包含一组信号,内核应在调用者阻塞在 pselect() 调用内部时解除阻塞(即从调用线程的信号屏蔽字中移除)(请参阅 sigaddset(3) 和 sigprocmask(2))。它可以为 NULL,在这种情况下,调用不会修改函数入口和出口处的信号屏蔽字。在这种情况下,pselect() 的行为将与 select() 完全相同。

结合信号和数据事件

如果您正在等待信号以及文件描述符准备好进行 I/O,pselect() 将非常有用。接收信号的程序通常仅使用信号处理程序来设置全局标志。全局标志将指示必须在程序的主循环中处理该事件。信号将导致 select()(或 pselect())调用返回并将 errno 设置为 EINTR。这种行为至关重要,以便可以在程序的主循环中处理信号,否则 select() 将无限期阻塞。现在,主循环中的某处会有一个条件检查全局标志。所以我们必须问:如果信号在条件检查之后但在 select() 调用之前到达会怎样?答案是,即使实际上有一个挂起的事件,select() 也会无限期阻塞。这种竞态条件通过 pselect() 调用得到解决。此调用可用于将信号屏蔽字设置为仅在 pselect() 调用内接收的信号集。例如,假设所讨论的事件是子进程的退出。在主循环开始之前,我们将使用 sigprocmask(2) 阻塞 SIGCHLD。我们的 pselect() 调用将通过使用空信号屏蔽字来启用 SIGCHLD。我们的程序将如下所示
static volatile sig_atomic_t got_SIGCHLD = 0;

static void
child_sig_handler(int sig)
{
    got_SIGCHLD = 1;
}

int
main(int argc, char *argv[])
{
    sigset_t sigmask, empty_mask;
    struct sigaction sa;
    fd_set readfds, writefds, exceptfds;
    int r;

   sigemptyset(&sigmask);
    sigaddset(&sigmask, SIGCHLD);
    if (sigprocmask(SIG_BLOCK, &sigmask, NULL) == -1) {
        perror("sigprocmask");
        exit(EXIT_FAILURE);
    }

   sa.sa_flags = 0;
    sa.sa_handler = child_sig_handler;
    sigemptyset(&sa.sa_mask);
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

   sigemptyset(&empty_mask);

   for (;;) {          /* main loop */
        /* Initialize readfds, writefds, and exceptfds
           before the pselect() call. (Code omitted.) */

       r = pselect(nfds, &readfds, &writefds, &exceptfds,
                    NULL, &empty_mask);
        if (r == -1 && errno != EINTR) {
            /* Handle error */
        }

       if (got_SIGCHLD) {
            got_SIGCHLD = 0;

           /* Handle signalled event here; e.g., wait() for all
               terminated children. (Code omitted.) */
        }

       /* main body of program */
    }
}

实践

那么 select() 的意义何在?难道我就不能随时随地读写我的描述符吗?select() 的意义在于它同时监控多个描述符,并在没有活动时适当地使进程进入睡眠状态。UNIX 程序员经常发现自己不得不处理来自多个文件描述符的 I/O,其中数据流可能是断断续续的。如果你只是创建一系列 read(2) 和 write(2) 调用,你会发现其中一个调用可能会因为等待来自/向文件描述符的数据而阻塞,而另一个文件描述符虽然已准备好进行 I/O,却未被使用。select() 可以有效地应对这种情况。

Select 法则

许多尝试使用 select() 的人会遇到难以理解的行为,并产生不可移植或边缘的结果。例如,上面的程序被仔细编写为不在任何地方阻塞,即使它没有将其文件描述符设置为非阻塞模式。很容易引入细微的错误,从而消除使用 select() 的优势,因此以下是使用 select() 时需要注意的基本事项列表。
1.

你应该总是尝试在没有超时的情况下使用 select()。如果没有数据可用,你的程序应该无事可做。依赖超时的代码通常不可移植且难以调试。

2.

为了效率,必须如上所述正确计算 nfds 的值。

3.

如果你不打算在 select() 调用后检查结果并作出适当响应,则绝不应将任何文件描述符添加到任何集合中。请参阅下一条规则。

4.

select() 返回后,应检查所有集合中的所有文件描述符,查看它们是否就绪。

5.

函数 read(2)、recv(2)、write(2) 和 send(2) *不一定*会读取/写入你请求的全部数据量。如果它们确实读写了全部数量,那是因为你的流量负载较低且数据流较快。情况并非总是如此。你应该处理函数只设法发送或接收单个字节的情况。

6.

除非你确实确定要处理的数据量很小,否则切勿一次仅读写单字节。每次读写尽可能多的缓冲数据是非常有效的。下面示例中的缓冲区为 1024 字节,尽管它们很容易做大。

7.

函数 read(2)、recv(2)、write(2) 和 send(2) 以及 select() 调用可能返回 -1,并将 errno 设置为 EINTR,或将 errno 设置为 EAGAIN (EWOULDBLOCK)。这些结果必须妥善管理(上面做得不够妥善)。如果你的程序不会接收任何信号,那么你不太可能得到 EINTR。如果你的程序没有设置非阻塞 I/O,你就不会得到 EAGAIN

8.

切勿使用零长度的缓冲区调用 read(2)、recv(2)、write(2) 或 send(2)。

9.

如果函数 read(2)、recv(2)、write(2) 和 send(2) 因第 7 点所列以外的错误而失败,或者其中一个输入函数返回 0(表示文件结束),则你不应再将该描述符传递给 select()。在下面的示例中,我立即关闭了描述符,然后将其设置为 -1,以防止它被包含在集合中。

10.

超时值必须在每次调用 select() 时进行初始化,因为某些操作系统会修改该结构。然而,pselect() 不会修改其超时结构。

11.

由于 select() 会修改其文件描述符集,如果该调用在循环中使用,则必须在每次调用之前重新初始化集合。

Usleep 仿真

在没有 usleep(3) 函数的系统上,你可以通过有限的超时且不传递文件描述符来调用 select(),如下所示
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 200000;  /* 0.2 seconds */
select(0, NULL, NULL, NULL, &tv);
然而,这仅保证在 UNIX 系统上有效。

返回值

成功时,select() 返回文件描述符集中仍存在的文件描述符总数。

如果 select() 超时,则返回值将为零。文件描述符集应该全部为空(但在某些系统上可能不是)。

返回值 -1 表示错误,并将 errno 设置为适当的值。如果发生错误,返回的集合的内容和 struct timeout 的内容是未定义的,不应使用。然而,pselect() 从不修改 ntimeout

说明

总的来说,所有支持套接字的操作系统也都支持 select()。select() 可用于以可移植且高效的方式解决许多问题,而这些问题天真的程序员可能会试图通过线程、fork、IPC、信号、内存共享等方式以更复杂的方式来解决。

poll(2) 系统调用具有与 select() 相同的功能,并且在监控稀疏文件描述符集时效率更高。如今它已被广泛使用,但从历史上看,它的可移植性不如 select()。

Linux 特有的 epoll(7) API 提供了一个接口,在监控大量文件描述符时比 select(2) 和 poll(2) 更高效。

示例

下面是一个更能体现 select() 真正用途的示例。下面的列表是一个 TCP 转发程序,它将数据从一个 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)) == -1) {
        perror("socket");
        return -1;
    }
    yes = 1;
    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
            &yes, sizeof(yes)) == -1) {
        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)) == -1) {
        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)) == -1) {
        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)) == -1) {
        perror("connect()");
        shutdown(s, SHUT_RDWR);
        close(s);
        return -1;
    }
    return s;
}

#define SHUT_FD1 do {                                \
                     if (fd1 >= 0) {                 \
                         shutdown(fd1, SHUT_RDWR);   \
                         close(fd1);                 \
                         fd1 = -1;                   \
                     }                               \
                 } while (0)

#define SHUT_FD2 do {                                \
                     if (fd2 >= 0) {                 \
                         shutdown(fd2, SHUT_RDWR);   \
                         close(fd2);                 \
                         fd2 = -1;                   \
                     }                               \
                 } while (0)

#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 == -1)
        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 == -1) {
            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 == -1) {
                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 == -1)
                    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;

               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;

               r = recv(fd2, &c, 1, MSG_OOB);
                if (r < 1)
                    SHUT_FD2;
                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) 调用并为每个流分配一个线程会更有效。这比你想象的要棘手得多。另一个想法是使用 fcntl(2) 设置非阻塞 I/O。这也存在问题,因为最终会使用效率低下的超时。

该程序一次不能处理超过一个并发连接,尽管可以通过缓冲区链表(每个连接一个)轻松扩展以处理多个连接。目前,新连接会导致当前连接被丢弃。

参见

accept(2), connect(2), ioctl(2), poll(2), read(2), recv(2), select(2), send(2), sigprocmask(2), write(2), sigaddset(3), sigdelset(3), sigemptyset(3), sigfillset(3), sigismember(3), epoll(7)