select函数和poll函数

IO多路复用

select,poll,epoll都是linux下的IO多路复用的技术。所谓的IO多路复用就是指同时可以检测多个描述符,在指定时间内有描述符产生指定事件则返回,否则阻塞指定时间。

select函数

传给select的参数告知内核:

  1. 所关心的描述符。
  2. 对每个描述符所关心的事件(读,写,异常)
  3. 愿意等待多长时间。

从select返回,内核告诉我们:

  1. 已准备好的描述符的总数量。
  2. 对于读,写或异常这三个条件中的每一个,哪些描述符已经准备好。
    使用这些消息,就可以调用相应的IO函数,并且可以确定这不会阻塞。
    1
    2
    3
    4
    5
    #include<sys/select.h>
    #include<sys/time.h>
    int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,
    const struct timeval *timeout);
    //返回值:若有就绪描述符则为其数目,若超时则为0,出错则为-1

timeout参数告知内核等待指定描述符中的任何一个就绪可花多长时间。其timeout结构用于指定这段时间的秒数和微秒数。

1
2
3
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds

  • timeout为NULL表示永远等待下去,在有任何一个描述符准备好IO时才返回;
  • 为0表示不等待,检查描述符后立即返回(轮询);
  • 为其他值表示要等待一段固定时间,如果不超过指定时间且有描述符就绪则返回,如果超时也返回。

阻塞期间可以被信号中断(如果没有进行信号屏蔽的话)。
中间三个参数readset,writeset,exceptset是指向描述符集的指针。这三个参数说明了我们所关心的可读,可写,或处于异常条件的描述符集合。每个描述符都存储在一个fd_set数据类型中。它为每一个描述符保持一位。

1
2
3
4
void FD_ZERO(fd_set *fdset);   //置零fdset集合中的每一位
void FD_SET(int fd,fd_set *fdset); //指定位(fd)置1
void FD_CLR(int fd,fd_set *fdset); //指定位(fd)置0
void FD_ISSET(int fd,fd_set *fdset); //判断指定位fd是否为1

使用这些接口可以对fd_set的数据类型进行相关操作。在声明了一个描述符集后,必须使用FD_ZERO将这个描述符集置为0,然后在其中设置关心的各个描述符的位。select返回时用FD_ISSET测试该集中的一个给定位是否仍处于打开状态。

举个例子:

1
2
3
4
5
fd_set rset;
FD_ZERO(&rset); //初始化set集合,每一位置0
FD_SET(1,&rset); //描述符1对应位置1
FD_SET(4,&rset); //描述符4对应位置1
FD_SET(5,&rset); //描述符5对应位置1

maxfdp1参数指定待测试的描述符个数,它的值是待测试的最大描述符加1,描述符0,1,2…一直到maxfdp1-1均将被测试。
调用select函数时,在指定时间内无描述符就绪则阻塞,有任一描述符就绪就返回。

  • 返回-1表示出错,即可能被信号打断。
  • 返回0表示没有描述符准备就绪。此时,所有描述符集都会被置为0
  • 返回正数表示已经就绪了的描述符个数。它是三个描述符集合中已准备好的描述符之和,所以如果同一描述符准备好读和写,在返回值中会对其计数两次。此时准备好的描述符依旧打开(置1),未准备好的置0。

描述符集内任何与未就绪描述符对应的位返回时均清成0,所以每次重新调用select函数时,都得再次把所有描述符集内所关心的位均置为1。

pselect函数

1
2
3
4
5
6
#include<sys/select.h>
#include<signal.h>
#include<time.h>
int pselect(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,
const struct timespec *timeout,const sigset_t *sigmask);
//返回值:若有就绪描述符则为其数目,超时则为0,出错为-1

pselect和select的区别如下:

  1. pselect使用timespec结构,而不是timeval结构。
    1
    2
    3
    struct timespec{
    time_t tv_sec; //seconds
    long tv_nsec; //nanoseconds

两个结构的区别在于第二个成员 tv_nsec,它指定纳秒数,而timeval结构的tv_usec指定微秒数。

  1. pselect增加了第六个参数:一个指向信号掩码的指针。该参数允许程序禁止递交某些信号,再测试由这些当前被禁止信号的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码。看一个例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //使用pselect确保SIGINT信号不会丢失
    sigset_t newmask,oldmask,zeromask;
    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask,SIGINT);
    sigprocmask(SIG_BLOCK,&newmask,&oldmask);
    if(intr_flag)
    hadnle_intr();
    if((nready = pselect(...,&zeromask)) < 0){
    if(errno == EINTR){
    if(intr_flag)
    hadnle_intr();
    }
    ...
    }

在测试intr_flag之前,阻塞SIGINT。当pselect被调用时,它先以空集替代进程的信号掩码,再检查描述符,并可能进入睡眠。当pselect函数返回时,进程的信号掩码又被重置为调用pselect之前的值(即SIGINT被阻塞)。

poll函数

poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。

1
2
3
#include<poll.h>
int poll(struct pollfd *fdarray,unsigned long nfds,int timeout);
//返回值:若有就绪描述符则为其数目,若超时为0,出错为-1

第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。

1
2
3
4
struct pollfd{
int fd;
short events;
short revents;

要测试的条件由events成员指定,函数在相应的revents成员中返回该描述符的状态。这两个成员中的每一个都由指定某个特定条件的一位或多位构成。
poll识别三类数据:普通、优先级带和高优先级。就TCP和UDP套接字而言,以下条件引起poll返回特定的revent。

  • 所有正规TCP数据和所有UDP数据都被认为是普通数据。
  • TCP的外带数据被认为是优先级带数据。
  • 当TCP连接的读半部关闭时,也被认为是普通数据,随后的读操作将返回0。
  • TCP连接存在错误既可以是普通数据,也可以是错误。
  • 在监听套接字上有新的连接可用既可以是普通数据,也可以是优先级数据。大多数实现视为普通数据。
  • 非阻塞式connect的完成被认为是使相应套接字可写。
    结构数组中元素个数是由nfds参数指定。
    timeout参数指定poll函数返回前等待多长时间。它是一个指定应等待毫秒数的正值。

INFTIM——永远等待
0——立即返回,不阻塞进程
0——等待指定数目的毫秒数

如果不关心某个特定描述符,可以把与它对应的pollfd结构的fd成员设置成一个负数。poll函数将忽略这样的pollfd结构的events成员,返回时将它的revents成员的值置为0。


参考资料:
《UNIX网络编程 v1》(UNP)
《UNIX环境高级编程 3th》(APUE)