NIO
2024-11-05 19:40:40 4 举报
AI智能生成
NIO(New Input/Output)是一个Java API,用于处理I/O操作,特别是文件操作。它提供了一种非阻塞的、高性能的I/O操作方式。NIO的核心组件包括通道(Channel)、缓冲区(Buffer)和选择器(Selector)。Channel用于在文件系统和设备间传输数据,Buffer用于存储数据,Selector用于管理Channel,以实现非阻塞IO。NIO可以提高应用程序的I/O性能,并允许一个线程同时管理多个I/O操作。
作者其他创作
大纲/内容
Socket
<b>服务端</b>
<b>客户端</b>
什么是NIO
NIO 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 BIO 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。NIO被称为 no-blocking io 或者 new io都说得通
N和BIO的主要区别
<b>面向流(Stream)与面向缓冲(Buffer)</b>
Java NIO和IO之间第一个最大的区别是,<b>IO是面向流的,NIO是面向缓冲区的</b>。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。<b>数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动</b>。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据
<b>阻塞与非阻塞</b>
<b>Java IO的各种流是阻塞的</b>。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。<br> Ja<b>va NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情</b>。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,<b>这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)</b>
三大组件
Buffer & ByteBuffer
<b>buffer 则用来缓冲读写数据</b>
<b>JDK NIO是面向缓冲的</b>。<b>Buffer就是这个缓冲,用于和NIO通道进行交互</b>。<b>数据是从通道读入缓冲区,从缓冲区写入到通道中的</b>。以写为例,应用程序都是将数据写入缓冲,再通过通道把缓冲的数据发送出去,读也是一样,数据总是先从通道读到缓冲,应用程序再读缓冲的数据。<br><font color="#e74f4c">缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存(其实就是数组)。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。</font>
NIO通讯的流程图
<b>正确使用步骤</b>
<b>ByteBuffer内部结构</b>
<br>
<b>常用方法</b>
<b>allocate</b><br>
<b>HeapByteBuffer</b>
java堆内存 读写效率较低,受到垃圾回收的影响
<b>DirectByteBuffer</b>
<ul><li>直接内存 读写效率高(少一次拷贝)使用的系统内存,不会受gc影响</li><li>分配内存的效率低,使用不当会造成内存泄漏</li></ul>
<b>put & get [绝对读写]</b>
<b><font color="#ed9745">put(int index, byte b)</font>:绝对写</b>,向byteBuffer底层的bytes中下标为index的位置插入byte b,<b>不改变position的值</b>。
<font color="#ed9745"><b>get(int index)</b></font>`: 属于绝对读,读取byteBuffer底层的bytes中下标为index的byte,<b>不改变position</b>
<b>rewind & get(index)</b><br>
<font color="#ed9745"><b>Buffer.rewind()</b></font> 将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。
<b>mark & reset</b>
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position
<font color="#e74f4c"> 注意:rewind 和 flip 都会清除 mark 位置</font>
<b>equals</b>
当满足下列条件时,表示两个Buffer相等:<br><ol><li><b>有相同的类型(byte、char、int等)。</b></li><li><b>Buffer中剩余的byte、char等的个数相等。</b></li><li><b>Buffer中所有剩余的byte、char等都相同。</b></li></ol>如你所见,<b>equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素</b>
<b>compareTo</b>
<b>compareTo()方法比较两个Buffer的剩余元素(byte、char等)</b>, 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:<br><ul><li>1. 第一个不相等的元素小于另一个Buffer中对应的元素 。</li><li>2. 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。</li></ul>
<b>buffer方法总结</b>
<b>字符串与 ByteBuffer</b>
<b>Scattering Reads [分散读]</b>
<b>Gathering Writes [集中写]</b><br>
Channel
<b>读写</b>数据的<b>双向通道</b>,可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel
<b>常见的Channel</b>
FileChannel<br>
DatagramChannel 【UDP】<br>
<b>SocketChannel</b><br>
TCP Socket套接字的监听通道,一个Socket套接字对应了一个客户端IP:端口 到 服务器IP:端口的通信连接
<b>ServerSocketChannel</b><br>
应用服务器程序的监听通道。只有通过这个通道,应用程序才能向操作系统注册支持“多路复用IO”的端口监听。同时支持UDP协议和TCP协议
<b>所有被Selector(选择器)注册的通道,只能是继承了SelectableChannel类的子类</b>
Selector
Selector的英文含义是“选择器”,也可以称为为“<b>轮询代理器”、“事件订阅器”、“channel容器管理机</b>”都行。<br><b>Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器(Selectors),然后使用一个单独的线程来操作这个选择器,进而“选择”通道</b>:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。<br>应<b>用程序将向Selector对象注册需要它关注的Channel,以及具体的某一个Channel会对哪些IO事件感兴趣</b>。Selector中也会维护一个“已经注册的Channel”的容器<br>
多线程版设计
<br>
缺点
线程池版本
缺点
Selector版设计<br>
优点
文件编程<br>
<b>FileChannel</b>
<b>不能直接打开 FileChannel</b>,必须通过 <b>FileInputStream</b>、<b>FileOutputStream </b>或者 <b>RandomAccessFile </b>来获取 FileChannel,它们都有 `<font color="#ed9745">getChannel</font>` 方法<br><ul><li>通过 <b>FileInputStream </b>获取的 channel <b>只能读</b></li><li>通过 <b>FileOutputStream </b>获取的 channel <b>只能写</b></li><li>通过 <b>RandomAccessFile </b>是否能读写根据构造 RandomAccessFile 时的<b>读写模式决定</b></li></ul>
<b>强制写入</b>
<b>操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘。可以调用` <font color="#e74f4c">force(true)</font> ` 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘</b><br>这个缓存是指在<b>内存中与外存映射的一些内存块</b>,也叫做<b>`页缓存</b>`,目的是减少真正的块IO。
案例:两个 Channel 传输数据
<b>Path & Paths</b><br>
<b>Files</b>
get
copy
createDirectory & createDirectorys
delete
<font color="#b71c1c"><b>walkFileTree</b></font>
网络编程<br>
阻塞 & 非阻塞
<b>阻塞</b>
阻塞模式下,相关方法都会导致线程暂停
<font color="#a23735">ServerSocketChannel.accept</font>会在没有连接建立时让线程暂停
<font color="#a23735">SocketChannel.read</font> 会在没有数据可读时让线程暂停
<font color="#e74f4c"><b>阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置</b></font>
<b>非阻塞</b>
多路复用
<b>I/O多路复用是将多个I/O的阻塞复用到一个select的阻塞上。从而使系统在<font color="#e74f4c">单线程的情况下可以处理多个客户端请求(Channel读写)</font></b>
<font color="#e74f4c"><b>多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用</b></font>
如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证<br><ul><li><b>有可连接事件时才去连接</b></li><li><b>有可读事件才去读取</b></li><li><b>有可写事件才去写入</b></li></ul>
Selector
<b>优点</b>
一<b>个线程配合 selector 就可以监控多个 channel 的事件</b>,<b>事件发生线程才去处理</b>。避免非阻塞模式下所做无用功
<b>让这个线程能够被充分利用</b>
<b>节约了线程的数量</b>
<b>减少了线程上下文切换</b>
<b>Channel绑定的事件(SelectionKey)</b>
channel 必须工作在<b>非阻塞模式 <font color="#ed9745">channel.configureBlocking(false)</font>;</b><br><b>FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用</b><br>
<b>SelectionKey</b>是一个抽象类,表示<b>selectableChannel</b>在Selector中注册的标识。每个Channel向Selector注册时,都将会创建一个<b>SelectionKey</b>。<b>SelectionKey</b>将<b>Channel</b>与<b>Selector</b>建立了关系,并维护了<b>channel</b>事件。<br>可以通过`<font color="#ed9745">cancel</font>`方法取消键,取消的键不会立即从selector中移除,而是添加到<b><font color="#ed9745">cancelledKeys</font></b>中,在下一次select操作时移除它。所以在调用某个key时,需要使用`<font color="#ed9745">isValid</font>`进行校验.
<b>SelectionKey的关注的事件类型</b><br>
JAVA NIO共定义了四种:<b>OP_READ</b>、<b>OP_WRITE</b>、<b>OP_CONNECT</b>、<b>OP_ACCEPT</b>(定义在SelectionKey中),分别对应 <b>读、写、请求连接、接受连接等网络Socket操作</b>
<b>服务端和客户端分别感兴趣的类型</b>
<b>ServerSocketChannel </b>和 <b>SocketChannel </b>可以注册自己感兴趣的操作类型,<b>当对应操作类型的就绪条件满足时OS会通知channel</b>,下表描述各种Channel允许注册的操作类型,Y表示允许注册,N表示不允许注册,其中服务器<b>SocketChannel</b>指由服务器 <font color="#f57e0e">ServerSocketChannel.accept() </font>返回的对象
<ul><li>服务器启动<b>ServerSocketChannel</b>,关注`<font color="#e74f4c">OP_ACCEPT</font>`事件,</li><li>客户端启动<b>SocketChannel</b>,连接服务器,关注`<font color="#e74f4c">OP_CONNECT</font>`事件</li><li>服务器接受连接,启动一个服务器的<b>SocketChannel</b>,这个 <b>SocketChannel </b>可以关注`<font color="#e74f4c">OP_READ</font>`、`<font color="#e74f4c">OP_WRITE</font>`事件,一般连接建立后会直接关注`<font color="#e74f4c">OP_READ</font>`事件</li><li>客户端这边的客户端<b>SocketChannel</b>发现连接建立后,可以关注`<font color="#e74f4c">OP_READ</font>`、`<font color="#e74f4c">OP_WRITE</font>`事件,一般是需要客户端需要发送数据了才关注`<font color="#e74f4c">OP_READ</font>`事件</li><li><b>连接建立后客户端与服务器端开始相互发送消息(读写),根据实际情况来关注OP_READ、OP_WRITE事件</b></li></ul>
<b>监听Channel事件</b>
<b style=""><font color="#1b5e20">select何时不阻塞?</font></b>
<b>处理accept事件</b>
<b style=""><font color="#314aa4">事件发生后能否不处理?</font></b>
<b>事件发生后,要么处理(remove),要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是<font color="#e74f4c">水平触发</font></b>
<b>处理read事件</b>
<b style=""><font color="#314aa4">为何要 iter.remove()?</font></b>
<b>cancel 的作用</b>
cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件<br><b>客户端不管是正常断开还是异常断开都会产生一个读事件</b>
<b>处理消息边界</b>
<b>ByteBuffer大小分配</b>
每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 channel 共同使用,因此需要为<b>每个 channel 维护一个独立的 ByteBuffer</b>
<b>ByteBuffer 不能太大</b>,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计<b>大小可变的 ByteBuffer</b>
<b>一种思路是首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能</b>,参考实现 [http://tutorials.jenkov.com/java-performance/resizable-array.html](http://tutorials.jenkov.com/java-performance/resizable-array.html)
<b>另一种思路是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组</b>,与前面的区别是<b>消息存储不连续解析复杂</b>,优点是<b>避免了拷贝引起的性能损耗</b>
<b>处理write事件</b>
<ul><li>非阻塞模式下,<b>无法保证把 buffer 中所有数据都写入 channel</b>,因此<b>需要追踪 write 方法的返回值(代表实际写入字节数)</b></li><li>用 selector 监听所有 channel 的可写事件,每个 channel 都需要一个 key 来跟踪 buffer,但这样又会导致占用内存过多,就有两阶段策略<br> ① <b>当消息处理器第一次写入消息时,才将 channel 注册到 selector 上</b><br> ② s<b>elector 检查 channel 上的可写事件,如果所有的数据写完了,就取消 channel 的注册(如果不取消,会每次可写均会触发 write 事件)</b><br></li></ul>
一次无法写完例子
<b><font color="#314aa4">write 为何要取消?</font></b>
只要向 channel 发送数据时,socket 缓冲可写,这个事件会频繁触发,因此应当只在 socket 缓冲区写不下时再关注可写事件,数据写完之后再取消关注
<b>更进一步(多线程)</b>
<b>简易流程图</b>
分两组选择器<br><ul><li><b>单线程配一个选择器,专门处理 accept 事件</b></li><li><b>创建 cpu 核心数的线程,每个线程配一个选择器,轮流处理 read,write事件</b></li></ul>
<b>这就是NettyReactor模式的简易版本</b>
<b>如何拿到CPU个数</b>
<font color="#f57c00"><b>Runtime.getRuntime().availableProcessors()</b></font>
NIO vs BIO
直接内存深入辨析
在所有的网络通信和应用程序中,每个TCP的Socket的内核中都有一个<b>发送缓冲区(SO_SNDBUF) </b>和一个 <b>接收缓冲区(SO_RECVBUF)</b>,(<b>默认最小大小为4kb </b>)可以使用相关套接字选项来更改该缓冲区大小。<br><br><ul><li>当某个应用进程调用<font color="#f2700c"><b>write</b></font>时,内核从该<b>应用进程的缓冲区</b>中复制所有数据到所写<b>套接字的发送缓冲区</b>。如果该套接字的发送缓冲区容不下 该应用进程的所有数据(或是应用进程的缓冲区大于套接字的发送缓冲区,或是套接字的发送缓冲区中已有其他数据),<b>假设该套接字是阻塞的,则该应用进程将被投入睡眠。</b></li><li>内核将不从write系统调用返回,直到应用进程缓冲区中的所有数据都复制到套接字发送缓冲区。因此,<b>从写一个TCP套接字的write调用成功返回仅仅表示我们可以重新使用原来的应用进程缓冲区,并不表明对端的TCP或应用进程已接收到数据</b>。</li></ul>
<b>堆外内存</b>
<b>直接内存</b>
在IO读写上,如果是使用堆内存,JDK会先创建一个<b>`DirectBuffer`(应用进程缓冲区</b>),再去执行真正的写操作。这是因为,<b><font color="#e74f4c">当我们把一个地址通过JNI传递给底层的C库的时候,有一个基本的要求,就是这个地址上的内容不能失效</font></b>。然而,<b>在GC管理下的对象是会在Java堆中移动的</b>。也就是说,有可能我把一个地址传给底层的write,但是这段内存却因为GC整理内存而失效了。所以<b>必须要把待发送的数据放到一个GC管不着的地方(堆外内存)</b>。这就是调用native方法之前,数据—定要在堆外内存的原因。<br><br>可见,<b>站在网络通信的角度DirectBuffer并没有节省什么内存拷贝,只是Java网络通信里因为HeapBuffer必须多做一次拷贝</b>,<b>使用DirectBuffer就会少一次内存拷贝</b>。相比没有使用堆内存的Java程序,使用直接内存的Java程序当然更快一点。<br>从垃圾回收的角度而言,<b>直接内存不受 GC(新生代的 Minor GC) 影响,只有当执行老年代的 Full GC 时候才会顺便回收直接内存,整理内存的压力也比数据放到HeapBuffer要小</b>
<b>为什么要使用直接内存【<font color="#e74f4c">优点</font>】</b><br>
直接内存,其实就是不受 JVM 控制的内存。相比于堆内存有几个优势:<br>1、 <b>减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作</b>;<br><b>2、 加快了复制的速度因为堆内在flush到远程时,会先复制到直接内存(非堆内存)</b>,然后再发送,而堆外内存相当于省略掉了这个工作;<br><b>3、 可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现</b>;<br><b>4、 可以扩展至更大的内存空间比如超过1TB甚至比主存还大的空间</b>;
在IO读写上,如果是使用堆内存,JDK会先创建一个`<b>DirectBuffer</b>`(应用进程缓冲区),再去执行真正的写操作。这是因为,<font color="#e74f4c"><b>当我们把一个地址通过JNI传递给底层的C库的时候,有一个基本的要求,就是这个地址上的内容不能失效</b></font>`。然而,<b>在GC管理下的对象是会在Java堆中移动的</b>。也就是说,有可能我把一个地址传给底层的write,但是这段内存却因为GC整理内存而失效了。所以<b><font color="#e74f4c">必须要把待发送的数据放到一个GC管不着的地方</font></b>。这就是调用native方法之前,数据—定要在堆外内存的原因。
<b><font color="#e74f4c">缺点</font></b>
<b>分配回收成本较高</b>
<b>不受JVM内存回收管理</b>
直接内存大小可以通过`<font color="#a23c73"><b>MaxDirectMemorySize</b></font>`设置如果不指定,<b>默认与堆的最大值-Xmx参数值一致</b>。
<b>堆外内存的分配</b>
<font color="#a23c73"><b>ByteBuffer#allocateDirect</b></font><br>
通过该方式分配堆外内存其实最底层还是使用的是Unsafe#allocateMemory进行分配内存,ByteBuffer只是对Unsafe做了一层封装
<font color="#a23c73"><b>Unsafe#allocateMemory</b></font>
<b>堆外内存回收</b>
<font color="#a23c73">Unsafe#freeMemory</font>
在Unsafe中提供了<b>freeMemory</b>的实现进行回收堆外内存,但是<b>前提是需要知道被分配的堆外内存地址才可以实现对应的内存回收</b>
<font color="#a23c73"><b>JVM回收堆外内存</b></font>
通过<b>ByteBuffer#allocateDirect</b>分配的堆外内存在JVM中其实也是存在一定的内存占用的,具体关联关系如下
当通过<b>ByteBuffer#allocateDirec</b>t分配堆外内存后,会将堆外内存的地址、大小等信息通过<b>DirectByteBuffer</b>进行关联,那么堆内存中就可以关联到堆外内存
<ul><li><font color="#e74f4c"><b>JVM执行Full GC时会将DirectByteBuffer进行回收,回收之后Clearner就不存在引用关系</b></font></li><li><b>再下一次发生GC时会将Cleaner对象放入ReferenceQueue中</b>,同时将Cleaner从链表中移除</li><li>最后调用<font color="#e74f4c">unsafe#freeMemory</font>清除堆外内存</li></ul><br>
<b>DirectByteBuffer 是存在堆内存中的对象,那么既然存在堆内存中就会发生GC晋级,即晋升到老年代中,在老年代中就会发生Full GC或者Old GC</b><br>
Stream VS Channel
<b>stream 不会自动缓冲数据,channel 会利用系统提供的发送缓冲区、接收缓冲区(<font color="#e74f4c">更为底层</font>)</b>
<b>stream 仅支持阻塞 API(BIO),channel 同时支持阻塞、非阻塞 AP</b>I,网络 channel 可配合 selector 实现多路复用
<b>二者均为全双工,即读写可以同时进行</b>
IO模型
<b>基础概念</b>
<b>模型</b>
<b>同步阻塞IO</b>
<b>同步非阻塞IO</b><br>
<b>同步多路复用</b><br>
<b>异步IO</b>
<b>阻塞IO vs 多路复用</b>
零拷贝
传统IO
<b>1. fileInputStream.read(buffer) 操作数据拷贝及状态转换分析</b>
<b>① 硬盘 ( 初始用户态 ) -> 内核缓冲区 ( 内核态 ) </b>: 首先将硬盘中的文件 , 进行 DMA [ 1 ] ^{[1]} [1] 拷贝 , 此处对应 read 方法 , 将文件数据从硬盘中拷贝到 内核缓冲区 ; ( 用户态切换成内核态 )<br><b>② 内核缓冲区 ( 内核态 ) -> 用户缓冲区 ( 用户态 )</b> : 将内核缓冲区中的数据 , 通过 CPU 拷贝 方式 , 拷贝到 用户缓冲区 ; ( 内核态切换成用户态 )
<b>2. socket.getOutputStream().write(buffer, 0, readLen) 操作数据拷贝及状态转换分析</b>
<b>① 用户缓冲区 ( 用户态 ) -> Socket 缓冲区 ( 内核态 ) </b>: 将用户缓冲区中的数据 , 再次通过 CPU 拷贝 方式 , 拷贝到 Socket 缓冲区 ; ( 用户态切换成内核态 )<br><b>② Socket 缓冲区 ( 内核态 ) -> 协议栈</b> : 再次使用 DMA [ 1 ] ^{[1]} [1] 拷贝 , 将 Socket 缓冲区中的数据拷贝到 协议栈 ( Protocol Engine ) 中 ;
<b>4次数据拷贝,3次用户态和内核态的切换【最终如果算上将内核态切换为用户态(执行应用代码逻辑),则为4次】</b><br>
<b>传统IO拷贝次数分析</b><br>
开始时数据存储在 硬盘文件 中 , 直接内存拷贝 ( Direct Memory Access ) 到 内核缓冲区 , CPU 拷贝 到 用户缓冲区 , CPU 拷贝 到 Socket 缓冲区 , 直接内存拷贝 ( Direct Memory Access ) 到 协议栈 ;
<b>硬盘文件 -> 内核缓冲区 ( 内核空间 ) -> 用户缓冲区 ( 用户空间 ) -> Socket 缓冲区 ( 内核空间 ) -> 协议栈</b><br>
<b>传统IO状态改变分析</b>
<b>用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态</b>
<font color="#e74f4c">DMA 全称 ( Direct Memory Access ) , 直接内存拷贝 , 该拷贝通过内存完成 , 不涉及 CPU 参与</font>
NIO优化
<b><font color="#b71c1c">DirectByteBuffer 直接内存(Mmap) </font><font color="#000000">【3次copy,3(4)次切换】</font></b><br>
<b>将硬盘中的文件映射到 内核缓冲区</b> , 用户空间中的应用程序也可以访问该 内核缓冲区 中的数据 , 使用这种机制 , <b>原来的 4 次数据拷贝减少到了 3 次</b>
<b>mmap 数据拷贝过程</b>
<b>① 硬盘文件 -> 内核缓冲区</b> : 硬盘文件数据 , DMA 拷贝到 内核缓冲区 中 , 应用程序可以直接访问该 内核缓冲区中的数据 ;<br><b>② 内核缓冲区 -> Socket 缓冲区</b> : 内核缓冲区 数据 , 通过 CPU 拷贝到 Socket 缓冲区 ;<br><b>③ Socket 缓冲区 -> 协议栈</b> : Socket 缓冲区 数据 , 通过 DMA 拷贝到 协议栈 ;
<b>硬盘文件 -> 内核缓冲区 ( 内核空间 ) -> Socket 缓冲区 ( 内核空间 ) -> 协议栈</b>
<b>mmap 状态切换</b>
<b>其状态切换还是 3 次</b> , 由初始状态<b> 用户态</b> , 在拷贝数据到内核缓冲区时 , 切换成<b>内核态</b> , 访问该内核缓冲区数据时 , 又切换成<b>用户态</b> , 将数据拷贝到 Socket 缓冲区时 , 切换成<b>内核态</b> , 最后再切换成<b>用户态</b> , 执行后续应用程序代码逻辑 ;
<b>用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态</b>
<b>【3次copy ,1(2)次切换】 </b><br>进一步优化(底层采用了 <b><font color="#b71c1c">linux 2.1 后提供的 `sendFile` 方法</font></b>),java 中对应着两个 channel 调用 <b><font color="#e74f4c">t</font><font color="#e74f4c">ransferTo/transferFrom</font></b><font color="#b71c1c"> </font>方法拷贝数据<br>
其实现了由 内核缓冲区 直接将数据拷贝到 Socket 缓冲区 , 该操作直接在内核空间完成 , 不经过用户空间 , 没有用户态参与 , 因此 减少了一次用户态切换
<b>数据拷贝分析</b>
<b>① 硬盘文件 -> 内核缓冲区</b> : 硬盘文件数据 , <b>DMA </b>拷贝到 内核缓冲区 中 ;<br><b>② 内核缓冲区 -> Socket 缓冲区 </b>: 内核缓冲区 数据 , 通过 <b>CPU </b>拷贝到 Socket 缓冲区 ;<br><b>③ Socket 缓冲区 -> 协议栈 </b>: Socket 缓冲区 数据 , 通过 <b>DMA </b>拷贝到 协议栈 ;
<b>硬盘文件 -> 内核缓冲区 ( 内核空间 ) -> Socket 缓冲区 ( 内核空间 ) -> 协议栈</b>
<b>sendFile 函数 状态切换分析</b>
<b>其状态切换只有 2次</b> , 由初始状态 <b>用户态</b> , 在拷贝数据到内核缓冲区时 , 切换成<b>内核态</b> , 在内核态直接将数据拷贝到 Socket 缓冲区时 , 还是处于内核状态 , 之后拷贝到协议栈时 , 变成<b>用户状态</b>
<b>用户态 -> 内核态 -> 用户态</b>
<b>【2次copy,1(2)次切换】 (全称 DMA 拷贝 , 没有 CPU 拷贝)<br></b>进一步优化(<b><font color="#b71c1c">linux 2.4</font></b>) <b>带有DMA收集拷贝功能的sendfile实现的零拷贝</b>
'
少量 CPU 拷贝 : 该机制还存在少量的 CPU 拷贝 , 其 对性能的消耗忽略不计 ; 这些 CPU 拷贝操作是从 内核缓冲区 中将数据的长度 ( Length ) , 偏移量 ( Offset ) 拷贝到 Socket 缓冲区 ;
<b>数据拷贝分析</b>
<b>① 硬盘文件 -> 内核缓冲区 :</b> 硬盘文件数据 , DMA 拷贝到 内核缓冲区 中 ;<br><b>② 内核缓冲区 -> -> 协议栈 </b>: 通过 DMA 拷贝 , 将 内核缓冲区 中的数据直接拷贝到 协议栈 ;
<b>硬盘文件 -> 内核缓冲区 ( 内核空间 ) -> 协议栈</b>
<b>sendFile 函数 状态切换分析</b>
<b>其状态切换只有 2 次</b> , 由初始状态 <b>用户态</b> , 在拷贝数据到内核缓冲区时 , 切换成<b>内核态</b> , 在内核态直接将数据拷贝到协议栈时 , 变成<b>用户状</b>态 ;
<b>用户态 -> 内核态 -> 用户态</b>
优点
AIO
Java生态圈中的零拷贝<br>
Linux提供的零拷贝技术 Java并不是全支持,<b>支持2种(内存映射mmap、sendfile)</b>
NIO提供的内存映射 MappedByteBuffer
NIO中的 <font color="#e74f4c"><b> FileChannel.map(</b></font>) 方法其实就是采用了操作系统中的内存映射方式,底层就是调用 <b>Linux mmap()</b> 实现的
<b>将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。这种方式适合读取大文件,同时也能对文件内容进行更改,但是如果其后要通过SocketChannel发送,还是需要CPU进行数据的拷贝</b>
NIO提供的sendfile<br>
Java NIO 中提供的 <b>FileChannel</b> 拥有 `<font color="#ed9745">transferTo</font>` 和 `<font color="#ed9745"><b>transferFrom</b></font>` 两个方法,可直接把 FileChannel 中的数据拷贝到另外一个 Channel,或者直接把另外一个 Channel 中的数据拷贝到 FileChannel。<b>该接口常被用于高效的网络 / 文件的数据传输和大文件拷贝</b>。在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,所以其性能一般高于 Java IO 中提供的方法
Kafka中的零拷贝<br>
Kafka两个重要过程都使用了零拷贝技术,且都是操作系统层面的狭义零拷贝,一是Producer生产的数据存到broker,二是 Consumer从broker读取数据。
<b>Producer生产的数据持久化到broke</b>r,broker里采用<b>mmap</b>文件映射,<b>实现顺序的快速写入</b>;
<b>Customer从broker读取数据</b>,broker里采用<b>sendfile</b>,将磁盘文件读到OS内核缓冲区后,直接转到socket buffer进行网络发送。
Netty的零拷贝实现
<b>网络通信</b><br>
- 在<b>网络通信</b>上,Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。<br>
<b>缓存操作</b>
Netty提供了`<font color="#ed9745"><b>CompositeByteBuf</b></font>` 类,它可以将多个ByteBuf合并为一个逻辑上的ByteBuf,<b>避免了各个ByteBuf之间的拷贝 </b>通过wrap操作,我们可以将byte[]数组、ByteBuf、 ByteBuffer 等包装成一个 Netty ByteBuf对象,进而避免了拷贝操作。<br>ByteBuf支持<font color="#ed9745"><b>slice </b></font>操作,因此可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf,避免了内存的拷贝。
<b>文件传输</b>
Netty 的通过`<font color="#ed9745"><b>FileRegion</b></font>`包装的 `<b><font color="#ed9745">FileChannel.tranferTo</font></b>` 实现文件传输,它可以直接将文件缓冲区的数据发送到目标 Channel,<b>避免了传统通过循环 write 方式导致的内存拷贝问题</b>
0 条评论
下一页