一、典型IO调用的问题
一个典型的web服务器传送静态文件(如CSS,JS,图片等)的过程如下:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
首先调用read将文件从磁盘读取到tmp_buf,然后调用write将tmp_buf写入到socket,在这过程中会出现四次数据 copy,过程如图1所示
图1
1。当调用read系统调用时,通过DMA(Direct Memory Access)将数据copy到内核模式
2。然后由CPU控制将内核模式数据copy到用户模式下的 buffer中
3。read调用完成后,write调用首先将用户模式下 buffer中的数据copy到内核模式下的socket buffer中
4。最后通过DMA copy将内核模式下的socket buffer中的数据copy到网卡设备中传送。
从上面的过程可以看出,数据白白从内核模式到用户模式走了一 圈,浪费了两次copy,而这两次copy都是CPU copy,即占用CPU资源。
二、Zero-Copy&Sendfile()
Linux 2.1版本内核引入了sendfile函数,用于将文件通过socket传送。
sendfile(socket, file, len);
该函数通过一次系统调用完成了文件的传送,减少了原来 read/write方式的模式切换。此外更是减少了数据的copy,sendfile的详细过程图2所示:
图2
通过sendfile传送文件只需要一次系统调用,当调用 sendfile时:
1。首先通过DMA copy将数据从磁盘读取到kernel buffer中
2。然后通过CPU copy将数据从kernel buffer copy到sokcet buffer中
3。最终通过DMA copy将socket buffer中数据copy到网卡buffer中发送
sendfile与read/write方式相比,少了一次模式切换一次CPU copy。但是从上述过程中也可以发现从kernel buffer中将数据copy到socket buffer是没必要的。
为此,Linux2.4内核对sendfile做了改进,如图3所示
图3
改进后的处理过程如下:
1。DMA copy将磁盘数据copy到kernel buffer中
2。向socket buffer中追加当前要发送的数据在kernel buffer中的位置和偏移量
3。DMA gather copy根据socket buffer中的位置和偏移量直接将kernel buffer中的数据copy到网卡上。
经过上述过程,数据只经过了2次copy就从磁盘传送出去了。
(可能有人要纠结“不是说Zero-Copy么?怎么还有两次copy啊”,事实上这个Zero copy是针对内核来讲的,数据在内核模式下是Zero-copy的。话说回来,文件本身在瓷盘上要真是完全Zero-copy就能传送,那才见鬼了 呢)。
当前许多高性能http server都引入了sendfile机制,如nginx,lighttpd等。
三、Java NIO中的transferTo()
Java NIO中
FileChannel.transferTo(long position, long count, WriteableByteChannel target)
方法将当前通道中的数据传送到目标通道target中,在支持Zero-Copy的linux系统中,transferTo()的实现依赖于 sendfile()调用。
四、参考文档
《Zero Copy I: User-Mode Perspective》http://www.linuxjournal.com/article/6345?page=0,0
《Efficient data transfer through zero copy》http://www.ibm.com/developerworks/linux/library/j-zerocopy
《The C10K problem》http://www.kegel.com/c10k.html
目前为止,我们已经学习了很多 Java 拷贝文件的方式,除了 FileChannel 提供的方法外,还包括使用 Files.copy() 或使用字节数组的缓冲/非缓冲流。那个才是最好的选择呢?这个问题很难回答,因为答案基于很多因素。本文将目光集中到一个因素,那就是速度,因为拷贝任务越快将会提高效率,在有些情况下,这是成功的关键。因此,本文将使用一个应用程序来比较下面这些拷贝方式的具体时间:
应用程序基于下面的条件:
下面将列出完整的应用程序:
import java.nio.MappedByteBuffer; import java.io.OutputStream; import java.io.InputStream; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.EnumSet; import static java.nio.file.LinkOption.NOFOLLOW_LINKS; public class Main { public static void deleteCopied(Path path){ try { Files.deleteIfExists(path); } catch (IOException ex) { System.err.println(ex); } } public static void main(String[] args) { final Path copy_from = Paths.get("C:/rafaelnadal/tournaments/2009/videos/ Rafa Best Shots.mp4"); final Path copy_to = Paths.get("C:/Rafa Best Shots.mp4"); long startTime, elapsedTime; int bufferSizeKB = 4; //also tested for 16, 32, 64, 128, 256 and 1024 int bufferSize = bufferSizeKB * 1024; deleteCopied(copy_to); //FileChannel and non-direct buffer System.out.println("Using FileChannel and non-direct buffer ..."); try (FileChannel fileChannel_from = (FileChannel.open(copy_from, EnumSet.of(StandardOpenOption.READ))); FileChannel fileChannel_to = (FileChannel.open(copy_to, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) { startTime = System.nanoTime(); // Allocate a non-direct ByteBuffer ByteBuffer bytebuffer = ByteBuffer.allocate(bufferSize); // Read data from file into ByteBuffer int bytesCount; while ((bytesCount = fileChannel_from.read(bytebuffer)) > 0) { //flip the buffer which set the limit to current position, and position to 0 bytebuffer.flip(); //write data from ByteBuffer to file fileChannel_to.write(bytebuffer); //for the next read bytebuffer.clear(); } elapsedTime = System.nanoTime() - startTime; System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) { System.err.println(ex); } deleteCopied(copy_to); //FileChannel and direct buffer System.out.println("Using FileChannel and direct buffer ..."); try (FileChannel fileChannel_from = (FileChannel.open(copy_from, EnumSet.of(StandardOpenOption.READ))); FileChannel fileChannel_to = (FileChannel.open(copy_to, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) { startTime = System.nanoTime(); // Allocate a direct ByteBuffer ByteBuffer bytebuffer = ByteBuffer.allocateDirect(bufferSize); // Read data from file into ByteBuffer int bytesCount; while ((bytesCount = fileChannel_from.read(bytebuffer)) > 0) { //flip the buffer which set the limit to current position, and position to 0 bytebuffer.flip(); //write data from ByteBuffer to file fileChannel_to.write(bytebuffer); //for the next read bytebuffer.clear(); } elapsedTime = System.nanoTime() - startTime; System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) { System.err.println(ex); } deleteCopied(copy_to); //FileChannel.transferTo() System.out.println("Using FileChannel.transferTo method ..."); try (FileChannel fileChannel_from = (FileChannel.open(copy_from, EnumSet.of(StandardOpenOption.READ))); FileChannel fileChannel_to = (FileChannel.open(copy_to, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) { startTime = System.nanoTime(); fileChannel_from.transferTo(0L, fileChannel_from.size(), fileChannel_to); elapsedTime = System.nanoTime() - startTime; System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) { System.err.println(ex); } deleteCopied(copy_to); //FileChannel.transferFrom() System.out.println("Using FileChannel.transferFrom method ..."); try (FileChannel fileChannel_from = (FileChannel.open(copy_from, EnumSet.of(StandardOpenOption.READ))); FileChannel fileChannel_to = (FileChannel.open(copy_to, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) { startTime = System.nanoTime(); fileChannel_to.transferFrom(fileChannel_from, 0L, (int) fileChannel_from.size()); elapsedTime = System.nanoTime() - startTime; System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) { System.err.println(ex); } deleteCopied(copy_to); //FileChannel.map System.out.println("Using FileChannel.map method ..."); try (FileChannel fileChannel_from = (FileChannel.open(copy_from, EnumSet.of(StandardOpenOption.READ))); FileChannel fileChannel_to = (FileChannel.open(copy_to, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) { startTime = System.nanoTime(); MappedByteBuffer buffer = fileChannel_from.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel_from.size()); fileChannel_to.write(buffer); buffer.clear(); elapsedTime = System.nanoTime() - startTime; System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) { System.err.println(ex); } deleteCopied(copy_to); //Buffered Stream I/O System.out.println("Using buffered streams and byte array ..."); File inFileStr = copy_from.toFile(); File outFileStr = copy_to.toFile(); try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(inFileStr)); BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outFileStr))) { startTime = System.nanoTime(); byte[] byteArray = new byte[bufferSize]; int bytesCount; while ((bytesCount = in.read(byteArray)) != -1) { out.write(byteArray, 0, bytesCount); } elapsedTime = System.nanoTime() - startTime; System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) { System.err.println(ex); } deleteCopied(copy_to); System.out.println("Using un-buffered streams and byte array ..."); try (FileInputStream in = new FileInputStream(inFileStr); FileOutputStream out = new FileOutputStream(outFileStr)) { startTime = System.nanoTime(); byte[] byteArray = new byte[bufferSize]; int bytesCount; while ((bytesCount = in.read(byteArray)) != -1) { out.write(byteArray, 0, bytesCount); } elapsedTime = System.nanoTime() - startTime; System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) { System.err.println(ex); } deleteCopied(copy_to); System.out.println("Using Files.copy (Path to Path) method ..."); try { startTime = System.nanoTime(); Files.copy(copy_from, copy_to, NOFOLLOW_LINKS); elapsedTime = System.nanoTime() - startTime; System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException e) { System.err.println(e); } deleteCopied(copy_to); System.out.println("Using Files.copy (InputStream to Path) ..."); try (InputStream is = new FileInputStream(copy_from.toFile())) { startTime = System.nanoTime(); Files.copy(is, copy_to); elapsedTime = System.nanoTime() - startTime; System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException e) { System.err.println(e); } deleteCopied(copy_to); System.out.println("Using Files.copy (Path to OutputStream) ..."); try (OutputStream os = new FileOutputStream(copy_to.toFile())) { startTime = System.nanoTime(); Files.copy(copy_from, os); elapsedTime = System.nanoTime() - startTime; System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException e) { System.err.println(e); } } }
输出结果排序比较复杂,其中包含了很多数据。下面我将主要的对比用图形的方式展示出来。图形中 Y 坐标表示消耗的时间(单位:秒),X 坐标表示缓冲的大小(或运行次数,跳过了前三次运行)。
从下图看来,如果缓存小于 256KB,那么非直接模式的 Buffer 快一点,而缓存大于 256KB 后,直接模式的 Buffer 快一点:
从下图看来,FileChannel.transferTo() 和 FileChannel.transferFrom 运行七次的速度都差不多,而 FileChannel.map 的速度就要差很多:
从下图看来,最快的是 Path 到 Path,其次是 Path 到 OutputStream,最慢的是 InputStream 到 Path:
最后,我们将前面最快的三种方式综合起来比较。从比较的结果来看,似乎 Path 到 Path 是最快的解决方案: