Netty学习笔记:三、NIO零拷贝

1、传统IO

4次上下文切换、4次内存拷贝

Netty学习笔记:三、NIO零拷贝_第1张图片

上图中,上半部分表示用户态和内核态的上下文切换。下半部分表示数据复制操作。下面说说他们的步骤:

  1. read 调用导致用户态到内核态的一次变化,同时,第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU 拷贝数据到内存,而是 DMA 引擎传输数据到内存,用于解放 CPU) 引擎从磁盘读取 index.html 文件,并将数据放入到内核缓冲区。

  2. 发生第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,同时,发生了一次用内核态到用户态的上下文切换。

  3. 发生第三次数据拷贝,我们调用 write 方法,系统将用户缓冲区的数据拷贝到 Socket 缓冲区。此时,又发生了一次用户态到内核态的上下文切换。

  4. 第四次拷贝,数据异步的从 Socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。这一段,不需要进行上下文切换。

  5. write 方法返回,再次从内核态切换到用户态。

2、mmap优化

4次上下文切换、3次内存拷贝

mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数;

Netty学习笔记:三、NIO零拷贝_第2张图片

3、sendFile优化

Linux2.1优化:3次上下文切换、3次内存拷贝

Linux2.1版本提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态无关,就减少了一次上下文切换;如下图:

Netty学习笔记:三、NIO零拷贝_第3张图片

Linux2.4优化:3次上下文切换、2次内存拷贝

Linux在2.4版本中,做了一些修改,避免了从内核缓冲区拷贝到SocketBuffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝,如下图

Netty学习笔记:三、NIO零拷贝_第4张图片

 第一次使用 DMA 引擎从文件拷贝到内核缓冲区,第二次从内核缓冲区将数据拷贝到网络协议栈;内核缓存区只会拷贝一些 offset 和 length 信息到 SocketBuffer,基本无消耗。

零拷贝理解(明明拷贝了2次)

1、我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有kernel buffer 有一份数据)。

2、零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享、以及无CPU校验和计算。

mmap和sendFile的区别

  1. mmap适合小数据量的读写, sendFile适合大文件传输。
  2. mmap需要4次上下文切换,3次数据拷贝;sendFile需要3次上下文切换,最少2次数据拷贝。
  3. sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)。

rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。

4、零拷贝案例

服务端代码NewIOServer

package com.study.demo.zerocopy;


import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class NewIOServer {

    public static void main(String[] args) throws IOException {
        InetSocketAddress address = new InetSocketAddress(7001);

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        ServerSocket serverSocket = serverSocketChannel.socket();

        serverSocket.bind(address);

        //创建buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();

            int readcount = 0;
            while (-1 != readcount) {
                try {
                    readcount = ((java.nio.channels.SocketChannel) socketChannel).read(byteBuffer);
                } catch (Exception e) {
                    e.printStackTrace();
                    break;
                }
                byteBuffer.rewind();//倒带 position = 0、 mark作废
            }
        }
    }
}

 客户端代码NewIOClient

package com.study.demo.zerocopy;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

public class NewIOClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();

        socketChannel.connect(new InetSocketAddress("127.0.0.1", 7001));

        String fileName = "123.zip";

        //得到一个文件channel
        FileChannel fileChannel = new FileInputStream(fileName).getChannel();

        //准备发射
        long startTime = System.currentTimeMillis();

        //在linux下 一个transferTo 方法就可以完成传输
        //在window下一次调用transferTo 只能发送8m,就需要分段传输文件,并且要主要传输时的位置
        //transferTo底层使用到零拷贝 **重点**
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);

        System.out.println("发送的总字节 = " + transferCount + " 耗时:" + (System.currentTimeMillis() - startTime));

        fileChannel.close();

    }
}

 演示效果:

发送的总字节 = 17287 耗时:1

 部分内容参考了博文:https://www.cnblogs.com/ericli-ericli/articles/12923420.html

你可能感兴趣的:(Netty,NIO零拷贝,mmap和sendfile,零拷贝,sendFile)