通過zero copy來實現高效的數據傳輸


  這段時間在學習一些系統底層的知識,真后悔大學沒有好好學習操作系統,導致好多文章看不懂。說到這不得不吐槽一下,像介紹系統層次的一些書籍好多都是中文翻譯版,而大部分的中文翻譯版大都語句晦澀,難懂,而且極易被誤導。網上也有一些介紹文章,好多是連作者自己都沒搞明白抑或是簡單的復制粘貼,真是越看越迷糊。當然不乏有好的翻譯書籍。不僅僅是我個人,好多大牛也都建議這樣的書籍直接讀英文原版好一些。有英語問題也沒辦法,程序員學習能力強,英語不行就干英語^_^。再回到主題,看到一篇不錯的文章,然后推薦給朋友,朋友一看英文的就懶得看,所以這里LZ打算將這篇文章用自己三腳貓的英語簡單的翻譯一下,以鞏固自己的所學,同時也分享給大家。以下是正文。

  這篇文章介紹了一個有大量io操作的運行在linux或者unix平台上的Java程序,如何用zero copy技術來提高IO性能。zero copy可以避免緩沖區間數據拷貝的次數,也可以減少用戶態和內核態之間的的切換。

  大部分web服務器都要處理大量的靜態內容,而其中大部分都是從磁盤文件中讀取數據然后寫到socket中。這種操作對cpu的消耗是比較小的,但也是十分低效的:內核首先從磁盤文件讀取數據,然后從內核空間將數據傳到用戶空間,應用程序又將數據從用戶空間返回到內核空間然后傳輸給socket(如果好奇數據為何如此來回傳輸,請繼續看下文)。實際上,應用程序就相當於是個低效的中間者,從磁盤拿數據放到socket。

  每次數據在內核空間和用戶空間傳輸就一次拷貝過程,這是需要占用一定的cpu周期和內存資源的。幸運的是你可以通過一個叫zero copy的技術來消除這些拷貝過程。使用了zero copy技術的應用程序的數據傳輸過程就是內核從磁盤文件讀取數據直接傳輸到socket中,不再經過應用程序這個中間者。zero copy大大改善了應用程序的性能並且減少了用戶態和內核態之間的切換次數。

  在linux或者unix系統上,Java類庫通過java.nio.channels.FileChannel的transferTo()方法來應用zero copy。你可以通過這個方法把一個channel中讀取到的字節傳輸到另一個channel,不再需要數據流經應用程序。在這篇文章中,我們首先展示了使用傳統數據復制方式的一些情況,然后又通過transferTo來使用zero copy實現一個更高性能的方式。

傳統的數據傳輸方式:

  像這種從文件讀取數據然后將數據通過網絡傳輸給其他的程序的方式(大部分應用服務器都是這種方式,包括web服務器處理靜態內容時,ftp服務器,郵件服務器等等)其核心操作就是如下兩個調用:

File.read(fileDesc,buf,len);
Socket.send(socket,buf,len);

  其上操作看上去只有兩個簡單的調用,但是其內部過程卻要經歷四次用戶態和內核態的切換以及四次的數據復制操作。

  圖一展示了數據從文件到socket的內部流程:

     

                               圖一,傳統的數據復制方式


  圖二是用戶態和內核態的切換過程:

   

                            圖二,傳統方式的上下文切換過程

  這些步驟涉及到如下過程:
    1、read()的調用引起了從用戶態到內核態的切換(看圖二),內部是通過sys_read()(或者類似的方法)發起對文件數據的讀取。數據的第一次復制是通過DMA(直接內存訪問)
       將磁盤上的數據復制到內核空間的緩沖區中。
    2、數據從內核空間的緩沖區復制到用戶空間的緩沖區后,read()方法也就返回了。此時內核態又切換回用戶態,現在數據也已經復制到了用戶地址空間的緩存中。
    3、socket的send()方法的調用又會引起用戶態到內核的切換,第三次數據復制又將數據從用戶空間緩沖區復制到了內核空間的緩沖區,這次數據被放在了不同於之前的內核緩沖區中,這個緩沖區與數                         據將要被傳輸到的socket關聯。
    4、send()系統調用返回后,就產生了第四次用戶態和內核態的切換。隨着DMA單獨異步的將數據從內核態的緩沖區中傳輸到協議引擎發送到網絡上,有了第四次數據復制。

  使用內核空間的緩沖區做中介(而不是直接將數據傳輸到用戶空間)或許看上去是低效的,然而內核緩沖區做中介的引入就是為了改善進程的性能。從當應用程序讀取文件數據這方面來說,如果讀取的數據小於這個中介緩沖區的容量,那么中介緩沖區就可以提前緩存一大部分數據以供程序下次讀取使用,從而提高性能。從應用程序寫數據來說,這個中介緩沖區可以用來實現異步功能(當數據緩沖區數據滿了后再寫出去,較少了系統調用的次數)。
  不幸的是,這種方式也有它自己的瓶頸。當應用程序讀取的數據比這個中介緩沖區的容量大很多的時候,數據就會在磁盤、內核空間、用戶空間之間復制多次后才最終被傳給應用程序。
  零拷貝技術就是通過消除這種多余的數據拷貝來改善性能的。

使用zero copy的數據傳輸方式:

  如果你再看一下傳統的方式,你會發現實際上第二次和第三次數據拷貝是沒有必要的。應用程序除了緩存一下數據然后傳回到socket的緩沖區中啥也沒干。我們可以通過直接從內核緩沖區把數據傳輸到socket關聯的緩沖區來代替傳統的方式。transferTo()方法可以幫你實現。下面是這個方法的定義:

public void transferTo(long position,long count,WritableByteChannel target);

  transferTo()方法將數據從一個channel傳輸到另一個可寫的channel上,其內部實現依賴於操作系統對zero copy技術的支持。在unix操作系統和各種linux的發型版本中,這種功能最終是通過sendfile()系統調用實現。下邊就是這個方法的定義:

#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

  可以通過調用transferTo()方法來替代上邊的File.read()、Socket.send()

transferTo(position, count, writableChannel);

  圖三 展示了通過transferTo實現數據傳輸的路徑:

         

            圖三,通過transferTo()實現數據拷貝

 

  圖四  展示了內核態用戶態的切換情況:

            

         圖四,tranferTo()下上下文的切換

 

  使用transferTo()方式所經歷的步驟:
    1、transferTo調用會引起DMA將文件內容復制到讀緩沖區(內核空間的緩沖區),然后數據從這個緩沖區復制到另一個與socket輸出相關的內核緩沖區中。
    2、第三次數據復制就是DMA把socket關聯的緩沖區中的數據復制到協議引擎上發送到網絡上。

  這次改善,我們是通過將內核、用戶態切換的次數從四次減少到兩次,將數據的復制次數從四次減少到三次(只有一次用到cpu資源)。但這並沒有達到我們零復制的目標。如果底層網絡適配器支持收集操作的話,我們可以進一步減少內核對數據的復制次數。在內核為2.4或者以上版本的linux系統上,socket緩沖區描述符將被用來滿足這個需求。這個方式不僅減少了內核用戶態間的切換,而且也省去了那次需要cpu參與的復制過程。從用戶角度來看依舊是調用transferTo()方法,但是其本質發生了變化:‘
    1、調用transferTo方法后數據被DMA從文件復制到了內核的一個緩沖區中。
    2、數據不再被復制到socket關聯的緩沖區中了,僅僅是將一個描述符(包含了數據的位置和長度等信息)追加到socket關聯的緩沖區中。DMA直接將內核中的緩沖區中的數據傳輸給協議引擎,消                       除了僅剩的一次需要cpu周期的數據復制。

  圖五展示了收集操作下transferTo的工作流程

         

            圖五

 

構建一個文件傳輸服務器

  接下來讓我們用一個在客戶端和服務器段傳輸文件的實例來實踐一下我們的zero copy技術(代碼到下面下載)。TraditionalClient.java和TraditionalServer.java 是基於傳統的實現方式。TraditionalServer.java 是一個服務器程序,綁定在一個端口等待客戶端的連接,然后從客戶端的連接中一次讀取4k字節的數據。TraditionalClient.java連接到服務器,然后從文件中讀取數據通過網絡傳送給服務器。

  同樣的,TransferToServer.java 和TransferToClient.java執行相同的功能,但是使用了transfeTo()方法將文件從服務器發送到客戶端。

性能比較

  我們在內核為2.6版本的linux上運行了這個例子程序,並測試了在不同文件大小的情況下,傳統的方式和transferTo方式所消耗的毫秒數,下邊是測試結果

File size Normal file transfer (ms) transferTo (ms)
7MB 156 45
21MB 337 128
63MB 843 387
98MB 1320 617
200MB 2124 1150
350MB 3631 1762
700MB 13498 4422
1GB 18399 8537

  我們可以看到,使用transferTo方式比傳統方式少大約65%的時間消耗。對於那些需要大量讀取io數據傳輸到另一個channel的服務器程序來說,使用zero copy方式性能上的提高是相當顯著地。比如web服務器。

摘要

  我們展示了transferTo比傳統方式上的性能優勢,在從一個channel讀取相同數據發送到另一個channel的操作上。如果有一個需要在channel間大量復制數據的應用程序,使用zero copy將會有一個更大的性能提高。

下載

描述 名字 大小
文章中的簡單例子程序 j-zerocopy.zip 3kb


免責聲明!

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



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