Zero-Copy&sendfile浅析

一、典型IO调用的问题
一个典型的web服务器传送静态文件(如CSSJS,图片等)的过程如下:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);


首先调用read将文件从磁盘读取到tmp_buf,然后调用writetmp_buf写入到socket,在这过程中会出现四次数据 copy,过程如图1所示

图1

 

1。当调用read系统调用时,通过DMADirect Memory Access)将数据copy到内核模式
2。然后由CPU控制将内核模式数据copy到用户模式下的 buffer
3read调用完成后,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方式的模式切换。此外更是减少了数据的copysendfile的详细过程图2所示:

图2

通过sendfile传送文件只需要一次系统调用,当调用 sendfile时:
1。首先通过DMA copy将数据从磁盘读取到kernel buffer
2。然后通过CPU copy将数据从kernel buffer copysokcet buffer
3。最终通过DMA copysocket buffer中数据copy到网卡buffer中发送
sendfileread/write方式相比,少了一次模式切换一次CPU copy。但是从上述过程中也可以发现从kernel buffer中将数据copysocket buffer是没必要的。

为此,Linux2.4内核对sendfile做了改进,如图3所示

图3

改进后的处理过程如下:
1
DMA copy将磁盘数据copykernel buffer
2
。向socket buffer中追加当前要发送的数据在kernel buffer中的位置和偏移量
3
DMA gather copy根据socket buffer中的位置和偏移量直接将kernel buffer中的数据copy到网卡上。
经过上述过程,数据只经过了2copy就从磁盘传送出去了。
(可能有人要纠结不是说ZeroCopy么?怎么还有两次copy,事实上这个Zero copy是针对内核来讲的,数据在内核模式下是Zerocopy的。话说回来,文件本身在瓷盘上要真是完全Zerocopy就能传送,那才见鬼了 呢)。
当前许多高性能http server都引入了sendfile机制,如nginxlighttpd等。

三、Java NIO
中的transferTo()
Java NIO

FileChannel.transferTo(long position, long count, WriteableByteChannel target)
方法将当前通道中的数据传送到目标通道target中,在支持Zero-Copylinux系统中,transferTo()的实现依赖于 sendfile()调用。

 

四、参考文档
Zero Copy I: User-Mode Perspectivehttp://www.linuxjournal.com/article/6345?page=0,0
Efficient data transfer through zero copyhttp://www.ibm.com/developerworks/linux/library/j-zerocopy
The C10K problemhttp://www.kegel.com/c10k.html


FileChannel 拷贝文件能力测试

目前为止,我们已经学习了很多 Java 拷贝文件的方式,除了 FileChannel 提供的方法外,还包括使用 Files.copy() 或使用字节数组的缓冲/非缓冲流。那个才是最好的选择呢?这个问题很难回答,因为答案基于很多因素。本文将目光集中到一个因素,那就是速度,因为拷贝任务越快将会提高效率,在有些情况下,这是成功的关键。因此,本文将使用一个应用程序来比较下面这些拷贝方式的具体时间:

  • FileChannel 和非直接模式的 ByteBuffer
  • FileChannel 和直接模式的 ByteBuffer
  • FileChannel.transferTo()
  • FileChannel.transferFrom()
  • FileChannel.map()
  • 使用字节数组和缓冲流
  • 使用字节数组和非缓冲流
  • File.copy()(Path 到 Path,InputStream 到 Path 和 Path 到 OutputStream)

应用程序基于下面的条件:

  • 拷贝文件类型 MP4 视频(文件名为 Rafa Best Shots.mp4,所在目录为 C:\rafaelnadal\tournaments\2009\videos)
  • 文件大小:58.3MB
  • 测试的缓冲区大小:4KB, 16KB, 32KB, 64KB, 128KB, 256KB, and 1024KB
  • 机器配置:Mobile AMD Sempron Processor 3400 + 1.80 GHz, 1.00GB RAM, 32-bit
    OS, Windows 7 Ultimate
  • 测量类型:使用 System.nanoTime() 方法
  • 连续运行三次后再获取时间;前三次运行将会被忽略。开始运行的时间总会比后面运行的时间要长一些。

下面将列出完整的应用程序:

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 坐标表示缓冲的大小(或运行次数,跳过了前三次运行)。

FileChannel 和非直接模式 Buffer vs. FileChannel 和直接模式 Buffer

从下图看来,如果缓存小于 256KB,那么非直接模式的 Buffer 快一点,而缓存大于 256KB 后,直接模式的 Buffer 快一点:

FileChannel.transferTo() vs. FileChannel.transferFrom() vs. FileChannel.map()

从下图看来,FileChannel.transferTo() 和 FileChannel.transferFrom 运行七次的速度都差不多,而 FileChannel.map 的速度就要差很多:

三种 Files.copy() 方法

从下图看来,最快的是 Path 到 Path,其次是 Path 到 OutputStream,最慢的是 InputStream 到 Path:

FileChannel 和非直接模式 Buffer vs. FileChannel.transferTo() vs. Path 到 Path

最后,我们将前面最快的三种方式综合起来比较。从比较的结果来看,似乎 Path 到 Path 是最快的解决方案:



你可能感兴趣的:(高性能)