select()函数和poll()函数均是主要用来处理多路I/O复用的情况。比如一个服务器既想等待输入终端到来,又想等待若干个套接字有客户请求到达,这时候就需要借助select或者poll函数了。


(一)select()函数


原型如下:


1 int select(int fdsp1, fd_set *readfds, fd_set *writefds, fd_set *errorfds, const struct timeval *timeout);


各个参数含义如下:


  • int fdsp1:最大描述符值 + 1
  • fd_set *readfds:对可读感兴趣的描述符集
  • fd_set *writefds:对可写感兴趣的描述符集
  • fd_set *errorfds:对出错感兴趣的描述符集
  • struct timeval *timeout:超时时间(注意:对于linux系统,此参数没有const限制,每次select调用完毕timeout的值都被修改为剩余时间,而unix系统则不会改变timeout值)

select函数会在发生以下情况时返回:


  1. readfds集合中有描述符可读
  2. writefds集合中有描述符可写
  3. errorfds集合中有描述符遇到错误条件
  4. 指定的超时时间timeout到了

当select返回时,描述符集合将被修改以指示哪些个描述符正处于可读、可写或有错误状态。可以用FD_ISSET宏对描述符进行测试以找到状态变化的描述符。如果select因为超时而返回的话,所有的描述符集合都将被清空。

select函数返回状态发生变化的描述符总数。返回0意味着超时。失败则返回-1并设置errno。可能出现的错误有:EBADF(无效描述符)、EINTR(因终端而返回)、EINVAL(nfds或timeout取值错误)。

设置描述符集合通常用如下几个宏定义:


1 FD_ZERO(fd_set *fdset); /* clear all bits in fdset */

2 FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fd_set */

3 FD_CLR(int fd, fd_set *fdset); /* turn off the bit for fd in fd_set */

4 int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset? */


如:


1 fd_set rset;

2 FD_ZERO(&rset); /* initialize the set: all bits off */

3 FD_SET(1, &rset); /* turn on bit for fd 1 */

4 FD_SET(4, &rset); /* turn on bit for fd 4 */

5 FD_SET(5, &rset); /* turn on bit for fd 5 */


当select返回的时候,rset位都将被置0,除了那些有变化的fd位。

当发生如下情况时认为是可读的:


  1. socket的receive buffer中的字节数大于socket的receive buffer的low-water mark属性值。(low-water mark值类似于分水岭,当receive buffer中的字节数小于low-water mark值的时候,认为socket还不可读,只有当receive buffer中的字节数达到一定量的时候才认为socket可读)
  2. 连接半关闭(读关闭,即收到对端发来的FIN包)
  3. 发生变化的描述符是被动套接字,而连接的三路握手完成的数量大于0,即有新的TCP连接建立
  4. 描述符发生错误,如果调用read系统调用读套接字的话会返回-1。

当发生如下情况时认为是可写的:


  1. socket的send buffer中的字节数大于socket的send buffer的low-water mark属性值以及socket已经连接或者不需要连接(如UDP)。
  2. 写半连接关闭,调用write函数将产生SIGPIPE
  3. 描述符发生错误,如果调用write系统调用写套接字的话会返回-1。

注意:

select默认能处理的描述符数量是有上限的,为FD_SETSIZE的大小。

对于timeout参数,如果置为NULL,则表示wait forever;若timeout->tv_sec = timeout->tv_usec = 0,则表示do not wait at all;否则指定等待时间。

如果使用select处理多个套接字,那么需要使用一个数组(也可以是其他结构)来记录各个描述符的状态。而使用poll则不需要,下面看poll函数。


(二)poll()函数


原型如下:


1 int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);


各参数含义如下:


  • struct pollfd *fdarray:一个结构体,用来保存各个描述符的相关状态。
  • unsigned long nfds:fdarray数组的大小,即里面包含有效成员的数量。
  • int timeout:设定的超时时间。(以毫秒为单位)

poll函数返回值及含义如下:


  • -1:有错误产生
  • 0:超时时间到,而且没有描述符有状态变化
  • >0:有状态变化的描述符个数

着重讲fdarray数组,因为这是它和select()函数主要的不同的地方:

pollfd的结构如下:


1 struct pollfd {

2 int fd; /* descriptor to check */

3 short events; /* events of interest on fd */

4 short revents; /* events that occured on fd */

5 };


其实poll()和select()函数要处理的问题是相同的,只不过是不同组织在几乎相同时刻同时推出的,因此才同时保留了下来。select()函数把可读描述符、可写描述符、错误描述符分在了三个集合里,这三个集合都是用bit位来标记一个描述符,一旦有若干个描述符状态发生变化,那么它将被置位,而其他没有发生变化的描述符的bit位将被clear,也就是说select()的readset、writeset、errorset是一个value-result类型,通过它们传值,而也通过它们返回结果。这样的一个坏处是每次重新select 的时候对集合必须重新赋值。而poll()函数则与select()采用的方式不同,它通过一个结构数组保存各个描述符的状态,每个结构体第一项fd代表描述符,第二项代表要监听的事件,也就是感兴趣的事件,而第三项代表poll()返回时描述符的返回状态。合法状态如下:



  • POLLIN: 有普通数据或者优先数据可读
  • POLLRDNORM: 有普通数据可读
  • POLLRDBAND: 有优先数据可读
  • POLLPRI: 有紧急数据可读
  • POLLOUT: 有普通数据可写
  • POLLWRNORM: 有普通数据可写
  • POLLWRBAND: 有紧急数据可写
  • POLLERR: 有错误发生
  • POLLHUP: 有描述符挂起事件发生
  • POLLNVAL: 描述符非法


对于POLLIN | POLLPRI等价与select()的可读事件;POLLOUT | POLLWRBAND等价与select()的可写事件;POLLIN 等价与POLLRDNORM | POLLRDBAND,而POLLOUT等价于POLLWRBAND。如果你对一个描述符的可读事件和可写事件以及错误等事件均感兴趣那么你应该都进行相应的设置。

对于timeout的设置如下:


    • INFTIM: wait forever
    • 0: return immediately, do not block
    • >0: wait specified number of milliseconds