java基础知识7-IO

2021-04-21  本文已影响0人  liwsh

1.IO模型

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

  1. 等待数据准备 (Waiting for the data to be ready)
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
    正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。

2.IO多路复用之select poll epoll

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
select的几大缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024
poll有缺点1和2,没有3
epoll缺点1,2,3都没有
epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。
  对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
  对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。
  对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
总结:
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

4. select channel buffer

image.png
image.png

buffer:
Buffer 分为直接缓冲区(DirectByteBuffer)和 堆缓冲区(HeapByteBuffer)
DirectByteBuffer 是堆外内存,不在 JVM 的管理范围内;HeapByteBuffer 是在 JVM 堆上分配的存储空间,JVM 可回收其内存。我们该如何选择呢?
读取网络数据分为两步:将数据从网卡拷贝到内核空间,再将数据从内核空间拷贝到用户空间。对于 NIO 来说,缓冲区可以使用 DirectByteBuffer 或 HeapByteBuffer。如果使用了 DirectByteBuffer,一般来说可以减少一次内核空间到用户空间的拷贝。但 Buffer 创建和销毁的成本更高,更不宜维护,通常会用内存池来提高性能。如果数据量比较小的中小应用情况下,可以考虑使用 heapBuffer;反之可以用 DirectByteBuffer。
channel:
Channel 代表和某一实体的连接,这个实体可以是文件、网络套接字等。
Channel 相对于传统 IO 的流,有以下特点:
(1)既可以从 Channel 中读取数据,又可以写数据到 Channel。但流的读写通常是单向的;
(2)Channel 可以非阻塞读写,如果数据未准备好,直接返回 0;
(3)Channel 中的数据只能通过 Buffer 进行读写操作。
根据实体的不同,通道可分为:
(1)FileChannel:读写文件;
(2)DatagramChannel: UDP协议网络通信;
(3)SocketChannel:TCP协议网络通信;
(4)ServerSocketChannel:监听TCP连接。
selector:
Channel 注册到 Selector 上,Selector 可同时监听多个 Channel,可避免了伪异步 I/O 模型线程频繁切换带来的系统开销

5. reactor模型

image.png

Synchronous Event Demultiplexer:同步事件分离器,用于监听各种事件,调用方调用监听方法的时候会被阻塞,直到有事件发生,才会返回。对于Linux来说,同步事件分离器指的就是IO多路复用模型,比如epoll,poll 等, 对于Java NIO来说, 同步事件分离器对应的组件就是selector,对应的阻塞方法就是select。
Handler:本质上是文件描述符,是一个抽象的概念,可以简单的理解为一个一个事件,该事件可以来自于外部,比如客户端连接事件,客户端的写事件等等,也可以是内部的事件,比如操作系统产生的定时器事件等等。
Event Handler:事件处理器,本质上是回调方法,当有事件发生后,框架会根据Handler调用对应的回调方法,在大多数情况下,是虚函数,需要用户自己实现接口,实现具体的方法。
Concrete Event Handler: 具体的事件处理器,是Event Handler的具体实现。
Initiation Dispatcher:初始分发器,实际上就是Reactor角色,提供了一系列方法,对Event Handler进行注册和移除;还会调用Synchronous Event Demultiplexer监听各种事件;当有事件发生后,还要调用对应的Event Handler

3. BIO、NIO、AIO适用场景

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

上一篇下一篇

猜你喜欢

热点阅读