![](http://image.uc.cn/s/wemedia/s/upload/2024/8911b73aad8a72f7a2f22eb276789f7b.jpg)
零拷貝技術(Zero-Copy)是一個大家耳熟能詳的技術名詞了,它主要用于提升 IO(Input & Output)的傳輸性能。
那麽問題來了,爲什麽零拷貝技術能提升 IO 性能?
1.零拷貝技術和性能在傳統的 IO 操作中,當我們需要讀取並傳輸數據時,我們需要在用戶態(用戶空間)和內核態(內核空間)中進行數據拷貝,它的執行流程如下:
![](http://image.uc.cn/s/wemedia/s/upload/2024/5aeacdca824ba20cbc84d521be4c5e4c.jpg)
從上述流程我們可以看出,在傳統的 IO 操作中,我們是需要 4 次拷貝和 4 次上下文切換(用戶態和內核態的切換)的。
而每次數據拷貝和上下文切換都有時間成本,會讓程序的執行時間變成,所以零拷貝技術的出現就是爲了減少數據的拷貝次數以及上下文的切換次數的。
1.1 什麽是用戶態和內核態?操作系統有用戶態和內核態之分,這是因爲計算機體系結構中的操作系統設計了兩個不同的執行環境,以提供不同的功能和特權級別。
用戶態(User Mode)是指應用程序運行時的執行環境。在用戶態下,應用程序只能訪問受限資源,如應用程序自身的內存空間、CPU 寄存器等,並且不能直接訪問操作系統的底層資源和硬件設備。內核態(Kernel Mode)是指操作系統內核運行時的執行環境。在內核態下,操作系統具有更高的權限,可以直接訪問系統的硬件和底層資源,如 CPU、內存、設備驅動程序等。1.2 什麽是DMA?DMA(Direct Memory Access,直接內存訪問)技術,繞過 CPU,直接在內存和外設之間進行數據傳輸。這樣可以減少 CPU 的參與,提高數據傳輸的效率。
2.Linux零拷貝技術Linux 下實現零拷貝的主要實現技術是 MMap、sendFile,它們的具體介紹如下。
2.1 MMapMMap(Memory Map)是 Linux 操作系統中提供的一種將文件映射到進程地址空間的一種機制,通過 MMap 進程可以像訪問內存一樣訪問文件,而無需顯式的複制操作。
使用 MMap 可以把 IO 執行流程優化成以下執行步驟:
![](http://image.uc.cn/s/wemedia/s/upload/2024/bd55f98655dab64b8791ed29a1c6df08.png)
傳統的 IO 需要四次拷貝和四次上下文(用戶態和內核態)切換,而 MMap 只需要三次拷貝和四次上下文切換,從而能夠提升程序整體的執行效率,並且節省了程序的內存空間。
2.2 senFile 方法在 Linux 操作系統中 sendFile() 是一個系統調用函數,用于高效地將文件數據從內核空間直接傳輸到網絡套接字(Socket)上,從而實現零拷貝技術。這個函數的主要目的是減少 CPU 上下文切換以及內存複制操作,提高文件傳輸性能。
使用 sendFile() 可以把 IO 執行流程優化成以下執行步驟:
![](http://image.uc.cn/s/wemedia/s/upload/2024/c320fedbff55b355ee4367bd6deb105e.png)
Netty 中的零拷貝和傳統 Linux 的零拷貝技術的實現不太一樣,Netty 中的零拷貝技術主要是通過優化用戶態的操作來提升 IO 的執行速度,從而實現零拷貝的。
PS:所有可以提升 IO 執行效率的操作或手段都可以稱之爲零拷貝技術。
Netty 中的零拷貝技術主要有以下 5 種實現:
使用堆外內存:避免 JVM 堆內存到堆外內存的數據拷貝,從而提升了 IO 的操作性能。使用 CompositeByteBuf 合並對象:可以組合多個 Buffer 對象合並成一個邏輯上的對象,避免通過傳統內存拷貝的方式將幾個 Buffer 合並成一個大的 Buffer。通過 Unpooled.wrappedBuffer 合並數據:可以將 byte 數組包裝成 ByteBuf 對象,包裝過程中不會産生內存拷貝。使用 ByteBuf.slice 共享對象:操作與 Unpooled.wrappedBuffer 相反,slice 操作可以將一個 ByteBuf 對象切分成多個 ByteBuf 對象,切分過程中不會産生內存拷貝,底層共享一個 byte 數組的存儲空間。使用 FileRegion 實現零拷貝:FileRegion 底層封裝了 FileChannel#transferTo() 方法,可以將文件緩沖區的數據直接傳輸到目標 Channel,避免內核緩沖區和用戶態緩沖區之間的數據拷貝,這屬于操作系統級別的零拷貝。它們的具體實現如下。
3.1 使用堆外內存正常情況下,JVM 需要將數據從 JVM 堆內存拷貝到堆外內存進行業務執行的,這是因爲:
操作系統並不感知 JVM 的堆內存,而且 JVM 的內存布局與操作系統所分配的是不一樣的,操作系統並不會按照 JVM 的行爲來讀寫數據。同一個對象的內存地址隨著 JVM GC 的執行可能會隨時發生變化,例如 JVM GC 的過程中會通過壓縮來減少內存碎片,這就涉及對象移動的問題了。而 Netty 在進行 I/O 操作時都是使用的堆外內存,可以避免數據從 JVM 堆內存到堆外內存的拷貝。
3.2 使用CompositeByteBuf合並對象CompositeByteBuf 可以理解爲一個虛擬的 Buffer 對象,它是由多個 ByteBuf 組合而成,但是在 CompositeByteBuf 內部保存著每個 ByteBuf 的引用關系,從邏輯上構成一個整體。使用 CompositeByteBuf 我們可以合並兩個 ByteBuf 對象,從而避免兩個對象合並時需要兩次 CPU 拷貝操作的問題,在沒有使用 CompositeByteBuf 時,我們的操作是這樣的:
ByteBuf httpBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());httpBuf.writeBytes(header);httpBuf.writeBytes(body);而實現 header 和 body 這兩個 ByteBuf 的合並,需要先初始化一個新的 httpBuf,然後再將 header 和 body 分別拷貝到新的 httpBuf。合並過程中涉及兩次 CPU 拷貝,這非常浪費性能,所以我們就可以使用 CompositeByteBuf 了,它的使用如下:
CompositeByteBuf httpBuf = Unpooled.compositeBuffer();httpBuf.addComponents(true, header, body);CompositeByteBuf 通過調用 addComponents() 方法來添加多個 ByteBuf,但是底層的 byte 數組是複用的,不會發生內存拷貝。
3.3 通過Unpooled.wrappedBuffer合並數據Unpooled.wrappedBuffer 的操作類似,使用它可以將不同的數據源的一個或者多個數據包裝成一個大的 ByteBuf 對象,其中數據源的類型包括 byte[]、ByteBuf、ByteBuffer。包裝的過程中不會發生數據拷貝操作,包裝後生成的 ByteBuf 對象和原始 ByteBuf 對象是共享底層的 byte 數組。
3.4 使用 ByteBuf.slice 共享對象ByteBuf.slice 和 Unpooled.wrappedBuffer 的邏輯正好相反,ByteBuf.slice 是將一個 ByteBuf 對象切分成多個共享同一個底層存儲的 ByteBuf 對象,從而避免對象分割時的數據拷貝,它的使用如下:
ByteBuf httpBuf = ...ByteBuf header = httpBuf.slice(0, 6);ByteBuf body = httpBuf.slice(6, 4);3.5 使用 FileRegion 實現文件零拷貝FileRegion 底層封裝了 FileChannel#transferTo() 方法,可以將文件緩沖區的數據直接傳輸到目標 Channel,避免內核緩沖區和用戶態緩沖區之間的數據拷貝,這屬于操作系統級別的零拷貝。
以下是 FileRegion 的默認實現類 DefaultFileRegion 的使用案例:
@Overridepublic void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { RandomAccessFile raf = null; long length = -1; try { raf = new RandomAccessFile(msg, "r"); length = raf.length(); } catch (Exception e) { ctx.writeAndFlush("ERR: " + e.getClass().getSimpleName() + ": " + e.getMessage() + '\n'); return; } finally { if (length < 0 && raf != null) { raf.close(); } } ctx.write("OK: " + raf.length() + '\n'); if (ctx.pipeline().get(SslHandler.class) == null) { // SSL not enabled - can use zero-copy file transfer. ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length)); } else { // SSL enabled - cannot use zero-copy file transfer. ctx.write(new ChunkedFile(raf)); } ctx.writeAndFlush("\n");}從上述代碼可以看出,可以通過 DefaultFileRegion 將文件內容直接寫入到 NioSocketChannel 中,從而避免了內核緩沖區和用戶態緩沖區之間的數據拷貝。
課後思考那麽問題來了,FileRegion 是如何實現零拷貝的呢?
本文已收錄到我的面試小站 [www.javacn.site](https://www.javacn.site),其中包含的內容有:Redis、JVM、並發、並發、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、設計模式、消息隊列等模塊。
純屬秀優越感,這些不都是背課文來的答案,問的人也就會背個概念而已,優秀的開源項目那麽多,有幾個中國程序員做出來的?說實話,估計1%都沒。不服的接受各種裝逼面試官同行反駁