基本介绍:

1.零拷贝是网络编程的关键,很多性能优化都离不开
2.零拷贝是哦那个操作系统角度来看的,是没有CPU拷贝
3.在java程序中,常用的零拷贝有 mmap (内存映射) 和 sendFile
DMA:直接内存拷贝(不使用CPU)

场景:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。

传统文件IO

image.png
1.很明显发生了4次拷贝
第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝是通过 DMA 的。
第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是应用程序就可以使用这部分数据了,这个拷贝是由 CPU 完成的。
第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然由 CPU 完成的。
第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到协议栈里,这个过程又是由 DMA 完成的。
2.发生了4次用户上下文切换,因为发生了两个系统调用read和write。一个系统调用对应两次上下文切换,所以上下文切换次数在一般情况下只可能是偶数。
想要优化文件传输的性能只有两个方向 1.减少上下文切换次数 2.减少数据拷贝次数 因为这两个操作是最耗时的

mmap

read()系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,为了减少这一步开销,我们可以用mmap()替换read()系统调用函数。mmap()系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间共享缓冲区,就不需要再进行任何的数据拷贝操作。
image.png
总的来说mmap减少了一次数据拷贝,总共4次上下文切换,3次数据拷贝

sendFile

Linux2.1版本提供了sendFile()函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 SocketBuffer
image.png
总的来说有2次上下文切换,3次数据拷贝。

sendFile再优化

Linux2.4版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socketbuffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝
image.png

mmap 和 sendFile的区别

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

零拷贝的再次理解

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

零拷贝案例 — transferTo
  1. //服务端
  2. public class NIOServer {
  3. public static void main(String[] args) throws Exception {
  4. InetSocketAddress address = new InetSocketAddress(7001);
  5. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
  6. serverSocketChannel.socket().bind(address);
  7. //创建buffer
  8. ByteBuffer buffer = ByteBuffer.allocate(4096);
  9. while (true) {
  10. SocketChannel socketChannel = serverSocketChannel.accept();
  11. int readCount = 0;
  12. while (readCount != -1) {
  13. try {
  14. readCount = socketChannel.read(buffer);
  15. } catch (Exception e) {
  16. break;
  17. }
  18. buffer.rewind(); //倒带
  19. //buffer.clear();
  20. }
  21. }
  22. }
  23. }
  1. //客户端
  2. public class NIOClient {
  3. public static void main(String[] args) throws Exception {
  4. SocketChannel socketChannel = SocketChannel.open();
  5. socketChannel.connect(new InetSocketAddress("127.0.0.1", 7001));
  6. String fileName = "f:\\1.txt";
  7. //得到文件channel
  8. FileInputStream fis = new FileInputStream(fileName);
  9. FileChannel fileChannel = fis.getChannel();
  10. long start = System.currentTimeMillis();
  11. //零拷贝 windows系统一次最多8MB
  12. //transferTo底层使用零拷贝
  13. long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
  14. System.out.println("总字节数 = " + transferCount);
  15. System.out.println("time:" + (System.currentTimeMillis() - start));
  16. fileChannel.close();
  17. }
  18. }