Java零拷貝


1、摘要

零拷貝的“零”是指用戶態和內核態間copy數據的次數為零。

傳統的數據copy(文件到文件、client到server等)涉及到四次用戶態內核態切換、四次copy。四次copy中,兩次在用戶態和內核態間copy需要CPU參與、兩次在內核態與IO設備間copy為DMA方式不需要CPU參與。零拷貝避免了用戶態和內核態間的copy、減少了兩次用戶態內核態間的切換。

2、介紹

java 的zero copy多在網絡應用程序中使用。Java的libaries在linux和unix中支持zero copy,關鍵的api是java.nio.channel.FileChannel的transferTo(),transferFrom()方法。我們可以用這兩個方法來把bytes直接從調用它的channel傳輸到另一個writable byte channel,中間不會使data經過應用程序,以便提高數據轉移的效率。

許多web應用都會向用戶提供大量的靜態內容,這意味着有很多data從硬盤讀出之后,會原封不動的通過socket傳輸給用戶。這種操作看起來可能不會怎么消耗CPU,但是實際上它是低效的:kernal把數據從disk讀出來,然后把它傳輸給user級的application,然后application再次把同樣的內容再傳回給處於kernal級的socket。這種場景下,application實際上只是作為一種低效的中間介質,用來把disk file的data傳給socket。

data每次穿過user-kernel boundary,都會被copy,這會消耗cpu,並且占用RAM的帶寬。幸運的是,你可以用一種叫做Zero-Copy的技術來去掉這些無謂的 copy。應用程序用zero copy來請求kernel直接把disk的data傳輸給socket,而不是通過應用程序傳輸。Zero copy大大提高了應用程序的性能,並且減少了kernel和user模式的上下文切換

使用kernel buffer做中介(而不是直接把data傳到user buffer中)看起來比較低效(多了一次copy)。然而實際上kernel buffer是用來提高性能的。在進行讀操作的時候,kernel buffer起到了預讀cache的作用。當寫請求的data size比kernel buffer的size小的時候,這能夠顯著的提升性能。在進行寫操作時,kernel buffer的存在可以使得寫請求完全異步。

悲劇的是,當請求的data size遠大於kernel buffer size的時候,這個方法本身變成了性能的瓶頸。因為data需要在disk,kernel buffer,user buffer之間拷貝很多次(每次寫滿整個buffer)。

而Zero copy正是通過消除這些多余的data copy來提升性能。

3、傳統方式及涉及到的上下文切換

通過網絡把一個文件傳輸給另一個程序,在OS的內部,這個copy操作要經歷四次user mode和kernel mode之間的上下文切換,甚至連數據都被拷貝了四次,如下圖:

具體步驟如下:

  1. read() 調用導致一次從user mode到kernel mode的上下文切換。在內部調用了sys_read() 來從文件中讀取data。第一次copy由DMA (direct memory access)完成,將文件內容從disk讀出,存儲在kernel的buffer中。
  2. 然后請求的數據被copy到user buffer中,此時read()成功返回。調用的返回觸發了第二次context switch: 從kernel到user。至此,數據存儲在user的buffer中。
  3. send() Socket call 帶來了第三次context switch,這次是從user mode到kernel mode。同時,也發生了第三次copy:把data放到了kernel adress space中。當然,這次的kernel buffer和第一步的buffer是不同的buffer。
  4. 最終 send() system call 返回了,同時也造成了第四次context switch。同時第四次copy發生,DMA egine將data從kernel buffer拷貝到protocol engine中。第四次copy是獨立而且異步的。

 

 

4、zero copy方式及涉及的上下文轉換

在linux 2.4及以上版本的內核中(如linux 6或centos 6以上的版本),開發者修改了socket buffer descriptor,使網卡支持 gather operation,通過kernel進一步減少數據的拷貝操作。這個方法不僅減少了context switch,還消除了和CPU有關的數據拷貝。user層面的使用方法沒有變,但是內部原理卻發生了變化:

transferTo()方法使得文件內容被copy到了kernel buffer,這一動作由DMA engine完成。 沒有data被copy到socket buffer。取而代之的是socket buffer被追加了一些descriptor的信息,包括data的位置和長度。然后DMA engine直接把data從kernel buffer傳輸到protocol engine,這樣就消除了唯一的一次需要占用CPU的拷貝操作。

 

5、Java NIO 零拷貝示例

NIO中的FileChannel擁有transferTo和transferFrom兩個方法,可直接把FileChannel中的數據拷貝到另外一個Channel,或直接把另外一個Channel中的數據拷貝到FileChannel。該接口常被用於高效的網絡/文件的數據傳輸和大文件拷貝。在操作系統支持的情況下,通過該方法傳輸數據並不需要將源數據從內核態拷貝到用戶態,再從用戶態拷貝到目標通道的內核態,同時也避免了兩次用戶態和內核態間的上下文切換,也即使用了“零拷貝”,所以其性能一般高於Java IO中提供的方法。

5.1、通過網絡把一個文件從client傳到server:

 1 /**
 2  * disk-nic零拷貝
 3  */
 4 class ZerocopyServer {
 5     ServerSocketChannel listener = null;
 6 
 7     protected void mySetup() {
 8         InetSocketAddress listenAddr = new InetSocketAddress(9026);
 9 
10         try {
11             listener = ServerSocketChannel.open();
12             ServerSocket ss = listener.socket();
13             ss.setReuseAddress(true);
14             ss.bind(listenAddr);
15             System.out.println("監聽的端口:" + listenAddr.toString());
16         } catch (IOException e) {
17             System.out.println("端口綁定失敗 : " + listenAddr.toString() + " 端口可能已經被使用,出錯原因: " + e.getMessage());
18             e.printStackTrace();
19         }
20 
21     }
22 
23     public static void main(String[] args) {
24         ZerocopyServer dns = new ZerocopyServer();
25         dns.mySetup();
26         dns.readData();
27     }
28 
29     private void readData() {
30         ByteBuffer dst = ByteBuffer.allocate(4096);
31         try {
32             while (true) {
33                 SocketChannel conn = listener.accept();
34                 System.out.println("創建的連接: " + conn);
35                 conn.configureBlocking(true);
36                 int nread = 0;
37                 while (nread != -1) {
38                     try {
39                         nread = conn.read(dst);
40                     } catch (IOException e) {
41                         e.printStackTrace();
42                         nread = -1;
43                     }
44                     dst.rewind();
45                 }
46             }
47         } catch (IOException e) {
48             e.printStackTrace();
49         }
50     }
51 }
52 
53 class ZerocopyClient {
54     public static void main(String[] args) throws IOException {
55         ZerocopyClient sfc = new ZerocopyClient();
56         sfc.testSendfile();
57     }
58 
59     public void testSendfile() throws IOException {
60         String host = "localhost";
61         int port = 9026;
62         SocketAddress sad = new InetSocketAddress(host, port);
63         SocketChannel sc = SocketChannel.open();
64         sc.connect(sad);
65         sc.configureBlocking(true);
66 
67         String fname = "src/main/java/zerocopy/test.data";
68         FileChannel fc = new FileInputStream(fname).getChannel();
69         long start = System.nanoTime();
70         long nsent = 0, curnset = 0;
71         curnset = fc.transferTo(0, fc.size(), sc);
72         System.out.println("發送的總字節數:" + curnset + " 耗時(ns):" + (System.nanoTime() - start));
73         try {
74             sc.close();
75             fc.close();
76         } catch (IOException e) {
77             System.out.println(e);
78         }
79     }
80 }
View Code

5.2、文件到文件的零拷貝:

 1 /**
 2  * disk-disk零拷貝
 3  */
 4 class ZerocopyFile {
 5     @SuppressWarnings("resource")
 6     public static void transferToDemo(String from, String to) throws IOException {
 7         FileChannel fromChannel = new RandomAccessFile(from, "rw").getChannel();
 8         FileChannel toChannel = new RandomAccessFile(to, "rw").getChannel();
 9 
10         long position = 0;
11         long count = fromChannel.size();
12 
13         fromChannel.transferTo(position, count, toChannel);
14 
15         fromChannel.close();
16         toChannel.close();
17     }
18 
19     @SuppressWarnings("resource")
20     public static void transferFromDemo(String from, String to) throws IOException {
21         FileChannel fromChannel = new FileInputStream(from).getChannel();
22         FileChannel toChannel = new FileOutputStream(to).getChannel();
23 
24         long position = 0;
25         long count = fromChannel.size();
26 
27         toChannel.transferFrom(fromChannel, position, count);
28 
29         fromChannel.close();
30         toChannel.close();
31     }
32 
33     public static void main(String[] args) throws IOException {
34         String from = "src/main/java/zerocopy/1.data";
35         String to = "src/main/java/zerocopy/2.data";
36         // transferToDemo(from,to);
37         transferFromDemo(from, to);
38     }
39 }
View Code

6、參考資料

https://my.oschina.net/cloudcoder/blog/299944

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM