五种模型出自:RFC标准。可参考: 《UNIX网络编程-卷一》 6.2
很多程序员是从高级语言的网络编程/文件操作了解到nio,继而了解到五种io模型的;
这五种io模型不止用于网络io
一共有五种IO模型
IO操作可分为两阶段看待:
1)进程向发起IO请求,等待数据准备(Waiting for the data to be ready),系统调用后进入内核态,内核操作数据到内核缓冲区
2)实际的IO操作,将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
I/O多路复用是阻塞在select,epoll这样 的系统调用,没有阻塞在真正的I/O系统调用
如recvfrom进程受阻于select,等待可能多个套接口中的任一个变为可读
IO多路复用使用两个系统调用(select和 recvfrom) ,blocking IO只调用了一个系统调用 (recvfrom)
多路复用模型中,每一个socket,设置为 non-blocking, 阻塞是被select()阻塞,而不是被 socket阻塞的
select/epoll 核心是可以同时处理多个 connection,而不是更快的处理单个connection,所以连接数不高的话,性能不一定比多线程+阻塞IO好
前四种IO模型都是同步IO操作,他们的区别在于第一阶段,而第二阶段是一样的,即在内核数据copy到用户空间时都是阻塞的。
而异步 I/O模型的进程在这两个阶段都是运行的。
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞。
同步IO和异步IO的区别就在于第二个步骤是否阻塞,实际IO读写阶段会阻塞进程,而在异步IO中,是由操作系统帮忙做完IO工作再直接返回结果。
几个核心点:
##NIO(非阻塞IO模型)
NIO,即Non-Blocking IO,是非阻塞IO模型。
NIO存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的CPU资源。可以考虑IO复用模型去解决这个问题。
不阻塞了,但是轮询doge
阻塞会让出cpu,轮询会一直占着cpu
非阻塞IO的流程如下:
对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
这种框架内部一般维护了请求的协议和请求号,可以维护一个以请求号为key,结果的result为future的map,结合NIO+长连接,获取非常不错的性能。
多路指多个TCP连接(即 socket或者channel),复用指复用一个或几个线程。
解决轮询的方法:先阻塞进程,等到内核数据准备好了,主动通知应用进程再去进行系统调用。
IO复用模型核心思路:系统给我们提供一类函数(如select、poll、epoll函数),它们可以同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用。
最简单的Reactor模式:注册所有感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。
Java的Selector对于Linux系统来说,有一个致命限制:同一个channel的select不能被并发的调用。因此,如果有多个I/O线程,必须保证:一个socket只能属于一个IoThread,而一个IoThread可以管理多个socket。
另外连接的处理和读写的处理通常可以选择分开,这样对于海量连接的注册和读写就可以分发。虽然read()和write()是比较高效无阻塞的函数,但毕竟会占用CPU,如果面对更高的并发则无能为力。
对于Redis来说,由于服务端是全局串行的,能够保证同一连接的所有请求与返回顺序一致。这样可以使用单线程+队列,把请求数据缓冲。然后pipeline发送,返回future,然后channel可读时,直接在队列中把future取回来,done()就可以了。
常见的RPC框架,如Thrift,Dubbo
这种框架内部一般维护了请求的协议和请求号,可以维护一个以请求号为key,结果的result为future的map,结合NIO+长连接,获取非常不错的性能。
IO多路复用之select
应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时应用进程再发起recvfrom请求去读取数据。
NIO中,需要轮询多次轮询系统调用直到可以读取数据,然而借助select的IO多路复用模型,只需要发起一次询问就够了,大大优化了性能。
select监视文件3类描述符: writefds、readfds、和 exceptfds。
调用后select函数会阻塞住,等有数据 可读、可写、出异常 或者 超时 就会返回。
但是,select有几个缺点:
poll
因为存在连接数限制,所以后来又提出了poll。与select相比,poll解决了连接数限制问题(用链表存储)。但是select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的socket。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。
IO多路复用之epoll
为了解决select/poll存在的问题,多路复用模型epoll诞生,它采用事件监听回调机制来实现,流程图如下:
epoll先通过epoll_ctl()来注册一个fd,一旦基于某个fd就绪时,内核会采用回调机制,迅速激活这个fd,当进程调用epoll_wait()时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。
epoll()在2.6内核中提出的,对比select和poll,epoll更加灵 活,没有描述符限制,用户态拷贝到内核态只需要使用事件通知
优点:
没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄
效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降。通过callback机制通知,内核和用户空间mmap同一块内存实现
1.4之前BIO
大型服务一般采用 C或者C++, 因为可以直接操作系统提供的异步IO,AIO.
NIO @Since(“1.4”)
NIO2.0 @Since(“1.7”),提供 AIO的功能,支持文件和网络套接字的异步IO.
一般情况下,I/O 复用机制需要事件分发器(event dispatcher)。 事件分发器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁谁谁的快递到了, 快来拿吧!开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数。
涉及到事件分发器的两种模式称为:Reactor和Proactor。Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。
Reactor相当于去医院,先用事件分发器向操作系统挂号,等叫到号了(被通知can read 或者 can write)应用线程再去实际IO
Proactor相当于网上办理银行业务,向操作系统告知自己要把数据调动到什么地方,然后等操作系统内核线程完成了再告诉事件分发器
有点类似有DMA的硬件设备的IO,这里的NIO是面向线程的
在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
而在Proactor模式中,事件处理者(或者代由事件分发器发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区、读的数据大小或用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分发器得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称为overlapped技术),事件分发器等IO Complete事件完成。这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。
linux
windows