在linux下,我们可以使用select来进行I/O复用,可以监视多个文件描述符,判断是否有符合条件的事件发生。
使用select函数时,我们可以查看是否有可读、可写或者错误的事件发生。
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
整形变量,它的值是所有文件描述符中最大的值加1。它的最大值为FD_SETSIZE,FD_SETSIZE一般定义为1024,不同的系统可能不一样。
这个文件描述集合用于监视此文件集中的文件是否有数据可读,比如我们监视串口是否有数据可读,可以将串口的文件描述符放到此集合中去。当监视到有可读的事件后,readfds文件描述集合将清除不可读的文件描述符,只保留可读的文件描述符。不需要时,将其设置为NULL。
这个文件描述集合用于监视是否有可写的事件发生,当监视到后,会清除不可写的文件描述符,只保留下可写的文件描述符。不需要时,将其设置为NULL。
这个文件描述集合用于监视是否发送错误。不需要时,将其设置为NULL。
用于所监视的文件集合中的事件没有发生时候的等待时间。为NULL表示阻塞等待一直到事件发生,为0表示立马返回,不为0表示等待的时间。
所谓的文件描述集合fd_set,文件描述符的集合,在linux下,万物都是文件,因此设备、管道、socket等等都是文件,都有自己的文件描述符。
在上面提到的文件描述集合fd_set,主要有以下4个宏可以进行操作。同时文件描述集合中的文件数量最大为FD_SETSIZE,超过此值,会有一些未知情况发生。
清空文件描述集合,将文件描述集合全部置0。
将某个文件描述符加入文件描述集合。
将某个文件描述符从文件描述集合中删除。
用于确定某个文件描述符是否是文件描述集合的成员。
当函数正常返回,即有事件发生时候,会清除掉没有发生的事件,所以在处理完事件之后,如果需要继续监听全部事件,就需要重新将全部文件描述符重新加入文件描述集合中(即先情况描述集合,然后将全部文件描述符重新加入)。
因为select函数会更新超时参数,比如设置超时时间为5s,然后超时退出后,select会将超时时间更新为0,所以如果需要再次监听,那么就需要重新设置超时时间。
如下,一个小demo,循环监听两个文件描述符,超时时间为5s。
int test() { int s32FD1 = 17; //文件描述符1 此处demo写为固定值 int s32FD2 = 18; //文件描述符2 此处demo写为固定值 fd_set fds; //文件描述集合 int s32MaxFd = s32FD2 + 1; //所有文件描述符中的最大值加1 int s32Ret = 0; struct timeval tv; while(1) { /* 重新加入文件描述集合 */ FD_ZERO(&fds); //清空文件描述集合 FD_SET(s32FD1, &fds); //加入描述集合 FD_SET(s32FD2, &fds); //加入描述集合 /* 重新设置超时时间 */ tv.tv_sec = 5; tv.tv_usec = 0; s32Ret = select(s32MaxFd, &fds, NULL, NULL, &tv); if (s32Ret > 0) { if (FD_ISSET(s32FD1, &fds) //FD1 文件有可读事件 { //do something... } else if (FD_ISSET(s32FD2, &fds) //FD2 文件有可读事件 { //do something... } } } return 0; }
在上面介绍了文件描述集合,到底文件描述集合是个什么样的东东,在下面来追踪一下。
查看fd_set结构体原型,如下:
/* The fd_set member is required to be an array of longs. */ typedef long int __fd_mask; /* fd_set for select and pselect. */ typedef struct { /* XPG4.2 requires this member name. Otherwise avoid the name from the global namespace. */ #ifdef __USE_XOPEN __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; # define __FDS_BITS(set) ((set)->fds_bits) #else __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; # define __FDS_BITS(set) ((set)->__fds_bits) #endif } fd_set;
从上面可以看出,fd_set结构体里面的成员是long int数组,在我的linux系统中 ,fd_set的长度为128位,FD_SETSIZE的值为1024。128*8=1024,因此每一位对应8个文件描述符。而该数组第一位元素对应于描述符0~31, 第二位元素对应于描述符32-63,一次类推。
在上面的示例中,我们将文件描述符17 18加入集合中,17 18处于第一位元素,因此我们可以打印第一位元素的值来进一步确定。
printf("fdset:%ld, sizeof:%d, FD_SETSIZE:%d\n", (long int)fds.fds_bits[0], sizeof(fd_set), FD_SETSIZE); #fdset:393216, sizeof:128, FD_SETSIZE:1024
fdset的值为393216,转换为2进制为110 0000 0000 0000 0000,可以看到从0开始计数,第17 18位被置为1.
当select监听17 18时候,17有可读事件,打印fd_set的值:
printf("fdset:%ld\n", (long int)fds.__fds_bits[0]); #fdset:131072
fdset的值为131072,转换为2进制为10 0000 0000 0000 0000,可以看到从0开始计数,第17 位被置为1, 而第18位被置为0,因此需要重新select时候,需要重新将全部的文件描述符加入fd_set。
select的文件描述集包含了所有需要被监听的文件描述符,如果数量很大,那么每一次调用,都需要从头开始遍历,增加了CPU的消耗。
当select返回时,我们需要遍历知道是那个文件描述符被触发。
当需要重新监听时,我们需要重新将所有的文件描述符加入文件描述符集中。