简介
零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。针对操作系统中的设备驱动程序、文件系统以及网络协议堆栈而出现的各种零拷贝技术极大地提升了特定应用程序的性能,并且使得这些应用程序可以更加有效地利用系统资源。这种性能的提升就是通过在数据拷贝进行的同时,允许 CPU 执行其他的任务来实现的。
1、传统文件发送
while((n = read(diskfd, buf, BUF_SIZE)) > 0)
socket.getOutputStream().write(sockfd, buf , n);
应用程序和磁盘之间想要传输数据,是没有办法直接进行传输的。
整个过程为:应用程序向操作系统发起一个读请求,那么首先磁盘中的数据会被读取到内核地址空间中,然后会把内核地址空间中的数据拷贝到用户地址空间中(其实就是 JVM 内存中),最后再把这个数据读取到应用程序中来。
上图共产生了四次数据拷贝,在用户态与内核态多次切换,是造成效率低下的主要原因。
java代码
public static void test1() throws Exception {
// 利用通道完成文件的复制(非直接缓冲区)
FileInputStream fis = new FileInputStream("a.txt");
FileOutputStream fos = new FileOutputStream("b.txt");
// 获取通道
FileChannel fisChannel = fis.getChannel();
FileChannel foschannel = fos.getChannel();
// 通道没有办法传输数据,必须依赖缓冲区
// 分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将通道中的数据存入缓冲区中
while (fisChannel.read(byteBuffer) != -1) { // fisChannel 中的数据读到 byteBuffer 缓冲区中
byteBuffer.flip(); // 切换成读数据模式
// 将缓冲区中的数据写入通道
foschannel.write(byteBuffer);
byteBuffer.clear(); // 清空缓冲区
}
foschannel.close();
fisChannel.close();
fos.close();
fis.close();
}
2、使用mmap+write发送
buf = mmap(file, len);
write(sockfd, buf, len);
mmap()
系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
使用mmap替代read很明显可以减少一次拷贝
java代码
public static void test2() throws Exception {
// 使用直接缓冲区完成文件的复制(内存映射文件)
/**
* 使用 open 方法来获取通道
* 需要两个参数
* 参数1:Path 是 JDK1.7 以后给我们提供的一个类,代表文件路径
* 参数2:Option 就是针对这个文件想要做什么样的操作
* --StandardOpenOption.READ :读模式
* --StandardOpenOption.WRITE :写模式
* --StandardOpenOption.CREATE :如果文件不存在就创建,存在就覆盖
*/
FileChannel inChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("c.txt"), StandardOpenOption.WRITE,
StandardOpenOption.READ, StandardOpenOption.CREATE);
/**
* 内存映射文件
* 这种方式缓冲区是直接建立在物理内存之上的
* 所以我们就不需要通道了
*/
MappedByteBuffer inMapped = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapped = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
// 直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inMapped.limit()];
inMapped.get(dst); // 把数据读取到 dst 这个字节数组中去
outMapped.put(dst); // 把字节数组中的数据写出去
inChannel.close();
outChannel.close();
}
3、使用sendfile
在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile()
,函数形式如下:
替代了前面的 read()
和 write()
这两个系统调用,减少一次系统调用,减少 2 次上下文切换的开销
java代码
public static void test3() throws Exception {
/**
* 通道之间的数据传输(直接缓冲区的方式)
* transferFrom
* transferTo
*/
FileChannel inChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
inChannel.transferTo(0, inChannel.size(), outChannel);
// 或者可以使用下面这种方式
//outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
}