随笔 - 1162  文章 - 0  评论 - 16  阅读 - 59万 

一、零拷贝基本介绍

(1)零拷贝是网络编程的关键, 很多性能优化都离不开。

(2)在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。那么,他们在OS里,到底是怎么样的一个的设计? 我们分析 mmap 和 sendFile 这两个零拷贝。

(3)另外我们看下NIO 中如何使用零拷贝。

二、传统IO数据读写

Java 传统 IO 和 网络编程的一段代码

File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);

传统IO模型

这其实经历了四次状态转换(用户态->内核态->用户态->内核态)和四次拷贝(两次DMA拷贝和两次CPU拷贝)

DMA:Direct Memory Access 直接内存拷贝(不使用CPU)

三、mmap优化

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

mmap示意图:

这其实经历了四次状态转换(用户态->内核态->用户态->内核态)和三次拷贝(两次DMA拷贝和一次CPU拷贝)

四、sendFile 优化

Linux2.1版本

Linux 2.1 版本提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer, 同时由于和用户态完全无关, 就减少了一次上下文切换。

示意图:

Linux2.4版本

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

这里其实有 一次cpu 拷贝kernel buffer -> socket buffer但是, 拷贝的信息很少, 比如lenght , offset , 消耗低, 可以忽略。

零拷贝的再次理解

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

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

注意:零拷贝从操作系统角度, 是指没有cpu 拷贝。

五、mmap 和 sendFile 的区别

(1)mmap 适合小数据量读写, sendFile 适合大文件传输。

(2)mmap 需要 4 次上下文切换, 3 次数据拷贝; sendFile 需要 3 次上下文切换, 最少 2 次数据拷贝。

(3)sendFile 可以利用 DMA 方式, 减少 CPU 拷贝, mmap 则不能(必须从内核拷贝到 Socket 缓冲区) 。

六、零拷贝案例

1、传统IO方式

服务器端:

//Java IO 的服务器
public class OldServer {
    public static void main(String[] args) throws Exception{
        ServerSocket serverSocket = new ServerSocket(7001);

        while (true) {
            Socket socket = serverSocket.accept();

            DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());;

            try {
                byte[] byteArray = new byte[4096];

                while (true) {
                    int readCount = dataInputStream.read(byteArray);

                    if (-1 == readCount) {
                        break;
                    }
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

客户端:

public class OldClient {
    public static void main(String[] args) throws Exception{
        Socket socket = new Socket("127.0.0.1"7001);

        //String fileName = "protoc-3.6.1-win32.zip";
        String fileName = "剑指offer.zip";

        FileInputStream fileInputStream = new FileInputStream(fileName);

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

        byte[] buffer = new byte[4096];

        long startTime = System.currentTimeMillis();

        long readCount = 0;
        long total = 0;

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

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

        dataOutputStream.close();
        socket.close();
        fileInputStream.close();
    }
}

2、使用NIO 零拷贝方式传递(transferTo)一个大文件

服务器端:

//NIO 服务器
public class NewIOServer {

    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        InetSocketAddress address = new InetSocketAddress(7001);
        serverSocketChannel.bind(address);

        ServerSocket serverSocket = serverSocketChannel.socket();

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

        while (true) {

            SocketChannel socketChannel = serverSocketChannel.accept();

            int readCount = 0;

            while (-1 != readCount) {
                try {
                    readCount = socketChannel.read(byteBuffer);
                } catch (Exception ex) {
                    ex.printStackTrace();
                    break;
                }

                byteBuffer.rewind(); //倒带 position = 0 mark 作废
            }

        }
    }
}

客户端:

public class NewIOClient {

    public static final long FILE_SIZE = 8 * 1014 * 1024;

    public static void main(String[] args) throws Exception{

        SocketChannel socketChannel = SocketChannel.open();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1"7001);

        socketChannel.connect(address);

        //String fileName = "protoc-3.6.1-win32.zip";
        String fileName = "剑指offer.zip";
        //得到一个文件channel
        FileChannel fileChannel = new FileInputStream(fileName).getChannel();

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

        System.out.println(fileChannel.size());

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

        long transferCount = 0;
        if (fileChannel.size() > FILE_SIZE) {
            transferCount = transfertoGT8M(fileChannel, socketChannel);
        } else {
            transferCount = transferToLT8M(fileChannel, socketChannel);
        }

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

        //关闭
        fileChannel.close();
    }

    private static long transferToLT8M(FileChannel src, WritableByteChannel target) throws IOException {
        return src.transferTo(0, src.size(), target);
    }

    private static long transfertoGT8M(FileChannel src, WritableByteChannel target) throws IOException {
        long fileSize = src.size();
        long count = fileSize % FILE_SIZE == 0 ? fileSize / FILE_SIZE : fileSize / FILE_SIZE + 1;

        System.out.println(count);

        long transferCount = 0;
        for (int i = 0; i < count; i++) {
            transferCount += src.transferTo(i * FILE_SIZE, FILE_SIZE, target);
        }
        return transferCount;
    }
}

通过上面的案例可以发现,使用了零拷贝的情况,可以提高60%左右的性能。

 

posted on   格物致知_Tony  阅读(205)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· 面试官:你是如何进行SQL调优的?
点击右上角即可分享
微信分享提示

目录导航