目录
我们可以先看一下官方的回答:
在Linux命令行输入:man 2 select
找到[BUGS],如下:
官方给予的回答是这样的:
Under Linux, select() may report a socket file descriptor as "ready for reading", while never? theless a subsequent read blocks. This could for example happen when data has arrived but upon examination has wrong checksum and is discarded. There may be other circumstances in which a file descriptor is spuriously reported as ready. Thus it may be safer to use O_NONBLOCK on sockets that should not block.
也就是说:当某个socket接收缓冲区有新数据分节到达,然后select报告这个socket描述符可读,但随后,协议栈检查到这个新分节检验和错误,然后丢弃这个分节,这时候调用read则无数据可读,如果socket没有设置nonblocking,此read将阻塞当前线程
简单来说就是:当数据到达socket缓冲区的时候,select会报告这个socket可读,但是随后因为一些原因,比如校验和错误,内核丢弃了这个数据,这个时候,如果采用了阻塞的IO,唤醒的程序去读取一个已经被丢弃的数据,肯定读不到,所以就会一直卡在那里,也就是阻塞,例如accept和recv函数就会阻塞。
所以这也就是IO复用需要非阻塞IO的原因之一。
原因二:达到缓冲区的数据有可能被别人抢走,比如多个进程accept同一个socket时引发的惊群现象,只有一个客户端连接到来,但是所有的监听程序都会被唤醒,最终只能有一个进程可以accept到这个请求,如果采用阻塞的IO,其他进程的accept就会产生阻塞。
原因三:ET(Edge-triggered)边缘模式下,必须要使用到非阻塞的IO,因为程序中需要循环读和写,直到EAGAIN的出现,如果使用阻塞的IO就容易被阻塞住。
这里提一下有关EAGAIN的知识
EAGAIN是一个错误代码,在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN
是其中比较常见的一个错误(比如用在非阻塞操作中)。
man accept找到EAGAIN的解释如下:
原文是这样说的:
The socket is marked nonblocking and no connections are present to be accepted.
POSIX.1-2001 allows either error to be returned for this case, and does not require these constants to have the same value, so a portable application should check for both
possibilities.
翻译过来的意思大概就是:如果socket的状态为非阻塞,但是accept函数没有找到可用的连接,就会返回EAGAIN错误。
我们时常会用一个while(true)死循环去接收缓冲区中客户端socket的连接,如果这个时候我们设置socket状态为非阻塞,那么accept如果在某个时间段没有接收到客户端的连接,因为是非阻塞的IO,accept函数会立即返回,并将errno设置为EAGAIN.
EAGAIN应用示例一:accept
我们时常会在死循环中设置if(errno==EAGAIN) break;
因为我们死循环只是为了接收缓冲区中的连接,一旦accept在缓冲区找不到可用的连接了,那么accept会将errno设置为EAGAIN,这个时候我们只需要判断errno==EAGAIN就说明了,accept已经将缓冲区中的连接读取完,所以可以直接break了。
EAGAIN应用示例二:recv、send
前提:非阻塞的IO、EPOLLET边缘触发模式
recv:在EPOLLIN|EPOLLET监视可读、边缘触发模式下,recv函数会时刻关注缓冲区中是否有数据可读,如果缓冲区中有数据未处理,EPOLLET模式下的epoll只会汇报一次socket有可读事件,当有新的数据加入缓冲区时,就会再次汇报可读。
根据上面的说明,假设现在缓冲区中有1000字节待recv读取,ET模式下会触发一次可读事件,为了避免我们我们读取不完缓冲区的数据(也就是说假设我们的recv目前一次只能读取200字节,所以一次肯定读取不玩),这个时候我们又会使用到死循环while(true),让recv一直读取缓冲区中的内容。我们知道缓冲区如果没有新的内容,是不会再触发可读事件的,所以当recv读取完缓冲区的内容之后,因为是非阻塞的IO模式,recv不会等待立即返回,并且会将errno设置为EAGAIN,此时的EAGAIN表示没有数据可以读取。
因此,我们可以像accept函数那样,在死循环里判断errno,也就是if(errno==EAGAIN) break;
send:在EPOLLOUT|EPOLLET监视可写、边缘触发模式下,send函数会时刻关注发送缓冲区是否已满,如果发送缓冲区未满,EPOLLET模式下就会触发一次可写事件,只有当缓冲区从满变为"有空"的时候,才会再次触发一次可写事件(假设缓冲区大小为1000,缓冲区中有300字节内容,客户端读取比服务端发送要快,这个时候缓冲区内容减少,但不会触发可写事件)。
根据上面的说明,假设缓冲区1000字节就满了,现如今缓冲区中有300字节的内容,那么ET模式会触发一次可写事件,在这个可写时间的处理代码中,你可以向客户端发送你想要发送的内容。当写入缓冲区满了的时候,因为是非阻塞IO,send会立即返回,并将errno设置为EAGAIN,此时的EAGAIN表示缓冲区内容已满,无法继续写入,所以我们也没必要让send阻塞在那里。
因此,我们可以像accept函数那样,当然循环的出口还是我们说的if(errno==EAGAIN) break;
EAGAIN这个错误代码在非阻塞IO的程序中会经常出现,上面只是一些经常使用到EAGAIN的场景;有时我们可以不把它看作是一个错误,而看作是一个工具,一个来寻找循环出口的工具。
以上便是文章的全部内容,如果讲解有误,还请指正,谢谢大家观看!