文章目录
1 select简介
在linux下,我们可以使用select来进行I/O复用,可以监视多个文件描述符,判断是否有符合条件的事件发生。
使用select函数时,我们可以查看是否有可读、可写或者错误的事件发生。
2 函数详解
2.1 函数原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
2.2 参数
2.2.1 nfds
整形变量,它的值是所有文件描述符中最大的值加1。它的最大值为FD_SETSIZE,FD_SETSIZE一般定义为1024,不同的系统可能不一样。
2.2.2 readfds
这个文件描述集合用于监视此文件集中的文件是否有数据可读,比如我们监视串口是否有数据可读,可以将串口的文件描述符放到此集合中去。当监视到有可读的事件后,readfds文件描述集合将清除不可读的文件描述符,只保留可读的文件描述符。不需要时,将其设置为NULL。
2.2.3 writefds
这个文件描述集合用于监视是否有可写的事件发生,当监视到后,会清除不可写的文件描述符,只保留下可写的文件描述符。不需要时,将其设置为NULL。
2.2.4 exceptfds
这个文件描述集合用于监视是否发送错误。不需要时,将其设置为NULL。
2.2.5 timeout
用于所监视的文件集合中的事件没有发生时候的等待时间。为NULL表示阻塞等待一直到事件发生,为0表示立马返回,不为0表示等待的时间。
2.3 文件描述集合操作
所谓的文件描述集合fd_set,文件描述符的集合,在linux下,万物都是文件,因此设备、管道、socket等等都是文件,都有自己的文件描述符。
在上面提到的文件描述集合fd_set,主要有以下4个宏可以进行操作。同时文件描述集合中的文件数量最大为FD_SETSIZE,超过此值,会有一些未知情况发生。
2.3.1 FD_ZERO()
清空文件描述集合,将文件描述集合全部置0。
2.3.2 FD_SET()
将某个文件描述符加入文件描述集合。
2.3.3 FD_CLR()
将某个文件描述符从文件描述集合中删除。
2.3.4 FD_ISSET()
用于确定某个文件描述符是否是文件描述集合的成员。
2.4 注意
当函数正常返回,即有事件发生时候,会清除掉没有发生的事件,所以在处理完事件之后,如果需要继续监听全部事件,就需要重新将全部文件描述符重新加入文件描述集合中(即先情况描述集合,然后将全部文件描述符重新加入)。
因为select函数会更新超时参数,比如设置超时时间为5s,然后超时退出后,select会将超时时间更新为0,所以如果需要再次监听,那么就需要重新设置超时时间。
2.5 示例
如下,一个小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;
}
3 文件描述符集合细探
在上面介绍了文件描述集合,到底文件描述集合是个什么样的东东,在下面来追踪一下。
查看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。
4 select的缺点
select的文件描述集包含了所有需要被监听的文件描述符,如果数量很大,那么每一次调用,都需要从头开始遍历,增加了CPU的消耗。
当select返回时,我们需要遍历知道是那个文件描述符被触发。
当需要重新监听时,我们需要重新将所有的文件描述符加入文件描述符集中。