• 【Linux Socket C++】为什么IO复用需要用到非阻塞IO?EAGAIN的简单介绍与应用


    目录

    为什么IO复用需要非阻塞的IO

    EAGAIN的介绍

    EAGAIN的应用


    为什么IO复用需要非阻塞的IO

    我们可以先看一下官方的回答:

    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的知识

    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的应用

    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的场景;有时我们可以不把它看作是一个错误,而看作是一个工具,一个来寻找循环出口的工具。

    以上便是文章的全部内容,如果讲解有误,还请指正,谢谢大家观看!

  • 相关阅读:
    计算机毕业设计(附源码)python疫情状态下的图书馆座位预约系统
    HTML5基础知识
    【毕设选题】flink大数据淘宝用户行为数据实时分析与可视化
    元年专利解析|元数据管理系统和使用其对模型对象进行建模的方法
    企业攻击面管理的七个最佳实践
    怡和嘉业在创业板上市:总市值约186亿元,前三季度业绩同比翻倍
    剑指offer-数据结构二
    【Linux】自己实现一个bash进程
    Dubbo源码(八) - 负载均衡
    2.1.5操作系统之线程概念与多线程模型
  • 原文地址:https://blog.csdn.net/qq_52572621/article/details/127792861