目錄
一:了解IO基礎概念
二:數據流動的層次結構
三:零拷貝
1.傳統IO文件讀寫
2.mmap 零拷貝技術
3.sendFile 零拷貝技術
一:了解IO基礎概念
理解CPU拷貝和DMA拷貝
? ????????我們知道,操作系統對于內存空間,是分為用戶態和內核態的。用戶態的應用程序無法直接操作硬件,需要通過內核空間進行操作轉換,才能真正操作硬件。這其實是為了保護操作系統的安全。正因為如此,應用程序需要與網卡、磁盤等硬件進行數據交互時,就需要在用戶態和內核態之間來回的復制數據。而這些操作,原本都是需要由CPU來進行任務的分配、調度等管理步驟的,早先這些IO接口都是由CPU獨立負責,所以當發生大規模的數據讀寫操作時,CPU的占用率會非常高。
之后,操作系統為了避免CPU完全被各種IO調用給占用,引入了DMA(直接存儲器存儲)。由DMA來負責這些頻繁的IO操作。DMA是一套獨立的指令集,不會占用CPU的計算資源。這樣,CPU就不需要參與具體的數據復制的工作,只需要管理DMA的權限即可。
? DMA拷貝極大的釋放了CPU的性能,因此他的拷貝速度會比CPU拷貝要快很多。但是,其實DMA拷貝本身,也在不斷優化。
? 引入DMA拷貝之后,在讀寫請求的過程中,CPU不再需要參與具體的工作,DMA可以獨立完成數據在系統內部的復制。但是,數據復制過程中,依然需要借助數據總進線。當系統內的IO操作過多時,還是會占用過多的數據總線,造成總線沖突,最終還是會影響數據讀寫性能。
? 為了避免DMA總線沖突對性能的影響,后來又引入了Channel通道的方式。Channel,是一個完全獨立的處理器,專門負責IO操作。既然是處理器,Channel就有自己的IO指令,與CPU無關,他也更適合大型的IO操作,性能更高。
? 這也解釋了,為什么Java應用層與零拷貝相關的操作都是通過Channel的子類實現的。這其實是借鑒了操作系統中的概念。
channel知識點:
在計算機系統中,“通道”(Channel) 的具體作用范圍取決于上下文(如硬件架構、操作系統或編程框架)。以下是不同場景下的解釋:
通道是 數據傳輸的路徑或抽象機制,通常不直接等同于“操作系統內存 ? 外設”的物理傳輸,而是分層協作中的一環。
(1) 硬件層的通道(如傳統大型機)
-
功能:
某些系統(如 IBM 大型機)的 I/O 通道 是專用硬件,直接管理外設(磁盤、磁帶)與內存的傳輸。 -
特點:
-
通道是獨立于 CPU 的處理器,可執行復雜的 I/O 指令(如協議解析、數據分塊)。
-
直接與外設控制器交互,完成物理數據傳輸(類似增強版 DMA)。
-
-
示例:
大型機中,通道從磁盤讀取數據到內存,無需 CPU 干預。
(2) 操作系統層的通道(如設備驅動)
-
功能:
操作系統通過 設備驅動 和 內核 I/O 子系統 管理外設與內存的交互。 -
特點:
-
通道在此上下文中更接近 邏輯抽象(如
/dev
下的設備文件)。 -
實際數據傳輸依賴 DMA 或 PIO(編程 I/O)。
-
(3) 編程框架中的通道(如 Java NIO)
-
功能:
Java NIO 的Channel
(如FileChannel
、SocketChannel
)是 用戶空間與內核空間之間的橋梁。 -
特點:
-
通過系統調用與內核交互,數據在用戶緩沖區(如
ByteBuffer
)和內核的 Page Cache 之間傳輸。 -
不直接操作外設,物理傳輸由操作系統和 DMA 完成。
-
二:數據流動的層次結構
計算機系統中,數據從程序到磁盤(或反向)的流動通常經過以下層級:
程序中的 IO 流 → 用戶空間緩沖區 → 操作系統緩存頁(內核空間) → 磁盤驅動 → 物理磁盤
示意圖
程序代碼 用戶空間 內核空間 硬件層
┌───────────┐ ┌─────────────┐ ┌──────────────┐ ┌────────┐
│ IO 流 │ → → → │ 用戶緩沖區 │ → → → │ Page Cache │ → → → │ 磁盤 │
└───────────┘ └─────────────┘ └──────────────┘ └────────┘(程序層) (操作系統層) (物理層)
?而零拷貝技術是減少用戶空間和內存空間之間數據傳輸的次數,接下來,將進入零拷貝的講解。
三:零拷貝
????????零拷貝(Zero-copy) 是一種優化技術,旨在 減少或消除數據在內存中的冗余復制操作,從而提升 I/O 性能。它主要作用于 用戶空間內存與內核空間內存之間的數據傳輸,但最終目標是減少整個數據鏈路(從磁盤到網絡、或內存到外設)中的復制次數。
??????? 對于Java應用層來說,零拷貝有mmap和sendFile兩種方式。
1.傳統IO文件讀寫
??????? 說零拷貝技術之前,要先了解傳統的IO文件讀寫是怎么樣的,才能更好的理解零拷貝技術,下面先說傳統IO的讀寫工作流程。
傳統IO文件讀寫
????? 傳統IO文件讀寫如下圖1-1所示:
??????????????????????????????????????????????? 圖1-1 傳統IO文件讀寫流程圖
傳統IO文件讀寫工作流程:
整體流程
Java 程序 → 用戶空間 → 內核空間(Page Cache) → 磁盤(修改數據) ↑↓(讀寫) (DMA 傳輸)
流程分三個階段:讀取數據 → 修改數據 → 寫回數
詳細步驟與層級交互
(1) 打開文件
-
Java 代碼:使用
FileInputStream
、FileChannel
或RandomAccessFile
打開文件。 -
系統調用:
open()
,觸發內核創建文件描述符,建立程序與文件的連接。
(2) 讀取數據(磁盤 → 內核空間 → 用戶空間)
-
磁盤到內核空間(Page Cache):
-
DMA 傳輸:磁盤控制器通過 DMA(直接內存訪問) 將文件數據直接讀取到內核的 Page Cache,無需 CPU 參與。
-
觸發方式:Java 調用
FileChannel.read(ByteBuffer)
或InputStream.read()
,底層觸發read()
系統調用。
-
-
內核空間到用戶空間:
-
數據拷貝:內核將 Page Cache 中的數據復制到用戶空間的緩沖區(如
byte[]
或ByteBuffer
)。 -
性能開銷:此拷貝由 CPU 完成,是小文件讀取的主要性能瓶頸。
-
(3) 修改數據(用戶空間內操作)
-
Java 操作:在用戶空間的緩沖區中修改數據(如字符串替換、字節操作)。
-
示例:
String content = new String(buffer.array(), StandardCharsets.UTF_8); String modifiedContent = content.replace("old", "new"); byte[] newData = modifiedContent.getBytes(StandardCharsets.UTF_8);
(4) 寫回數據(用戶空間 → 內核空間 → 磁盤)
-
用戶空間到內核空間(Page Cache):
-
數據拷貝:用戶空間的修改后數據通過
FileChannel.write(ByteBuffer)
或OutputStream.write()
觸發write()
系統調用,將數據復制到內核的 Page Cache。 -
延遲寫入:數據暫存于 Page Cache,不會立即寫入磁盤。
-
-
內核空間到磁盤:
-
DMA 傳輸:操作系統通過 DMA 將 Page Cache 中的數據異步寫入磁盤。
-
刷盤時機:
-
定時刷盤:由內核線程(如
pdflush
)定期將臟頁(修改過的數據)寫入磁盤。 -
強制刷盤:調用
FileChannel.force(true)
觸發fsync()
系統調用,確保數據持久化。
-
-
(5) 關閉文件
-
Java 代碼:調用
close()
釋放文件描述符。 -
系統調用:
close()
,釋放內核資源。
2.mmap
零拷貝技術
? mmap
零拷貝技術 的核心是通過內存映射文件(Memory-Mapped File)將文件內容直接映射到用戶空間的虛擬內存,從而避免傳統 I/O 中用戶空間與內核空間之間的數據拷貝。
mmap零拷貝技術IO讀寫
?????? IO文mmap零拷貝技術文件讀寫如下圖1-2所示:
????????????????????????????????????????圖2-1 mmap零拷貝技術文件讀寫流程圖
mmap零拷貝IO文件讀寫工作流程:
工作流程概述
磁盤 → Page Cache →(內存映射)→ 用戶空間虛擬內存 →(修改數據)→ Page Cache → 磁盤
?詳細步驟
1. 打開文件并創建內存映射
-
用戶空間:
使用FileChannel.map()
將文件映射到用戶空間的虛擬內存。 -
內核空間:
內核將文件的磁盤塊映射到 Page Cache,并建立用戶空間虛擬內存與 Page Cache 的映射關系。 -
代碼示例:
FileChannel channel = FileChannel.open(Paths.get("file.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE); MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
2. 讀取數據
-
用戶空間:
用戶程序直接通過MappedByteBuffer
訪問數據,無需顯式調用read()
。 -
內核空間:
若數據未加載到 Page Cache,觸發缺頁中斷,內核從磁盤讀取數據到 Page Cache。 -
零拷貝:
數據直接從 Page Cache 映射到用戶空間,無需復制到用戶緩沖區。
3. 修改數據
-
用戶空間:
用戶程序直接修改MappedByteBuffer
中的數據。 -
內核空間:
修改后的數據標記為 臟頁(Dirty Page),暫存于 Page Cache。
4. 寫回磁盤
-
用戶空間:
調用buffer.force()
強制將臟頁刷回磁盤。 -
內核空間:
內核將臟頁從 Page Cache 寫回磁盤的對應位置。 -
代碼示例:
buffer.force(); // 強制刷盤
5. 關閉映射
-
用戶空間:
關閉FileChannel
,釋放映射的內存區域。 -
內核空間:
解除內存映射,釋放相關資源。 -
代碼示例:
channel.close();
3.sendFile 零拷貝技術
??????? 早期的sendFile其實和mmap一樣,實現機制還是依靠CPU進行頁緩存與socket緩存區之間的數據拷貝,如圖3-1所示。
?????????????????????????????????圖3-1 早期的sendFile 讀寫流程圖
?????????從Linux內核2.6.33版本開始,引入了對 Scatter-Gather DMA(分散-聚集 DMA) 的支持,優化了實現機制,在拷貝過程中,并不直接拷貝文件的內容,而是只拷貝一個帶有文件位置和長度等信息的文件描述符FD,這樣子就大大減少了需要傳遞的數據。而真實的數據內容,會交由DMA控制器,從頁緩存中打包異步發送到socket中。如圖3-2所示。
?????????????????????????????????????? 圖3-2 Linux內核2.6.33 版本 sendFile 讀寫流程圖
注意: sendfile
系統調用 的主要設計目標是 將文件數據高效地發送到網絡套接字,因此它 不支持將用戶空間的數據直接寫入磁盤。
工作流程概述
磁盤 → Page Cache →(sendfile)→ 網卡
詳細步驟
1. 打開文件
-
用戶空間:
使用FileChannel
打開文件。 -
內核空間:
內核將文件的磁盤塊映射到 Page Cache。 -
代碼示例:
FileChannel fileChannel = new FileInputStream("file.txt").getChannel();
2. 打開網絡套接字
-
用戶空間:
使用SocketChannel
打開網絡連接。 -
內核空間:
內核創建 Socket Buffer,用于管理網絡數據。 -
代碼示例:
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("host", 8080));
3. 使用 sendfile
發送數據
-
用戶空間:
調用FileChannel.transferTo()
,底層使用sendfile
系統調用。 -
內核空間:
數據直接從 Page Cache 通過 DMA 發送到網卡,繞過用戶空間。 -
代碼示例:
fileChannel.transferTo(0, fileChannel.size(), socketChannel); // 零拷貝發送
4. 關閉資源
-
用戶空間:
關閉FileChannel
和SocketChannel
,釋放資源。 -
內核空間:
釋放 Page Cache 和 Socket Buffer 資源。 -
代碼示例:
fileChannel.close(); socketChannel.close();