聊聊同步、异步、阻塞与非阻塞 - 简书 https://www.jianshu.com/p/aed6067eeac9
聊聊Linux 五种IO模型 - 简书 https://www.jianshu.com/p/486b0965c296
socket初探 https://www.jianshu.com/p/02ec5504b919
聊聊IO多路复用之select、poll、epoll详解 - 简书 https://www.jianshu.com/p/dfd940e7fca2
一,概念描述
同步与异步#
首先来解释同步和异步的概念,这两个概念与消息的通知机制有关。也就是同步与异步主要是从消息通知机制角度来说的。
概念描述:
所谓同步
就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列
。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
所谓异步
是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了
。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,
所以它是不可靠的任务序列
。
异步io模型:异步的概念相同,在网络socket读写的时候(read函数,receive函数等,产生系统调用,把数据拷贝到当内核当前的缓冲区里面去),例如用read读数据,发起系统调用时,只需把系统调用信息通知给内核,线程此时就可以返回了,由内核来处理后续的事宜,内核有队列来接收信号并保存起来,当要读的socket数据到达就绪时,内核回来判断是否丢数据包,是否丢字节,是否有序等等,数据就绪后内核会把数据拷贝到发送者给他传递的某个描述符文件中或者buffer中去,内核的
拷贝完成后,通知发送者,即进程或线程。
非阻塞io:当产生了系统调用,内核数据没有准备好,但是内核不会把调用者的CPU剥夺掉,这时间隔某个时间段再次问系统资源是否准备好,直到所需的资源准备就绪,准备就绪后,
等待内核调用API把数据拷贝到用户态里面去,等待的过程是阻塞的,或执行的时间到了,才会剥夺(此时为主动剥夺)
非阻塞的瓶颈:一次读取数据可能多次切入切出内核,切入切出过程需要上下文保护,需要把当前堆栈中的信息进行保存。
阻塞模式读取socket数据时:当数据没有准备好,即内核的buffer没有准备好,会发生阻塞,即当前进程或线程的CPU被剥夺,则其无法执行其他代码,所以对用户来说,这个进程或线程就停止了,
用户量很大的时候,非阻塞就不适合了,因为读取每一个socket描述符时,都会导致CPU无法被其他进程或线程使用,直到数据就绪,服务器编程用这种模型时,因为网络环境不可预知,可能阻塞,而且用户网络可能比较差,因此会导致整个CPU消耗很大,但是利用率很低。
问题:那么异步的时候剥夺CPU了吗?
答:CPU也同样没有被剥夺,可以去做其他的事情,当内核把数据拷贝执行完成之后,才去通知调用者。
io复用函数:select poll epoll 当他们返回已经就绪的描述符时,需要执行其他API来消费这个就绪的事件,消费的过程实际上是阻塞的,即读和写。
因此io复用的作用是能够一次性批量的监听和收集描述符,但是描述符就绪后需要读写进行消费,消费过程仍然是阻塞的。
过程:获取到就绪的描述符后,加入到描述符表中(加入的方式不一样),加入后设置非阻塞API。
Linux的io模型
网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作
对于一次IO访问(以read举例),
数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间
将上述过程细化:数据发送到当前网卡,网卡驱动程序把数据取出来,拷贝到对应的内核中事先建立好的某一个进程中,拷到这个进程内部维护的某一个文件描述符对应的缓冲区。
对应到编程中,先定义xbuffer,然后read的API参数,第一个为fd,从哪个fd中读,传给内核,内核知道fd是由哪个进程维护的。
当一个read操作发生时,它会经历两个阶段:
第一阶段:等待数据准备 (Waiting for the data to be ready)。(对方数据发送,在路由器中传送,驱动程序读取数据,读取后找到进程、文件描述符和buffer来拷贝,拷贝后数据已经交付给内核,内核通过一系列规则来判断是否丢包,有丢包则重发,直到数据没问题;没问题后唤醒进程或线程,唤醒后在进程线程持有CPU执行读取数据之前也会产生等待,因为调度延迟,当系统负载很高时,内核有可能无法持有CPU去唤醒)
第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。拷贝过程是一个阻塞的过程。
对于socket流而言
第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区(某个socket所分配的读写缓冲区)。
第二步:把数据从内核缓冲区复制到应用进程缓冲区。(进程缓冲区,task_struct、mm_struct)
网络应用需要处理的无非就是两大类问题,
网络IO,数据计算
。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者.因此编程的优化就是如何给CPU准备好数据,是CPU的利用率提高。
网络IO的模型大致有如下几种:(划分的方式有很多,这里是按照同步和异步来区分)
同步模型
(synchronous IO)
阻塞IO(bloking IO)
非阻塞IO(non-blocking IO) 调整一个API就可以设置成非阻塞
多路复用IO(multiplexing IO)
信号驱动式IO(signal-driven IO) 暂时没讲,不需要学,实际中已经不怎么用了
异步IO
(asynchronous IO)
同步和异步区别在于读和写数据的过程中与内核交互的过程,其他的地方相同,如tcp过程都需要创建socket,bind,listen,accept和close。
同步非阻塞方式相比同步阻塞方式
:
优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
io多路复用
目的:在同一个socket(复用)上能够同时处理多种类型(多路)的描述符。如udp和tcp,还能同时处理多种事件监听描述符事件,读写事件,异常事件。
场景描述
餐厅安装了电子屏幕用来显示点餐的状态,这样我和女友逛街一会,回来就不用去询问服务员了,直接看电子屏幕就可以了。这样每个人的餐是否好了,都直接看电子屏幕就可以了,
这就是典型的IO多路复用。
电子屏幕:io复用中返回的就绪描述符,遍历描述符询问类型(是读是写还是listen),然后调用对应的API去处理这个描述符。
网络模型
由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。如果轮询不是进程的用户态,而是有人帮忙就好了(即寻找一个代理,减少切入切出的状态,用代理来问内核,在内核里判断)。
那么这就是所谓的 “IO 多路复用”
。UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事情是一样的)。
IO多路复用有两个特别的系统调用select、poll、epoll函数。select调用是内核级别的,select轮询相对非阻塞的轮询的区别在于---前者可以等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读(具体多少数目就绪好再返回,阻塞非阻塞等是由select最后一个参数,超时机制来控制的)。然后进程再进行recvform系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。
过程描述:用户将关心的描述符通过调用select函数注册给内核,内核在超时时间内判断多个描述符就绪,时间到达时,若没有描述符就绪,会超时返回;如果有描述符就绪,就提前返回。
select或poll调用之后,会阻塞进程,与blocking IO阻塞(即常规阻塞)不同在于,此时的select不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理。
过程描述:如超时时间为1s,在1s时间到达之前,有一部分数据就绪了,就可以返回。而之前的常规阻塞,读数据描述符没有就绪时,只能等待,等待就绪并处理完成之后,才能去处理下一个描述符。下一个事件可能是accept或者继续读数据,但每一次调用必须只能处理一个,处理完成才能进入下一件处理事件。
常规阻塞返回条件:读到数据或者对方把连接关闭掉才能返回,而且它必须要返回,后面所有客户端连接,读数据,写数据才能被处理。
代码:在accept之上有一个while或for死循环,指的是不断accept新的连接,如果新的连接有,那么accept返回,返回后读数据,如果读数据时对方数据一直无法就绪,那么循环无法继续往下行走,于是产生阻塞,当事件就绪处理完成后,才能进入下一次循环,处理新的连接。 因此阻塞模型,每时每刻只能处理一个客户端。
阻塞模型+fork的作用:是让用户1与用户2之间不会产生相互等待的关系。
如何知道有一部分数据是否到达,监视的事情交给了内核,内核负责数据到达的处理。也可以理解为"非阻塞"吧。
过程描述:数据到网卡,由驱动程序读,放在对应进程的某个buffer中去,放的时候会把描述符的状态进行修改,把描述符的集合返回去,用户拿到由select和poll返回的描述符集合列表,去处理对应的描述符里面的数据。此时描述符的集合有没有就绪,什么时候就绪,这些处理是由内核直接完成的,内核不需要每一次针对每一个描述符进行多次用户态与内核态之间的切换。
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用I/O操作函数。
对于多路复用,也就是轮询多个socket。
注:这里的socket不是创建描述符的socket,是accept函数返回的socket文件描述符,这个文件描述符不会占用新的端口。
多路复用既然可以处理多个IO,也就带来了新的问题,多个IO之间的顺序变得不确定了
,当然也可以针对不同的编号。比如批量注册100个描述符,但是先注册的描述符可能后被处理
图中描述的只是一个描述符返回就绪
流程描述
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。(这也是本身的目标,因为以前的阻塞模型,拿一个进程编程的时候,一旦某一个io阻塞,整个while或for循环就不能向下进行了;现在虽然也是一个进程,但是能够处理多个网络连接了)它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block
,而同时,kernel会“监视”所有select负责的socket,
当任何一个socket中的数据准备好了,select就会返回
。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
多路复用的特点是
通过一种机制一个进程能同时等待多个IO文件描述符
,内核监视这些文件描述符(套接字描述符),判断有没有就绪,就绪了该什么时候返回,其中的任意一个进入读就绪状态,select, poll,epoll函数就可以返回。对于监视的方式,又可以分为 select, poll, epoll三种方式。
如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。(select/epoll的优势并不是对于单个连接能处理得更快(因为可能会有延迟),而是在于能处理更多的连接。)
原因:因为多线程+阻塞io操作,例如可以开40个线程,阻塞在每一个io上,如果io准备就绪了,就可以第一时间返回,返回后线程可以拥有CPU来处理后续的动作;如果用select或poll,先注册的描述符可能在后面处理,即顺序会变掉。会造成延迟增大。
但正常情况下 大多数情况还是使用io复用,根据用户量确定,这里只是表示他不是万能的。
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。
只不过process是被select这个函数block,而不是被socket IO给block
。io复用中调用io复用函数也会产生阻塞把当前进程或线程阻塞掉,阻塞的原因并不是某个io不就绪,而且因为阻塞后让内核有时间执行某一些描述符上的检测以及数据的拷贝动作。
所以
IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。这个真正的io系统调用阻塞是无法避免的。
io多路复用划分到同步阻塞模型中去,因为几个函数本身就是阻塞的,返回数据读写时也是阻塞的。
在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源,
io复用的主要场景:
服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。
服务器需要同时处理多种网络协议的套接字。
前面三种IO模式,在用户进程进行系统调用的时候,
他们在等待数据到来的时候,处理的方式不一样,直接等待,轮询,select或poll轮询
,两个阶段过程:
第一个阶段有的阻塞(阻塞模型),有的不阻塞(非阻塞模型),有的可以阻塞又可以不阻塞(select或poll)。
第二个阶段都是阻塞的。无论通过上面哪一种方式,当判断描述符就绪了,消费事件时,就是阻塞的。
从整个IO过程来看,他们都是顺序执行的,因此可以归为同步模型(synchronous)。都是进程主动等待且向内核检查状态。非阻塞模型--多次自己向系统调用检查,io复用--内核帮助检查,阻塞--内核帮助检查,由内核负责唤醒
高并发的程序一般使用同步非阻塞方式而非多线程 + 同步阻塞方式。要理解这一点,首先要扯到并发和并行的区别。比如去某部门办事需要依次去几个窗口,办事大厅里的人数就是并发数,而窗口个数就是并行度。也就是说并发数是指同时进行的任务数(如同时服务的 HTTP 请求),而并行数是可以同时工作的物理资源数量(如 CPU 核数)。通过合理调度任务的不同阶段,并发数可以远远大于并行度,这就是区区几个 CPU 可以支持上万个用户并发请求的奥秘。在这种高并发的情况下,为每个任务(用户请求)创建一个进程或线程的开销非常大(占有很多系统资源)。而同步非阻塞方式可以把多个 IO 请求丢到后台去,这就可以在一个进程里服务大量的并发 IO 请求。
并行:物理上,CPU只有一个,在物理上某一刻只能执行一个实体
并发:逻辑上,在某一时间段内,执行多个进程
异步非阻塞
场景描述
女友不想逛街,又餐厅太吵了,回家好好休息一下。于是我们叫外卖,打个电话点餐,然后我和女友可以在家好好休息一下,饭好了送货员送到家里来。这就是典型的异步,只需要打个电话说一下,然后可以做自己的事情,饭好了就送来了。
异步:只管发送信号,相信对方能够执行任务。
网络模型
相对于同步IO,异步IO不是顺序执行。
用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程(即把CPU的使用控制权交还给进程),然后用户态进程可以去做别的事情
。等到socket数据准备好了,内核直接复制数据给进程,
然后从内核向进程发送通知
。
IO两个阶段,进程都是非阻塞的
。(用户进程发起读写事件,以及内核内部的拷贝过程,两个过程,进程都是无感知的)
同步必须要,先创建描述符,监听描述符,等待就绪,就绪后才能读写,读写完成后才能进行其他事件。因此必须顺序执行。
流程描述
用户进程发起aio_read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read(非阻塞请求)之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成(数据发送,数据检查,拷贝),然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程,告诉它read操作完成了。
在Linux系统中,通知的方式就是“信号”
五种io模型总结
阻塞与非阻塞的区别
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
同步io与异步io的区别
看文章里
epoll优点
- 没有最大并发连接的限制(这个poll也没有,但是poll需要把每次关心的描述符拷贝到内核里去),能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
- 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数(回调函数);即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
- 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
epoll对文件描述符的操作有两种模式:
LT(level trigger)和ET(edge trigger)
。LT模式是默认模式,LT模式与ET模式的区别在文章中有,实际中用的更多的是LT的模式。