• NIO零拷贝与传统IO的文件传输性能比较


    【README】

    1.本文总结自B站《netty-尚硅谷》,很不错;

    2.本文部分内容参考自  NIO效率高的原理之零拷贝与直接内存映射 - 腾讯云开发者社区-腾讯云


    【1】零拷贝原理

    【1.1】传统IO的文件拷贝

     【图解】

    • step1)调用 sys_read系统调用,从用户态进入内核态;借助DMA通道把磁盘驱动数据 读入内核读缓冲区(无需拷贝,因为零拷贝讲的是 无需耗费CPU资源的拷贝,而是DMA拷贝
    • step2)借助CPU把 内核缓冲区数据 读取到用户态缓冲区后,从sys_read系统调用返回,从内核态切换回用户态;(第1次拷贝
    • step3)调用 sys_write 系统调用,从用户态进入内核态;借助CPU把 用户态缓冲区 数据写入 socket 缓冲区(第2次拷贝
    • step4)把 socket 缓冲区数据 借助DMA通道 写入到 网卡驱动(无需cpu参与的拷贝,仅DMA);
    • step5)写入完成后,从内核态返回用户态;

    小结: 上述过程中,操作系统底层 有4次用户态与内核态的切换,2次cpu拷贝(DMA拷贝不占CPU,不计入);文件读写(拷贝)性能较低;

    补充:

    • 虽然DMA拷贝不占用cpu,但它占用系统总线,一定程度上也会影响cpu性能(但这不是本文重点,可以忽略不计);

    【1.2】零拷贝

    1)零拷贝 :

    • Linux 在 2.4 版本中,做了一些修改,sendFile() 系统调用 避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。

    2)零拷贝流程图 :

     【图解】

    • step1)调用 sys_read系统调用,从用户态进入内核态,借助DMA通道 把磁盘驱动数据 读入到 内核读缓冲区;(DMA拷贝)
    • step2)接着在内核态中,调用系统调用 sendfile ,cpu把 读缓冲区的数据写出到 socket缓冲区;(第1次CPU拷贝
    • step3)借助DMA通道,把 socket缓冲区数据 写出到 网卡缓冲区;(DMA拷贝)
    • step4)最后从内核态返回到用户态;

    【小结】

    • 上述过程中,操作系统底层 有2次用户态与内核态的切换,1次cpu拷贝(非真正零拷贝,因为有1次cpu拷贝)
    • 显然,相比传统IO过程,NIO的零拷贝技术的文件传输性能更高
    • 补充*若网卡驱动支持 gather操作,DMA可以直接把数据从内核读缓冲区直接拷贝到网卡驱动,而无需cpu拷贝(真正实现CPU零拷贝);

    NIO零拷贝适用于以下场景:

    • 文件较大,读写较慢,追求速度;
    • JVM内存不足,不能加载太大数据;
    • 内存带宽不够,即存在其他程序或线程存在大量的IO操作,导致带宽本来就小;

    【2】代码实现

    【2.1】基于传统IO传输文件

    注意:字节缓冲 4M (字节缓冲大小会影响传输性能,当然了,一定条件下,缓冲越大越好);

    1)服务器:

    1. /**
    2. * @Description 传统IO服务器
    3. * @author xiao tang
    4. * @version 1.0.0
    5. * @createTime 2022年08月20日
    6. */
    7. public class OldIOServer {
    8. public static void main(String[] args) throws IOException {
    9. // 服务器监听端口 7001
    10. ServerSocket serverSocket = new ServerSocket(7001);
    11. while (true) {
    12. // 阻塞式等待客户端请求链接
    13. Socket socket = serverSocket.accept();
    14. DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
    15. try {
    16. byte[] byteArr = new byte[4096];
    17. // 读取客户端的数据到字节数组
    18. while (dataInputStream.read(byteArr, 0, byteArr.length) != -1) ;
    19. } catch (Exception e) {
    20. e.printStackTrace();
    21. }
    22. }
    23. }
    24. }

    2)客户端:

    1. /**
    2. * @Description 传统IO客户端
    3. * @author xiao tang
    4. * @version 1.0.0
    5. * @createTime 2022年08月20日
    6. */
    7. public class OldIOClient {
    8. public static void main(String[] args) throws IOException {
    9. Socket socket = new Socket("127.0.0.1", 7001);
    10. // 传输 一个 zip文件
    11. InputStream inputStream = new FileInputStream("D:\\cmb\\studynote\\netty\\temp\\springboot.zip");
    12. DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
    13. byte[] buffer = new byte[4096];
    14. long readCount;
    15. long total = 0;
    16. long startTime = System.currentTimeMillis();
    17. while ((readCount = inputStream.read(buffer)) >= 0) {
    18. total += readCount;
    19. dataOutputStream.write(buffer);
    20. }
    21. long cost = System.currentTimeMillis() - startTime;
    22. System.out.println("发送总字节数 " + total + ", 耗时 = " + cost);
    23. // 关闭资源
    24. dataOutputStream.close();
    25. socket.close();
    26. inputStream.close();
    27. }
    28. }

    3)效果:

    发送总字节数 4491230, 耗时 = 50


    【2.2】基于NIO零拷贝传输文件

    注意:字节缓冲 4M (字节缓冲大小会影响传输性能,当然了,一定条件下,缓冲越大越好);

    1)服务器:

    1. /**
    2. * @Description nio实现零拷贝服务器
    3. * @author xiao tang
    4. * @version 1.0.0
    5. * @createTime 2022年08月20日
    6. */
    7. public class ZeroCopyNIOServer {
    8. public static void main(String[] args) throws IOException {
    9. // 服务器套接字通道
    10. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    11. // 绑定端口
    12. serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1", 7001));
    13. ByteBuffer buffer = ByteBuffer.allocate(4096);
    14. while (true) {
    15. // 等待客户端连接
    16. SocketChannel socketChannel = serverSocketChannel.accept();
    17. // 读取数据
    18. while (socketChannel.read(buffer) != -1) {
    19. // 缓冲倒带, 设置 position=0, mark作废
    20. buffer.rewind();
    21. }
    22. }
    23. }
    24. }

    2)客户端:

    1. /**
    2. * @Description nio实现零拷贝服务器
    3. * @author xiao tang
    4. * @version 1.0.0
    5. * @createTime 2022年08月20日
    6. */
    7. public class ZeroCopyNIOClient {
    8. public static void main(String[] args) throws IOException {
    9. SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 7001));
    10. FileChannel fileChannel = new FileInputStream("D:\\cmb\\studynote\\netty\\temp\\springboot.zip").getChannel();
    11. long startTime = System.currentTimeMillis();
    12. // 在 linux 下,一次调用 transferTo 方法就可以完成传输
    13. // 在 window下,一次调用 transferTo 只能发送 8M,如果大于8M,则需要分段传输文件
    14. // transferTo 底层就用到了 零拷贝
    15. long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
    16. long cost = System.currentTimeMillis() - startTime;
    17. System.out.println("客户端发送数据成功,耗时= " + cost + ", 传输的字节总数 = " + transferCount);
    18. }
    19. }

     3)效果:

    客户端发送数据成功,耗时= 10, 传输的字节总数 = 4491230

    4)补充: 关于 FileChannel.transferTo 方法 

    • 在 linux 下,一次调用 FileChannel.transferTo 方法就可以完成传输;
    • 在 window下,一次调用 transferTo 只能发送 8M,如果大于8M,则需要分段传输文件;
    • transferTo 底层就用到了 零拷贝;

    5)transferTo方法底层使用的是 sendfile 系统调用(零拷贝)

    • 该系统调用 实现了数据直接从内核的读缓冲区传输到套接字缓冲区,避免了用户态(User-space) 与内核态(Kernel-space) 之间的数据拷贝。

    【4】性能比较

    【表】传统IO与NIO零拷贝传输性能对比 (文件大小 4M

    Java IO类型

    传输耗时

    备注

    传统IO

    50ms

    NIO零拷贝

    10ms

    性能更优

  • 相关阅读:
    jar包导入到本地仓库无法引用
    LeetCode 每日一题 2022/11/28-2022/12/4
    springBoot--ssm整合
    国科大课程自动评价脚本JS
    【Java】数组的深浅拷贝问题(二维数组举例)(136)
    Partial differential equation
    7. 吴恩达机器学习-PCA
    易基因|深度综述:RNA m5C修饰的生物学及在肿瘤发生和免疫治疗中的作用
    FFmpeg源代码简单分析-其他-libavdevice的gdigrab
    【深度学习】实验2答案:构建自己的多层感知机
  • 原文地址:https://blog.csdn.net/PacosonSWJTU/article/details/126442150