学校免费网站建设,安徽省建设工程造价管理网站,html5 微网站,朋友圈推广文案高性能IO模型浅析
服务器端编程经常需要构造高性能的IO模型#xff0c;常见的IO模型有四种#xff1a;
#xff08;1#xff09;同步阻塞IO#xff08;Blocking IO#xff09;#xff1a;即传统的IO模型。
#xff08;2#xff09;同步非阻塞IO#xff08;Non-blo…高性能IO模型浅析
服务器端编程经常需要构造高性能的IO模型常见的IO模型有四种
1同步阻塞IOBlocking IO即传统的IO模型。
2同步非阻塞IONon-blocking IO默认创建的socket都是阻塞的非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIONew IO库。
3IO多路复用IO Multiplexing即经典的Reactor设计模式有时也称为异步阻塞IOJava中的Selector和Linux中的epoll都是这种模型。
4异步IOAsynchronous IO即经典的Proactor设计模式也称为异步非阻塞IO。
同步和异步的概念描述的是用户线程与内核的交互方式同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行而异步是指用户线程发起IO请求后仍继续执行当内核IO操作完成后会通知用户线程或者调用用户线程注册的回调函数。
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式阻塞是指IO操作需要彻底完成后才返回到用户空间而非阻塞是指IO操作被调用后立即返回给用户一个状态值无需等到IO操作彻底完成。
另外Richard Stevens 在《Unix 网络编程》卷1中提到的基于信号驱动的IOSignal Driven IO模型由于该模型并不常用本文不作涉及。接下来我们详细分析四种常见的IO模型的实现原理。为了方便描述我们统一使用IO的读操作作为示例。
一、同步阻塞IO
同步阻塞IO模型是最简单的IO模型用户线程在内核进行IO操作时被阻塞。 图1 同步阻塞IO
如图1所示用户线程通过系统调用read发起IO读操作由用户空间转到内核空间。内核等到数据包到达后然后将接收的数据拷贝到用户空间完成read操作。
用户线程使用同步阻塞IO模型的伪代码描述为 { read(socket, buffer); process(buffer); } 即用户需要等待read将socket中的数据读取到buffer后才继续处理接收的数据。整个IO请求的过程中用户线程是被阻塞的这导致用户在发起IO请求时不能做任何事情对CPU的资源利用率不够。
二、同步非阻塞IO
同步非阻塞IO是在同步阻塞IO的基础上将socket设置为NONBLOCK。这样做用户线程可以在发起IO请求后可以立即返回。 图2 同步非阻塞IO
如图2所示由于socket是非阻塞的方式因此用户线程发起IO请求时立即返回。但并未读取到任何数据用户线程需要不断地发起IO请求直到数据到达后才真正读取到数据继续执行。
用户线程使用同步非阻塞IO模型的伪代码描述为 { while(read(socket, buffer) ! SUCCESS); process(buffer); } 即用户需要不断地调用read尝试读取socket中的数据直到读取成功后才继续处理接收的数据。整个IO请求的过程中虽然用户线程每次发起IO请求后可以立即返回但是为了等到数据仍需要不断地轮询、重复请求消耗了大量的CPU的资源。一般很少直接使用这种模型而是在其他IO模型中使用非阻塞IO这一特性。
三、IO多路复用
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。 图3 多路分离函数select
如图3所示用户首先将需要进行IO操作的socket添加到select中然后阻塞等待select系统调用返回。当数据到达时socket被激活select函数返回。用户线程正式发起read请求读取数据并继续执行。
从流程上来看使用select函数进行IO请求和同步阻塞模型没有太大的区别甚至还多了添加监视socket以及调用select函数的额外操作效率更差。但是使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket然后不断地调用select读取被激活的socket即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中必须通过多线程的方式才能达到这个目的。
用户线程使用select函数的伪代码描述为
{select(socket);while(1) {sockets select();for(socket in sockets) {if(can_read(socket)) {read(socket, buffer);process(buffer);}}}}
其中while循环前将socket添加到select监视中然后在while内一直调用select获取被激活的socket一旦socket可读便调用read函数将socket中的数据读取出来。
然而使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求但是每个IO请求的过程还是阻塞的在select函数上阻塞平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求然后去做自己的事情等到数据到来时再进行处理则可以提高CPU的利用率。
IO多路复用模型使用了Reactor设计模式实现了这一机制。 图4 Reactor设计模式
如图4所示EventHandler抽象类表示IO事件处理器它拥有IO文件句柄Handle通过get_handle获取以及对Handle的操作handle_event读/写等。继承于EventHandler的子类可以对事件处理器的行为进行定制。Reactor类用于管理EventHandler注册、删除等并使用handle_events实现事件循环不断调用同步事件多路分离器一般是内核的多路分离函数select只要某个文件句柄被激活可读/写等select就返回阻塞handle_events就会调用与文件句柄关联的事件处理器的handle_event进行相关操作。 图5 IO多路复用
如图5所示通过Reactor的方式可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作异步而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时则通知相应的用户线程或执行用户线程的回调函数执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的因此多路IO复用模型也被称为异步阻塞IO模型。注意这里的所说的阻塞是指select函数执行时线程被阻塞而不是指socket。一般在使用IO多路复用模型时socket都是设置为NONBLOCK的不过这并不会产生影响因为用户发起IO请求时数据已经到达了用户线程一定不会被阻塞。
用户线程使用IO多路复用模型的伪代码描述为 void UserEventHandler::handle_event() { if(can_read(socket)) { read(socket, buffer); process(buffer); }} { Reactor.register(new UserEventHandler(socket)); } 用户需要重写EventHandler的handle_event函数进行读取数据、处理数据的工作用户线程只需要将自己的EventHandler注册到Reactor即可。Reactor中handle_events事件循环的伪代码大致如下。 Reactor::handle_events() { while(1) { sockets select(); for(socket in sockets) { get_event_handler(socket).handle_event(); }}} 事件循环不断地调用select获取被激活的socket然后根据获取socket对应的EventHandler执行器handle_event函数即可。
IO多路复用是最常使用的IO模型但是其异步程度还不够“彻底”因为它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO而非真正的异步IO。
四、异步IO
“真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中事件循环将文件句柄的状态事件通知给用户线程由用户线程自行读取数据、处理数据。而在异步IO模型中当用户线程收到通知时数据已经被内核读取完毕并放在了用户线程指定的缓冲区内内核在IO完成后通知用户线程直接使用即可。
异步IO模型使用了Proactor设计模式实现了这一机制。 图6 Proactor设计模式
如图6Proactor模式和Reactor模式在结构上比较相似不过在用户Client使用方式上差别较大。Reactor模式中用户线程通过向Reactor对象注册感兴趣的事件监听然后事件触发时调用事件处理函数。而Proactor模式中用户线程将AsynchronousOperation读/写等、Proactor以及操作完成时的CompletionHandler注册到AsynchronousOperationProcessor。AsynchronousOperationProcessor使用Facade模式提供了一组异步操作API读/写等供用户使用当用户线程调用异步API后便继续执行自己的任务。AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作实现真正的异步。当异步IO操作完成时AsynchronousOperationProcessor将用户线程与AsynchronousOperation一起注册的Proactor和CompletionHandler取出然后将CompletionHandler与IO操作的结果数据一起转发给ProactorProactor负责回调每一个异步操作的事件完成处理函数handle_event。虽然Proactor模式中每个异步操作都可以绑定一个Proactor对象但是一般在操作系统中Proactor被实现为Singleton模式以便于集中化分发操作完成事件。 图7 异步IO
如图7所示异步IO模型中用户线程直接使用内核提供的异步IO API发起read请求且发起后立即返回继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时由内核负责读取socket中的数据并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部ProactorProactor将IO完成的信息通知给用户线程一般通过调用用户线程注册的完成事件处理函数完成异步IO。
用户线程使用异步IO模型的伪代码描述为 void UserCompletionHandler::handle_event(buffer) { process(buffer); } { aio_read(socket, new UserCompletionHandler); } 用户需要重写CompletionHandler的handle_event函数进行处理数据的工作参数buffer表示Proactor已经准备好的数据用户线程直接调用内核提供的异步IO API并将重写的CompletionHandler注册即可。
相比于IO多路复用模型异步IO并不十分常用不少高性能并发服务程序使用IO多路复用模型多线程任务处理的架构基本可以满足需求。况且目前操作系统对异步IO的支持并非特别完善更多的是采用IO多路复用模型模拟异步IO的方式IO事件触发时不直接通知用户线程而是将数据读写完毕后放到用户指定的缓冲区中。
由于在看redis看到了I/O多路复用补一下原理知识。