服务端编程需要构建高性能的IO模型,常见的IO模型主要有以下四种
- 同步阻塞IO
- 同步非阻塞IO 默认创建的socket都是阻塞的,非阻塞IO要求socket设置为NONBLOCK
- IO多路复用 经典Reactor设计模式,异步阻塞IO,select epoll
- 异步IO 异步非阻塞IO
同步与异步 用户线程与内核的交互方式;同步是指用户发起IO请求后,需要等待或者轮询内核IO操作完成后才能继续执行;异步是指用户线程发起IO请求后继续执行,当内核操作完成后会通知线程或者调用用户线程注册的回调函数
阻塞与非阻塞 用户线程调用内核IO操作的方式;阻塞是指IO操作需要彻底完成后才返回到用户空间,而非阻塞是指IO操作被调用后立即返回给用户一个状态值
同步阻塞IO
用户线程通过系统调用read发起IO操作,由用户空间转到内核空间,内核等到数据包到达以后,将接受的数据拷贝到用户空间,完成read,用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据,整个IO请求过程中,用户线程是被阻塞的,导致用户发起请求时,不能做任何事情,对CPU资源利用不够。
同步非阻塞IO
同步非阻塞io,在同步阻塞io的基础上,将socket设置为nonblock,用户线程可以在发起io请求后立即返回;socket是非阻塞的方式,用户线程发起IO请求时立即返回,但并未读取到任何数据,用户线程需要不断发起IO请求,直到数据到达后,才真正读取到数据,继续执行;在整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求、消耗大量CPU资源,一般很少使用这种模型,而是在其他IO模型中使用非阻塞IO
IO多路复用
IO多路复用,是建立在内核上提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题;用户将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回,用户线程发起read请求,读取数据并继续执行。使用select函数进行IO请求与同步阻塞模型并无太大区别,甚至多添加监视socket,select函数额外操作,使用优势主要在于用户可以在一个线程内同时处理多个socket的IO请求,用户可以注册多个socket,然后不断调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的,同步阻塞模型中,必须使用多线程。
使用select允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的,平均时间甚至比同步阻塞IO模型还要长,IO多路复用模型使用Reactor设计模式实现了这一机制,用户线程只注册自己感兴趣的socket或者IO请求,去做自己的事情,等到数据到来时再进行处理,可以提高cpu利用率;EventHandler抽象类表示IO事件处理器,拥有IO句柄get-handle,以及对Handle的操作handle-event,继承于EventHandler的子类可以对事件处理器的行为进行定制,Reactor类用于管理EventHandler注册、删除,并使用handle-events实现事件循环,不断调用内核中的多路分离函数select,只要某个文件句柄被激活,select就返回,就会调用handle-event事件处理器进行操作。
通过reactor的方式,将用户线程轮询IO操作状态的工作交给handle-even进行处理,用户线程进行事件注册之后进行其他工作(异步),而reactor线程负责调用内核select函数,当存在socket被激活时,通知相应的用户线程,执行handle-event进行数据读取、处理工作,由于select函数是阻塞的,所以多路IO复用也被称为异步阻塞IO模型,socket是不被阻塞的,用户发起IO请求时,数据已经到达,用户线程一定不会被阻塞。其使用会阻塞线程的select系统调用,因此IO多路复用只能称为异步阻塞IO,而非真正的异步IO。
异步IO
真正的异步IO,需要操作系统更强的支持,在IO多路复用中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据,而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。异步模型使用Proactor设计模式实现这一机制。
Proactor和Reactor模式在结构上比较相似,在Client使用方式上差别较大,Proactor模式中,用户线程将AO、Proactor以及操作完成时的CompletionHandler注册到AOP。AOP使用Facade模式提供一组异步操作API供用户使用,当用户线程调用异步API后,便执行自己的任务。AOP会开启独立的内核线程执行异步操作,当异步IO完成时,AOP将用户线程与AOP一起注册的Proactor和CompletionHandler取出,然后将CompletionHandler与IO操作的结果一致转发给Proactor,Proactor负责回调每一个异步操作事件完成处理函数handle-event,Proactor模式中每个异步操作都可以绑定一个proactor对象,一般操作系统中Proactor为单例模式,以便集中化分发操作完成事件。
异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,发起后立即返回,继续执行用户线程代码。此时用户线程已经将调用的AO与CH注册到了内核,然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CH分发给内部Proactor,Proactor将IO完成的信息通知给用户线程,完成异步IO。
异步IO并不常见,高性能并发服务程序,使用IO多路复用模型+多线程任务处理的架构基本可以满足要求,目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式,IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中。