IO 的五种模型
阻塞和非阻塞有什么区别呢?同步和异步又有什么区别呢?用户空间和内核空间操作系统的核心是内核,它独立于普通的应用程序,可以访问受保护的内核空间,也有访问底层硬件设备的所有权限。为了保护内核的安全,现在操作系统一般都强制用户进程不能直接操作内核、所以操作系统把内存空间划分成了两个部分:内核空间和用户空间。这就好比,饭店老板把整个饭店划分成两个部分:大厅和厨房。大厅用于顾客吃饭,厨房用于厨师做饭,厨房的
什么是水平触发?什么是边缘触发?能否举个例子说明一下呢
回复:
水平触发:
点单后,菜(数据)做好了,服务员端上来问吃不吃(读),你不吃或者吃不完,她过会还会端过来问你吃不吃,提醒你,还没吃完,可以继续吃,反反复复。
边缘触发:
服务员端上菜后,你一次没有吃完,好了,等你想吃剩下的时候,也别吃了,除非再点菜,才能吃到刚没吃完的。
关于提到的水平触发,边缘触发,前面的评论给出了很形象的解释。我尝试从“水平”和“边缘”的由来再解释一次。
水平触发和边缘触发借鉴的是电子触发的概念。在各种数字电子元器件中,输出是随着输入变化而逻辑变化的,最常见的有“与电门”。在与电门中,两个输入的电极同为正电压(真),则输出一个正电压(真);任意一个输入的电位变成负电压(假),则输出一个负电压(假)。问题在于,什么时候触发电位的变化呢?有两种方案:
- 在一个输入的电位变化时(边缘触发)
2.在输入的电位状态变成目标状态时(水平触发)
__| |
(1) (2)
你可能会问,这不是一个概念么?在电子学的概念里,不是。而在这里,网络通信借鉴了这个概念,将消息到达后,读取(触发)的时机形象地分为“水平触发”和“边缘触发”。边缘触发是指消息到来的时刻进行消费,如果一次到达的消息超过了一次消费的最大值,剩余的消息不会被继续消费(类似于高电位保持并不会边缘触发的输出变法),要消费这一部分消息要么等到下一次消息的到来,要么在这次消费之后主动触发消费剩余消息。至于水平触发,则是以是否有剩余消息为标准,有剩余,就一直主动消费直到无消息。
阻塞和非阻塞有什么区别呢?
同步和异步又有什么区别呢?
用户空间和内核空间(操作系统区分的内存空间)
操作系统的核心是内核,
它独立于普通的应用程序,可以访问受保护的内核空间,也有访问底层硬件设备的所有权限。
为了保护内核的安全,现在操作系统一般都强制用户进程不能直接操作内核、
所以操作系统把内存空间划分成了两个部分:内核空间和用户空间。
这就好比,饭店老板把整个饭店划分成两个部分:大厅和厨房。大厅用于顾客吃饭,厨房用于厨师做饭,厨房的门上面一般还会写着: “ 厨房重地,闲人免进 ” ,也就是顾客一般不具有直接使用厨房的特性。
所以,当我们使用 TCP 发送数据的时候,需要先将数据从用户空间拷贝到内核空间,再由内核操作将数据从内核空间发送出去;当我们使用 TCP 读取数据的时候,数据先在内核空间准备好,再从内核空间拷贝到用户空间供用户进程使用。
这就好比,当我们在饭店吃饭的时候,先在客厅点好菜,再由服务员把我们的菜单传递进厨房;当厨房做好了菜,再从厨房由服务员传递到客厅一样。
所以,一次 IO 的读取操作分为两个阶段(写入操作类似):
1 等待内核空间数据准备阶段
2 数据从内核空间拷贝到用户空间
为此, Unix 根据这两个阶段又把 IO 分成了以下五种 IO 模型:
阻塞型 IO
非阻塞型 IO
IO 多路复用
信号驱动 IO
异步 IO
阻塞型 IO
阻塞型 IO ,即当用户进程发起请求时,一直阻塞直到数据拷贝到用户空间为止才返回。
阻塞型 IO 在两个阶段是连续阻塞着的,直到数据返回.
这就好比,你去路边买快餐,这家店比较低级,只有一辆车一个老板。点完餐后,你傻傻地看着老板开始打菜,然后拿给你。整个过程中,你只能看着老板打完菜并拿给你,这两个阶段你都是阻塞的。
非阻塞型 IO
非阻塞型 IO ,用户进程不断询问内核,数据准备好了吗?一直重试,直到内核说数据准备好了,然后把数据从内核空间拷贝到用户空间,返回成功,开始处理数据。
非阻塞型 IO 第一阶段不阻塞,第二阶段阻塞。
这就好比,你去小炒店,这家店高级一点,有独立的店面。点完餐后,你可以边玩手机边等。隔了一会你跑过去问一下老板 “ 我的菜好了没 ” ,老板说 “ 还没好 ” ;隔一会你又跑过去问了下 “ 我的菜好了没 ” ,老板说 “ 还没有 ” ;几次后,你又说 “ 老板,我的菜好了没 ” ,老板说 “ 来了来了 ” ,然后你看着他把菜端到你面前。整个过程中,询问 “ 菜好了没 ” 你不用阻塞,老板立即回应你,你可以立即玩手机,但是端菜的时候你是傻傻地看着他端的,这期间你无法玩手机,你是阻塞的。
IO 多路复用
IO 多路复用,多个 IO 操作共同使用一个 selector (选择器)去询问哪些 IO 准备好了, selector 负责通知那些数据准备好了的 IO ,它们再自己去请求内核数据。
IO 多路复用,第一阶段会阻塞在 selector 上,第二阶段拷贝数据也会阻塞。
这就好比,你去川菜馆吃饭,这家饭店比较大,人也多,还有个美女服务员。你点完菜后,勾搭了一下美女服务员“ 美女,我点个辣子鸡丁,好了通知我一下哦 ” ,美女也没搭理你。其它人也是这么勾搭美女的。然后,美女忙得不可开交,隔一会去厨房看一下,哪些菜好了,每次出来,都会喊 “ 那谁谁谁,你的啥啥菜好了,自己过来端一下。” 。整个过程中,美女去厨房看菜是阻塞的,因为没有菜好的时候她还要等一会;你跑过去端菜也是阻塞的。一部分阻塞在美女身上,一部分阻塞在你身上。
信号驱动 IO
信号驱动 IO ,用户进程发起读取请求之前先注册一个信号给内核说明自己需要什么数据,这个注册请求立即返回,等内核数据准备好了,主动通知用户进程,用户进程再去请求读取数据,此时,需要等待数据从内核空间拷贝到用户空间再返回。
信号驱动,第一阶段不阻塞,第二阶段阻塞。
这就好比,你去 “ 金拱门 ” 吃麦当劳一样。你在旁边的机器上点完餐后出来一张小票 “1024 号 ” ,然后你边玩手机边等。过了一会,喇叭喊, “1024 号,请取餐。 1024 号,请取餐。 ” ,然后,你屁颠屁颠地跑过去取餐。整个过程中,点餐是立即返回的,之后想干啥干啥,不阻塞(也就是说你不用傻等着餐做好);取餐的过程你需要从柜台端到你的位置上,是阻塞的。
异步 IO
异步 IO ,用户进程发起读取请求后立马返回,当数据完全拷贝到用户空间后通知用户直接使用数据。
异步 IO ,两个阶段都不阻塞。
这就好比,你去吃 “ 五谷渔粉 ” 。扫码点餐后,你完全不用管,过了一会,一个大妈把饭菜端到你面前,还贴心地说了句“ 客官,请慢用 ” ,然后你幸福地吃下了这碗 “ 金汤渔粉 ” 。整个过程中,你既不用傻等着渔粉做好,也不用看着大妈把菜端到你面前或者你自己去端,完全不阻塞,纯异步。所以,这种体验是最好的。
所以,如果把吃饭的过程分成两个部分: “ 准备饭菜 ” 和 “ 端菜 ” ,那么:
- 如果你傻等着两个阶段完成,就是阻塞 IO ;
- 如果你隔一会询问一下 “ 菜做好了没 ” ,期间你可以玩手机,但是端菜的时候你傻傻地看着老板端过来,就是非阻塞 IO ;
- 如果你和其他人都委托服务员帮你们隔一会看一下 “ 菜做好了没 ” ,但是端菜需要自己去端,就是 IO 多路复用;
- 如果是机器点餐,机器喊话取餐,就是信号驱动 IO ;
- 如果是扫码点餐,自动上餐,就是异步 IO ;
阻塞与非阻塞
阻塞,是指调用结果返回之前,当前线程会被挂起,直到调用结果返回。比如,你傻等着端菜结束,你就是阻塞的。
非阻塞,是指不能立即得到结果之前,当前线程不被挂起,而是可以继续做其它的事。比如,你边玩手机边等饭菜
准备好,你就是非阻塞的。简单点,就是阻塞调用你必须挂起傻等着结果返回,非阻塞调用你不关心结果,调用之后你爱干嘛干嘛。
同步与异步
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
同步,调用者会被阻塞直到 IO 操作完成,调用的结果随着请求的结束而返回。
异步,调用者不会被阻塞,调用的结果不随着请求的结束而返回,而是通过通知或回调函数的形式返回。
阻塞 / 非阻塞,更关心的是当前线程是不是被挂起。
同步 / 异步,更关心的是调用结果是不是随着请求结束而返回。
这里的阻塞是指整个 IO 过程中是否有阻塞,更确切地说是 recvfrom 这个系统调用是否会阻塞,在我们的案例中,可以理解为 “ 端菜 ” 这个行为对于你来说是不是阻塞的。
NIO 的传输方式和传统的基于 BIO 的传输方式区别
1 BIO 是面向流的,而 NIO 是面向 Channel 或者面向缓冲区的,它的效率更高。
2 流是单向的,所以又分成 InputStream 和 OutputStream,而 Channel 是双向的,既可读也可写。
3 流只支持同步读写,而 Channel 是可以支持异步读写的。
4 流一般与字节数组或者字符数组配合使用,而 Channel 一般与 Buffer 配合使用。
I/O复用模型
netty用户指南
Netty的非阻塞I/O的实现关键是基于I/O复用模型,这里用Selector对象表示。
Netty的IO线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端连接。当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起,一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
基于buffer
传统的I/O是面向字节流或字符流的,以流式的方式顺序地从一个Stream 中读取一个或多个字节, 因此也就不能随意改变读取指针的位置。
在NIO中, 抛弃了传统的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中, 只能从Channel中读取数据到Buffer中或将数据 Buffer 中写入到 Channel。
基于buffer操作不像传统IO的顺序操作, NIO 中可以随意地读取任意位置的数据。
线程模型
数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,线程模型的不同,对性能的影响也非常大。
事件驱动模型
设计一个事件处理模型的程序有两种思路:
1 轮询方式
线程不断轮询访问相关事件发生源有没有发生事件,有发生事件就调用事件处理逻辑
2 事件驱动方式
发生事件,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式,其实是设计模式中观察者模式的思路。
以GUI的逻辑处理为例,说明两种逻辑的不同:
1 轮询方式
线程不断轮询是否发生按钮点击事件,如果发生,调用处理逻辑
2 事件驱动方式
发生点击事件把事件放入事件队列,在另外线程消费的事件列表中的事件,根据事件类型调用相关事件处理逻辑
主要包括4个基本组件:
事件队列(event queue):接收事件的入口,存储待处理事件
分发器(event mediator):将不同的事件分发到不同的业务逻辑单元
事件通道(event channel):分发器与处理器之间的联系渠道
事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作
可以看出,相对传统轮询模式,事件驱动有如下优点:
可扩展性好,分布式的异步架构,事件处理器之间高度解耦,可以方便扩展事件处理逻辑
高性能,基于队列暂存事件,能方便并行异步处理事件。
Reactor线程模型
Reactor是反应堆的意思,Reactor模型,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多了复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一。
Reactor模型中有2个关键组成:
Reactor
Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人
Handlers
处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作。
取决于Reactor的数量和Hanndler线程数量的不同,Reactor模型有3个变种
单Reactor单线程
单Reactor多线程
主从Reactor多线程
可以这样理解,Reactor就是一个执行while (true) { selector.select(); …}循环的线程,会源源不断的产生新的事件,称作反应堆很贴切。
Netty线程模型
Netty主要基于主从Reactors多线程模型(如下图)做了一定的修改,
其中主从Reactor多线程模型有多个Reactor:
MainReactor和SubReactor:
MainReactor 负责客户端的连接请求,并将请求转交给SubReactor。
SubReactor 负责相应通道的IO读写请求。
非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理
这里引用Doug Lee大神的Reactor介绍:
Scalable IO in Java里面关于主从Reactor多线程模型的图。
异步处理
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
Netty中的I/O操作是异步的,包括bind、write、connect等操作会简单的返回一个ChannelFuture,调用者并不能立刻获得结果,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。
当future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操,常见有如下操作
Selector
Netty基于Selector对象实现I/O多路复用,通过 Selector, 一个线程可以监听多个连接的Channel事件, 当向一个Selector中注册Channel 后,Selector 内部的机制就可以自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。
NioEventLoop
NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:
I/O任务 即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。
非IO任务 添加到taskQueue中的任务,如register0、bind0等任务,由runAllTasks方法触发。
两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的时间与IO任务的执行时间相等。
NioEventLoopGroup
NioEventLoopGroup,主要管理eventLoop的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应于一个线程。
入站事件由自下而上方向的入站处理程序处理,如图左侧所示。 入站Handler处理程序通常处理由图底部的I / O线程生成的入站数据。 通常通过实际输入操作(例如SocketChannel.read(ByteBuffer))从远程读取入站数据。
出站事件由上下方向处理,如图右侧所示。 出站Handler处理程序通常会生成或转换出站传输,例如write请求。 I/O线程通常执行实际的输出操作,例如SocketChannel.write(ByteBuffer)。
在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应, 它们的组成关系如下:
一个 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰。
更多推荐
所有评论(0)