早期的數據IO,由用戶進程向CPU發起,應用程序與磁盤之間的 I/O 操作都是通過 CPU 的中斷完成的。
CPU還要負責將磁盤緩沖區拷貝到內核緩沖區(pageCache),再從內核緩沖區拷貝到用戶緩沖區。
為了減少CPU占用,產生了DMA技術,大大解放了CPU
DMA 的全稱叫直接內存存取(Direct Memory Access),是一種允許外圍設備(硬件子系統)直接訪問系統主內存的機制。
目前大多數的硬件設備,包括磁盤控制器、網卡、顯卡以及聲卡等都支持 DMA 技術。
傳統IO方式及DMA技術圖解可以參考之前一篇 https://www.cnblogs.com/ttaall/p/13738562.html
DMA技術存在的問題:
舉個例子,從本地文件中發一張圖片給你的好盆友。
傳統的訪問方式是通過 write() 和 read() 兩個系統調用實現的,通過 read() 函數讀取圖片到到緩存區中,然后通過 write() 方法把緩存中的圖片輸出到網絡端口。
read操作:
當應用程序執行 read 系統調用讀取一塊數據的時候,如果這塊數據已經存在於用戶進程的頁內存中,就直接從內存中讀取數據。
如果數據不存在,則先將數據從磁盤加載數據到內核空間的讀緩存(read buffer)中,再從讀緩存拷貝到用戶進程的頁內存中。
write操作:
當應用程序准備好數據,執行 write 系統調用發送網絡數據時,先將數據從用戶空間的頁緩存拷貝到內核空間的網絡緩沖區(socket buffer)中,
然后再將寫緩存中的數據拷貝到網卡設備完成數據發送。
需要進行兩次DMA拷貝,兩次CPU拷貝,四次上下文切換
總共四次拷貝,四次切換,代價屬實有點點大。
-
上下文切換:當用戶程序向內核發起系統調用時,CPU 將用戶進程從用戶態切換到內核態;當系統調用返回時,CPU 將用戶進程從內核態切換回用戶態。
-
CPU 拷貝:由 CPU 直接處理數據的傳送,數據拷貝時會一直占用 CPU 的資源。
-
DMA 拷貝:由 CPU 向DMA磁盤控制器下達指令,讓 DMA 控制器來處理數據的傳送,數據傳送完畢再把信息反饋給 CPU,從而減輕了 CPU 資源的占有率。
零拷貝的想法
1.用戶態可以直接操作讀寫,不需要切換用戶態內核態
2.盡量減少拷貝次數,盡量減少上下文切換次數
3.寫時復制,需要寫操作的時候再拷貝,只是讀操作沒必要拷貝
用戶態直接IO
用戶態直接 I/O 使得應用進程或運行在用戶態(user space)下的庫函數直接訪問硬件設備。
用戶態直接 I/O 只能適用於不需要內核緩沖區處理的應用程序,這些應用程序通常在進程地址空間有自己的數據緩存機制,稱為自緩存應用程序,如數據庫管理系統 就是一個代表。
其次,這種零拷貝機制會直接操作磁盤 I/O,由於 CPU 和磁盤 I/O 之間的執行時間差距,會造成大量資源的浪費,解決方案是配合異步 I/O 使用。
寫時復制
寫時復制指的是當多個進程共享同一塊數據時,如果其中一個進程需要對這份數據進行修改,那么就需要將其拷貝到自己的進程地址空間中。
這樣做並不影響其他進程對這塊數據的操作,每個進程要修改的時候才會進行拷貝,所以叫寫時拷貝。
mmap+write零拷貝技術
以mmap+write的方式替代傳統的read+write的方式,減少了一次拷貝
mmap 是 Linux 提供的一種內存映射文件方法,即將一個進程的地址空間中的一段虛擬地址映射到磁盤文件地址
使用 mmap 的目的是將內核中讀緩沖區(read buffer)的地址與用戶空間的緩沖區(user buffer)進行映射。
從而實現內核緩沖區與應用程序內存的共享,省去了將數據從內核讀緩沖區(read buffer)拷貝到用戶緩沖區(user buffer)的過程。
整個拷貝過程會發生 4 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝。
mmap 主要的用處是提高 I/O 性能,特別是針對大文件。對於小文件,內存映射文件反而會導致碎片空間的浪費。
Sendfile零拷貝技術
通過 Sendfile 系統調用,數據可以直接在內核空間內部進行 I/O 傳輸,從而省去了數據在用戶空間和內核空間之間的來回拷貝。
將要讀取的文件緩沖區的文件 fd 和要發送的Socket緩沖區的Socket fd 傳給sendfile函數,
Sendfile 調用中 I/O 數據對用戶空間是完全不可見的。也就是說,這是一次完全意義上的數據傳輸過程。
也就是說用戶程序不能對數據進行修改,而只是單純地完成了一次數據傳輸過程。
整個拷貝過程會發生 2 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝。
Sendfile+DMA gather copy
它只適用於將數據從文件拷貝到 socket 套接字上的傳輸過程。
它將內核空間的讀緩沖區(read buffer)中對應的數據描述信息(內存地址、地址偏移量)記錄到相應的網絡緩沖區( socket buffer)中,
由 DMA 根據內存地址、地址偏移量將數據批量地從讀緩沖區(read buffer)拷貝到網卡設備中。
這樣 DMA 引擎直接利用 gather 操作將頁緩存中數據打包發送到網絡中即可,本質就是和虛擬內存映射的思路類似。
整個拷貝過程會發生 2 次上下文切換、0 次 CPU 拷貝以及 2 次 DMA 拷貝。
Splice零拷貝技術
Splice相當於在Sendfile+DMA gather copy上的提升
Splice 系統調用可以在內核空間的讀緩沖區(read buffer)和網絡緩沖區(socket buffer)之間建立管道(pipeline),從而避免了兩者之間的 CPU 拷貝操作。
基於 Splice 系統調用的零拷貝方式,整個拷貝過程會發生 2 次上下文切換,0 次 CPU 拷貝以及 2 次 DMA 拷貝。
-
用戶進程通過 splice() 函數向內核(kernel)發起系統調用,上下文從用戶態(user space)切換為內核態(kernel space)。
-
CPU 利用 DMA 控制器將數據從主存或硬盤拷貝到內核空間(kernel space)的讀緩沖區(read buffer)。
-
CPU 在內核空間的讀緩沖區(read buffer)和網絡緩沖區(socket buffer)之間建立管道(pipeline)。
-
CPU 利用 DMA 控制器將數據從網絡緩沖區(socket buffer)拷貝到網卡進行數據傳輸。
-
上下文從內核態(kernel space)切換回用戶態(user space),Splice 系統調用執行返回。
對比
無論是傳統 I/O 拷貝方式還是引入零拷貝的方式,2 次 DMA Copy 是都少不了的,因為兩次 DMA 都是依賴硬件完成的。
kafka實現零拷貝: sendfile函數
2.4 內核改進的的sendfile函數 + 硬件提供的DMA Gather Copy實現零拷貝,將文件通過socket傳送
Kafka底層基於java.nio包下的FileChannel的transferTo: transferTo 可將一個文件直接傳輸到另一個文件
transferTo將FileChannel關聯的文件發送到指定channel,當Comsumer消費數據,Kafka Server基於FileChannel將文件中的消息數據發送到SocketChannel
RocketMq零拷貝實現: mmap+write()
RocketMQ基於mmap + write的方式實現零拷貝
內部實現基於nio提供的java.nio.MappedByteBuffer,基於FileChannel的map方法得到mmap的緩沖區:
Netty零拷貝實現:
- 1 基於操作系統實現的零拷貝,底層基於FileChannel的transferTo方法
- 2 基於Java 層操作優化,對數組緩存對象(ByteBuf )進行封裝優化,通過對ByteBuf數據建立數據視圖,支持ByteBuf 對象合並,切分,當底層僅保留一份數據存儲,減少不必要拷貝
加上selectorIO多路復用