另外参见

accept(2),connect(2),poll(2),read(2),recv(2),select(2),send(2),sigprocmask(2),write(2),epoll(7)

示例

这是一个更好地说明select()的实用程序的示例。下面的清单是一个TCP转发程序,可以从一个TCP端口转发到另一个。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/select.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 addr;
    int lfd;
    int yes;

    lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1) {
        perror("socket");
        return -1;
    }

    yes = 1;
    if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,
            &yes, sizeof(yes)) == -1) {
        perror("setsockopt");
        close(lfd);
        return -1;
    }

    memset(&addr, 0, sizeof(addr));
    addr.sin_port = htons(listen_port);
    addr.sin_family = AF_INET;
    if (bind(lfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
        perror("bind");
        close(lfd);
        return -1;
    }

    printf("accepting connections on port %d\n", listen_port);
    listen(lfd, 10);
    return lfd;
}

static int
connect_socket(int connect_port, char *address)
{
    struct sockaddr_in addr;
    int cfd;

    cfd = socket(AF_INET, SOCK_STREAM, 0);
    if (cfd == -1) {
        perror("socket");
        return -1;
    }

    memset(&addr, 0, sizeof(addr));
    addr.sin_port = htons(connect_port);
    addr.sin_family = AF_INET;

    if (!inet_aton(address, (struct in_addr *) &addr.sin_addr.s_addr)) {
        fprintf(stderr, "inet_aton(): bad IP address format\n");
        close(cfd);
        return -1;
    }

    if (connect(cfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
        perror("connect()");
        shutdown(cfd, SHUT_RDWR);
        close(cfd);
        return -1;
    }
    return cfd;
}

#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 = 0, buf1_written = 0;
    int buf2_avail = 0, buf2_written = 0;

    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 ready, nfds = 0;
        ssize_t nbytes;
        fd_set readfds, writefds, exceptfds;

        FD_ZERO(&readfds);
        FD_ZERO(&writefds);
        FD_ZERO(&exceptfds);
        FD_SET(h, &readfds);
        nfds = max(nfds, h);

        if (fd1 > 0 && buf1_avail < BUF_SIZE)
            FD_SET(fd1, &readfds);
            /* Note: nfds is updated below, when fd1 is added to
               exceptfds. */
        if (fd2 > 0 && buf2_avail < BUF_SIZE)
            FD_SET(fd2, &readfds);

        if (fd1 > 0 && buf2_avail - buf2_written > 0)
            FD_SET(fd1, &writefds);
        if (fd2 > 0 && buf1_avail - buf1_written > 0)
            FD_SET(fd2, &writefds);

        if (fd1 > 0) {
            FD_SET(fd1, &exceptfds);
            nfds = max(nfds, fd1);
        }
        if (fd2 > 0) {
            FD_SET(fd2, &exceptfds);
            nfds = max(nfds, fd2);
        }

        ready = select(nfds + 1, &readfds, &writefds, &exceptfds, NULL);

        if (ready == -1 && errno == EINTR)
            continue;

        if (ready == -1) {
            perror("select()");
            exit(EXIT_FAILURE);
        }

        if (FD_ISSET(h, &readfds)) {
            socklen_t addrlen;
            struct sockaddr_in client_addr;
            int fd;

            addrlen = sizeof(client_addr);
            memset(&client_addr, 0, addrlen);
            fd = accept(h, (struct sockaddr *) &client_addr, &addrlen);
            if (fd == -1) {
                perror("accept()");
            } else {
                SHUT_FD1;
                SHUT_FD2;
                buf1_avail = buf1_written = 0;
                buf2_avail = buf2_written = 0;
                fd1 = fd;
                fd2 = connect_socket(forward_port, argv[3]);
                if (fd2 == -1)
                    SHUT_FD1;
                else
                    printf("connect from %s\n",
                            inet_ntoa(client_addr.sin_addr));

                /* Skip any events on the old, closed file
                   descriptors. */

                continue;
            }
        }

        /* NB: read OOB data before normal reads */

        if (fd1 > 0 && FD_ISSET(fd1, &exceptfds)) {
            char c;

            nbytes = recv(fd1, &c, 1, MSG_OOB);
            if (nbytes < 1)
                SHUT_FD1;
            else
                send(fd2, &c, 1, MSG_OOB);
        }
        if (fd2 > 0 && FD_ISSET(fd2, &exceptfds)) {
            char c;

            nbytes = recv(fd2, &c, 1, MSG_OOB);
            if (nbytes < 1)
                SHUT_FD2;
            else
                send(fd1, &c, 1, MSG_OOB);
        }
        if (fd1 > 0 && FD_ISSET(fd1, &readfds)) {
            nbytes = read(fd1, buf1 + buf1_avail,
                      BUF_SIZE - buf1_avail);
            if (nbytes < 1)
                SHUT_FD1;
            else
                buf1_avail += nbytes;
        }
        if (fd2 > 0 && FD_ISSET(fd2, &readfds)) {
            nbytes = read(fd2, buf2 + buf2_avail,
                      BUF_SIZE - buf2_avail);
            if (nbytes < 1)
                SHUT_FD2;
            else
                buf2_avail += nbytes;
        }
        if (fd1 > 0 && FD_ISSET(fd1, &writefds) && buf2_avail > 0) {
            nbytes = write(fd1, buf2 + buf2_written,
                       buf2_avail - buf2_written);
            if (nbytes < 1)
                SHUT_FD1;
            else
                buf2_written += nbytes;
        }
        if (fd2 > 0 && FD_ISSET(fd2, &writefds) && buf1_avail > 0) {
            nbytes = write(fd2, buf1 + buf1_written,
                       buf1_avail - buf1_written);
            if (nbytes < 1)
                SHUT_FD2;
            else
                buf1_written += nbytes;
        }

        /* 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。这也有其问题,因为您最终会使用无效的超时。

该程序一次不能处理多个同时连接,尽管可以很容易地扩展它以使用链接的缓冲区列表来进行此操作-每个连接一个。此刻,新连接导致当前连接断开。

备注

一般而言,所有支持套接字的操作系统也都支持select()。 select()可用于以可移植且有效的方式解决许多问题,而天真的程序员尝试使用线程,分支,IPC,信号,内存共享等以更复杂的方式解决这些问题。

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

特定于Linux的epoll(7)API提供的接口在监视大量文件描述符时比select(2)和poll(2)更有效。

说明

select()和pselect()系统调用用于有效监视多个文件描述符,以查看它们中的任何一个是否为"就绪";也就是说,查看是否有可能进行I / O操作,或者在任何文件描述符上是否发生了"异常情况"。

该页面提供有关使用这些系统调用的背景和教程信息。有关select()和pselect()的参数和语义的详细信息,请参见select(2)。

Combining signal and data events

如果您正在等待信号以及文件描述符准备好进行I / O操作,则pselect()很有用。接收信号的程序通常仅使用信号处理程序来引发全局标志。全局标志将指示必须在程序的主循环中处理事件。信号将导致将errno设置为EINTR的情况下返回select()(或pselect())调用。此行为至关重要,因此可以在程序的主循环中处理信号,否则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 */
    }
}

Practical

那么select()有什么意义呢?我是否可以随时随地读写文件描述符? select()的要点是它可以同时监视多个描述符,并且如果没有活动,则可以使进程正常进入睡眠状态。 UNIX程序员经常发现自己处于必须处理多个文件描述符中的I / O的位置,在这些描述符中数据流可能是断断续续的。如果仅创建一系列read(2)和write(2)调用,您会发现其中一个调用可能会阻止等待文件描述符的数据,而另一个文件描述符虽然已准备好供我使用,但未使用/ O。 select()有效地应对这种情况。

Select law

许多尝试使用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()的调用可能会因错误EINTR而失败,而对read(2),recv(2),write(2),并且将errno设置为EAGAIN(EWOULDBLOCK)时send(2)可能会失败。必须妥善管理这些结果(上述操作未正确完成)。如果您的程序不接收任何信号,则不太可能获得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()修改了其文件描述符集,因此,如果在循环中使用该调用,则必须在每次调用之前重新初始化这些集。

名称

select,pselect-同步I / O复用

返回值

请参见select(2)。

SELECT_TUT - Linux手册页

Linux程序员手册 第2部分
更新日期: 2020-04-11

出版信息

这个页面是Linux手册页项目5.08版的一部分。有关项目的说明、有关报告错误的信息以及此页面的最新版本,请访问https://www.kernel.org/doc/man-pages/

语法

参见select(2)

日期:2019-08-20 17:59:19 来源:oir作者:oir