SELECT_TUT - Linux手册页
Linux程序员手册 第2部分
更新日期: 2020-04-11
名称
select,pselect-同步I / O复用
语法
参见select(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(2)。
备注
一般而言,所有支持套接字的操作系统也都支持select()。 select()可用于以可移植且有效的方式解决许多问题,而天真的程序员尝试使用线程,分支,IPC,信号,内存共享等以更复杂的方式解决这些问题。
poll(2)系统调用具有与select()相同的功能,并且在监视稀疏文件描述符集时效率更高。如今,它广泛可用,但从历史上讲,它不如select()轻便。
示例
这是一个更好地说明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。这也有其问题,因为您最终会使用无效的超时。
该程序一次不能处理多个同时连接,尽管可以很容易地扩展它以使用链接的缓冲区列表来进行此操作-每个连接一个。此刻,新连接导致当前连接断开。
出版信息
这个页面是Linux手册页项目5.08版的一部分。有关项目的说明、有关报告错误的信息以及此页面的最新版本,请访问https://www.kernel.org/doc/man-pages/。