• NIO 与 零拷贝


    零拷贝介绍

    1. 零拷贝是网络编程的关键, 很多性能优化都需要零拷贝。
    2. 在 Java程序中, 常用的零拷贝方式有m(memory)map[内存映射] 和 sendFile。它们在OS中又是怎样的设计?
    3. NIO中如何使用零拷贝?

    NIO 与 传统IO对比

    • 传统IO流程示意图

      1575855435947

      • user context: 用户态
      • kernel context: 内核态
      • User space: 用户空间
      • Kernel space: 内核空间
      • Syscall read: 系统调用读取
      • Syscall write: 系统调用写入
      • Hard drive: 硬件驱动
      • kernel buffer: 内核态缓冲区
      • user buffer: 用户态缓冲区
      • socket buffer: 套接字缓存
      • protocol engine: 协议引擎
      • DMA: Direct Memory Access: 直接内存拷贝(不使用CPU)
      • 总结: 4次拷贝, 3次状态切换, 效率不高
    • mmap优化流程示意图

      1575855870723

      • mmap 通过内存映射, 将文件映射到内核缓冲区, 同时, 用户空间可以共享内核空间的数据。
      • 这样, 在进行网络传输时, 就可以减少内核空间到用户空间的拷贝次数。
      • 总结: 3次拷贝, 3次状态切换, 不是真正意义上的零拷贝。
    • sendFile Linux2.1版本优化流程示意图

      1575856449573

      • 数据根本不经过用户态, 直接从内核缓冲区进入到Socket Buffer, 同时, 由于和用户台完全无关, 就减少了一次上下文切换。
      • 但是仍然有一次CPU拷贝, 不是真正的零拷贝(没有CPU拷贝)。
      • 总结: 3次拷贝, 2次切换
    • sendFile Linux

      1575856952257

      • 避免了从内核缓冲区拷贝到Socket buffer的操作, 直接拷贝到协议栈, 从而再一次减少了数据拷贝。
      • 其实是有一次cpu拷贝的, kernel buffer -> socket buffer, 但是拷贝的信息很少, length, offset, 消耗低, 基本可以忽略。
      • 总结: 2次拷贝(如果忽略消耗低的cpu拷贝的话), 2次切换, 基本可以认为是零拷贝了。

    零拷贝理解

    • 零拷贝是从操作系统的角度来看的。内核缓冲区之间, 没有数据是重复的(只有kernel buffer有一份数据)。
    • 零拷贝不仅仅带来更少的数据复制, 还能带来其他的性能优势: 如更少的上下文切换, 更少的 CPU 缓存伪共享以及无CPU校验和计算。

    mmap 与 sendFile 总结

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

    NIO实现零拷贝

    • 服务端

      package com.ronnie.nio.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(8096);
      
              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 = socketChannel.read(byteBuffer);
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                      // 倒带, position设为0, mark重置为-1
                      byteBuffer.rewind();
                  }
              }
          }
      }
      
      
    • 客户端

      package com.ronnie.nio.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("localhost", 8096));
      
              String filename = "flink-1.9.0-bin-scala_2.12.tgz";
      
              // 得到一个文件channel
              FileChannel fileChannel = new FileInputStream(filename).getChannel();
      
              // 准备发送
              long startTime = System.currentTimeMillis();
      
              // 在Linux下一次transferTo方法就可以完成传输
              // 在Windows下一次调用transferTo 只能发送 8M, 就需要分段传输文件, 而且主要传输时的位置需要记录
      
              long transferCount = 0L;
      
              if (fileChannel.size() <= 8){
                  // transferTo() 参数1: 从什么位置开始, 参数2: 截多少, 参数3: 可写的管道对象)
                  transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
              } else {
                  int times = (int) (fileChannel.size() / 8 + 1);
                  for (int i = 1; i < times; i++){
                      transferCount += fileChannel.transferTo(8 * i, 8 * i + 8, socketChannel);
                  }
              }
      
              System.out.println("Total byte count: " + transferCount + " time consumed: " + (System.currentTimeMillis() - startTime));
      
              // 关闭
              fileChannel.close();
          }
      }
      
      

    核心方法transferTo()

    • 代码(这是fileChannelImpl中的反编译代码)

          public long transferTo(long var1, long var3, WritableByteChannel var5) throws IOException {
              // 确认当前管道已经开启, 检查到未开启会抛出异常
              this.ensureOpen();
              // 如果传入的管道未开启, 抛出异常
              if (!var5.isOpen()) {
                  throw new ClosedChannelException();
                  // 如果当前管道不可读, 抛出异常
              } else if (!this.readable) {
                  throw new NonReadableChannelException();
                  // 如果传入的管道是实现类 且 该管道不可写, 抛出异常
              } else if (var5 instanceof FileChannelImpl && !((FileChannelImpl)var5).writable) {
                  throw new NonWritableChannelException();
                  // 如果 position >= 0 且 count >= 0
              } else if (var1 >= 0L && var3 >= 0L) {
                  // 获取当前管道的长度
                  long var6 = this.size();
                  // 如果position已经超过当前管道末尾, 就返回0
                  if (var1 > var6) {
                      return 0L;
                  } else {
                      // 将count数与2147483647L比较并获取其中最小值, 再转换成int, 传给var8, 其实这里就是做了一个防止count越界的处理
                      int var8 = (int)Math.min(var3, 2147483647L);
                      // 如果管道末尾到position之间的长度小于var8
                      if (var6 - var1 < (long)var8) {
                          // 就把该值赋给var8
                          var8 = (int)(var6 - var1);
                      }
      
                      long var9;
                      // transferToDirectly 直接传输
                      if ((var9 = this.transferToDirectly(var1, var8, var5)) >= 0L) {
                          return var9;
                      } else {
                          // transferToTrustedChannel 传输到可靠的管道
                          // transferToArbitraryChannel 传输到任意的管道
                          // 其实就是先尝试传输到可靠的管道, 如果传输失败, 再用任意管道继续传输
                          return (var9 = this.transferToTrustedChannel(var1, (long)var8, var5)) >= 0L ? var9 : this.transferToArbitraryChannel(var1, var8, var5);
                      }
                  }
              } else {
                  throw new IllegalArgumentException();
              }
          }
      
  • 相关阅读:
    注意力机制的命名实体识别
    tfidf_CountVectorizer 与 TfidfTransformer 保存和测试
    转载:UML学习(三)-----序列图(silent)
    转载:UML学习(二)-----类图(silent)
    转载:UML学习(一)-----用例图 (silent)
    转载:Practical UML™: A Hands-On Introduction for Developers
    转载:gc的概念,如果A和B对象循环引用,是否可以被GC?
    转载:Java高并发,如何解决,什么方式解决
    转载:消息队列应用场景
    转载:详解Java 自动装箱与拆箱的实现原理
  • 原文地址:https://www.cnblogs.com/ronnieyuan/p/12009692.html
Copyright © 2020-2023  润新知