select一直返回0的问题解决和总结

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
2
3
4
5
6
7
#include <sys/select.h>   
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
返回值:做好准备的文件描述符的个数,超时为0,错误为 -1.
struct timeval{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
}

首先我们先看一下最后一个参数。它指明我们要等待的时间,有如下三种情况:

  • 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
9
fd_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类型的集合中。
代码对比:

img


原文