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/。

