select_tut() - Unix,Linux系统调用
Tutorials Point


  Unix初学者教程
  Unix Shell编程
  高级Unix
  Unix有用参考
  Unix有用资源
  精选阅读

版权所有 © 2014 tutorialspoint



  首页     参考     讨论论坛     关于TP  

select_tut() - Unix,Linux系统调用


previous next AddThis Social Bookmark Button

广告

名称

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

概要

#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);

int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *ntimeout, sigset_t *sigmask);

FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);

描述

select()(或pselect())是大多数C程序的关键函数,它以高效的方式处理多个同时的文件描述符(或套接字句柄)。它的主要参数是三个文件描述符数组:readfdswritefdsexceptfdsselect() 的常用方法是在等待一个或多个文件描述符的“状态变化”时阻塞。“状态变化”是指从文件描述符获得更多字符,或者内核内部缓冲区中有空间可以写入更多数据到文件描述符,或者文件描述符出现错误(对于套接字或管道,这是指连接的另一端已关闭)。

总之,select() 只监视多个文件描述符,是执行此操作的标准 Unix 调用。

文件描述符数组称为文件描述符集。每个集合都声明为fd_set类型,其内容可以使用宏FD_CLR()、FD_ISSET()、FD_SET()和FD_ZERO()进行更改。FD_ZERO() 通常是新声明集合中使用的第一个函数。此后,可以使用FD_SET()逐个添加感兴趣的单个文件描述符。select() 会根据下面描述的规则修改集合的内容;调用select() 后,可以使用FD_ISSET()宏测试文件描述符是否仍然存在于集合中。如果描述符存在,FD_ISSET() 返回非零值;如果不存在,则返回零。FD_CLR() 从集合中删除文件描述符。

参数

标签描述
readfds
 监视此集合以查看其任何文件描述符是否可用于读取数据。select() 返回后,readfds将清除所有文件描述符,只保留那些可以使用recv()(对于套接字)或read()(对于管道、文件和套接字)调用立即读取数据的文件描述符。
writefds
 监视此集合以查看其任何文件描述符是否有空间写入数据。select() 返回后,writefds将清除所有文件描述符,只保留那些可以使用send()(对于套接字)或write()(对于管道、文件和套接字)调用立即写入数据的文件描述符。
exceptfds
 监视此集合中任何文件描述符的异常或错误。但是,这实际上只是一个传闻。exceptfds 的使用方法是监视带外(OOB)数据。OOB 数据是使用MSG_OOB标志发送到套接字上的数据,因此exceptfds 实际上只适用于套接字。请参阅recv(2) 和send(2)了解详情。select() 返回后,exceptfds将清除所有文件描述符,只保留那些可用于读取 OOB 数据的描述符。但是,你只能读取一个字节的 OOB 数据(使用recv()完成),并且可以随时写入 OOB 数据(使用send()完成),并且不会阻塞。因此,不需要第四个集合来检查套接字是否可用于写入 OOB 数据。
nfds 这是一个整数,比任何集合中任何文件描述符的最大值大一。换句话说,当你忙于向集合中添加文件描述符时,必须计算所有文件描述符的最大整数值,然后将此值加一,然后将其作为nfds传递给select()。
utimeout
 这是select()在返回之前必须等待的最长时间,即使没有任何有趣的事情发生。如果此值作为NULL传递,则select()将无限期阻塞以等待事件。utimeout可以设置为零秒,这将导致select()立即返回。struct timeval结构定义如下:

struct timeval {
    time_t tv_sec;    /* seconds */
    long tv_usec;     /* microseconds */
};
ntimeout
 此参数与utimeout的含义相同,但struct timespec具有纳秒精度,如下所示:

struct timespec {
    long tv_sec;    /* seconds */
    long tv_nsec;   /* nanoseconds */
};
sigmask
 此参数包含一组在执行pselect()调用时允许的信号(请参阅sigaddset(3)和sigprocmask(2))。它可以作为NULL传递,在这种情况下,它不会修改函数入口和出口时允许的信号集。然后它的行为就像select()一样。

组合信号和数据事件

如果同时等待来自文件描述符的信号和数据,则必须使用pselect()。通常使用信号处理程序来仅设置全局标志的程序。全局标志将指示必须在程序的主循环中处理事件。信号将导致select()(或pselect())调用返回,并设置errnoEINTR。此行为至关重要,以便可以在程序的主循环中处理信号,否则select()将无限期阻塞。现在,主循环中的某个地方将有一个条件来检查全局标志。因此,我们必须问:如果在条件之后但在select()调用之前到达信号会怎样?答案是select()将无限期阻塞,即使实际上有事件正在等待。pselect()调用解决了这个竞争条件。此调用可用于屏蔽除了在pselect()调用中之外不应接收的信号。例如,假设事件是子进程的退出。在主循环开始之前,我们将使用sigprocmask()阻塞SIGCHLD。我们的pselect()调用将使用原始信号掩码启用SIGCHLD。我们的程序将如下所示:

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() 的意义在于它同时监视多个描述符,并在没有活动时正确地使进程休眠。它在允许你处理多个同时的管道和套接字的同时执行此操作。Unix程序员经常发现自己必须处理来自多个文件描述符的I/O,其中数据流可能是间歇性的。如果你只是创建一系列read()和write()调用,你会发现你的一个调用可能会阻塞等待来自/到文件描述符的数据,而另一个文件描述符虽然可用但未被使用。select() 有效地解决了这种情况。

可以在select(2)手册页中找到select()用法的简单示例。

端口转发示例

这是一个更好地演示select()真正实用性的示例。下面的清单是一个TCP转发程序,它将一个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)) < 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", (int) 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 (1); }

signal (SIGPIPE, SIG_IGN);

forward_port = atoi (argv[2]);

h = listen_socket (atoi (argv[1])); if (h < 0) exit (1);

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 (1); } 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; } } return 0; }

上述程序正确转发大多数类型的TCP连接,包括由telnet服务器传输的OOB信号数据。它处理了同时在两个方向上都有数据流的棘手问题。你可能会认为使用fork()调用并将一个线程专门用于每个流更有效。这比你想象的要棘手得多。另一个想法是使用ioctl()调用设置非阻塞I/O。这也有其问题,因为你最终必须使用低效的超时。

该程序一次不处理多个同时连接,尽管可以使用连接缓冲区的链表(每个连接一个)轻松扩展它来实现此功能。目前,新的连接会导致当前连接断开。

SELECT规则

许多尝试使用select()的人都会遇到难以理解的行为,并产生不可移植或临界结果。例如,上述程序被仔细编写,即使它根本没有将其文件描述符设置为非阻塞模式(参见ioctl(2)),也不会在任何点阻塞。很容易引入会消除使用select()优势的细微错误,因此我将列出使用select()调用时需要注意的一些要点。
标签描述
1. 你应该始终尝试在没有超时的情况下使用select()。如果没有任何数据可用,你的程序不应该做任何事情。依赖超时的代码通常不可移植且难以调试。
2. 如上所述,必须正确计算值nfds以提高效率。
3. 如果您不打算在select()调用后检查文件描述符的结果并做出相应的响应,则绝不能将任何文件描述符添加到任何集合中。参见下一条规则。
4. select()返回后,应检查所有集合中的所有文件描述符以查看它们是否已准备好。
5. 函数read()、recv()、write()和send()并不一定读取/写入您请求的全部数据量。如果它们读取/写入全部数据量,那是因为您的网络流量较低且流速很快。但这并非总是如此。您应该应对函数仅能发送或接收单个字节的情况。
6. 除非您确定要处理少量数据,否则绝不要一次只读取/写入单个字节。每次读取/写入尽可能多的数据进行缓冲效率要高得多。上面示例中的缓冲区大小为 1024 字节,但可以轻松地增大。
7. 函数read()、recv()、write()和send()以及select()调用都可能返回-1,其中errno设置为EINTR,或errno设置为EAGAINEWOULDBLOCK)。必须正确处理这些结果(上面没有正确处理)。如果您的程序不接收任何信号,则不太可能获得EINTR。如果您的程序未设置非阻塞I/O,则不会获得EAGAIN。尽管如此,为完整起见,您仍然应该处理这些错误。
8. 切勿使用长度为零的缓冲区调用read()、recv()、write()或send()。
9. 如果函数read()、recv()、write()和send()因7.中列出的错误以外的错误而失败,或其中一个输入函数返回 0(表示文件结尾),则您不应再次将该描述符传递给select()。在上面的示例中,我立即关闭了描述符,然后将其设置为 -1 以防止将其包含在集合中。
10. 每次调用select()时都必须初始化超时值,因为某些操作系统会修改该结构。但是,pselect()不会修改其超时结构。
11. 我听说Windows套接字层无法正确处理OOB数据。当根本没有设置文件描述符时,它也无法处理select()调用。没有设置文件描述符是一种使用超时使进程以亚秒精度休眠的有用方法。(参见下文。)

USLEEP 模拟

在没有usleep()函数的系统上,您可以使用有限的超时和没有文件描述符的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已正确设置。发生错误时,返回的集合和超时结构内容未定义,不应使用。但是,pselect()永远不会修改ntimeout

注释

一般来说,所有支持套接字的操作系统也支持select()。许多类型的程序在不使用select()的情况下会变得极其复杂。select()可以用来以一种可移植且高效的方式解决许多问题,而缺乏经验的程序员则尝试使用更复杂的方法来解决这些问题,例如使用线程、fork、IPC、信号、内存共享等等。

poll(2)系统调用具有与select()相同的函数,并且在监视稀疏文件描述符集时效率更高。它现在已广泛可用,但在历史上不如select()可移植。

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

另请参见



previous next Printer Friendly

广告


  

广告



广告