当前位置: 首页 > 科技观察

高性能IO模型解析_0

时间:2023-03-20 17:11:52 科技观察

高性能IO模型解析服务器端编程往往需要构建高性能IO模型。常见的IO模型有四种:(1)同步阻塞IO(BlockingIO):传统的IO模型。(2)同步非阻塞IO(Non-blockingIO):默认创建的socket都是阻塞的,非阻塞IO需要socket设置为NONBLOCK。注意这里说的NIO并不是Java的NIO(NewIO)库。(3)IO多路复用(IOMultiplexing):经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模式。(4)异步IO(AsynchronousIO):经典的Proactor设计模式,也称为异步非阻塞IO。同步和异步的概念描述了用户线程和内核之间的交互:同步是指用户线程发起IO请求后,需要等待或轮询内核IO操作完成后才能继续执行;继续执行,当内核IO操作完成后,会通知用户线程,或者调用用户线程注册的回调函数。阻塞和非阻塞的概念描述了用户线程调用内核IO操作的方式:阻塞是指IO操作需要完全完成才返回用户空间;非阻塞是指IO操作立即返回给用户一个状态值,而不用等到IO操作完全完成。另外,RichardStevens在《Unix网络编程》Volume1中提到的信号驱动IO(SignalDrivenIO)模型,由于该模型不常用,本文不做介绍。下面详细分析四种常见IO模型的实现原理。为了描述方便,我们统一以IO读操作为例。1、同步阻塞IO同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时被阻塞。如图1所示,用户线程通过系统调用read发起IO读操作,从用户空间转移到内核空间。内核等待数据包到达,然后将接收到的数据复制到用户空间,完成读操作。用户线程使用同步阻塞IO模型的伪代码描述为:{read(socket,buffer);process(buffer);},即用户需要等待read将socket中的数据读入buffer中,才能继续处理接收到的数据。在整个IO请求过程中,用户线程处于阻塞状态,导致用户在发起IO请求时无能为力,CPU的资源利用率不够。2.同步非阻塞IO同步非阻塞IO是基于同步阻塞IO,socket设置为NONBLOCK。这样用户线程发起IO请求后就可以立即返回。如图2所示,由于socket是非阻塞的,用户线程发起IO请求后立即返回。但是一直没有读取到数据,需要用户线程不断的发起IO请求,直到数据到来,才真正读取到数据,继续执行。用户线程使用同步非阻塞IO模型的伪代码描述为:{while(read(socket,buffer)!=SUCCESS);process(buffer);}即用户需要不断调用read来尝试读取socket中的数据,读取成功后才处理接收到的数据。在整个IO请求过程中,虽然每次发起IO请求后用户线程都可以立即返回,但是为了等待数据,仍然需要不断的轮询和重复请求,消耗大量的CPU资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO特性。3.IO多路复用IO多路复用模型是基于内核提供的多路分解函数select。使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。如图3所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用的返回。当数据到达时,套接字被激活,选择函数返回。用户线程正式发起读请求,读取数据并继续执行。从流程上看,IO请求使用select函数和同步阻塞模型没有太大区别。甚至还有添加监听socket和调用select函数的额外操作,效率较低。但是使用select最大的好处就是用户可以在一个线程中同时处理多个socketIO请求。用户可以注册多个socket,然后不断调用select读取激活的socket,从而达到在同一个线程中同时处理多个IO请求的目的。在同步阻塞模型中,这个目标必须通过多线程来实现。用户线程使用select函数的伪代码描述为:{select(socket);while(1){sockets=select();for(socketinsockets){if(can_read(socket)){read(socket,buffer);process(buffer);}}}}在while循环之前,将socket加入select监听,然后在while中调用select,获取激活的socket。一旦套接字可读,调用read函数读取套接字中的数据。然而,使用select函数的优势并不止于此。上面的方法虽然可以让多个IO请求在一个线程中处理,但是每个IO请求的进程仍然是阻塞的(阻塞在select函数上),平均时间比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后自己做自己的事情,等待数据到达再处理,可以提高CPU的利用率。IO多路复用模型使用Reactor设计模式实现此机制。如图4所示,EventHandler抽象类代表IO事件处理器,有IO文件句柄Handle(通过get_handle获取),以及Handle操作的handle_event(读/写等)。从EventHandler继承的子类可以自定义事件处理程序的行为。Reactor类用于管理EventHandler(注册、删除等),并使用handle_events实现事件循环,不断调用同步事件多路分离器(通常是内核)的多路分离函数select,只要一个文件句柄被激活(可读/可写等),select会返回(block),handle_events会调用与文件句柄关联的事件处理器的handle_event进行相关操作。如图5所示,通过Reactor可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环处理。用户线程注册事件处理程序后,可以继续执行其他工作(异步),Reactor线程负责调用内核的select函数检查socket状态。当一个socket被激活时,通知对应的用户线程(或执行用户线程的回调函数),执行handle_event读取和处理数据。由于select函数是阻塞的,所以多路复用IO复用模型也称为异步阻塞IO模型。注意,这里的阻塞指的是执行select函数时被阻塞的线程,而不是socket。一般在使用IO多路复用模型时,socket设置为NONBLOCK,但这不会影响,因为当用户发起IO请求时,数据已经到达,用户线程不会阻塞。用户线程使用IO多路复用模型的伪代码描述为:voidUserEventHandler::handle_event(){if(can_read(socket)){read(socket,buffer);process(buffer);}}{Reactor.register(newUserEventHandler(socket)));}用户需要重写EventHandler的handle_event函数来读取和处理数据。用户线程只需要将自己的EventHandler注册到Reactor即可。Reactor中handle_events事件循环的伪代码大致如下。Reactor::handle_events(){while(1){sockets=select();for(socketinsockets){get_event_handler(socket).handle_event();}}}事件循环不断调用select获取激活的socket,然后根据到获取socket对应的EventHandler,executor的handle_event函数就可以了。IO多路复用是最常用的IO模型,但是它的异步性不够“彻底”,因为它使用了阻塞线程的select系统调用。因此,IO多路复用只能称为异步阻塞IO,不能称为真正的异步IO。4、异步IO“真”异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环通知用户线程文件句柄的状态事件,用户线程自己读取和处理数据。在异步IO模型中,当用户线程收到通知时,数据已经被内核读取并放入用户线程指定的缓冲区中。IO完成后,内核通知用户线程直接使用。异步IO模型使用Proactor设计模式实现此机制。如图6所示,Proactor模式和Reactor模式在结构上很相似,但是用户(Client)使用它们的方式却有很大的不同。在Reactor模式下,用户线程向Reactor对象注册感兴趣的事件监听器,然后在事件触发时调用事件处理函数。在Proactor模式下,用户线程在操作完成后将AsynchronousOperation(读/写等)、Proactor、CompletionHandler注册到AsynchronousOperationProcessor。AsynchronousOperationProcessor采用Facade模式提供了一套异步操作API(read/write等)供用户使用。当用户线程调用异步API时,它继续执行自己的任务。AsynchronousOperationProcessor会开启一个独立的内核线程进行异步操作,实现真正的异步。当异步IO操作完成后,AsynchronousOperationProcessor取出注册到用户线程和AsynchronousOperation的Proactor和CompletionHandler,然后将CompletionHandler和IO操作的结果数据转发给Proactor,Proactor负责回调事件完成处理每个异步操作的函数handle_event。Proactor模式下虽然每个异步操作都可以绑定一个Proactor对象,但是在操作系统中一般会将Proactor实现为Singleton模式,以方便操作完成事件的集中分发。如图7所示,在异步IO模型中,用户线程直接使用内核提供的异步IOAPI发起读请求,发起后立即返回,继续执行用户线程代码。但是此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核中,然后操作系统启动一个独立的内核线程来处理IO操作。当read请求的数据到达时,内核负责读取socket中的数据,写入用户指定的缓冲区。***内核将读取到的数据和用户线程注册的CompletionHandler分发给内部的Proactor,Proactor将IO完成信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数)来完成异步IO。用户线程使用异步IO模型的伪代码描述为:voidUserCompletionHandler::handle_event(buffer){process(buffer);}{aio_read(socket,newUserCompletionHandler);}用户需要重写CompletionHandler的handle_event函数来处理数据,parametersBuffer表示Proactor已经准备好的数据。用户线程直接调用内核提供的异步IOAPI,注册重写的CompletionHandler。与IO多路复用模型相比,异步IO并不是很常用。很多高性能并发服务程序采用IO多路复用模型+多线程任务处理架构基本可以满足需求。而且目前的操作系统对异步IO的支持还不是特别完善,更多的是使用IO多路复用模型来模拟异步IO(触发IO事件时,不直接通知用户线程,而是读取数据并写入并放置在用户指定的缓冲区中)。Java7开始支持异步IO,有兴趣的读者可以尝试使用。本文从基本概念、工作流、代码示例三个层面简要介绍四种常见的高性能IO模型的结构和原理,理清容易混淆的同步、异步、阻塞、非阻塞概念。通过对高性能IO模型的了解,可以在服务端程序的开发中选择更符合实际业务特点的IO模型来提高服务质量。希望本文对您有所帮助。本文版权归作者及博客园所有,作者:Florian。