前言:
在Java程序中,零拷贝技术分为两种:mmap(内存映射)和sendFile,首先要了解零拷贝的概念:所谓的零拷贝不是不拷贝,而是不经过CPU拷贝,它还是需要拷贝的(比如将数据从硬盘拷贝到内核态),这个零拷贝是从操作系统(CPU)的角度看的
传统的IO拷贝
首先将硬盘上的数据拷贝到内核,然后在经过CPU拷贝将数据从内核拷贝到应用程序内存(用户态),在应用程序内存,用户可以对数据进行操作修改等,然后在经过CPU拷贝将数据从用户缓冲区拷贝到socket缓冲区,然后在经过DMA拷贝,将数据从socket缓冲区拷贝到网卡。 那么经过以上的步骤,完成了将数据从本地硬盘传输到网络上的过程, 那这个过程数据的拷贝经过了: 硬盘—>内核—>应用程序内存(可以理解为用户态,相当于jvm中)—>socket缓冲区—>网卡,这4次拷贝的过程。 上下文的切换经过了:用户态–>内核态–>用户态 这三次上下文切换
mmap:
首先将硬盘上面的数据数据拷贝到内核,(因为mmap技术,做到了内核态数据和用户态数据共享,此时不需要将数据从内核拷贝到用户态,用户态也可以对数据进行修改),再将内核态的数据经过CPU拷贝,拷贝到socket缓冲区,在经过DMA拷贝,将socket缓冲区的数据拷贝到协议栈; 那经过了mmap的优化后,数据的读写少了一次拷贝的过程,但是mmap还不是真正意思上的零拷贝,因为它还是进行了拷贝 那这个过程数据的拷贝经过了: 硬盘—>内核—>socket缓冲区—>网卡,这3次拷贝的过程。 上下文的切换经过了:用户态–>内核态–>用户态 这三次上下文切换
sendFile
sendFile是Linux2.1版本提供的函数,基本的原理就是数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换; 过程: 首先将硬盘上面的数据经过DMA拷贝(直接内存拷贝),将数据拷贝到内核,再将内核态的数据经过CPU拷贝,拷贝到socket缓冲区,再由DMA拷贝将数据从socket缓冲区拷贝到协议栈。 目前来看sendFile和mmap拷贝是差不多的,但是sendFile比mmap拷贝少了一次上下文的切换, 但是就目前看来,这两种拷贝都没有达到真正的零拷贝。
sendFile(增强)
在Linux2.4的版本中,对sendFile做了一些修改,真正实现了零拷贝; 过程: 首先将硬盘上面的数据拷贝到内核,此时在经过CPU拷贝将数据的描述信息拷贝到socket缓冲区(注意:此时拷贝的是数据的描述信息,数据量很小,所以此次拷贝可以忽略不记),然后此时数据其实还是在内核的(因为刚刚的CPU拷贝只是拷贝了一些数据描述),再将数据从内核直接拷贝到网卡。 在这个版本中,sendFile就实现了真正的零拷贝,虽然还是经过了一次CPU的拷贝,但是数据量很小,所以可以忽略不记。
Java+NIO实现零拷贝
在java中可以使用: fileChannel.transferTo() 和 fileChannel.map() 等函数来实现零拷贝
其中: mmap适合小数据量的传输, RocketMQ使用的就是mmap。 sendFile 适合大数据量的传输,Kafka使用的就是sendFile。
Java 实现 mmap示例
package org.xhs.json;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class MmpTest {
public static void main(String[] args) {
try {
FileChannel readChannel = FileChannel.open(Paths.get("C://test/jay.txt"), StandardOpenOption.READ);
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
FileChannel writeChannel = FileChannel.open(Paths.get("E://test1/siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
writeChannel.write(data);
readChannel.close();
writeChannel.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}
Java实现sendFile示例:
示例1:
package org.xhs.json;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class SendfileTest {
public static void main(String[] args) {
try {
FileChannel readChannel = FileChannel.open(Paths.get("C://test/jay.txt"), StandardOpenOption.READ);
long len = readChannel.size();
long position = readChannel.position();
FileChannel writeChannel = FileChannel.open(Paths.get("E://test1/siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
readChannel.transferTo(position, len, writeChannel);
readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
示例2:
public void writeHtml(File file) {
if (file.exists() && file.isFile()) {
try {
System.err.println("文件存在");
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
FileChannel fileChannel = new FileInputStream(file).getChannel();
writeBuffer.put(HttpProtocolUtil.sendHead(fileChannel.size(),"200"));
writeBuffer.flip();
channel.write(writeBuffer);
long count = fileChannel.transferTo(0, fileChannel.size(), channel);
System.err.println("传输的总的字节大小:"+count);
close();
} catch (Exception e) {
e.printStackTrace();
System.err.println("写出静态资源失败");
}
} else {
doWrite(HttpProtocolUtil.send404("404 资源未找到"));
close();
}
}
|