文章目录

典型的一次I/O的两个阶段分为 数据准备和数据读写

服务器接收客户端的请求,得先监听客户端有没有数据过来,这是数据准备状态,然后就是数据过来了该怎么去读写,这是数据读写状态

一、网络IO阶段一

数据准备阶段分为以下两种

  • 阻塞: 让调用I/O的线程进入阻塞状态 ,数据准备好了就唤醒
  • 非阻塞: 不会改变线程的状态,通过返回值判断连接断开、无数据非阻塞IO返回、有数据返回等情况

I/O的阻塞、非阻塞、同步和异步_后端

sockfd相当于就是系统的文件描述符,代表1个I/O,创建的时候默认是阻塞,当我调用1个阻塞I/O的话,如果sockfd上没有数据可读,这个recv不会返回,造成当前线程阻塞,直到sockfd上有数据到来

如果我们在创建sockfd的时候设置是非阻塞,recv的体现是:如果sockfd上没有数据到来的话,recv直接返回回来,不会造成当前线程阻塞。sockfd上如果没有数据准备好,一般我们会编写一个循环,不断recv,不断让CPU空转

非阻塞情况下,我们一般这样判断

  • ​size==-1​​,表示错误,是系统的内部错误,需要查看错误码errno,可能要close(sockfd)
  • ​size==0 && errno==EAGAIN​​,表示正常的非阻塞返回,sockfd上没有数据,但是连接正常
  • ​size==0 && 无errno​​,表示网络对端关闭了连接,对端close(sockfd)
  • ​size>0​​,表示sockfd上有数据

二、网络IO阶段二

网络IO阶段2,数据读写阶段根据应用程序和内核的IO交互方式分为以下两种:同步、异步

1. I/O同步

I/O的阻塞、非阻塞、同步和异步_后端_02


IO同步方式下,我在应用程序上调用recv函数,这个sockfd我不管它工作在阻塞模式还是非阻塞模式,真的有数据准备好了之后(TCP的接收缓冲区有数据了),我们要读这个数据,这个buff是应用层自己定义的,recv会停在这里,然后从内核的TCP接收缓冲区搬数据到应用层上的buff,没搬完之前,recv不会返回。搬完了,recv才返回,size表示应用程序从TCP接收缓冲区搬了多少数据

I/O同步的意思就是:当我调用网络I/O的接口,当I/O阶段1数据准备好之后,在数据读写的时候,应用层自己去读写内核TCP缓冲区,耗时的过程都花在应用层上。数据没有从内核完全搬到应用层,recv就不会返回

2. I/O异步

我们把sockfd告诉内核,我们对sockfd上的数据感兴趣
我们把buff告诉内核,我们需要用buff存放sockfd上的数据
我们把SIGIO告诉内核,buff中准备数据好了,用SIGIO通知我

内核把sockfd对应的TCP接收缓冲区的数据搬到buff里面,搬完以后,通过信号SIGIO给应用程序通知一下。应用程序收到SIGIO通知以前,应用程序就可以玩自己的,做任何事都可以

I/O的阻塞、非阻塞、同步和异步_应用程序_03


当我们应用程序调用异步I/O接口的时候,我们就把sockfd,buff,SIGIO(通知方式,也可以通过回调,我们在这里用的是sigio)通过异步I/O接口都塞给了操作系统。应用程序做自己的任何事情都可以,当操作系统通过SIGIO通知应用层的时候,buff中就已经有数据了,应用程序不用自己去TCP缓冲区搬数据,不用像I/O同步一样一直阻塞等待或者非阻塞空转

在同步I/O调用的时候,内核TCP缓冲区有数据准备好了,应用程序自己去内核搬数据,搬完以后recv才返回,耗的是应用程序的时间

异步,一定要记住通知这个特点(异步最大的标识,是异步就有通知),异步I/O优点是效率高,缺点是编程复杂

linux给我们提供的典型异步I/O接口:aio_read,aio_write

I/O的阻塞、非阻塞、同步和异步_非阻塞_04


给异步IO接口传的数据,就是给内核传的数据

struct aiocb {
int aio_fildes; // 文件描述符
off_t aio_offset;
volatile void *aio_buf; // 用户空间的buff
size_t aio_nbytes;
int aio_reqprio;
struct sigevent aio_sigevent; // 通知信号
int aio_lio_opcode;
}

I/O的阻塞、非阻塞、同步和异步_数据_05

陈硕:在处理 IO 的时候,阻塞和非阻塞都是同步 IO;只有使用了特殊的 API 才是异步IO

这里说的特殊的API就是平台提供的异步API,我们只管把参数给API,让内核把所有的IO相关事情都完成,自己等通知就行

epoll是同步的I/O

epoll_wait在调用的时候,我们传参数以后,最后一个参数的timeout,如果不自定义时间,相当于工作在阻塞状态,有事件发生,会返回发生事件的event,我们从event上读,如果event对应的事件是它在sockfd上有可读数据,我们调用recv读,应用程序自己去内核TCP接收缓冲区搬数据,所以这还是同步的IO

我们如果有设置timeout超时时间后,我们也得检查有没有发生事件event,没有的话,我们继续循环epoll_wait监听

三、业务角度的同步和异步

同步就是A操作等待B操作做完事情,得到返回值,继续处理

异步就是A操作告诉B操作它感兴趣的事件以及通知方式,A操作继续执行自己的业务逻辑,等B监听到相应事件发生后,B会 通知 A,A开始相应的数据操作处理逻辑

四、总结

阻塞,非阻塞,同步,异步描述的都是I/O的一些状态,一个典型的网络I/O包含2个阶段:数据准备(数据就绪)和数据读写

比如说recv,传一个sockfd,buff,buff的大小,数据就绪指的是远端有没有数据过来,就是内核相应的sockfd对应的TCP接收缓冲区是否有数据可读,若sockfd工作在阻塞模式,当我们调用recv的时候,如果数据没有就绪,当前线程会阻塞在recv

如果这个sockfd是工作在非阻塞模式下的话,当我们去调用系统I/O接口recv的时候,recv会直接返回的,我们一般根据返回值判断

  • ​size==-1​​,表示错误,是系统的内部错误,需要查看错误码errno,可能要close(sockfd)
  • ​size==0 && errno==EAGAIN​​,表示正常的非阻塞返回,sockfd上没有数据,但是连接正常
  • ​size==0 && 无errno​​,表示网络对端关闭了连接,对端close(sockfd)
  • ​size>0​​,表示sockfd上有数据

如果是同步I/O

远端有数据过来存放在内核的TCP接收缓冲区,应用程序调用recv这个接口,应用程序自己花时间,把数据从内核的TCP接收缓冲区拷贝到给recv传的buff(应用程序的缓冲区),拷贝数据的过程中,应用程序是一直等待数据拷贝完成后,recv才会返回

如果是异步I/O

调用系统给我们提供异步I/O接口的时候,我们要传入

  • sockfd:对应一个TCP接收缓冲区,从远端接收数据的
  • buff:如果有数据,要把内核缓冲区的数据搬到应用程序的缓冲区中
  • 通知方式:通过信号或者回调,告诉操作系统,到时候内核负责监听sockfd上是否有数据可读,有的话把数据从内核的TCP缓冲区搬到buff上,数据搬完后,内核最后通过应用程序告知他的通知方式来通知应用程序