Zero Copy 簡介


轉自:http://blog.csdn.net/zzz_781111/article/details/7534649

 

許多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模式的上下文切換。


Java的libaries在linux和unix中支持zero copy,一個關鍵的api是java.nio.channel.FileChannel的transferTo()方法。我們可以用transferTo()來把bytes直接從調用它的channel傳輸到另一個writable byte channel,中間不會使data經過應用程序。本文首先描述傳統的copy是怎樣坑爹的,然后再展示zero-copy技術在性能上是多么的給力以及為什么給力。


Date transfer: The traditional approach


考慮一下這個場景,通過網絡把一個文件傳輸給另一個程序。這個操作的核心代碼就是下面的兩個函數:


Listing 1. Copying bytes from a file to a socket


File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
盡管看起來很簡單,但是在OS的內部,這個copy操作要經歷四次user mode和kernel mode之間的上下文切換,甚至連數據都被拷貝了四次!Figure 1描述了data是怎么移動的。






Figure 2 描述了上下文切換


Figure 2. Traditional context switches




其中的步驟如下:


read() 引入了一次從user mode到kernel mode的上下文切換。實際上調用了sys_read() 來從文件中讀取data。第一次copy由DMA完成,將文件內容從disk讀出,存儲在kernel的buffer中。
然后data被copy到user buffer中,此時read()成功返回。這是觸發了第二次context switch: 從kernel到user。至此,數據存儲在user的buffer中。
send() socket call 帶來了第三次context switch,這次是從user mode到kernel mode。同時,也發生了第三次copy:把data放到了kernel adress space中。當然,這次的kernel buffer和第一步的buffer是不同的兩個buffer。
最終 send() system call 返回了,同時也造成了第四次context switch。同時第四次copy發生,DMA將data從kernel buffer拷貝到protocol engine中。第四次copy是獨立而且異步的。
使用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來提升性能。


Data Transfer:The Zero Copy Approach


如果重新檢查一遍traditional approach,你會注意到實際上第二次和第三次copy是毫無意義的。應用程序僅僅緩存了一下data就原封不動的把它發回給socket buffer。實際上,data應該直接在read buffer和socket buffer之間傳輸。transferTo()方法正是做了這樣的操作。Listing 2是transferTo()的函數原型:


public void transferTo(long position, long count, WritableByteChannel target);
transferTo()方法把data從file channel傳輸到指定的writable byte channel。它需要底層的操作系統支持zero copy。在UNIX和各種Linux中,會執行List 3中的系統調用sendfile(),該命令把data從一個文件描述符傳輸到另一個文件描述符(Linux中萬物皆文件):


#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
在List 1中的file.read()和socket.send()可以用一句transferTo()替代,如List 4:


transferTo(position, count, writableChannel);
Figure 3 展示了在使用transferTo()之后的數據流向






Figure 4 展示了在使用transferTo()之后的上下文切換




在像Listing 4那樣使用transferTo()之后,整個過程如下:


transferTo()方法使得文件內容被DMA engine直接copy到一個read buffer中。然后數據被kernel再次拷貝到和output socket相關聯的那個kernel buffer中去。
第三次拷貝由DMA engine完成,它把kernel buffer中的data拷貝到protocol engine中。
這是一個很明顯的進步:我們把context switch的次數從4次減少到了2次,同時也把data copy的次數從4次降低到了3次(而且其中只有一次占用了CPU,另外兩次由DMA完成)。但是,要做到zero copy,這還差得遠。如果網卡支持 gather operation,我們可以通過kernel進一步減少數據的拷貝操作。在2.4及以上版本的linux內核中,開發者修改了socket buffer descriptor來適應這一需求。這個方法不僅減少了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的拷貝操作。
Figure 5描述了新的transferTo()方法中的data copy:





免責聲明!

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



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