"狼哥,面試又跪了,碰到了知識盲區"
"哪個?"
"一面還可以,二面面試官問我零拷貝的原理,懵逼了...這塊內容沒去研究過"
"哦,這個知識點,我之前應該有講過,你沒注意到?"
"這東西工作中用不到,可能被我忽略了"
"嘖嘖嘖..."
"哎,有空和我大概講講?"
"先從簡單開始,實現下這個場景:從一個文件中讀出數據并將數據傳到另一臺服務器上?"
"為啥寫這個?"
"你先寫"
"行..."
1分鐘后
"我寫了偽代碼"
File.read(file, buf, len);
Socket.send(socket, buf, len);
"這里涉及到了幾次數據拷貝?"
"2次?磁盤拷貝到內存,內存拷貝到Socket?"
"emmm,怪不得掛了,一點不冤。"
"這種方式一共涉及了4次數據拷貝,知道用戶態和內核態的區別嗎?"
"了解"
"行,文字有點干癟,你先看這個圖"
1、應用程序中調用read()
方法,這里會涉及到一次上下文切換(用戶態->內核態),底層采用DMA(direct memory access)讀取磁盤的文件,并把內容存儲到內核地址空間的讀取緩存區。
2、由于應用程序無法讀取內核地址空間的數據,如果應用程序要操作這些數據,必須把這些內容從讀取緩沖區拷貝到用戶緩沖區。這個時候,read()
調用返回,且引發一次上下文切換(內核態->用戶態),現在數據已經被拷貝到了用戶地址空間緩沖區,這時,如果有需要,應用程序可以操作修改這些內容。
3、我們最終目的是把這個文件內容通過Socket傳到另一個服務中,調用Socket的send()
方法,這里又涉及到一次上下文切換(用戶態->內核態),同時,文件內容被進行第三次拷貝,被再次拷貝到內核地址空間緩沖區,但是這次的緩沖區與目標套接字相關聯,與讀取緩沖區沒有半點關系。
4、send()
調用返回,引發第四次的上下文切換,同時進行第四次的數據拷貝,通過DMA把數據從目標套接字相關的緩存區傳到協議引擎進行發送。
"在整個過程中,過程1和4是由DMA負責,并不會消耗CPU,只有過程2和3的拷貝需要CPU參與,整明白了?"
"我消化一下..."
半小時后...
"狼哥,這個過程,感覺好幾次的數據拷貝都是多余的,很影響性能啊"
"對,所以才有了零拷貝技術"
"具體咋實現?"
"慢慢來,如果在應用程序中,不需要操作內容,過程2和3就是多余的,如果可以直接把內核態讀取緩存沖區數據直接拷貝到套接字相關的緩存區,是不是可以達到優化的目的?"
這種實現,可以有以下幾點改進:
- 上下文切換的次數從四次減少到了兩次
- 數據拷貝次數從四次減少到了三次(其中DMA copy 2次,CPU copy 1次)
"怎么實現?"
"在Java中,正好FileChannel的transferTo() 方法可以實現這個過程,該方法將數據從文件通道傳輸到給定的可寫字節通道, 上面的file.read()
和 socket.send()
調用動作可以替換為 transferTo()
調用"
public void transferTo(long position, long count, WritableByteChannel target);
在 UNIX 和各種 Linux 系統中,此調用被傳遞到 sendfile()
系統調用中,最終實現將數據從一個文件描述符傳輸到了另一個文件描述符。
"確實改善了很多,但還沒達到零拷貝的要求,還有其它黑技術嗎?"
"對的,如果底層網絡接口卡支持收集操作的話,就可以進一步的優化。"
"怎么優化?"
在 Linux 內核 2.4 及后期版本中,針對套接字緩沖區描述符做了相應調整,DMA自帶了收集功能,對于用戶方面,用法還是一樣的,但是內部操作已經發生了改變:
- 第一步,transferTo() 方法引發 DMA 將文件內容拷貝到內核讀取緩沖區。
- 第二步,把包含數據位置和長度信息的描述符追加到套接字緩沖區,避免了內容整體的拷貝,DMA 引擎直接把數據從內核緩沖區傳到協議引擎,從而消除了最后一次 CPU參與的拷貝動作。