在上一篇推文中讲解了零拷贝思想在Linux系统中的实现,主要有mmap、sendfile、splice、tee等,但在Java中目前主要实现了mmap和sendfile。
Java I/O的发展史
在第一篇的零拷贝的概述中,我们了解到为了降低内核接口调用的复杂度和提高编码效率,高级语言一般都为程序开发者提供了封装的类库,如C语言的标准库、Java的JDK等。
在JDK1.3之前Java的I/O一直比较传统,是采用Stream阻塞模式。在JDK1.4 的发布版中正式引入NIO,加入了缓冲区Buffer和通道Channel的概念,提供了非阻塞的方式。然而JDK1.4主要是为Socket通讯进行的优化,随后在JDK1.7版本中的NIO2不仅增强了文件系统的处理能力,还做到了真正的异步I/O—AIO。
mmap的实现 - MappedByteBuffer
JDK NIO提供的MappedByteBuffer底层就是调用mmap来实现的,FileChannel.map用来建立内存映射关系:把用户空间和内存空间的虚拟内存地址映射到同一块物理内存。mmap对大文件比较合适,对小文件则容易造成内存碎片,反而不是最佳使用场景。
编码示例如下
public void mmap4zeroCopy(String from, String to) throws IOException {
FileChannel source = null;
FileChannel destination = null;
try {
source = new RandomAccessFile(from, "r").getChannel();
destination = new RandomAccessFile(to, "rw").getChannel();
MappedByteBuffer inMappedBuf =
source.map(FileChannel.MapMode.READ_ONLY, 0, source.size());
destination.write(inMappedBuf);
} finally {
if (source != null) {
source.close();
}
if (destination != null) {
destination.close();
}
}
}
sendfile的实现 - transferTo
NIO提供的FileChannel.transferTo方法可以直接将一个channel传递给另一个channel,结合上一篇推文看,channel像极了内核缓冲区。
编码示例如下
public void sendfile4zeroCopy(String from, String to) throws IOException{
FileChannel source = null;
FileChannel destination = null;
try {
source = new FileInputStream(from).getChannel();
destination = new FileOutputStream(to).getChannel();
source.transferTo(0, source.size(), destination);
} finally {
if (source != null) {
source.close();
}
if (destination != null) {
destination.close();
}
}
}
传统I/O vs mmap vs sendfile
通过实战来对比下传统I/O、mmap、sendfile的性能及在用户空间和内核空间中消耗的CPU时间,代码如下
import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* 公众号:码农神说 示例代码
*/
public class JioChannel {
public static void main(String[] args) {
JioChannel channel = new JioChannel();
try {
if (args.length < 3) {
System.out.println("usage: JioChannel <source> "+
"<destination> <mode>\n");
return;
}
if ("1".equals(args[2])) { //传统方式的复制
channel.copy(args[0], args[1]);
} else if ("2".equals(args[2])) { //mmap的方式
channel.mmap4zeroCopy(args[0], args[1]);
} else if ("3".equals(args[2])) { //sendfile的方式
channel.sendfile4zeroCopy(args[0], args[1]);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 传统方式的复制
*
* @param from
* @param to
* @throws IOException
*/
public void copy(String from, String to) throws IOException {
byte[] data = new byte[8 * 1024];
FileInputStream fis = null;
FileOutputStream fos = null;
long bytesToCopy = new File(from).length();
long bytesCopied = 0;
try {
fis = new FileInputStream(from);
fos = new FileOutputStream(to);
while (bytesCopied < bytesToCopy) {
fis.read(data);
fos.write(data);
bytesCopied += data.length;
}
fos.flush();
} finally {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
}
}
/**
* mmap的方式复制
*
* @param from
* @param to
* @throws IOException
*/
public void mmap4zeroCopy(String from, String to) throws IOException {
FileChannel source = null;
FileChannel destination = null;
try {
source = new RandomAccessFile(from, "r").getChannel();
destination = new RandomAccessFile(to, "rw").getChannel();
MappedByteBuffer inMappedBuf =
source.map(FileChannel.MapMode.READ_ONLY, 0, source.size());
destination.write(inMappedBuf);
} finally {
if (source != null) {
source.close();
}
if (destination != null) {
destination.close();
}
}
}
/**
* sendfile的方式复制文件
*
* @param from
* @param to
* @throws IOException
*/
public void sendfile4zeroCopy(String from, String to) throws IOException {
FileChannel source = null;
FileChannel destination = null;
try {
source = new FileInputStream(from).getChannel();
destination = new FileOutputStream(to).getChannel();
source.transferTo(0, source.size(), destination);
} finally {
if (source != null) {
source.close();
}
if (destination != null) {
destination.close();
}
}
}
}
首先进行代码编译java javac JioChannel.java,它的执行方法是JioChannel <source> <destination> <mode>,其中mode值1为传统方式I/O,2为mmap方式I/O,3为sendfile方式I/O。
执行和输出如下(a.zip为130M的压缩文件)
$ time java JioChannel a.zip b.zip 1
real 0m0.199s
user 0m0.090s
sys 0m0.117s
$ time java JioChannel a.zip b.zip 2
real 0m0.172s
user 0m0.074s
sys 0m0.102s
$ time java JioChannel a.zip b.zip 3
real 0m0.162s
user 0m0.057s
sys 0m0.108s
user+sys之和是该执行进程的耗费CPU的总时间,可见mmap和sendfile方式效率高于传统方式,而且用户空间user耗费CPU的时间占比总耗费时间也有所降低。
Linux的time命令
time是linux shell内置的命令,它用于统计/测量系统的资源使用情况,如CPU、内存、I/O等,用法如下
time [ -apqvV ] [ -f FORMAT ] [ -o FILE ]
[ --append ] [ --verbose ] [ --quiet ] [ --portability ]
[ --format=FORMAT ] [ --output=FILE ] [ --version ]
[ --help ] COMMAND [ ARGS ]
内存、I/O等资源可参看time手册,不展开叙述。测量CPU的主要角度是其耗费的时间:实际总耗费时间、用户空间和内核空间各自耗费的时间。
写在最后
虽然JDK没有实现所有的Linux零拷贝模式,但如果能把mmap和sendfile发挥到极致在性能上也能具有非常可观的提升,比如kafka、netty都是以零拷贝而业界瞩目。下一篇推文将简单介绍下kafka、netty的零拷贝思想及实现,这也是面试经常遇到的问题,敬请关注。
End
版权归@码农神说所有,转载须经授权,翻版必究