零拷贝
零拷贝主要是优化内核缓冲区和用户缓存区的之间拷贝次数
怎么出现一步一步出现零拷贝的呢,下面跟大家讲一下。
下图是当用户发出读写请求到操作系统进行交互的简单流程图
传统模式
从上图描述,把数据从内核缓冲区拷贝到用户缓冲区只是一次读操作,但是在网络编程中,该操作需要4次拷贝,4次上下文切换,因此性能低。
零拷贝模式(主要有mmap和sendFlie)
先了解一个概念:
DMA拷贝(direct memory access):直接内存拷贝,不使用CPU
mmap优化
mmap通过内存映射,将内存缓冲区中的数据映射到用户缓冲区中,这样用户空间可以共享内核空间的数据。在进行网络传输时,就可以减少内核空间到用户空间的CPU拷贝次数。但是还需要将内核缓冲区中的数据通过CPU拷贝到socket缓冲区中。该操作有3次拷贝,4次上下文切换。
sendFile优化(有两个版本)
1、在Linux2.1版本中使用sendFile系统调用函数时,把用户缓冲区去掉了,直接将内核缓冲区通过CPU拷贝到socket缓冲区中(由于把用户缓冲区去掉了,因此就减少了两次上下文切换)。该操作3次拷贝,2次上下文切换。
2、在Linux在2.4版本中真正实现了零拷贝,对2.1版本进行了修改,避免了将内核缓冲区中的数据通过CPU拷贝到socket缓冲区的操作,而是直接将内核缓冲区中的数据通过DMA拷贝到网卡中。该操作有2次拷贝,2次上下文切换。
1、与传统模式相比,零拷贝实现了更少的数据复制、更少的上下文切换、更少的CPU缓存伪共享以及CPU校验和计算。
2、sendFile可以利用DMA方式来减少CPU拷贝;mmap不可以利用DMA的方式来减少CPU拷贝,而是必须从内核缓冲区将数据通过CPU拷贝到socket缓冲区。
在NIO中,使用fileChannel.transferTo就可以实现零拷贝
- 在linux一次调用transferTo方法就可以完成传输
- 在windows每次调用transferTo只能发送8MB,就需要分段传输文件
案例如下
NewIOServer.java服务端
服务端代码
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 Exception {
InetSocketAddress address = new InetSocketAddress(7001);
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 (Exception ex) {
// ex.printStackTrace();
break;
}
//
byteBuffer.rewind(); //倒带 position = 0 mark 作废
}
}
}
}
NewIOClient.java客户端
客户端代码
import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class NewIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 7001));
String filename = "protoc-3.6.1-win32.zip";
//得到一个文件channel
FileChannel fileChannel = new FileInputStream(filename).getChannel();
//准备发送
long startTime = System.currentTimeMillis();
//在 linux 下一个 transferTo 方法就可以完成传输
//在 windows 下一次调用 transferTo 只能发送 8m, 就需要分段传输文件
//transferTo 底层使用到零拷贝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送的总的字节数 = " + transferCount + " 耗时: " + (System.currentTimeMillis() - startTime));
//关闭
fileChannel.close();
}
}