淺談零拷貝機制

NO IMAGE

預備知識

我相信來看本文章的應該都對操作系統有了一些瞭解,不過在談零拷貝之前,還需要講一些別的東西,避免到時候大家看的暈乎

  • 內核態和用戶態:這兩種不同的狀態分別賦予進程不同的權限,內核態下可以訪問內存的所有數據,能夠訪問外圍設備;而用戶態下則只能訪問受限的內存。所以如果一個進程想要執行深度操作,就需要涉及用戶態到內核態的切換
  • 系統調用:系統調用不是一種調用行為,可以理解成操作系統開放的編程接口,通過使用“系統調用”接口,可以達到一些在用戶態下無法完成的行為,當然,會涉及到用戶態到內核態的狀態切換
  • 緩衝區:緩衝區是內存中的一塊區域,是IO操作的基礎。任何IO操作都可以理解為數據在緩衝區之間的拷貝,用戶空間和內核空間都有對應的緩衝區。操作系統不能直接將數據從磁盤拷貝到用戶空間的緩衝區中
  • DMA:DMA即Direct Memory Access,負責將數據從一個地址空間轉移到另一個地址空間,傳輸動作由CPU初始化,但由DMA來執行。在DMA執行操作期間,CPU可以處理其他的事情
  • 虛擬內存:內存地址分為虛擬內存地址和物理內存地址兩種,進程可見的是虛擬內存地址,操作系統會將虛擬內存地址映射到物理內存上,虛擬內存是連續的,但是物理內存可以是不連續的

零拷貝解決什麼問題

在講零拷貝之前,先得明白拷貝是什麼。這裡的拷貝指“I/O操作時,數據在緩衝區之間的拷貝”。不明白沒關係,我們先來看linux中的將數據從磁盤讀取,並通過網絡發送出去的一整個過程是怎樣的:

  1. DMA將數據從磁盤拷貝到內核緩衝區中

  2. cpu將數據從內核緩衝區拷貝到用戶緩衝區中(讀過程結束,進程可以從用戶緩衝區直接讀取數據)

  3. cpu將數據從用戶緩衝區拷貝到內核socket緩衝區中

  4. DMA將數據從socket緩衝區發送到協議引擎中(寫過程結束,數據被協議引擎通過網絡發送)

這裡借用一下理解零拷貝原理中的圖:

淺談零拷貝機制

整個讀寫的過程大致是這樣的,涉及2次用戶態和內核態的狀態切換,2次DMA拷貝,和2次cpu拷貝

我們同樣也發現,在一個讀寫過程中,第2步和第3步彷彿在做“無用功”,數據如果直接從內核緩衝拷貝到Socket緩衝就可以了,為什麼還要通過用戶緩衝中轉呢?沒錯,操作系統的開發者也意識到了這個問題,所以零拷貝就是為了解決這個問題,所以利用零拷貝機制,可以避免狀態切換,並減少了cpu的拷貝次數

零拷貝的實現方式

雖然看起來只需要允許數據從內核緩衝拷貝到socket緩衝,即可解決數據拷貝的問題,但是具體卻有很多種實現方式,我們來一一介紹一下

sendFile

sendFile方式的系統調用為sendfile system call,也是最經典的零拷貝解決方案。採用sendFile方式的的讀寫過程為:

  1. DMA將數據從磁盤拷貝到內核緩衝
  2. cpu將數據從內核緩衝拷貝到內核socket緩衝
  3. DMA將數據從socket緩衝拷貝到協議引擎中

這種標準方式就不用過多介紹了,就是我們在剛才提到的解決方案,接下來我們看優化版本的sendFile的讀寫過程是怎樣的:

  1. DMA將數據從磁盤拷貝到內核緩衝
  2. cpu將描述符[1]從內核緩衝拷貝到socket緩衝
  3. DMA將數據從socket緩衝拷貝到協議引擎中

在優化的sendFile方式中,第2步不再是拷貝數據,而是拷貝描述符,這樣就真正實現了數據的零拷貝

mmap

mmap,也可以成為內存映射,效果比傳統的I/O要好,但是代價比sendFile要大。我們來看mmap方式下的讀寫操作:

  1. DMA將數據從磁盤拷貝到內核緩衝/用戶緩衝
  2. cpu將數據從內核緩衝/用戶緩衝拷貝到socket緩衝
  3. DMA將數據從socket緩衝拷貝到協議引擎中

這裡畫了個斜線,表示內核緩衝和用戶緩衝是同一塊區域,mmap採用虛擬內存映射,雖然進程認為自己的用戶緩衝區域是內存中獨立的區域,但是實際上用戶緩衝和內核緩衝指向的同一塊區域,這種方式也能避免數據拷貝問題

FileChannel

FileChannel是Java NIO的解決方案,提供transferTo/transferFrom接口,用於將一個通道連接到另一個通道,中間就避免了不必要的數據拷貝

更嚴格地說,FileChannel並不能算是零拷貝的一種解決方案,實際上還是需要依賴操作系統的sendFile

Netty Zero-Copy

Netty中在FileRegion封裝了Java NIO的transferTo/transferFrom,也可以實現零拷貝,但是這不是重點,Netty還提供了另一種形式的零拷貝,也是我們學習的重點
在數據傳輸時,通常一份完整的消息數據會被切分成多個數據包進行傳輸,而這些單個的數據包是沒有意義的,只有組合在一起才有具體的含義,才能被程序進行處理。Netty可以通過零拷貝的方式,將這些數據組合成完成的消息來供使用,減少數據拷貝次數,此時零拷貝的作用範圍僅侷限於用戶空間

同時,Netty可以直接在堆外分配內存,避免了數據從堆內拷貝到堆外的過程

總結

零拷貝的這些方式中,sendFile雖然看起來最美好,但是如果我們的應用在讀取數據後還需要進行更改的話,這種方式就不適用了,就需要使用代價更高的mmap內存映射方式

零拷貝的內容大致就是這些了,如果想更為深入地去學習零拷貝知識的話,可以去學習一下netty的源碼,還是很有必要的


  1. 這裡我不敢確定是內存描述符還是數據描述符,我傾向於內存描述符,是一個類似指針的數據,如果我這裡理解錯誤歡迎指正 ↩︎

相關文章

淺談Kafka特性與架構

淺談Spring事務中的7種傳播特性

隨便分享點不那麼常規的面試題(二)

隨便分享點不那麼常規的面試題(一)