典型的一次IO的两个阶段是什么?
实际上不管是内存、网络还是磁盘I/O都包含两个阶段,这里我们主要说的就是网络I/O,它包括数据准备和数据读写。我们作为服务器,接收客户端的请求,得先监听客户端有没有数据过来,这是一个状态,还有就是数据过来了该怎么去读写,这又是一个状态。实际上,阻塞,非阻塞,同步,异步,分别是这两种状态下的体系。
一、网络I/O阶段一
数据准备:根据系统IO操作的就绪状态分为阻塞和非阻塞
阻塞:让调用I/O的线程进入阻塞状态 ,数据准备好了就唤醒
非阻塞:不会改变线程的状态,通过返回值判断
sockfd相当于就是系统的文件描述符,代表1个I/O,创建的时候默认是阻塞,当我调用1个阻塞I/O的话,如果sockfd上没有数据可读,这个recv不会返回,造成当前线程阻塞,等待sockfd上有数据到来。
如果返回了,就是有数据可读了,接下去就是数据读写了。返回的是最终读的数据的大小。一直等着。
如果在recv之前把sockfd设置成非阻塞,如果sockfd没有数据到来的话,recv直接返回回来,不会造成当前线程阻塞。sockfd没有数据准备好的话,不断的空转CPU。
我们都是这么判断:
- 如果size==-1的话,表示远端断开连接了
- 如果size==0&&errno==EAGAIN,表示正常的非阻塞返回,连接是正常的,sockfd上没有网络事件发生
- 如果size>0,就是表示有数据过来了。
这两个错误表示一样的情况
二、网络I/O阶段二
数据读写:根据应用程序和内核的交互方式分为同步和异步。
有2种同步和异步:I/O的同步和异步;应用层并发的同步和异步,两者相似。
我们研究网络层的同步和异步
1. I/O同步
现在呢,我在应用程序上调用recv函数,这个sockfd我不管它工作在阻塞模式还是非阻塞模式,真的有数据准备好了之后(TCP的接收缓冲区有数据了,就是数据可读了),我们要读这个数据,这个buf是用户层自己定义的,recv就可以开始接收了,是应用程序卡在这里,从内核的TCP接收缓冲区搬数据到应用层上的buf,在搬的过程中,因为size>0,这就表示从内核搬了多少字节的数据,我们就要访问buf了,没搬完之前,不会进入到下面的if语句。搬完了,recv才返回过来,看看size是多少,就是搬了多少数据。
I/O同步的意思就是:当我调用网络I/O的接口,当I/O阶段1数据准备好之后,在数据读写的时候,应用层自己调用网络I/O接口自己去读写,都花在应用层上。
recv和send是同步的I/O接口
2. I/O异步
当我请求内核的时候,我比较关心sockfd上的数据,远端如果发过来数据,我需要读sockfd上的数据,我有一个buf,到时候如果有数据来了,内核能不能帮忙把数据放到buf里面,我再给内核注册一个sigio信号,也就是说,对一个操作系统级别的异步的I/O接口来说,我先塞给内核一个sockfd,对这个sockfd上的事件感兴趣,如果sockfd上有数据可读的话,麻烦操作系统内核把数据搬到buf里面。
内核把内核缓冲区-sockfd对应的TCP接收缓冲区的数据搬到buf里面,搬完以后,通过信号sigio给应用程序通知一下。应用程序就可以玩自己的了,做任何事清都可以。
当我们应用程序调用异步I/O接口的时候,我们就把sockfd,buf,sigio(通知方式,也可以通过回调,我们在这里用的是sigio)通过异步I/O接口都塞给了操作系统。应用程序做自己的任何事情都可以,当操作系统sigio通知你的时候,你看到的是buf的数据已经准备好了,应用程序不用搬,不用花应用程序的时间,不用像I/O同步一样一直阻塞等待recv或者空转。
异步,一定要记住这个词语:通知(异步最大的标识,是异步就有通知)
在同步I/O调用的时候,有数据准备好了,数据是应用程序自己花时间搬的,搬完以后,recv才返回,把数据从内核的接收缓冲区搬到用户的buf里面,耗的是应用程序的时间。
看操作系统有没有提供异步I/O接口让你调用。 异步I/O效率高,但是编程复杂,出了问题不好排查!
linux的aio_read,aio_write
就是典型的linux给我们提供的异步I/O接口。
就是aio_read需要的参数
就是应用程序在调用异步I/O接口需要传的参数,也就是给内核传的参数
3. IO同步和异步总结
应用程序并发的同步和异步其实是一样的,所谓的同步就是我们在调用应用层或者使用的第三方库或者是自己写的库,就是一些API接口,就和我们这里的I/O同步是一样的,要么是一直等待数据数据的读,要么是在这空转等待数据的到来,数据如果来了,搬运还得应用程序自己去做。异步IO就是把该给内核的都给他,然后就可以自己做自己的事情了,到时候数据准备好了,内核帮应用程序读好,再用事先约定好的通知方式通知应用程序,应用程序再去处理数据就可以。
Node.js基于异步非阻塞模式下的高性能服务器
陈硕大神原话:在处理 IO 的时候,阻塞和非阻塞都是同步 IO。只有使用了特殊的 API 才是异步IO。
强调:epoll是同步IO!
epoll_wait在调用的时候,我们传参数以后,最后一个参数的timeout,如果不自定义时间,相当于工作在阻塞状态,有事件发生,会返回发生事件的event,我们从event上读,如果event对应的事件是它在sockfd上有可读数据,我们读,调用recv,当然是我们应用程序自己读了。我们如果有设置timeout超时时间后,我们也得检查有没有发生事件event,没有的话,我们继续循环。
业务层面上的逻辑处理是同步还是异步:
同步就是 A操作等待B操作做完事情,得到返回值,继续处理。
异步就是A操作告诉B操作它感兴趣的事件以及通知方式,A操作继续执行自己的业务逻辑,等B监听到相应事件发生后,B会通知A,A开始相应的数据操作处理逻辑。
一个典型的网络IO接口调用,分为两个阶段,分别是“数据就绪”和“数据读写”,数据就绪阶段分为阻塞和非阻塞,表现得结果就是,阻塞当前线程或是直接返回。
同步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),数据的读写都是由请求方A自己来完成的(不管是阻塞还是非阻塞);
异步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),向B传入请求的事件以及事件发生时通知的方式,A就可以处理其它逻辑了,当B监听到事件处理完成后,会用事先约定好的通知方式,通知A处理结果。
同步阻塞 int size = recv(fd, buf, 1024, 0)
同步非阻塞 int size = recv(fd, buf, 1024, 0)
异步阻塞 (不合理,A本来就可以做其他事情,没有必要阻塞等待B做完事情给A通知,这是在浪费线程A的能力)
异步非阻塞(Node.js)
三、面试问题:解释阻塞、非阻塞、同步和异步
阻塞、非阻塞、同步和异步描述的都是IO的一些状态,一个典型的网络IO包含两阶段:数据准备(数据就绪)和数据读写。
比如说recv,传一个sockfd,buf,buf的大小,数据就绪就是远端有没有数据过来,就是内核相应的sockfd对应的TCP接收缓冲区是否有数据可读,当sockfd,当I/O工作在阻塞模式下的话,当我们调用recv的时候,如果数据没有就绪,recv是会阻塞当前的线程的。如果这个sockfd是工作在非阻塞模式下的话,当我们去调用系统I/O接口recv的时候,recv会直接返回的,我们都是这么判断:
- 如果size==-1的话,表示连接出现问题,连接异常或连接中断
- 如果size==0&&errno==EAGAIN,表示正常的非阻塞返回,sockfd上没有网络事件发生。
- 如果size>0,就是表示有数据过来了。
如果是同步I/O,数据就绪的时候,远端有数据过来了,数据准备好了,开始进行读写了,应用程序调用recv这个接口,recv会继续,相当于应用程序花自己的时间,把数据从内核的TCP接收缓冲区拷贝到给recv传的buf(应用程序的缓冲区),拷贝数据的过程中,应用程序是一直等待数据拷贝完成后,recv才返回,应用程序才自己向后走。
如果是异步I/O的话,调用系统给我们提供异步I/O接口的时候,我们要传入sockfd(对应一个TCP接收缓冲区,从远端接收数据的),buf(如果有数据,要把内核缓冲区的数据搬到应用程序的缓冲区中)和通知方式(通过信号或者回调,告诉操作系统异步I/O,到时候内核负责监听sockfd上是否有数据可读,有的话把数据从内核的TCP缓冲区搬到应用程序传的buf上,内核最后通过应用程序告知他的通知方式来通知应用程序,应用程序就可以处理这些事情,而且数据已经拿到手了。