(轉)zero copy原理

轉自:java

http://blog.csdn.net/zzz_781111/article/details/7534649linux

 

Zero Copy 簡介

許多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:
 

Zero Copy 簡介

標籤: buffersocketdescriptoruserapplicationlinux內核
  8134人閱讀  評論(3)  收藏  舉報



許多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:




相關文章
相關標籤/搜索