Unix的五种IO模型
阻塞IO:应用进程从发起IO系统调用,至内核空间中的数据就绪,这个期间处于阻塞状态。
非阻塞IO:应用进程发起IO系统调用后立刻返回。应用进程可以不断(轮询)发起IO系统调用,直至数据就绪,再将数据从内核空间拷贝到用户空间进行数据处理。(在拷贝数据的过程,进程仍然属于阻塞状态)。优点是进程发起I/O操作时,不会因为数据还没就绪而阻塞。缺点是增大了响应延迟,因为每过一段时间才会发起系统调用检查数据是否就绪,而任务可能在两次轮询之间的时间完成,这会导致整体数据吞吐量的降低;尤其是在本地I/O,内核读取数据很快,这种模式下多了至少一次系统调用,而系统调用是比较消耗CPU的操作。
IO多路复用:应用进程借助select或poll或epoll,通过发起这种系统调用,可以让内核监听注册的多个事件(传入file descriptor和感兴趣的事件readable、writable等),(发起后应用进程被该系统调用block)当其中有就绪的数据后,该系统调用会返回结果,再由应用进程发起真正的IO系统调用(read、recvfrom等),来完成数据读取。优点是优化了非阻塞IO大量发起系统调用的问题,且一次可以监控大量的连接/fd。但处理的连接数不是很高的话,使用select/epoll的不一定比使用multi-threading + blocking IO性能更好,可能延迟还更大,因为select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
在IO 多路复用模型中,对于每一个socket,一般都设置成为non-blocking,虽然多路复用产生的效果,完全可以由用户态去遍历文件描述符并调用其非阻塞的 read 函数实现。但是多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,变成了一次系统调用 + 内核层遍历这些文件描述符。
- select(): 等待内核返回后,需要轮询所有fd找出就绪的fd,随着fd数量增加,性能逐渐下降
- poll(): 和select()差不多,只有linux支持poll。
- epoll(): 不需要轮询,直接返回就绪的fd,用以替代select()和poll()
信号驱动IO:应用进程向内核注册一个信号处理程序,发起系统调用后立即返回,不会阻塞。当内核中数据就绪后,会发送信号给应用进程,应用进程在信号处理程序中发起真正的 IO 系统调用完成数据读取。
异步IO:应用进程程发起aio_read系统调用,无论内核数据是否就绪,都会直接返回给用户进程,然后用户进程不会阻塞,而是可以去做别的事情。等数据就绪后,内核直接复制数据到用户空间给到进程,然后内核向进程发送在aio_read中指定的信号。异步IO在IO的两个阶段,进程都是非阻塞的。它就像用户进程将整个IO操作交给了内核来完成,然后内核完成后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动地去拷贝数据。
POSIX对同步IO和异步IO定义是:同步I/O操作将会造成请求进程阻塞,直到I/O操作完成;异步I/O操作不会造成进程阻塞。 因此根据此定义,前面4种I/O模型都是同步I/O,因为它们在第二阶段的I/O操作(recvfrom
)都会造成进程阻塞。只有最后一个I/O模型匹配异步I/O的定义。
Java IO 与 Unix IO 不严谨的对应关系:
BIO - 阻塞式IO | NIO - IO多路复用 | AIO - 异步IO
IO设计模式
TPR (Thread Per Request): 服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。此模式下可以采用线程池的方式,避免为每一个客户端都要创建线程带来的资源浪费,使得线程可以重用。但线程池比较适合大量的短连接应用,如果连接大多是长连接,可能会导致在一段时间内,线程池中的线程都被占用,那么再有用户请求连接时,由于没有可用的空闲线程来处理,客户端连接就会失败。
Reactor (反应器): 将所有要处理的I/O事件注册到一个中心I/O多路复用器上,当I/O事件到来或准备就绪(可读写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。Reactor是用来处理并发的同步IO的一种常见模式,是基于事件驱动的设计模式。(感知的是就绪可读写事件)Reactor的常见实现方案有三种:
阻塞IO中,每个连接需要一个线程(Handler)完成read,业务处理和send;针对阻塞IO缺点,Reactor采用IO多路复用模型使得多个连接共用一个阻塞对象(ServiceHandler/Reactor),采用线程池复用线程资源(EventHandler)进行连接完成后的业务处理。
- 单Reactor单线程:一个线程(包含Reactor、Acceptor和Handler对象)多路复用搞定所有IO操作。通过Reactor的select监听多路连接请求,dispatch进行分发;通过Acceptor的accept处理连接请求;通过Handler进行连接完成后的后续业务处理。
- 单Reactor多线程:Handler只响应事件,即执行读写操作,不执行业务操作。而添加了一个Worker线程池,处理业务的非I/O操作从主线程转移到Worker线程池中执行(处理完毕返回给Handler)。这样能提高Reactor主线程的I/O响应,不会因为一些耗时的业务逻辑而延迟对后面I/O请求的处理。缺点是Reactor主线程仍在单线程中运行,高并发场景下很容易称为性能瓶颈。
- 主从Reactor多线程:Reactor在多线程下运行,分为Reactor主线程和一或多个Reactor子线程,Reactor主线程中的MainReactor对象通过select监听连接事件,通过Acceptor对象处理连接事件,然后将连接分配给SubReactor;Reactor子线程中的SubReactor对象通过read读取数据,交给Worker线程池通过decode、compute、encode进行业务处理返回给SubReactor后,再通过send发送数据给客户端。[nginx/netty使用此方案]
Proactor (主动器): 和异步I/O相关,也是基于事件驱动的设计模式。依赖操作系统对异步的支持,不需要把数据从内核复制到用户空间,这步由操作系统完成。(感知的是已完成的读写事件)
NIO三大组件
Selector:监听一个或多个Channel是否处于可读/可写,当Channel中有感兴趣的事件发生时,...,从而实现单线程管理多个Channels;SelectableChannel需要注册到Selector并指定监听感兴趣的事件(可读/可写/连接/接收);选择器查询的并非Channel的某种操作,而是Channel某种操作的就绪状态;“选择键”的概念类似于“事件“。
Buffer:本身是一块内存,底层实现是数组;数据的读写都通过Buffer实现(同一块Buffer区域既可以读也可以写)。NIO中定义了IntBuffer、CharBuffer、DoubleBuffer、ByteBuffer(通常使用ByteBuffer,其他的实现也是对它的包装)等实现,其中MappedByteBuffer 用于实现内存映射文件。
Channel:是数据来源或数据写入的目的地,是内核区域与IO设备进行通信的桥梁;Channel本身并不直接读取或写入数据,而是通过Buffer进行;Channel的四个主要实现是:
- FileChannle:用于文件数据的读写
- DatagramChannel:用于UDP数据的读写
- ServerSocketChannel:用于TCP数据的读写
- SocketChannel:用于TCP数据的读写
Netty组件
NIO 开发客户端和服务端的开发和维护成本较高,因此我们更多采用Netty框架这个高性能、异步事件驱动的NIO框架。Netty主要包含下面几个的组件:
- Bootstrap:【引导类】用于引导Netty的启动并把其他各部分连接起来;客户端的Bootstrap连接到远程主机和端口,服务器端的ServerBootstrap绑定到一个本地的端口。
- Channel:【网络通信组件】用于网络IO操作,代表一个Socket的连接。
- EventLoopGroup:一个Group包含多个EventLoop,可以理解为线程池。
- Boss Group:[主从Reactor多线程中的MainReactor] 轮询处理accept事件,与clien建立连接,生成NIOSocketChannel...
- Worker Group:轮询处理IO(read/write)事件,在对应NIOSocketChannel处理
- EventLoop:处理具体Channel上的事件,一个EventLoop对应一个Selector,一个EventLoop可以处理多个Channel,可以理解为线程。
- ChannelPipeline:用于处理IO事件或者拦截IO操作,ChannelPipeline是一个handler的集合,每个Channel绑定一个Pipeline。
- Handler:对消息或连接的具体处理,分为Inbound和Outbound类型代表消息接收的处理和消息发送的处理。
- ChannelFuture:【异步IO操作的结果】处理异步操作,等待完成或注册监听。Netty中的IO都是异步的,所有的IO调用都会立即返回一个ChannelFuture实例提供IO操作的结果或状态信息。
Netty可以作为 RPC 框架的网络通信工具 ,也可以用来实现HTTP 服务器、即时通讯系统、消息推送系统等。
Reference