select是Linux/Unix环境下的高级网络I/O编程接口,它使我们能够进行基于I/O多路转接。I/0多路转接(multiplexing)的核心思想是:先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已经准备好进行I/O时,该函数才返回。在返回时,它告诉进程哪些描述符已准备好可以进行I/O操作。
在Linux中,我们可以使用select函数实现I/O端口的复用(多路转接),传递给select函数的参数会告诉内核:
我们所关心的描述符,可能为文件描述符或网络套接字描述符。
对每个描述符,我们所关心的状态。(我们是要想从一个文件描述符中读或者写,还是关注一个描述符中是否出现异常)
我们愿意等待多长时间。(可以无限等待,等待固定的一段时间,或者完全不等待)
从 select函数返回后,内核告诉我们一下信息:
对我们的要求已经做好准备的描述符的个数
对于三种状态(读,写或异常)中的每一个,哪些描述符已经做好准备.
有了这些返回信息,我们可以调用合适的I/O函数(通常是 read 或 write),并且这些函数不会再阻塞.
1 |
|
首先我们先看一下最后一个参数。它指明我们要等待的时间,有如下三种情况:
timeout == NULL 等待无限长的时间。等待可以被一个信号中断。当有一个描述符做好准备或者是捕获到一个信号时函数会返回。如果捕获到一个信号, select函数将返回 -1,并将变量 erro设为 EINTR。
timeout->tv_sec == 0 &&timeout->tv_usec == 0不等待,直接返回。加入描述符集的描述符都会被测试,并且返回满足要求的描述符的个数。这种方法通过轮询,无阻塞地获得了多个文件描述符状态。
timeout->tv_sec !=0 ||timeout->tv_usec!= 0 等待指定的时间。当有描述符符合条件或者超过超时时间的话,函数返回。在超时时间即将用完但又没有描述符合条件的话,返回 0。对于第一种情况,等待也会被信号所中断。
接着,我们看看中间的三个参数 readset, writset, exceptset,指向描述符集。这些参数指明了我们关心哪些描述符,和需要满足什么条件(可写,可读,异常)。一个文件描述集保存在 fd_set 类型中。fd_set类型变量每一位代表了一个描述符。我们也可以认为它只是一个由很多二进制位构成的数组。
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set;
(2) FD_ZERO(&set);则set用位表示是0000,0000。
(3)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(4)若再加入fd=2,fd=1,则set变为0001,0011
(5)执行select(6,&set,0,0,0)阻塞等待
(6)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。由于我是服务器端主程序,只关心是否收到对端发来的消息或通知事件,因此我只需要监听某个端口,采用select检查相应的套接字描述符是否有数据可读。调用FD_ZERO(&readfds)将一个指定的fd_set变量(read_fds)所有位设置为0,调用FD_SET(m_server_sock, &readfds)将read_fds变量的第m_server_sock个位置1。
如果select返回-1,说明有错误;如果为0, 说明超时了;否者说明我们关心的描述符准备好了。对于本文,我关心的是只有一个读文件描述符,当有数据可读时,内核(I/O)根据状态修改文件描述符集,select返回一个大于0的数,该数值表示已经准备好的描述符个数(本文是1,由于我只关心一个描述符)。准备好是什么意思呢?意思是,我关心的读集readfds中的其中一个描述符m_sock_fd描述符,有数据可读了,对其read操作不会阻塞。
select调用是在while 循环loop里,而FD的设置却在while loop之外,即:1
2
3
4
5
6
7
8
9fd_set readfds;
FD_ZERO(&readfds);
FD_SET(m_server_sock, &readfds);
while(1)
{
int sockfd = -1;
int ret = select(m_server_sock +1, &readfds, NULL, NULL, NULL);
......
}
是不是这个逻辑有问题呢?于是想到试试看:把FD_SET操作都放到select之前,即统一放到while Loop循环里。没想到,这么一改问题直接就解决了。
初步分析认为:
select返回后, 会把以前加入的但并无事件发生的fd从fd_set清除,因此需要重新调用select 前再次把关心的fd添加到FD_SET。否则就会出现本文的现象。
问题解决:每次调用select之前,调用FD_ZERO清空可读文件句柄集,并调用FD_SET把TCP套接字添加到该fd_set类型的集合中。
代码对比: