IO多路复用
select,poll,epoll都是linux下的IO多路复用的技术。所谓的IO多路复用就是指同时可以检测多个描述符,在指定时间内有描述符产生指定事件则返回,否则阻塞指定时间。
select函数
传给select的参数告知内核:
- 所关心的描述符。
- 对每个描述符所关心的事件(读,写,异常)
- 愿意等待多长时间。
从select返回,内核告诉我们:
- 已准备好的描述符的总数量。
- 对于读,写或异常这三个条件中的每一个,哪些描述符已经准备好。
使用这些消息,就可以调用相应的IO函数,并且可以确定这不会阻塞。1
2
3
4
5
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,
const struct timeval *timeout);
//返回值:若有就绪描述符则为其数目,若超时则为0,出错则为-1
timeout参数告知内核等待指定描述符中的任何一个就绪可花多长时间。其timeout结构用于指定这段时间的秒数和微秒数。1
2
3struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
- timeout为NULL表示永远等待下去,在有任何一个描述符准备好IO时才返回;
- 为0表示不等待,检查描述符后立即返回(轮询);
- 为其他值表示要等待一段固定时间,如果不超过指定时间且有描述符就绪则返回,如果超时也返回。
阻塞期间可以被信号中断(如果没有进行信号屏蔽的话)。
中间三个参数readset,writeset,exceptset是指向描述符集的指针。这三个参数说明了我们所关心的可读,可写,或处于异常条件的描述符集合。每个描述符都存储在一个fd_set数据类型中。它为每一个描述符保持一位。1
2
3
4void 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
5fd_set rset;
FD_ZERO(
FD_SET(1,
FD_SET(4,
FD_SET(5,
maxfdp1参数指定待测试的描述符个数,它的值是待测试的最大描述符加1,描述符0,1,2…一直到maxfdp1-1均将被测试。
调用select函数时,在指定时间内无描述符就绪则阻塞,有任一描述符就绪就返回。
- 返回-1表示出错,即可能被信号打断。
- 返回0表示没有描述符准备就绪。此时,所有描述符集都会被置为0
- 返回正数表示已经就绪了的描述符个数。它是三个描述符集合中已准备好的描述符之和,所以如果同一描述符准备好读和写,在返回值中会对其计数两次。此时准备好的描述符依旧打开(置1),未准备好的置0。
描述符集内任何与未就绪描述符对应的位返回时均清成0,所以每次重新调用select函数时,都得再次把所有描述符集内所关心的位均置为1。
pselect函数
1 |
|
pselect和select的区别如下:
- pselect使用timespec结构,而不是timeval结构。
1
2
3struct timespec{
time_t tv_sec; //seconds
long tv_nsec; //nanoseconds
两个结构的区别在于第二个成员 tv_nsec,它指定纳秒数,而timeval结构的tv_usec指定微秒数。
- 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
int poll(struct pollfd *fdarray,unsigned long nfds,int timeout);
//返回值:若有就绪描述符则为其数目,若超时为0,出错为-1
第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。1
2
3
4struct 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)