什麼是"零拷貝"技術

前言

從字面意思理解就是數據不需要來回的拷貝,大大提升了系統的性能;這個詞我們也經常在java nio,netty,kafka,RocketMQ等框架中聽到,經常作爲其提升性能的一大亮點;下面從I/O的幾個概念開始,進而在分析零拷貝。

I/O概念

1、緩衝區

緩衝區是所有I/O的基礎,I/O講的無非就是把數據移進或移出緩衝區;進程執行I/O操作,就是向操作系統發出請求,讓它要麼把緩衝區的數據排幹(寫),要麼填充緩衝區(讀);下面看一個java進程發起read請求加載數據大致的流程圖:

面試被問到“零拷貝”!你真的理解嗎?

進程發起read請求之後,內核接收到read請求之後,會先檢查內核空間中是否已經存在進程所需要的數據,如果已經存在,則直接把數據copy給進程的緩衝區;如果沒有內核隨即向磁盤控制器發出命令,要求從磁盤讀取數據,磁盤控制器把數據直接寫入內核read緩衝區,這一步通過DMA完成;接下來就是內核將數據copy到進程的緩衝區;

如果進程發起write請求,同樣需要把用戶緩衝區裏面的數據copy到內核的socket緩衝區裏面,然後再通過DMA把數據copy到網卡中,發送出去;

你可能覺得這樣挺浪費空間的,每次都需要把內核空間的數據拷貝到用戶空間中,所以零拷貝的出現就是爲了解決這種問題的;

關於零拷貝提供了兩種方式分別是:mmap+write方式,sendfile方式;

2、虛擬內存

所有現代操作系統都使用虛擬內存,使用虛擬的地址取代物理地址,這樣做的好處是:

1)一個以上的虛擬地址可以指向同一個物理內存地址,

2)虛擬內存空間可大於實際可用的物理地址;

利用第一條特性可以把內核空間地址和用戶空間的虛擬地址映射到同一個物理地址,這樣DMA就可以填充對內核和用戶空間進程同時可見的緩衝區了,大致如下圖所示:

面試被問到“零拷貝”!你真的理解嗎?

省去了內核與用戶空間的往來拷貝,java也利用操作系統的此特性來提升性能,下面重點看看java對零拷貝都有哪些支持。

3、mmap+write方式

使用mmap+write方式代替原來的read+write方式,mmap是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關係;這樣就可以省掉原來內核read緩衝區copy數據到用戶緩衝區,但是還是需要內核read緩衝區將數據copy到內核socket緩衝區,大致如下圖所示:

面試被問到“零拷貝”!你真的理解嗎?

4、sendfile方式

sendfile系統調用在內核版本2.1中被引入,目的是簡化通過網絡在兩個通道之間進行的數據傳輸過程。sendfile系統調用的引入,不僅減少了數據複製,還減少了上下文切換的次數,大致如下圖所示:

面試被問到“零拷貝”!你真的理解嗎?

數據傳送只發生在內核空間,所以減少了一次上下文切換;但是還是存在一次copy,能不能把這一次copy也省略掉,Linux2.4內核中做了改進,將Kernel buffer中對應的數據描述信息(內存地址,偏移量)記錄到相應的socket緩衝區當中,這樣連內核空間中的一次cpu copy也省掉了;

 

mmap 和 sendFile 的區別:

  1. mmap 適合小數據量讀寫,sendFile 適合大文件傳輸。
  2. mmap 需要 4 次上下文切換,3 次數據拷貝;sendFile 需要 3 次上下文切換,最少 2 次數據拷貝。
  3. sendFile 可以利用 DMA 方式,減少 CPU 拷貝,mmap 則不能(必須從內核拷貝到 Socket 緩衝區)。

在這個選擇上:rocketMQ 在消費消息時,使用了 mmap。kafka 使用了 sendFile。

 

Java零拷貝

1、MappedByteBuffer

java nio提供的FileChannel提供了map()方法,該方法可以在一個打開的文件和MappedByteBuffer之間建立一個虛擬內存映射,MappedByteBuffer繼承於ByteBuffer,類似於一個基於內存的緩衝區,只不過該對象的數據元素存儲在磁盤的一個文件中;調用get()方法會從磁盤中獲取數據,此數據反映該文件當前的內容,調用put()方法會更新磁盤上的文件,並且對文件做的修改對其他閱讀者也是可見的;下面看一個簡單的讀取實例,然後在對MappedByteBuffer進行分析:

面試被問到“零拷貝”!你真的理解嗎?

主要通過FileChannel提供的map()來實現映射,map()方法如下:

面試被問到“零拷貝”!你真的理解嗎?

分別提供了三個參數,MapMode,Position和size;分別表示:

MapMode:映射的模式,可選項包括:READ_ONLY,READ_WRITE,PRIVATE;

Position:從哪個位置開始映射,字節數的位置;

Size:從position開始向後多少個字節;

重點看一下MapMode,請兩個分別表示只讀和可讀可寫,當然請求的映射模式受到Filechannel對象的訪問權限限制,如果在一個沒有讀權限的文件上啓用READ_ONLY,將拋出NonReadableChannelException;PRIVATE模式表示寫時拷貝的映射,意味着通過put()方法所做的任何修改都會導致產生一個私有的數據拷貝並且該拷貝中的數據只有MappedByteBuffer實例可以看到;該過程不會對底層文件做任何修改,而且一旦緩衝區被施以垃圾收集動作(garbage collected),那些修改都會丟失;大致瀏覽一下map()方法的源碼:

面試被問到“零拷貝”!你真的理解嗎?

大致意思就是通過native方法獲取內存映射的地址,如果失敗,手動gc再次映射;最後通過內存映射的地址實例化出MappedByteBuffer,MappedByteBuffer本身是一個抽象類,其實這裏真正實例話出來的是DirectByteBuffer;

2、DirectByteBuffer

DirectByteBuffer繼承於MappedByteBuffer,從名字就可以猜測出開闢了一段直接的內存,並不會佔用jvm的內存空間;上一節中通過Filechannel映射出的MappedByteBuffer其實際也是DirectByteBuffer,當然除了這種方式,也可以手動開闢一段空間:

面試被問到“零拷貝”!你真的理解嗎?

如上開闢了100字節的直接內存空間;

3、Channel-to-Channel傳輸

經常需要從一個位置將文件傳輸到另外一個位置,FileChannel提供了transferTo()方法用來提高傳輸的效率,首先看一個簡單的實例:

面試被問到“零拷貝”!你真的理解嗎?

通過FileChannel的transferTo()方法將文件數據傳輸到System.out通道,接口定義如下:

面試被問到“零拷貝”!你真的理解嗎?

幾個參數也比較好理解,分別是開始傳輸的位置,傳輸的字節數,以及目標通道;transferTo()允許將一個通道交叉連接到另一個通道,而不需要一箇中間緩衝區來傳遞數據;

注:這裏不需要中間緩衝區有兩層意思:第一層不需要用戶空間緩衝區來拷貝內核緩衝區,另外一層兩個通道都有自己的內核緩衝區,兩個內核緩衝區也可以做到無需拷貝數據;

Netty零拷貝

netty提供了零拷貝的buffer,在傳輸數據時,最終處理的數據會需要對單個傳輸的報文,進行組合和拆分,Nio原生的ByteBuffer無法做到,netty通過提供的Composite(組合)和Slice(拆分)兩種buffer來實現零拷貝;看下面一張圖會比較清晰:

面試被問到“零拷貝”!你真的理解嗎?

TCP層HTTP報文被分成了兩個ChannelBuffer,這兩個Buffer對我們上層的邏輯(HTTP處理)是沒有意義的。但是兩個ChannelBuffer被組合起來,就成爲了一個有意義的HTTP報文,這個報文對應的ChannelBuffer,纔是能稱之爲”Message”的東西,這裏用到了一個詞”Virtual Buffer”。

可以看一下netty提供的CompositeChannelBuffer源碼:

面試被問到“零拷貝”!你真的理解嗎?

components用來保存的就是所有接收到的buffer,indices記錄每個buffer的起始位置,lastAccessedComponentId記錄上一次訪問的ComponentId;CompositeChannelBuffer並不會開闢新的內存並直接複製所有ChannelBuffer內容,而是直接保存了所有ChannelBuffer的引用,並在子ChannelBuffer裏進行讀寫,實現了零拷貝。

其他零拷貝

RocketMQ的消息採用順序寫到commitlog文件,然後利用consume queue文件作爲索引;RocketMQ採用零拷貝mmap+write的方式來回應Consumer的請求;

同樣kafka中存在大量的網絡數據持久化到磁盤和磁盤文件通過網絡發送的過程,kafka使用了sendfile零拷貝方式;

 

Java NIO中對零拷貝的使用有哪些呢?

NIO DirectByteBuffer

Java NIO引入了用於通道的緩衝區的ByteBuffer。 ByteBuffer有三個主要的實現:

HeapByteBuffer

在調用ByteBuffer.allocate()時使用。 它被稱爲堆,因爲它保存在JVM的堆空間中,因此您可以獲得所有優勢,如GC支持和緩存優化。 但是,它不是頁面對齊的,這意味着如果您需要通過JNI與本地代碼交談,JVM將不得不復制到對齊的緩衝區空間。

DirectByteBuffer

在調用ByteBuffer.allocateDirect()時使用。 JVM將使用malloc()在堆空間之外分配內存空間。 因爲它不是由JVM管理的,所以你的內存空間是頁面對齊的,不受GC影響,這使得它成爲處理本地代碼的完美選擇。 然而,你要C程序員一樣,自己管理這個內存,必須自己分配和釋放內存來防止內存泄漏。

MappedByteBuffer

在調用FileChannel.map()時使用。 與DirectByteBuffer類似,這也是JVM堆外部的情況。 它基本上作爲OS mmap()系統調用的包裝函數,以便代碼直接操作映射的物理內存數據。

總結

零拷貝是操作系統底層的一種實現,我們在網絡編程中,利用操作系統這一特性,可以大大提高數據傳輸的效率。這也是目前網絡編程框架中都會採用的方式,理解好零拷貝,有助於我們進一步學習Netty等網絡通信框架的底層原理。

 

 

 

 

操作系統層面的解釋:

#零拷貝

零複製(英語:Zero-copy;也譯零拷貝)技術是指計算機執行操作時,CPU不需要先將數據從某處內存複製到另一個特定區域。這種技術通常用於通過網絡傳輸文件時節省CPU週期和內存帶寬。 [1] 

中文名:零複製

外文名: Zero-copy

又    稱: 零拷貝

本    質 : 一種計算機執行操作

 

操作系統某些組件(例如驅動程序文件系統和網絡協議棧)若採用零複製技術,則能極大地增強了特定應用程序的性能,並更有效地利用系統資源。通過使CPU得以完成其他而非將機器中的數據複製到另一處的任務,性能也得到了增強。另外,零複製操作減少了在用戶空間與內核空間之間切換模式的次數。

舉例來說,如果要讀取一個文件並通過網絡發送它,傳統方式下每個讀/寫週期都需要複製兩次數據和切換兩次上下文,而數據的複製都需要依靠CPU。通過零複製技術完成相同的操作,上下文切換減少到兩次,並且不需要CPU複製數據。

零複製協議對於網絡鏈路容量接近或超過CPU處理能力的高速網絡尤爲重要。在這種網絡下,CPU幾乎將所有時間都花在複製要傳送的數據上,因此將成爲使通信速率低於鏈路容量的瓶頸。

硬件實現

最早的實現爲IBMOS/360,其中一個程序可以指示通道子系統從一個文件或設備複製數據塊到另一處,無需先轉移數據。

實現零複製的軟件通常依靠基於直接存儲器訪問(DMA)的複製,以及通過內存管理單元(MMU)的內存映射。這些功能需要特定硬件的支持,並通常涉及到特定存儲器的對齊。

一種較新的方式爲使用異構系統架構(HSA),便於CPUGPU以及其他處理器傳遞指針。這需要CPU和GPU使用統一地址空間。

程序訪問

數種操作系統都通過特定API支持文件的零複製。

Linux內核通過各個系統調用支持零複製,例如sys/socket.h的sendfile、sendfile64以及splice。它們部分在POSIX中指定,因此也存在於BSD內核或IBM AIX中,部分則是Linux內核API中獨有。

Microsoft Windows通過TransmitFile API支持零複製。

Java輸入流可以通過java.nio.channels支持零複製。FileChannel的transferTo()方法也可以支持零複製(如果底層操作系統支持)。

遠程直接內存訪問(RDMA)協議深度依賴零複製技術。 [2] 

 

#DMA

 

DMA(直接存儲器訪問)

DMA(Direct Memory Access,直接內存存取) 是所有現代電腦的重要特色,它允許不同速度的硬件裝置來溝通,而不需要依賴於 CPU 的大量中斷負載。否則,CPU 需要從來源把每一片段的資料複製到暫存器,然後把它們再次寫回到新的地方。在這個時間中,CPU 對於其他的工作來說就無法使用。

中文名:直接存儲器訪問

外文名:Direct Memory Access

縮    寫:DMA

功    能:不同速度的硬件裝置來溝通

來    源:嵌入式底層驅動

 

原理

DMA 傳輸將數據從一個地址空間複製到另外一個地址空間。當CPU 初始化這個傳輸動作,傳輸動作本身是由 DMA 控制器來實行和完成。典型的例子就是移動一個外部內存的區塊到芯片內部更快的內存區。像是這樣的操作並沒有讓處理器工作拖延,反而可以被重新排程去處理其他的工作。DMA 傳輸對於高效能 嵌入式系統算法和網絡是很重要的。

在實現DMA傳輸時,是由DMA控制器直接掌管總線,因此,存在着一個總線控制權轉移問題。即DMA傳輸前,CPU要把總線控制權交給DMA控制器,而在結束DMA傳輸後,DMA控制器應立即把總線控制權再交回給CPU。一個完整的DMA傳輸過程必須經過DMA請求、DMA響應、DMA傳輸、DMA結束4個步驟。

請求

CPU對DMA控制器初始化,並向I/O接口發出操作命令,I/O接口提出DMA請求。

響應

DMA控制器對DMA請求判別優先級及屏蔽,向總線裁決邏輯提出總線請求。當CPU執行完當前總線週期即可釋放總線控制權。此時,總線裁決邏輯輸出總線應答,表示DMA已經響應,通過DMA控制器通知I/O接口開始DMA傳輸。

傳輸

DMA控制器獲得總線控制權後,CPU即刻掛起或只執行內部操作,由DMA控制器輸出讀寫命令,直接控制RAM與I/O接口進行DMA傳輸。

在DMA控制器的控制下,在存儲器和外部設備之間直接進行數據傳送,在傳送過程中不需要中央處理器的參與。開始時需提供要傳送的數據的起始位置和數據長度。

結束

當完成規定的成批數據傳送後,DMA控制器即釋放總線控制權,並向I/O接口發出結束信號。當I/O接口收到結束信號後,一方面停 止I/O設備的工作,另一方面向CPU提出中斷請求,使CPU從不介入的狀態解脫,並執行一段檢查本次DMA傳輸操作正確性的代碼。最後,帶着本次操作結果及狀態繼續執行原來的程序。

由此可見,DMA傳輸方式無需CPU直接控制傳輸,也沒有中斷處理方式那樣保留現場和恢復現場的過程,通過硬件爲RAM與I/O設備開闢一條直接傳送數據的通路,使CPU的效率大爲提高。

 

發佈了19 篇原創文章 · 獲贊 151 · 訪問量 81萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章