零拷贝剖析

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/m0_38060977/article/details/102757201

#一 一次普通网络请求资源的过程
image.png

1.1具体步骤说明

1.用户空间对内核空间发出一次read()的系统调用
2.操作系统接收到系统调用后,上下文切换。切换到了内核空间
3.内核空间向磁盘请求数据, 通过DMA将数据发送到了内核空间缓存区
4.将内核空间缓冲区的数据原封不动地拷贝到了用户空间缓存区。
==第二阶段=
5.jvm发出wirte()的系统调用,并且将用户空间缓存区的数据搬回了内核空间缓冲区。这个缓冲区和上面的缓冲区不一样,这个是属于socket的缓冲区(是内核空间,但不和上面一样。只要发送就需要配先放到socket缓冲区
6.将内核空间缓存区的数据写到网络
7.wirte()返回。

总结
1.一共存在4次上下文的切换
2.两次不必要的拷贝。

二 操作系统意义上的零拷贝

首先需要操作系统提供支持,纯技术(语言层面)上无法实现
image.png
没有数据和用户空间交互

nio中的MapByteBuffer就是一种实现,transTo,transFrom如果底层操作系统支持,就会使用零拷贝

三 进一步改善

image.png

总结
上面两种都是零拷贝,nio都会使用。主要看底层操作系统是否支持更好的那种

四 缓存IO、直接IO与内存映射

缓存IO对应第一节
image.png

直接IO对应第二节
image.png

直接内存映射
如果说直接IO和缓存IO是相对的、而内存映射是另外一个概念
image.png

内存映射方式是指操作系统将内存中的某一块区域与磁盘中的文件关联起来、当要访问内存中一段数据时、转换为访问文件的某一段数据、这种方式的目的同样是减少数据从内核空间缓存到用户空间缓存的数据复制操作、因为这两个空间的数据是共享的

所以直接内存映射还是将数据读取到了内核空间,只不过不需要继续拷贝`到用户空间。

Java NIO Buffer中的非直接缓冲区指的就是缓存IO、而直接缓冲区并不是Linux概念上面的直接IO、而是内存映射、请看下面的对比图
image.png
image.png

Java NIO中建立直接缓冲区、其实是在JVM内存外开辟堆外内存、在每次调用基础操作系统的一个本机IO之前或者之后、虚拟机都会避免将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容)、缓冲区的内容驻留在物理内存内、会少一次复制过程、如果需要循环使用缓冲区、用直接缓冲区可以很大地提高性能、虽然直接缓冲区使JVM可以进行高效的IO操作、但它使用的内存是操作系统分配的、绕过了JVM堆栈、nmap的建立和销毁比堆栈上的缓冲区要更大的开销

参考:缓存IO、直接IO与内存映射

五 实战

##5.1 BIO方式
客户端

public class OldClient {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("localhost",8899);
        InputStream inputStream = new FileInputStream("E:/download/jdk-8u191-linux-x64.tar.gz");

        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());

        byte[] buffer = new byte[4096];
        long readCount;
        long total = 0l;

        long startTime = System.currentTimeMillis();

        while((readCount = inputStream.read(buffer))>=0){
            total += readCount;
            dataOutputStream.write(buffer);
        }

        System.out.println("发送字节总数:"+total+" ,耗时:"+(System.currentTimeMillis()-startTime));

        dataOutputStream.close();
        socket.close();
        inputStream.close();


    }
}

服务端

public class OldServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8899);
        while (true){
            Socket socket = serverSocket.accept();
            DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());

            try {
                byte[] byteArray = new byte[4096];
                while (true){
                    int size =dataInputStream.read(byteArray,0,byteArray.length);

                    if(-1 == size){
                        break;
                    }
                }
            }catch (Exception e){


            }
        }

    }

}

##5.2 NIO方式

nio形式服务端

public class NewServer {

    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocket.bind(new InetSocketAddress(8899));


        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

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

            int readCount = 0;
            try {
                while(-1 != readCount){
                    readCount = socketChannel.read(byteBuffer);

                    byteBuffer.rewind();

                }
                socketChannel.close();
            }catch (Exception e){

            }

        }

    }

}

5.3 结论

1.在本机上nio方式在140ms,bio在1000多ms。但当设置byte[] buffer = new byte[833608];时,bio方式也可以达到180ms左右。nio稍快几十毫秒(180mb的文件大小)
再次测试900mb的文件大小:bld方式(第一次10s多,之后1s多,有了缓存)。换个差不多大小的文件,nio方式下平均700ms,确实有提升
2.客户端和服务端不一定必须是同一种io,应该是因为底层都是socket,是一样的

六 linux上真正的零拷贝

image.png
这是一次网络IO
原来有3次拷贝(最后一次可不算,那就是两次)

  1. 磁盘到内核空间
  2. 内核空间到socket缓存区
  3. socket缓存区到protocol engine(协议引擎,真正网络发送的地方。)

现在两次(最后一次可不算)

  1. 磁盘到内核空间
  2. 内核空间到socket缓存区(不拷贝数据,只存储了内核空间数据对应的文件描述符)
  3. protocol engine结合socket缓存区(文件描述符)和内核空间读数据(第二次拷贝)

什么是零拷贝?
就是只读取一次,将数据从磁盘读取到内存。(协议引擎的读取不算)
上面通过transTo发送,底层就是用到了零拷贝

猜你喜欢

转载自blog.csdn.net/m0_38060977/article/details/102757201
今日推荐