上一篇《
聊聊同步、异步、阻塞与非阻塞》已经通俗的讲解了,要理解同步、异步、阻塞与非阻塞重要的两个概念点了,没有看过的,建议先看这篇博文理解这两个概念点。在认知上,创建统一的模型。这样,你们在继续看本篇时,才不会理解有误差。
那么,在正式开始讲Linux IO模型前,好比:同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不一样的人在不一样的上下文下给出的答案是不一样的。因此先限定一下本文的上下文。
1 概念说明
在进行解释以前,首先要说明几个概念:
用户空间和内核空间
进程切换
进程的阻塞
文件描述符
缓存 IO
1.1 用户空间与内核空间
如今操做系统都是采用虚拟存储器,那么对32位操做系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也有访问底层硬件设备的全部权限。为了保证用户进程不能直接操做内核(kernel),保证内核的安全,操做系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操做系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0×00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
1.2 进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复之前挂起的某个进程的执行。这种行为被称为进程切换。所以能够说,任何进程都是在操做系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另外一个进程上运行,这个过程当中通过下面这些变化:
- 保存处理机上下文,包括程序计数器和其余寄存器。
- 更新PCB信息。
- 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
- 选择另外一个进程执行,并更新其PCB。
- 更新内存管理的数据结构。
- 恢复处理机上下文。
注:总而言之就是很耗资源,具体的能够参考这篇文章:进程切换。
1.3 进程的阻塞
正在执行的进程,因为期待的某些事件未发生,如请求系统资源失败、等待某种操做的完成、新数据还没有到达或无新工做作等,则由系统自动执行阻塞原语(Block),使本身由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也所以只有处于运行态的进程(得到CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
1.4 文件描述符fd
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者建立一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写每每会围绕着文件描述符展开。可是文件描述符这一律念每每只适用于UNIX、Linux这样的操做系统。
1.5 缓存 IO
缓存 IO 又被称做标准 IO,大多数文件系统的默认 IO 操做都是缓存 IO。在 Linux 的缓存 IO 机制中,操做系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 IO 的缺点:
数据在传输过程当中须要在应用程序地址空间和内核进行屡次数据拷贝操做,这些数据拷贝操做所带来的 CPU 以及内存开销是很是大的。
2 Linux IO模型
网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO能够理解为对流的操做。刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。因此说,当一个read操做发生时,它会经历两个阶段:
- 第一阶段:等待数据准备 (Waiting for the data to be ready)。
- 第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
对于socket流而言,
- 第一步:一般涉及等待网络上的数据分组到达,而后被复制到内核的某个缓冲区。
- 第二步:把数据从内核缓冲区复制到应用进程缓冲区。
网络应用须要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者。网络IO的模型大体有以下几种:
- 同步模型(synchronous IO)
- 阻塞IO(bloking IO)
- 非阻塞IO(non-blocking IO)
- 多路复用IO(multiplexing IO)
- 信号驱动式IO(signal-driven IO)
- 异步IO(asynchronous IO)
注:因为signal driven IO在实际中并不经常使用,因此我这只说起剩下的四种IO Model。
在深刻介绍Linux IO各类模型以前,让咱们先来探索一下基本 Linux IO 模型的简单矩阵。以下图所示:
每一个 IO 模型都有本身的使用模式,它们对于特定的应用程序都有本身的优势。本节将简要对其一一进行介绍。常见的IO模型有阻塞、非阻塞、IO多路复用,异步。以一个生动形象的例子来讲明这四个概念。周末我和女朋友去逛街,中午饿了,咱们准备去吃饭。周末人多,吃饭须要排队,我和女朋友有如下几种方案。
2.1 同步阻塞 IO(blocking IO)
2.1.1 场景描述
我和女朋友点完餐后,不知道何时能作好,只好坐在餐厅里面等,直到作好,而后吃完才离开。女朋友本想还和我一块儿逛街的,可是不知道饭能何时作好,只好和我一块儿在餐厅等,而不能去逛街,直到吃完饭才能去逛街,中间等待作饭的时间浪费掉了。这就是典型的阻塞。
2.1.2 网络模型
同步阻塞 IO 模型是最经常使用的一个模型,也是最简单的模型。在linux中,默认状况下全部的socket都是blocking。它符合人们最多见的思考逻辑。阻塞就是进程 "被" 休息, CPU处理其它进程去了。
在这个IO模型中,用户空间的应用程序执行一个系统调用(recvform),这会致使应用程序阻塞,什么也不干,直到数据准备好,而且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据处处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络IO。调用应用程序处于一种再也不消费 CPU 而只是简单等待响应的状态,所以从处理的角度来看,这是很是有效的。在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程,大体以下图:
2.1.3 流程描述
当用户进程调用了recv()/recvfrom()这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来讲,不少时候数据在一开始尚未到达。好比,尚未收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程须要等待,也就是说数据被拷贝到操做系统内核的缓冲区中是须要一个过程的。而在用户进程这边,整个进程会被阻塞(固然,是进程本身选择的阻塞)。第二个阶段:当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,而后kernel返回结果,用户进程才解除block的状态,从新运行起来。
因此,blocking IO的特色就是在IO执行的两个阶段都被block了。
优势:
- 可以及时返回数据,无延迟;
- 对内核开发者来讲这是省事了;
缺点:
- 对用户来讲处于等待就要付出性能的代价了;
2.2 同步非阻塞 IO(nonblocking IO)
2.2.1 场景描述
我女朋友不甘心白白在这等,又想去逛商场,又担忧饭好了。因此咱们逛一会,回来询问服务员饭好了没有,来来回回好屡次,饭都还没吃都快累死了啦。这就是非阻塞。须要不断的询问,是否准备好了。
2.2.2 网络模型
同步非阻塞就是 “每隔一下子瞄一眼进度条” 的轮询(polling)方式。在这种模型中,设备是以非阻塞的形式打开的。这意味着 IO 操做不会当即完成,read 操做可能会返回一个错误代码,说明这个命令不能当即知足(EAGAIN 或 EWOULDBLOCK)。
在网络IO时候,非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不同,”非阻塞将大的整片时间的阻塞分红N多的小的阻塞, 因此进程不断地有机会 ‘被’ CPU光顾”。
也就是说非阻塞的recvform系统调用调用以后,进程并无被阻塞,内核立刻返回给进程,若是数据还没准备好,此时会返回一个error。进程在返回以后,能够干点别的事情,而后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程一般被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。须要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
在linux下,能够经过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操做时,流程如图所示:
2.2.3 流程描述
当用户进程发出read操做时,若是kernel中的数据尚未准备好,那么它并不会block用户进程,而是马上返回一个error。从用户进程角度讲,它发起一个read操做后,并不须要等待,而是立刻就获得了一个结果。用户进程判断结果是一个error时,它就知道数据尚未准备好,因而它能够再次发送read操做。一旦kernel中的数据准备好了,而且又再次收到了用户进程的system call,那么它立刻就将数据拷贝到了用户内存,而后返回。
因此,nonblocking IO的特色是用户进程须要不断的主动询问kernel数据好了没有。
同步非阻塞方式相比同步阻塞方式:
优势:可以在等待任务完成的时间里干其余活了(包括提交其余任务,也就是 “后台” 能够有多个任务在同时执行)。
缺点:任务完成的响应延迟增大了,由于每过一段时间才去轮询一次read操做,而任务可能在两次轮询之间的任意时间完成。这会致使总体数据吞吐量的下降。
2.3 IO 多路复用( IO multiplexing)
2.3.1 场景描述
与第二个方案差很少,餐厅安装了电子屏幕用来显示点餐的状态,这样我和女朋友逛街一会,回来就不用去询问服务员了,直接看电子屏幕就能够了。这样每一个人的餐是否好了,都直接看电子屏幕就能够了,这就是典型的IO多路复用。
2.3.2 网络模型
因为同步非阻塞方式须要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。若是轮询不是进程的用户态,而是有人帮忙就行了。那么这就是所谓的 “IO 多路复用”。UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,作的事情是同样的)。
IO多路复用有两个特别的系统调用select、poll、epoll函数。select调用是内核级别的,select轮询相对非阻塞的轮询的区别在于—前者能够等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,而后进程再进行recvform系统调用,将数据由内核拷贝到用户进程,固然这个过程是阻塞的。select或poll调用以后,会阻塞进程,与blocking IO阻塞不一样在于,此时的select不是等到socket数据所有到达再处理, 而是有了一部分数据就会调用用户进程来处理。如何知道有一部分数据到达了呢?监视的事情交给了内核,内核负责数据到达的处理。也能够理解为"非阻塞"吧。
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,可是和阻塞I/O所不一样的的,这两个函数能够同时阻塞多个I/O操做。并且能够同时对多个读操做,多个写操做的I/O函数进行检测,直到有数据可读或可写时(注意不是所有数据可读或可写),才真正调用I/O操做函数。
对于多路复用,也就是轮询多个socket。多路复用既然能够处理多个IO,也就带来了新的问题,多个IO之间的顺序变得不肯定了,固然也能够针对不一样的编号。具体流程,以下图所示:
2.3.3 流程描述
IO multiplexing就是咱们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就能够同时处理多个网络链接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的全部socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”全部select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操做,将数据从kernel拷贝到用户进程。
多路复用的特色是经过一种机制一个进程能同时等待IO文件描述符,内核监视这些文件描述符(套接字描述符),其中的任意一个进入读就绪状态,select, poll,epoll函数就能够返回。对于监视的方式,又能够分为 select, poll, epoll三种方式。
上面的图和blocking IO的图其实并无太大的不一样,事实上,还更差一些。由于这里须要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。可是,用select的优点在于它能够同时处理多个connection。
因此,若是处理的链接数不是很高的话,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。(select/epoll的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。)
在IO multiplexing Model中,实际中,对于每个socket,通常都设置成为non-blocking,可是,如上图所示,整个用户的process实际上是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。因此IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。
了解了前面三种IO模式,在用户进程进行系统调用的时候,他们在等待数据到来的时候,处理的方式不同,直接等待,轮询,select或poll轮询,两个阶段过程:
第一个阶段有的阻塞,有的不阻塞,有的能够阻塞又能够不阻塞。
第二个阶段都是阻塞的。
从整个IO过程来看,他们都是顺序执行的,所以能够归为同步模型(asynchronous)。都是进程主动等待且向内核检查状态。【此句很重要!!!】
高并发的程序通常使用同步非阻塞方式而非多线程 + 同步阻塞方式。要理解这一点,首先要扯到并发和并行的区别。好比去某部门办事须要依次去几个窗口,办事大厅里的人数就是并发数,而窗口个数就是并行度。也就是说并发数是指同时进行的任务数(如同时服务的 HTTP 请求),而并行数是能够同时工做的物理资源数量(如 CPU 核数)。经过合理调度任务的不一样阶段,并发数能够远远大于并行度,这就是区区几个 CPU 能够支持上万个用户并发请求的奥秘。在这种高并发的状况下,为每一个任务(用户请求)建立一个进程或线程的开销很是大。而同步非阻塞方式能够把多个 IO 请求丢到后台去,这就能够在一个进程里服务大量的并发 IO 请求。
注意:IO多路复用是同步阻塞模型仍是异步阻塞模型,在此给你们分析下:
此处仍然不太清楚的,强烈建议你们在细究
《聊聊同步、异步、阻塞与非阻塞》中讲同步与异步的根本性区别,同步是须要主动等待消息通知,而异步则是被动接收消息通知,经过回调、通知、状态等方式来被动获取消息。IO多路复用在阻塞到select阶段时,用户进程是主动等待并调用select函数获取数据就绪状态消息,而且其进程状态为阻塞。因此,把IO多路复用归为同步阻塞模式。
2.4 信号驱动式IO(signal-driven IO)
信号驱动式I/O:首先咱们容许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,能够在信号处理函数中调用I/O操做函数处理数据。过程以下图所示:
2.5 异步非阻塞 IO(asynchronous IO)
2.5.1 场景描述
女朋友不想逛街,又餐厅太吵了,回家好好休息一下。因而咱们叫外卖,打个电话点餐,而后我和女朋友能够在家好好休息一下,饭好了送货员送到家里来。这就是典型的异步,只须要打个电话说一下,而后能够作本身的事情,饭好了就送来了。
2.5.2 网络模型
相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用以后,不管内核数据是否准备好,都会直接返回给用户进程,而后用户态进程能够去作别的事情。等到socket数据准备好了,内核直接复制数据给进程,而后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
Linux提供了AIO库函数实现异步,可是用的不多。目前有不少开源的异步IO库,例如libevent、libev、libuv。异步过程以下图所示:
2.5.3 流程描述
用户进程发起aio_read操做以后,马上就能够开始去作其它的事。而另外一方面,从kernel的角度,当它受到一个asynchronous read以后,首先它会马上返回,因此不会对用户进程产生任何block。而后,kernel会等待数据准备完成,而后将数据拷贝到用户内存,当这一切都完成以后,kernel会给用户进程发送一个signal或执行一个基于线程的回调函数来完成此次 IO 处理过程,告诉它read操做完成了。
在 Linux 中,通知的方式是 “信号”:
若是这个进程正在用户态忙着作别的事(例如在计算两个矩阵的乘积),那就强行打断之,调用事先注册的信号处理函数,这个函数能够决定什么时候以及如何处理这个异步任务。因为信号处理函数是忽然闯进来的,所以跟中断处理程序同样,有不少事情是不能作的,所以保险起见,通常是把事件 “登记” 一下放进队列,而后返回该进程原来在作的事。
若是这个进程正在内核态忙着作别的事,例如以同步阻塞方式读写磁盘,那就只好把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
若是这个进程如今被挂起了,例如无事可作 sleep 了,那就把这个进程唤醒,下次有 CPU 空闲的时候,就会调度到这个进程,触发信号通知。
异步 API 说来轻巧,作来难,这主要是对 API 的实现者而言的。Linux 的异步 IO(AIO)支持是 2.6.22 才引入的,还有不少系统调用不支持异步 IO。Linux 的异步 IO 最初是为数据库设计的,所以经过异步 IO 的读写操做不会被缓存或缓冲,这就没法利用操做系统的缓存与缓冲机制。
不少人把 Linux 的 O_NONBLOCK 认为是异步方式,但事实上这是前面讲的同步非阻塞方式。须要指出的是,虽然 Linux 上的 IO API 略显粗糙,但每种编程框架都有封装好的异步 IO 实现。操做系统少作事,把更多的自由留给用户,正是 UNIX 的设计哲学,也是 Linux 上编程框架百花齐放的一个缘由。
从前面 IO 模型的分类中,咱们能够看出 AIO 的动机:
同步阻塞模型须要在 IO 操做开始时阻塞应用程序。这意味着不可能同时重叠进行处理和 IO 操做。
同步非阻塞模型容许处理和 IO 操做重叠进行,可是这须要应用程序根据重现的规则来检查 IO 操做的状态。
这样就剩下异步非阻塞 IO 了,它容许处理和 IO 操做重叠进行,包括 IO 操做完成的通知。
IO多路复用除了须要阻塞以外,select 函数所提供的功能(异步阻塞 IO)与 AIO 相似。不过,它是对通知事件进行阻塞,而不是对 IO 调用进行阻塞。
2.6 关于异步阻塞
有时咱们的 API 只提供异步通知方式,例如在 node.js 里,但业务逻辑须要的是作完一件过后作另外一件事,例如数据库链接初始化后才能开始接受用户的 HTTP 请求。这样的业务逻辑就须要调用者是以阻塞方式来工做。
为了在异步环境里模拟 “顺序执行” 的效果,就须要把同步代码转换成异步形式,这称为 CPS(Continuation Passing Style)变换。BYVoid 大神的 continuation.js 库就是一个 CPS 变换的工具。用户只需用比较符合人类常理的同步方式书写代码,CPS 变换器会把它转换成层层嵌套的异步回调形式。
另一种使用阻塞方式的理由是下降响应延迟。若是采用非阻塞方式,一个任务 A 被提交到后台,就开始作另外一件事 B,但 B 还没作完,A 就完成了,这时要想让 A 的完成事件被尽快处理(好比 A 是个紧急事务),要么丢弃作到一半的 B,要么保存 B 的中间状态并切换回 A,任务的切换是须要时间的(无论是从磁盘载入到内存,仍是从内存载入到高速缓存),这势必下降 A 的响应速度。所以,对实时系统或者延迟敏感的事务,有时采用阻塞方式比非阻塞方式更好。
2.7 五种IO模型