服务器端编程通常需要构建高性能IO模型,常见IO有四种模型:
(1)同步阻塞IO(Blocking IO):即传统的IO模型。
(2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是堵塞,不是堵塞IO要求socket被设置为NONBLOCK。注意这里说的话NIO并非Java的NIO(New IO)库。
(3)IO多路复用(IO Multiplexing):即经典的Reactor设计模式有时也被称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。
(4)异步IO(Asynchronous IO):即经典的Proactor设计模式又称异步非阻塞IO。
同步和异步概念描述了用户线程与内核的交互:同步是指用户线程启动IO请求后需要等待或轮询核查IO操作完成后才能继续执行;异步是指用户线程启动IO请求继续执行后,当内核继续执行时IO通知用户线程或调用用户线程注册的回调函数。
阻塞和非阻塞概念描述了用户线程调用的核心IO操作方法:堵塞是指IO在返回用户空间之前,操作需要完全完成;而非阻塞是指IO操作调用后立即返回到用户的状态值,无需等待IO完成操作。
另外,Richard Stevens 基于信号驱动IO(Signal 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资源利用率不够。
更多linux内核视频教程文本资料免费获取后台私信【内核】。
二、同步非阻塞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轮询等待模型中的问题。
图5 IO多路复用
如图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函数的优点不限于此。
然而,使用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返回(阻塞),handle_events与文件句柄相关的事件处理器将被调用handle_event相关操作。图5 IO多路复用
如图5所示,通过Reactor用户线程轮查询的方式IO操作状态的工作统一交给handle_events处理事件循环。其他工作(异步)可以在用户线程注册事件处理器后继续执行,Reactor负责调用内核的线程select函数检查socket状态。其他工作(异步)可以在用户线程注册事件处理器后继续执行,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模型,但它的异步程度不够彻底,因为它使用会阻塞线程select因此,系统调用IO多路复用只能称为异步阻塞IO,而不是真正的异步IO。
四、异步IO真异步IO操作系统需要更强的支持。在IO在多路复用模型中,事件循环将文件句柄的状态事件通知用户线程,用户线程自行读取和处理数据。而在异步IO在模型中,当用户线程收到通知时,数据已被内核读取并放置在用户线程指定的缓冲区内IO通知用户线程完成后直接使用。异步IO模型使用了Proactor该机制实现了设计模式。图6 Proactor设计模式如图6,Proactor模式和Reactor模式在结构上相似,但在用户中(Client)使用方法差别很大。Reactor在模式中,用户线程通过方向Reactor监控对象注册感兴趣的事件,然后在事件触发时调用事件处理函数。而Proactor用户线程将在模式中AsynchronousOperation(读/写等),Proactor操作完成时CompletionHandler注册到AsynchronousOperationProcessor。AsynchronousOperationProcessor使用Facade该模式提供了一组异步操作API当用户线程调用异步时,(读/写等)供用户使用API之后,他继续执行自己的任务。AsynchronousOperationProcessor 执行异步操作将打开独立的内核线程,实现真正的异步。当异步IO操作完成时,AsynchronousOperationProcessor用户线程与AsynchronousOperation一起注册的Proactor和CompletionHandler取出,然后把它拿出来CompletionHandler与IO一起转发操作结果数据Proactor,Proactor处理函数负责回调每个异步操作事件handle_event。虽然Proactor每个异步操作都可以绑定到模式中Proactor对象,但一般在操作系统中,Proactor被实现为Singleton集中分发操作完成事件的模式。图7 异步IO
如图7所示,异步IO在模型中,用户线程直接使用内核提供的异步IO API发起read请求,启动后立即返回,继续执行用户线程代码。
图7 异步IO如图7所示,异步IO在模型中,用户线程直接使用内核提供的异步IO API发起read请求发起后立即返回,继续执行用户线程代码。但此时使用 户线电脑