傳統讀操作
JAVA用傳統方式進行讀操作時,整體流程如上圖,具體如下:
1、應用程序發起讀數據操作,JVM會發起read()系統調用。
2、這時操作系統OS會進行 一次上下文切換(把用戶空間切換到內核空間)
3、通過 磁盤控制器把數據copy到內核緩衝區中,這裏的就發生了 一次DMACopy
4、然後內核將 數據copy到用戶空間的應用緩衝區中, 發生了一次CPU Copy
5、read調用返回後,會 再進行一次上下文切換(把內核空間切換到用戶空間)
我們看一下一個讀操作,發了2次上下文切換,和2次數據copy,一次是DMA Copy,一次是CPU Copy。
注意一點的是 內核從磁盤上面讀取數據 是 不消耗CPU時間的,是通過磁盤控制器完成;稱之爲DMA Copy。
傳統寫操作
上圖是JAVA傳統的寫操作,具體流程:
1、應用發起寫操作,OS進行 一次上下文切換(從用戶空間切換爲內核空間)
2、並且把數據copy到內核緩衝區Socket Buffer, 做了一次CPU Copy
3、內核空間再把數據copy到磁盤或其他存儲(網卡,進行網絡傳輸), 進行了DMA Copy
4、寫入結束後返回,又從 內核空間切換到用戶空間
傳統IO
我們可以看出傳統的IO讀寫操作,總共進行了4次上下文切換,4次Copy動作。我們可以看到數據在內核空間和應用空間之間來回複製,其實他們什麼都沒有做,就是複製而已,這個機制太浪費時間了,而且是浪費的CPU的時間。
那我們能不能讓數據不要來回複製呢?零拷貝這個技術就是來解決這個問題。關於零拷貝提供了兩種解決方式:mmap+write方式、sendfile方式
虛擬內存
所有現代操作系統都使用虛擬內存,使用虛擬地址取代物理地址,這樣做的好處就是:
1、多個虛擬內存可以指向同一個物理地址
2、虛擬內存空間可以遠遠大於物理內存空間
我們利用第一條特性可以優化一下上面的設計思路,就是把內核空間和用戶空間的虛擬地址映射到同一個物理地址,這樣就不需要來回複製了,看圖:
mmap+write方式
使用mmap+write方式替換原來的傳統IO方式,就是利用了虛擬內存的特性,看圖
mmap讀流程
mmap寫流程
整體流程的核心區別就是,把數據讀取到內核緩衝區後,應用程序進行寫入操作時,直接是把內核的Read Buffer的數據複製到 Socket Buffer 以便進行寫入,這次內核之間的複製也是需要CPU參與的。
注意:最後把Socket Buffer數據拷貝到很多地方,統稱protocol engine(協議引擎)
這個流程就少了一個CPU Copy,提升了IO的速度。不過發現上下文的切換還是4次,沒有減少,因爲還是要應用程序發起write操作。那能不能減少上下文切換呢?
sendfile方式
這種方式可以替換上面的mmap+write方式,如:
-
mmap();
-
write();
替換爲
sendfile();
這樣就減少了一次上下文切換,因爲少了一個應用程序發起write操作,直接發起sendfile操作。
到這裏就只有3次Copy,其中只有1次CPU Copy;3次上下文切換。那能不能把CPU Copy減少到沒有呢?
gather
Linux2.4內核進行了優化,提供了gather操作,這個操作可以把最後一次CPU Copy去除,什麼原理呢?就是在內核空間Read Buffer和Socket Buffer不做數據複製,而是將Read Buffer的內存地址、偏移量記錄到相應的Socket Buffer中,這樣就不需要複製(其實本質就是和虛擬內存的解決方法思路一樣,就是內存地址的記錄),如圖: