Netty對零拷貝(Zero Copy)三個層次的實現

首先我們來看一下維基百科對零拷貝給出的定義:

零拷貝描述了一種計算機中的操作,即CPU在執行某項任務時不需要先將數據從內存中的一個位置移動到另一個位置就可以完成操作,從而節省了CPU時鐘週期和內存帶寬。

從上面的描述可以看出,其實只要是節省了一次或多次數據的複製就可以稱之爲零拷貝了,這其實是一種廣義的定義。在Netty中對於零拷貝有三個層次的實現,我們就一條條分析一下。

避免數據流經用戶空間

在操作系統層面,將數據從來源設備A發送到目標設備B時,需要先將數據~~從A的內核空間讀緩衝區複製到用戶空間緩衝區(即應用程序提供的一塊buffer),再從用戶空間緩衝區~~複製到B的內核空間寫緩衝區。零拷貝的作用就是省去了上面用線劃掉的這個過程,將數據從A的內核讀緩衝直接移動到B的內核寫緩衝裏,即整個數據的流動都在內核空間完成,不需要再向用戶空間裏走一遍了。例如將磁盤中的一個文件發送到網絡中時,正常情況下我們需要在程序中開闢一塊buffer, 先將數據從磁盤中讀到這個buffer中,這個過程裏就發生了數據從磁盤內核空間讀緩衝再到用戶空間程序buffer的數據流動。隨後我們再從自己的buffer中將數據寫入到socket,實際上發生了從用戶空間程序buffer內核空間socket寫緩衝的數據流動。Netty在這一層對零拷貝實現就是FileRegion類的transferTo()方法,我們可以不提供buffer完成整個文件的發送,不再需要開闢buffer循環讀寫。

這裏再多囉嗦一句,如果有朋友疑惑爲什麼OS要把內存分成內核空間和用戶空間兩部分然後還要來回複製,那就應該去複習一下操作系統原理了,簡單來說就是爲了保護用戶程序不會破壞操作系統內核,同時不允許用戶程序直接操作硬件而是應該讓操作系統代勞。

避免數據從JVM Heap到C Heap的拷貝

在JVM層面,每當程序需要執行一個I/O操作時,都需要將數據先從JVM管理的堆內存複製到使用C malloc()或類似函數分配的Heap內存中才能夠觸發系統調用完成操作,這部分內存站在Java程序的視角來看就是堆外內存,但是以操作系統的視角來看其實都屬於進程的堆區,OS並不知道JVM的存在,都是普通的用戶程序。發現了沒有,這樣一來JVM在I/O時永遠比使用native語言編寫的程序多一次數據複製,這是所有基於VM的編程語言都繞不開的問題,而且是純粹的人爲多增加了一個步驟。那麼問題來了,爲什麼不直接使用JVM堆區數據的地址而是要複製一下呢?原因很簡單,虛擬機只是一個用戶程序,它本身並沒有直接訪問硬件的能力,因此所有的I/O操作都需要藉助於系統調用來實現。在Linux系統中,與I/O相關的read()write()系統調用,都需要傳入一個指向你在程序中分配的一片內存區域起始地址的指針,然後操作系統會將數據填入這片區域或者從這片區域中讀出數據。這裏如果直接使用JVM堆中對應byte[]類型的地址的話就會有兩個無法解決的問題:一是Java中的對象實際的內存佈局跟C是不一樣的,不同的JVM可能有不同的實現,byte[]的首地址可能只是個對象頭,並不是真實的數據;二是垃圾收集器的存在使得JVM會經常移動對象的位置,這樣同一個對象的真實內存地址隨時都有可能發生變化,JVM知道地址變了,但是操作系統可不知道。明確上面這些以後我們就不難理解,Netty中對零拷貝思想的第二處實現,就是在適當的位置直接使用堆外內存從而避免了數據從JVM Heap到C Heap的拷貝。

減少數據在用戶空間的多次拷貝

在我們寫代碼時有很多時候會將數據多次移動來實現一些功能,比如在Netty中我們可能會先將ByteBuffer中的字節數據讀到自己開闢的一處byte[]中再遍歷處理,這樣就多了一次數據的複製。有時候可能需要將多個ByteBuffer組合起來使用才能完成某些業務邏輯,這樣就需要再開闢一個更大的字節數組將所有ByteBuffer都複製過來,更要命了。這裏Netty的第三個層次的實現,就是提供了CompositeByteBuf類,它提供了對多個ByteBuffer的一個"視圖",可以將它們邏輯上當成一個完整的ByteBuffer來操作,這樣就免去了重新分配空間再複製數據的開銷。

明白上面這三條以後相信大家對零拷貝就有了一個全面的認識了。對於操作系統來說,它指的是數據在內核空間直接流動而不需要經過用戶空間;對於普通程序員來說,零拷貝又多出了VM缺陷引起的複製和用戶自己業務邏輯上的複製兩個層次的概念。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章