Select函数
http://hi.baidu.com/%B1%D5%C4%BF%B3%C9%B7%F0/blog/item/e7284ef16bcec3c70a46e05e.html
select 函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你,系统提供 select 函数来实现多路复用输入 / 输出模型,原型:
1 |
|
参数:
- maxfd 是需要监视的最大的文件描述符值 + 1;
- rdset,wrset,exset 分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。
- struct timeval 结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0。
- fd_set (它比较重要所以先介绍一下)是一组文件描述字 (fd) 的集合,它用一位来表示一个 fd(下面会仔细介绍),
对于 fd_set 类型通过下面四个宏来操作:
- FD_ZERO(fd_set *fdset);
将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。 - FD_SET(fd_set *fdset);
用于在文件描述符集合中增加一个新的文件描述符。 - FD_CLR(fd_set *fdset);
用于在文件描述符集合中删除一个文件描述符。 - FD_ISSET(int fd,fd_set *fdset);
用于测试指定的文件描述符是否在该集合中。
过去,一个 fd_set 通常只能包含 <32 的 fd(文件描述字),因为 fd_set 其实只用了一个 32 位矢量来表示 fd;现在, UNIX 系统通常会在头文件 < sys/select.h> 中定义常量 FD_SETSIZE,它是数据类型 fd_set 的描述字数量,其值通常是 1024,这样就能表示 < 1024 的 fd。根据 fd_set 的位矢量实现,我们可以重新理解操作 fd_set 的四个宏:
1 | fd_set set; |
―――――――――――――――――――――――――――――――――――――――
注意 fd 的最大值必须 < FD_SETSIZE。
―――――――――――――――――――――――――――――――――――――――
select 函数的接口比较简单:
1 | int select(int nfds, fd_set *readset, fd_set *writeset,fd_set* exceptset, struct tim *timeout); |
功能:
测试指定的 fd 可读?可写?有异常条件待处理?
参数:
nfds
需要检查的文件描述字个数(即检查到 fd_set 的第几位),数值应该比三组 fd_set 中所含的最大 fd 值更大,一般设为三组 fd_set 中所含的最大 fd 值加 1(如在 readset,writeset,exceptset 中所含最大的 fd 为 5,则 nfds=6,因为 fd 是从 0 开始的)。设这个值是为提高效率,使函数不必检查 fd_set 的所有 1024 位。
readset
用来检查可读性的一组文件描述字。
writeset
用来检查可写性的一组文件描述字。
exceptset
用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)
timeout
用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0。
有三种可能:
- timeout=NULL(阻塞:select 将一直被阻塞,直到某个文件描述符上发生了事件)
- timeout 所指向的结构设为非零时间(等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
- timeout 所指向的结构,时间设为 0(非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
返回值:
返回对应位仍然为 1 的 fd 的总数。
Remarks:
三组 fd_set 均将某些 fd 位置 0,只有那些可读,可写以及有异常条件待处理的 fd 位仍然为 1。
举个例子,比如 recv(), 在没有数据到来调用它的时候, 你的线程将被阻塞, 如果数据一直不来, 你的线程就要阻塞很久. 这样显然不好. 所以采用 select 来查看套节字是否可读 (也就是是否有数据读了)
步骤如下:
1 | socket s; |
理解 select 模型的关键在于理解 fd_set, 为说明方便,取 fd_set 长度为 1 字节,fd_set 中的每一 bit 可以对应一个文件描述符 fd。则 1 字节长的 fd_set 最大可以对应 8 个 fd。
1 | (1)执行 fd_set set; FD_ZERO(&set); 则 set 用位表示是 0000,0000。 |
基于上面的讨论,可以轻松得出 select 模型的特点:
- 可监控的文件描述符个数取决与 sizeof(fd_set) 的值。
我这边服务 器上 sizeof(fd_set)=512,每 bit 表示一个文件描述符,则我服务器上支持的最大文件描述符是 512*8=4096。
据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。
本人对调整 fd_set 的大小不太感兴趣,参考 技术系列之 网络模型(二) 中的模型 2(1)可以有效突破 select 可监控的文件描述符上限。 - 将 fd 加入 select 监控集的同时,还要再使用一个数据结构
array保存放到 select 监控集中的 fd.
一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。
二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个 参数。 - 可见
select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。
下面给一个伪码说明基本 select 模型的服务器模型:
1 | array[slect_len]; |
使用 select 函数的过程一般是:
先调用宏 FD_ZERO 将指定的 fd_set 清零,然后调用宏 FD_SET 将需要测试的 fd 加入 fd_set,接着调用函数 select 测试 fd_set 中的所有 fd,最后用宏 FD_ISSET 检查某个 fd 在函数 select 调用后,相应位是否仍然为 1。
以下是一个测试单个文件描述字可读性的例子:
1 | int isready(int fd) { |
下面还有一个复杂一些的应用:
// 这段代码将指定测试 Socket 的描述字的可读可写性,因为 Socket 使用的也是 fd
1 | uint32 SocketWait(TSocket *s,bool rd,bool wr,uint32 timems) { |
阻塞监听写操作
1 | // family: AF_INET type: SOCK_STREAM protocol: TCP |
读操作状态
1 | while(socket_status == RUNNING){ |