徹底搞懂零拷貝:提升系統性能的關鍵技術
在當今數據洪流奔涌的時代,系統性能的優化可謂至關重要。你是否好奇,為何有的系統能在海量數據傳輸中,依舊保持高效流暢,而有的卻陷入卡頓?答案或許就藏在一項神秘的技術 —— 零拷貝(Zero-Copy)中。它宛如一位隱匿在幕后的高手,默默地提升著系統性能,讓數據傳輸實現飛躍式的加速。
想象一下,在傳統的數據傳輸模式里,數據如同一位歷經波折的旅人,在磁盤、內核緩沖區、用戶緩沖區以及網絡緩沖區之間來回輾轉,每一次的 “落腳” 與 “啟程”,都伴隨著 CPU 的忙碌搬運。這不僅耗費了大量的 CPU 資源,還讓數據傳輸的速度大打折扣。而零拷貝技術的出現,就像是為數據搭建了一條高速公路,減少甚至避免了這些不必要的 “旅途”,讓數據能夠快速、高效地抵達目的地。零拷貝技術究竟有著怎樣的魔力?它與 DMA、PageCache 又有著怎樣千絲萬縷的聯系?接下來,就讓我們一同揭開零拷貝技術的神秘面紗,深入探索其背后的奧秘,相信你定會為這項神奇的技術所折服。
Part1.零拷貝(zero-copy)技術
1.1傳統數據拷貝的困境
在深入了解零拷貝之前,讓我們先來剖析一下傳統數據拷貝的工作方式。想象一下,你正在從服務器上下載一個文件,這個看似簡單的操作背后,其實涉及到了一系列復雜的數據傳輸過程。
當你發起下載請求時,操作系統會先從磁盤中讀取文件數據。這個過程中,數據首先會被讀取到內核緩沖區,然后再被拷貝到用戶空間的應用程序緩沖區。接著,應用程序將數據發送到網絡,數據又會從用戶空間緩沖區拷貝到內核空間的 socket 緩沖區,最后通過網卡發送出去。
具體來說,傳統 I/O 的數據傳輸過程如下:
- 用戶態到內核態切換:應用程序調用 read 系統調用,請求讀取文件數據。此時,CPU 從用戶態切換到內核態,開始執行內核中的代碼。
- 磁盤數據讀取到內核緩沖區:內核通過 DMA(直接內存訪問)技術,將磁盤數據直接拷貝到內核緩沖區。DMA 技術可以讓硬件設備(如磁盤控制器)直接訪問內存,而不需要 CPU 的干預,從而減輕 CPU 的負擔。
- 內核緩沖區數據拷貝到用戶緩沖區:數據從內核緩沖區拷貝到用戶空間的應用程序緩沖區。這一步需要 CPU 的參與,因為用戶空間和內核空間是相互隔離的,數據不能直接在兩者之間傳遞。
- 內核態到用戶態切換:read 系統調用返回,CPU 從內核態切換回用戶態,應用程序可以處理用戶緩沖區中的數據。
- 用戶態到內核態切換:應用程序調用 write 系統調用,請求將數據發送到網絡。CPU 再次從用戶態切換到內核態。
- 用戶緩沖區數據拷貝到 socket 緩沖區:數據從用戶緩沖區拷貝到內核空間的 socket 緩沖區,準備通過網絡發送出去。這一步同樣需要 CPU 的參與。
- socket 緩沖區數據發送到網卡:內核通過 DMA 技術,將 socket 緩沖區中的數據拷貝到網卡緩沖區,然后通過網絡發送出去。
- 內核態到用戶態切換:write 系統調用返回,CPU 從內核態切換回用戶態,數據傳輸完成。
從上述過程可以看出,傳統 I/O 在一次簡單的文件傳輸中,就涉及了 4 次用戶態與內核態的上下文切換,以及 4 次數據拷貝(其中 2 次是 DMA 拷貝,2 次是 CPU 拷貝)。上下文切換和數據拷貝都會消耗 CPU 資源和時間,在高并發的場景下,這些開銷會嚴重影響系統的性能。例如,在一個高并發的文件服務器中,如果每一次文件傳輸都要經歷如此繁瑣的過程,那么服務器的吞吐量將會受到極大的限制,響應速度也會變慢,用戶體驗也會大打折扣。
1.2零拷貝的定義與核心思想
為了突破傳統數據拷貝的性能瓶頸,零拷貝技術應運而生。零拷貝(zero-copy)并不是指完全不進行數據拷貝,而是一種通過操作系統內核優化,減少數據在用戶空間(User Space)與內核空間(Kernel Space)之間冗余拷貝的技術,甚至完全避免不必要的 CPU 數據搬運,從而顯著提升數據傳輸效率、降低 CPU 占用率。其核心目標可以總結為以下幾點:
- 減少數據拷貝次數:傳統的數據傳輸方式通常需要進行多次數據拷貝,而零拷貝技術通過巧妙的設計,盡可能地減少了這種不必要的復制。例如,在某些實現方式中,數據可以直接在內核空間中進行傳輸,避免了在用戶空間和內核空間之間的來回拷貝,將傳統的 4 次拷貝減少到最少 0 次 CPU 拷貝。
- 減少上下文切換次數:上下文切換是指 CPU 從一個任務切換到另一個任務時,需要保存和恢復任務的狀態信息,這個過程會消耗一定的時間和資源。零拷貝技術通過減少系統調用的次數,從而減少了用戶態和內核態之間的上下文切換次數。例如,傳統的 I/O 操作需要 2 次系統調用(read/write),會導致 4 次用戶態→內核態切換,而零拷貝技術可以將系統調用次數減少到 1 次,大大降低了上下文切換的開銷。
- 避免內存冗余占用:在傳統的數據傳輸中,數據往往需要在用戶空間緩沖區中重復存儲,這不僅浪費了內存資源,還增加了數據管理的復雜性。零拷貝技術通過讓數據直接在內核空間中流轉,避免了數據在用戶空間的重復存儲,提高了內存的利用率。
以 Linux 系統中的sendfile系統調用為例,這是一種常見的零拷貝實現方式。在使用sendfile時,數據可以直接從內核緩沖區傳輸到 socket 緩沖區,而不需要經過用戶空間。具體過程如下:
- 用戶態到內核態切換:應用程序調用sendfile系統調用,請求將文件數據發送到網絡。CPU 從用戶態切換到內核態。
- 磁盤數據讀取到內核緩沖區:內核通過 DMA 技術,將磁盤數據直接拷貝到內核緩沖區。
- 內核緩沖區數據直接傳輸到 socket 緩沖區:內核直接將內核緩沖區中的數據傳輸到 socket 緩沖區,而不需要經過用戶空間。這一步利用了內核的特殊機制,直接在內核空間中完成數據的傳輸,避免了數據在用戶空間和內核空間之間的拷貝。
- socket 緩沖區數據發送到網卡:內核通過 DMA 技術,將 socket 緩沖區中的數據拷貝到網卡緩沖區,然后通過網絡發送出去。
- 內核態到用戶態切換:sendfile系統調用返回,CPU 從內核態切換回用戶態,數據傳輸完成。
對比傳統 I/O 和零拷貝技術的數據傳輸路徑,可以明顯看出零拷貝技術的優勢。在傳統 I/O 中,數據需要在用戶空間和內核空間之間多次拷貝,而在零拷貝技術中,數據可以直接在內核空間中傳輸,減少了數據拷貝的次數和上下文切換的開銷。這就好比在物流運輸中,傳統 I/O 就像是貨物需要多次裝卸、轉運,而零拷貝技術則像是貨物可以直接從起點運輸到終點,中間不需要多次中轉,大大提高了運輸的效率。
避免數據拷貝:
- 避免操作系統內核緩沖區之間進行數據拷貝操作。
- 避免操作系統內核和用戶應用程序地址空間這兩者之間進行數據拷貝操作。
- 用戶應用程序可以避開操作系統直接訪問硬件存儲。
- 數據傳輸盡量讓 DMA 來做。
將多種操作結合在一起
- 避免不必要的系統調用和上下文切換。
- 需要拷貝的數據可以先被緩存起來。
- 對數據進行處理盡量讓硬件來做。
對于高速網絡來說,零拷貝技術是非常重要的。這是因為高速網絡的網絡鏈接能力與 CPU 的處理能力接近,甚至會超過 CPU 的處理能力。
如果是這樣的話,那么 CPU 就有可能需要花費幾乎所有的時間去拷貝要傳輸的數據,而沒有能力再去做別的事情,這就產生了性能瓶頸,限制了通訊速率,從而降低了網絡連接的能力。一般來說,一個 CPU 時鐘周期可以處理一位的數據。舉例來說,一個 1 GHz 的處理器可以對 1Gbit/s 的網絡鏈接進行傳統的數據拷貝操作,但是如果是 10 Gbit/s 的網絡,那么對于相同的處理器來說,零拷貝技術就變得非常重要了。
對于超過 1 Gbit/s 的網絡鏈接來說,零拷貝技術在超級計算機集群以及大型的商業數據中心中都有所應用。然而,隨著信息技術的發展,1 Gbit/s,10 Gbit/s 以及 100 Gbit/s 的網絡會越來越普及,那么零拷貝技術也會變得越來越普及,這是因為網絡鏈接的處理能力比 CPU 的處理能力的增長要快得多。傳統的數據拷貝受限于傳統的操作系統或者通信協議,這就限制了數據傳輸性能。零拷貝技術通過減少數據拷貝次數,簡化協議處理的層次,在應用程序和網絡之間提供更快的數據傳輸方法,從而可以有效地降低通信延遲,提高網絡吞吐率。零拷貝技術是實現主機或者路由器等設備高速網絡接口的主要技術之一。
現代的 CPU 和存儲體系結構提供了很多相關的功能來減少或避免 I/O 操作過程中產生的不必要的 CPU 數據拷貝操作,但是,CPU 和存儲體系結構的這種優勢經常被過高估計。存儲體系結構的復雜性以及網絡協議中必需的數據傳輸可能會產生問題,有時甚至會導致零拷貝這種技術的優點完全喪失。在下一章中,我們會介紹幾種 Linux 操作系統中出現的零拷貝技術,簡單描述一下它們的實現方法,并對它們的弱點進行分析。
Part2.DMA技術:零拷貝的硬件基石
2.1 RDMA 是何方神圣?
RDMA,全稱遠程直接數據存取(Remote Direct Memory Access),是一種創新性的網絡通信技術。在傳統網絡通信模式下,數據傳輸往往需要經過操作系統及多層軟件協議棧的處理,這會導致大量的 CPU 資源被占用,數據傳輸延遲較高。而 RDMA 技術的出現,旨在解決這些問題,它能夠讓計算機直接訪問遠程計算機的內存,而無需在本地和遠程計算機之間進行繁瑣的數據復制,從而顯著降低數據傳輸的延遲,提高數據處理效率,這使得它在現代網絡通信中占據著至關重要的地位,尤其在對網絡性能要求極高的領域,如高性能計算(HPC)、數據中心、云計算等,發揮著不可或缺的作用。
圖片
2.2 DMA的工作原理
DMA(Direct Memory Access,直接內存訪問)是一種能夠讓硬件設備(如磁盤控制器、網卡等)直接與內存進行數據傳輸的技術,而不需要 CPU 全程參與數據搬運過程。在傳統的數據傳輸方式中,CPU 需要親自將數據從一個存儲區域搬運到另一個存儲區域,這就像一個人需要親自搬著貨物從一個倉庫運到另一個倉庫,不僅耗費體力(CPU 資源),而且效率低下。而 DMA 技術就像是引入了一輛自動搬運車,它可以自己按照設定的路線(傳輸參數),將貨物(數據)從一個倉庫(源地址)搬運到另一個倉庫(目標地址),而人(CPU)則可以去做其他更重要的事情。
DMA 的工作流程可以分為以下幾個關鍵步驟:
- 初始化階段:在數據傳輸開始之前,CPU 需要對 DMA 控制器進行初始化配置。這就好比給自動搬運車設定好出發地、目的地和搬運貨物的數量。CPU 會向 DMA 控制器寫入源地址(數據的來源位置,例如磁盤的某個扇區)、目標地址(數據要傳輸到的位置,例如內存的某個區域)以及傳輸數據的長度等參數 。
- DMA 請求階段:當硬件設備準備好要傳輸的數據時,它會向 DMA 控制器發送一個 DMA 請求信號,就像貨物已經準備好了,通知自動搬運車來取貨。
- 總線控制權獲取階段:DMA 控制器接收到請求后,會向 CPU 發送總線請求信號,請求接管系統總線的控制權。因為在同一時刻,系統總線只能被一個設備使用,所以 DMA 控制器需要先獲取總線控制權,才能進行數據傳輸。CPU 在完成當前的總線操作后,會響應 DMA 請求,將總線控制權交給 DMA 控制器,就像人暫時放下手中與總線相關的工作,讓自動搬運車使用運輸通道(總線)。
- 數據傳輸階段:在獲得總線控制權后,DMA 控制器開始按照預先設定的參數,直接在硬件設備和內存之間進行數據傳輸。它會從源地址讀取數據,然后寫入到目標地址,這個過程不需要 CPU 的干預,自動搬運車按照設定路線,將貨物從源倉庫搬運到目標倉庫。
- 傳輸完成通知階段:當數據傳輸完成后,DMA 控制器會向 CPU 發送一個中斷信號,通知 CPU 數據傳輸已經結束,就像自動搬運車完成搬運任務后,通知人可以繼續其他工作了。CPU 收到中斷信號后,會進行相應的處理,例如檢查數據傳輸是否正確,或者啟動下一個任務。
在這個過程中,涉及到的硬件組件主要有 DMA 控制器、硬件設備(如磁盤、網卡)和內存。DMA 控制器是整個數據傳輸過程的核心控制單元,它負責協調硬件設備和內存之間的數據傳輸;硬件設備是數據的來源或目的地;內存則是數據存儲和中轉的地方。它們之間通過系統總線進行數據和信號的傳輸,共同完成高效的數據傳輸任務。
下面是 RDMA 整體框架架構圖,從圖中可以看出,RDMA 提供了一系列 Verbs 接口,可在應用程序用戶空間,操作RDMA硬件。RDMA繞過內核直接從用戶空間訪問RDMA 網卡。RNIC(RDMA 網卡,RNIC(NIC=Network Interface Card ,網絡接口卡、網卡,RNIC即 RDMA Network Interface Card)中包括 Cached Page Table Entry,用來將虛擬頁面映射到相應的物理頁面。
圖片
(1)直接內存訪問機制
RDMA 的核心在于其直接內存訪問機制。在傳統的網絡通信中,數據傳輸需要 CPU 的深度參與,例如數據從應用程序緩沖區拷貝到內核緩沖區,再通過網絡協議棧進行封裝和傳輸,接收端則需要逆向操作,將數據從內核緩沖區拷貝到應用程序緩沖區,整個過程涉及多次數據拷貝和 CPU 上下文切換,效率較低。
而 RDMA 允許計算機直接存取其他計算機的內存,繞過了處理器的繁瑣處理過程,數據傳輸的大部分工作由硬件來執行,直接在遠程系統的內存之間進行讀寫操作,極大地提高了數據傳輸的效率和速度,減少了 CPU 的負擔,使得系統能夠將更多的資源用于實際的數據處理任務,從而提升整體性能。
(2)零拷貝與內核旁路
零拷貝技術是 RDMA 的另一大關鍵特性。在傳統通信模式下,數據在傳輸過程中需要在不同的內存區域之間進行多次拷貝,例如從用戶空間拷貝到內核空間,再從內核空間拷貝到網絡設備緩沖區等,這些拷貝操作不僅消耗 CPU 資源,還會增加數據傳輸的延遲。而 RDMA 實現了零拷貝,使得數據能夠直接在應用程序的緩沖區與網絡之間進行傳輸,無需中間的拷貝環節,大大減少了數據傳輸的開銷和延遲。
內核旁路也是 RDMA 提升性能的重要手段。在傳統網絡通信中,應用程序與網絡設備之間的通信需要經過操作系統內核的干預,這會導致上下文切換和系統調用的開銷。而 RDMA 允許應用程序在用戶態直接與網卡進行交互,避免了內核態與用戶態之間的上下文切換,進一步降低了 CPU 的負擔,提高了數據傳輸的效率和響應速度。例如,在一些對實時性要求極高的金融交易系統中,RDMA 的零拷貝和內核旁路技術能夠確保交易數據的快速傳輸和處理,減少交易延遲,提高交易效率。
(3)RDMA通信協議
目前,有三種支持RDMA的通信技術:
- InfiniBand(IB): 基于 InfiniBand 架構的 RDMA 技術,需要專用的 IB 網卡和 IB 交換機。從性能上,很明顯Infiniband網絡最好,但網卡和交換機是價格也很高。
- RoCE:即RDMA over Ethernet(RoCE), 基于以太網的 RDMA 技術,也是由 IBTA 提出。RoCE支持在標準以太網基礎設施上使用RDMA技術,但是需要交換機支持無損以太網傳輸,只不過網卡必須是支持RoCE的特殊的NIC。
- iWARP:Internet Wide Area RDMA Protocal,基于 TCP/IP 協議的 RDMA 技術(在現有TCP/IP協議棧基礎上實現RDMA技術,在TCP協議上增加一層DDP),由 IETF 標 準定義。iWARP 支持在標準以太網基礎設施上使用 RDMA 技術,而不需要交換機支持無損以太網傳輸,但服務器需要使用支持iWARP 的網卡。與此同時,受 TCP 影響,性能稍差。
這三種技術都可以使用同一套API來使用,但它們有著不同的物理層和鏈路層;需要注意的是,上述幾種協議都需要專門的硬件(網卡)支持。
2.3 DMA 在零拷貝中的角色
在零拷貝技術中,DMA 扮演著至關重要的角色,它是實現數據高效傳輸的關鍵硬件支撐。零拷貝的核心目標是減少數據在用戶空間和內核空間之間的拷貝次數,以及降低 CPU 在數據傳輸過程中的參與度,而 DMA 技術正好能夠滿足這些需求,實現數據在內核空間或不同硬件設備間的直接傳輸。
以網絡數據接收為例,當網卡接收到網絡數據包時,傳統的方式是先將數據傳輸到內核緩沖區,然后 CPU 再將數據從內核緩沖區拷貝到用戶空間的應用程序緩沖區。這個過程不僅涉及多次數據拷貝,而且 CPU 需要花費大量時間在數據搬運上。而借助 DMA 技術,網卡可以直接通過 DMA 控制器將接收到的數據包傳輸到內存中的內核緩沖區,無需 CPU 參與這一數據傳輸過程。
CPU 可以在 DMA 傳輸數據的同時,去處理其他任務,如運行應用程序的業務邏輯等。這樣一來,大大減少了 CPU 的負載,提高了系統的整體性能和響應速度,就像在物流配送中,貨物可以直接從發貨地通過高效的運輸工具(DMA)送到倉庫(內核緩沖區),而不需要人工(CPU)一次次地搬運,節省了人力和時間成本。
再看磁盤數據讀取的場景,當應用程序需要從磁盤讀取數據時,如果沒有 DMA 技術,CPU 需要不斷地從磁盤讀取數據塊,并將其拷貝到內存中。而有了 DMA,磁盤控制器可以通過 DMA 控制器將磁盤數據直接傳輸到內存中的指定位置。在 Linux 系統中,當應用程序調用 read 系統調用來讀取磁盤文件時,內核會初始化 DMA 操作,將磁盤數據直接讀取到內核的 PageCache(頁緩存)中。這個過程中,CPU 只需要初始化 DMA 傳輸參數,然后就可以去執行其他任務,數據的實際傳輸由 DMA 控制器負責。
數據讀取完成后,DMA 控制器會通知 CPU,CPU 再根據需要將數據從 PageCache 拷貝到用戶空間(如果需要的話)。相比傳統方式,這種借助 DMA 的零拷貝方式減少了 CPU 的干預,提高了數據讀取的效率,使得系統能夠更快速地響應用戶的請求,就像在圖書館借書,以前需要讀者自己在書架間查找并搬運書籍,現在有了自動借書系統(DMA),讀者只需要在系統上登記(CPU 初始化參數),系統就會自動將書籍送到指定位置,讀者可以利用等待的時間去做其他事情。
Part3.PageCache:零拷貝的軟件加速器
3.1 PageCache 的工作機制
PageCache 是 Linux 內核中一種至關重要的磁盤數據緩存機制,它猶如一個智能的高速數據中轉站,極大地提升了文件系統的讀寫性能。PageCache 主要由物理內存中的頁面組成,這些頁面的內容對應于磁盤上的物理塊,其大小通常為 4KB。PageCache 就像一個精心管理的倉庫,將磁盤上的數據緩存到內存中,使得后續對這些數據的訪問能夠直接從內存中快速獲取,而無需頻繁地訪問速度較慢的磁盤。
當用戶進程對文件發起讀取請求時,內核會首先如同一個敏銳的偵察兵,迅速檢查該文件的內容是否已被加載到內存中的 PageCache 中。如果數據在緩存中,即發生了緩存命中,這就好比在倉庫中輕松找到了所需的貨物,內核可以直接從內存中讀取數據并返回給用戶進程,這個過程非常迅速,就像從身邊的架子上直接取物一樣,避免了耗時的磁盤 I/O 操作。例如,當我們多次讀取同一個配置文件時,第一次讀取后數據被緩存到 PageCache 中,后續讀取就可以直接從緩存中獲取,大大提高了讀取速度。
然而,如果數據不在緩存中,即發生了緩存未命中,情況就會稍微復雜一些。此時,內核會如同接到緊急任務的調度員,發起磁盤 I/O 操作。內核會通過 DMA 技術,利用 DMA 控制器這個高效的搬運工,將磁盤上的數據直接傳輸到內存中的 PageCache 中。這個過程就像是從遠方的倉庫緊急調配貨物到本地倉庫。數據傳輸完成后,內核會將數據填充到 PageCache 中,并建立起文件與緩存頁面之間的映射關系,就像給貨物貼上標簽并分類存放,以便后續訪問。同時,為了進一步提高性能,內核還可能會采用預讀技術,根據局部性原理,預測用戶接下來可能需要訪問的數據,并提前將其讀取到 PageCache 中,就像提前準備好可能會用到的貨物,隨時待命。
在寫數據時,PageCache 采用了一種延遲寫(write - back)策略,這是一種高效的數據寫入優化方式。當用戶進程請求寫入數據到文件時,內核并不會立即將數據寫入磁盤,而是先如同將貨物暫存在倉庫的臨時區域一樣,將數據寫入 PageCache 中,并將對應的頁面標記為 “臟頁”,表示該頁面中的數據與磁盤上的數據不一致。這樣做的好處是顯而易見的,它允許內核將多次寫操作合并為一次,就像將多個小訂單合并成一個大訂單一起處理,減少了磁盤 I/O 的次數。例如,當我們編輯一個文檔并多次保存時,數據并不會每次都立即寫入磁盤,而是先緩存在 PageCache 中,等到合適的時機再統一寫入磁盤。
那么,什么時候會將臟頁中的數據真正寫入磁盤呢?這主要有以下幾種觸發條件:一是時間間隔,系統會定期如每隔一段時間(例如 5 秒、30 秒等)喚醒回寫線程,這個回寫線程就像一個定時巡檢員,掃描臟頁并將其寫回磁盤;二是內存壓力,當系統可用內存不足時,內核需要回收 PageCache 頁來分配新內存,此時會先將臟頁寫回磁盤,就像倉庫空間不足時,需要將暫存的貨物整理歸位;三是臟頁比例,當系統中臟頁數量超過一定閾值時,也會觸發回寫,以保證系統的穩定性;四是顯式同步,應用程序調用 fsync ()、fdatasync () 或 sync () 等系統調用時,會強制刷新文件和文件系統的臟頁,將數據立即寫入磁盤,這就像是緊急調用,要求立即將特定的貨物發送出去;五是文件關閉或卸載時,通常也會觸發相關臟頁回寫,確保數據的完整性。在回寫過程中,回寫線程會找到臟頁,發起磁盤 I/O 將數據寫回到磁盤文件對應的位置,就像將貨物準確無誤地送回遠方的倉庫,寫成功后,該頁標記為 “干凈”,表示數據已同步。
3.2 PageCache 與零拷貝的協同效應
PageCache 與零拷貝技術之間存在著緊密的協同關系,它們相互配合,如同默契的搭檔,共同為高效的數據傳輸提供了強大的支持。在傳統的數據傳輸過程中,數據往往需要在用戶空間和內核空間之間多次拷貝,這不僅耗費時間,還占用大量的 CPU 資源。而 PageCache 和零拷貝技術的結合,巧妙地解決了這一問題。
以 sendfile 系統調用為例,這是實現零拷貝的一種重要方式。當應用程序調用 sendfile 系統調用來傳輸文件時,數據首先會從磁盤讀取到 PageCache 中。如果 PageCache 中已經緩存了所需的數據,那么就可以直接從 PageCache 中獲取,避免了再次從磁盤讀取數據的開銷。這一步就像是在本地倉庫中直接找到了要發送的貨物,無需再從遠方倉庫調配。然后,數據可以直接從 PageCache 拷貝到 socket 緩沖區,準備通過網絡發送出去。
在這個過程中,數據不需要經過用戶空間,減少了一次 CPU 拷貝,實現了數據在內核空間的直接傳輸。這就好比貨物從本地倉庫直接被搬運到發貨區,無需經過其他中間環節,大大提高了傳輸效率。最后,通過 DMA 技術將 socket 緩沖區中的數據傳輸到網卡,發送到網絡中,這個過程同樣減少了 CPU 的參與,提高了傳輸速度。
在實際應用中,像 Nginx 這樣的高性能 Web 服務器就充分利用了 PageCache 和零拷貝技術。當 Nginx 處理靜態文件的傳輸時,通過 sendfile 系統調用,借助 PageCache 的緩存功能,將文件數據直接從磁盤緩存傳輸到網絡,減少了數據拷貝和上下文切換的開銷,從而能夠快速地響應大量的客戶端請求,提高了服務器的吞吐量和性能。在一個高并發的 Web 服務場景中,大量用戶同時請求下載靜態資源,Nginx 利用 PageCache 和零拷貝技術,能夠迅速地將文件數據傳輸給用戶,使得用戶能夠快速地獲取到所需的資源,提升了用戶體驗。
Part4.零拷貝技術的實現方式
4.1 mmap+write
mmap+write 是零拷貝技術的一種實現方式,它通過將文件映射到內存,實現了數據的高效傳輸。在傳統的數據傳輸方式中,當應用程序需要讀取文件數據并發送時,數據需要從磁盤讀取到內核緩沖區,再從內核緩沖區拷貝到用戶空間緩沖區,最后從用戶空間緩沖區拷貝到 socket 緩沖區進行發送,這個過程涉及多次數據拷貝和上下文切換,效率較低。
而 mmap+write 方式則巧妙地利用了虛擬內存的特性,減少了數據拷貝的次數。具體來說,mmap 系統調用會將文件的內容映射到進程的虛擬地址空間中,使得應用程序可以直接訪問文件數據,就像訪問內存中的數據一樣。這個映射過程實際上是在內核空間中完成的,文件數據并沒有真正拷貝到用戶空間,而是通過頁表機制建立了文件與內存之間的映射關系。此時,應用程序對映射內存區域的任何讀寫操作,實際上都是對文件的讀寫操作,數據的修改會直接反映到文件中。
當應用程序需要將文件數據發送到網絡時,只需要調用 write 系統調用,將映射內存區域的數據寫入 socket 緩沖區。由于映射內存區域與文件數據是共享的,所以這個過程中不需要再次將數據從用戶空間拷貝到內核空間,只需要進行一次從內核空間的文件映射區域到 socket 緩沖區的拷貝,然后通過 DMA 技術將 socket 緩沖區的數據發送到網卡,實現數據的網絡傳輸。
mmap+write 方式的優勢在于減少了一次 CPU 拷貝,即將數據從內核緩沖區拷貝到用戶緩沖區的過程。這使得數據傳輸的效率得到了顯著提升,特別是在處理大文件或者高并發的數據傳輸場景中,能夠有效降低 CPU 的使用率,提高系統的整體性能。例如,在一個文件服務器中,當大量用戶同時請求下載文件時,使用 mmap+write 方式可以快速地將文件數據發送給用戶,減少了服務器的負載,提升了用戶體驗。
mmap+write 工作流程:
- 第一,調用mmap函數將文件和進程虛擬地址空間映射。
- 第二,將磁盤數據讀取到頁高速緩存。
- 第三,調用write函數將頁高速緩存數據直接寫入套接字緩沖區。
- 第四,將套接字緩沖區的數據寫入網卡。
mmap+write 數據傳輸流程:
- 用戶進程調用mmap函數,向內核發起調用,CPU 從用戶態切換到內核態。
- 建立文件物理地址和虛擬內存映射區域的映射,或者說是內核緩沖區 (頁高速緩存) 和虛擬內存映射區域的映射。
- CPU 向磁盤 DMA 控制器發送讀取指定位置和大小的指令,DMA 控制器將數據從磁盤拷貝到內核緩沖區。
- mmap系統調用結束返回,CPU 從內核態切換到用戶態。
- 用戶進程調用write函數,向內核發起調用,CPU 從用戶態切換到內核態。
- CPU 將頁高速緩存中的數據拷貝到套接字緩沖區。
- CPU 向磁盤 DMA 控制器發送 DMA 寫指令,DMA 控制器從套接字緩沖區調用協議棧處理,最后把數據拷貝到網卡。
- write系統調用結束返回,CPU 從內核態切換到用戶態。
圖片
mmap對大文件傳輸有一定優勢,但是小文件可能出現碎片,并且在多個進程同時操作文件時可能產生引發coredump的signal。
4.2 sendfile
sendfile 是 Linux 系統中實現零拷貝的重要系統調用,它為文件到網絡的數據傳輸提供了一種高效的方式。sendfile 最早在 Linux 2.1 版本中被引入,隨著內核版本的不斷更新,其功能和性能也在不斷優化和提升。
在早期的 Linux 內核版本(如 2.1 - 2.3 版本)中,sendfile 系統調用已經能夠實現將文件數據直接從內核緩沖區傳輸到 socket 緩沖區,避免了數據在用戶空間的拷貝。當應用程序調用 sendfile 時,數據首先通過 DMA 技術從磁盤讀取到內核的 PageCache 中,然后內核直接將 PageCache 中的數據拷貝到 socket 緩沖區,最后通過 DMA 將 socket 緩沖區的數據發送到網卡。這個過程減少了傳統方式中數據從內核緩沖區到用戶緩沖區,再從用戶緩沖區到 socket 緩沖區的兩次拷貝,提高了數據傳輸效率。
到了 Linux 2.4 版本,sendfile 系統調用引入了 SG - DMA(Scatter - Gather Direct Memory Access,分散 / 聚集直接內存訪問)技術,實現了真正意義上的零拷貝。在這種模式下,當調用 sendfile 時,數據從磁盤讀取到內核緩沖區后,不再需要將數據從內核緩沖區拷貝到 socket 緩沖區,而是通過 SG - DMA 技術,直接根據數據描述符(包含數據的位置和長度等信息)從內核緩沖區讀取數據并發送到網卡。這使得數據傳輸過程中完全沒有 CPU 參與的數據拷貝操作,進一步降低了 CPU 的使用率,提高了數據傳輸的速度和系統的吞吐量。
splice 系統調用可以在內核緩沖區和socket緩沖區之間建立管道來傳輸數據,避免了兩者之間的 CPU 拷貝操作。
圖片
splice也有一些局限,它的兩個文件描述符參數中有一個必須是管道設備。
以下使用 FileChannel.transferTo 方法,實現 zero-copy:
SocketAddress socketAddress = new InetSocketAddress(HOST, PORT);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(socketAddress);
File file = new File(FILE_PATH);
FileChannel fileChannel = new FileInputStream(file).getChannel();
fileChannel.transferTo(0, file.length(), socketChannel);
fileChannel.close();
socketChannel.close();相比傳統方式,零拷貝的執行流程如下圖:
圖片
可以看到,相比傳統方式,零拷貝不走數據緩沖區減少了一些不必要的操作。
4.3 splice 和 tee
splice 和 tee 是 Linux 內核中另外兩種實現零拷貝的方式,它們為數據在不同文件描述符之間的傳輸提供了更加靈活和高效的解決方案。
splice 系統調用允許在兩個文件描述符之間直接傳輸數據,而無需經過用戶空間。它通過在內核空間中建立一個管道緩沖區,實現了數據的高效流動。當使用 splice 進行數據傳輸時,數據可以從一個文件描述符(如文件、管道、socket 等)直接 “拼接” 到另一個文件描述符中。
具體來說,當調用 splice 時,內核會在兩個文件描述符對應的內核緩沖區之間建立一個數據傳輸通道,數據通過 DMA 技術直接在這兩個緩沖區之間傳輸,避免了數據在用戶空間的拷貝和 CPU 的參與。這種方式非常適用于需要在不同設備或數據源之間進行數據傳輸的場景,比如將文件數據直接傳輸到 socket 進行網絡發送,或者在兩個文件之間進行數據復制等。例如,在一個網絡代理服務器中,splice 可以用于將客戶端發送的數據直接轉發到目標服務器,提高數據轉發的效率。
splice 函數在網絡編程和文件處理等領域有著廣泛的應用。在網絡編程中,它可以用于實現高效的網絡數據轉發、文件上傳下載等功能。在文件處理中,splice 可用于在不同文件描述符之間快速地移動數據,例如將一個大文件的內容快速地復制到另一個文件中。
以下是一個使用 splice 函數實現簡單回顯服務的代碼示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>
int main(int argc, char **argv) {
if (argc <= 2) {
printf("usage: %s ip port\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
// 創建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
// 設置端口復用
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
// 綁定套接字
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
inet_pton(AF_INET, ip, &address.sin_addr);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret!= -1);
// 監聽連接
ret = listen(sock, 5);
assert(ret!= -1);
// 接受連接
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if (connfd < 0) {
printf("errno is: %s\n", strerror(errno));
} else {
// 創建管道
int pipefd[2];
ret = pipe(pipefd);
assert(ret!= -1);
// 將客戶端數據定向到管道
ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret!= -1);
// 將管道數據定向回客戶端
ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret!= -1);
// 關閉連接
close(connfd);
}
// 關閉套接字
close(sock);
return 0;
}在這段代碼中,首先創建了一個 TCP 套接字,并將其綁定到指定的 IP 地址和端口進行監聽。當有客戶端連接時,接受客戶端的連接請求。接著,創建一個管道,用于在客戶端和服務器之間傳輸數據。通過兩次調用 splice 函數,將客戶端發送的數據通過管道回顯給客戶端。在這個過程中,數據直接在內核空間中通過管道和 socket 進行傳輸,避免了數據在用戶空間的拷貝,提高了數據傳輸的效率。
通過使用 splice 函數實現回顯服務,可以看到其在網絡編程中的優勢。與傳統的數據傳輸方式相比,splice 減少了數據拷貝和上下文切換的開銷,尤其在處理大量數據傳輸或高并發的網絡請求時,能夠顯著提升網絡應用的性能。它為開發者提供了一種高效、簡潔的方式來實現數據的快速傳輸和處理,使得網絡編程在數據傳輸方面更加高效和可靠。
tee 系統調用則主要用于在兩個管道文件描述符之間復制數據,并且復制過程不會消耗數據,即源管道中的數據不會因為復制而被刪除。這意味著數據可以同時被發送到多個目標,且不影響原來的數據流。tee 的工作原理是在內核空間中創建一個臨時緩沖區,將源管道中的數據復制到這個緩沖區,然后再將緩沖區中的數據分別寫入到目標管道中。這種特性使得 tee 非常適合用于日志記錄和實時數據分析等場景。在一個分布式系統中,需要將某個關鍵數據的副本發送到多個不同的處理節點進行分析和處理,同時又要保證原始數據的完整性,此時就可以使用 tee 系統調用將數據復制到多個管道,分別發送到不同的節點,實現數據的多向傳輸和處理 。
Part5.零拷貝的應用場景
5.1網絡傳輸
在網絡傳輸領域,零拷貝技術發揮著至關重要的作用,它為提升數據傳輸效率和網絡性能帶來了顯著的優勢。
在網絡服務器中,零拷貝技術被廣泛應用于加速文件傳輸和提高網絡吞吐量。以高性能 Web 服務器 Nginx 為例,當用戶請求下載一個靜態文件時,Nginx 通過 sendfile 系統調用,借助零拷貝技術,將文件數據直接從磁盤的 PageCache 傳輸到網絡 socket 緩沖區,然后發送給客戶端。這個過程避免了數據在用戶空間和內核空間之間的多次拷貝,減少了 CPU 的使用率和上下文切換次數,從而能夠快速地響應大量的客戶端請求,提高了服務器的并發處理能力和文件傳輸速度。在一個高并發的電商網站中,大量用戶同時請求下載商品圖片、文檔等靜態資源,Nginx 利用零拷貝技術,可以輕松應對這些請求,確保用戶能夠快速獲取所需資源,提升了用戶體驗。
CDN(Content Delivery Network,內容分發網絡)內容分發網絡也是零拷貝技術的重要應用場景。CDN 的主要任務是將內容(如視頻、音頻、網頁等)快速分發給分布在不同地理位置的用戶。在 CDN 節點中,當需要將緩存的內容發送給用戶時,零拷貝技術可以減少數據傳輸的時間和資源消耗。通過零拷貝,數據可以直接從磁盤或內存緩存中傳輸到網絡,提高了內容分發的速度和效率,確保用戶能夠流暢地觀看視頻、瀏覽網頁等,減少了卡頓和加載時間。在視頻流媒體服務中,CDN 利用零拷貝技術,將視頻內容快速傳輸給用戶,保證了視頻播放的流暢性,讓用戶能夠享受高質量的觀看體驗。
5.2文件處理
在文件處理方面,零拷貝技術同樣展現出了強大的性能優勢,它能夠顯著減少 I/O 操作,提升文件處理的效率。
在文件讀寫場景中,傳統的文件讀寫方式通常需要多次數據拷貝,導致 I/O 性能較低。而零拷貝技術通過 mmap 或 sendfile 等方式,可以實現數據的高效讀寫。使用 mmap 將文件映射到內存后,應用程序可以直接通過內存操作來讀寫文件,避免了傳統的 read 和 write 系統調用帶來的數據拷貝開銷。在處理大文件時,這種方式可以大大提高文件的讀寫速度,減少 I/O 等待時間。在日志處理系統中,大量的日志數據需要頻繁地寫入磁盤,使用零拷貝技術可以快速地將日志數據寫入文件,提高了日志處理的效率。
在數據庫備份場景中,零拷貝技術也發揮著重要作用。數據庫備份通常涉及大量數據的讀取和傳輸,如果采用傳統方式,會消耗大量的時間和資源。利用零拷貝技術,數據庫可以直接將數據從磁盤傳輸到備份存儲設備或網絡,減少了數據在內存中的拷貝次數,提高了備份的速度和效率。在一些大型數據庫系統中,每天都需要進行全量或增量備份,使用零拷貝技術可以大大縮短備份時間,減少對數據庫正常運行的影響。
Part6.java提供的零拷貝方式
6.1 Java NIO對mmap的支持
Java NIO有一個MappedByteBuffer的類,可以用來實現內存映射。它的底層是調用了Linux內核的mmap的API。
public class MmapTest { public static void main(String[] args) { try {
FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); //數據傳輸 writeChannel.write(data);
readChannel.close();
writeChannel.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}6.2Java NIO對sendfile的支持
FileChannel的transferTo()/transferFrom(),底層就是sendfile() 系統調用函數。Kafka 這個開源項目就用到它,平時面試的時候,回答面試官為什么這么快,就可以提到零拷貝sendfile這個點。
public class SendFileTest { public static void main(String[] args) { try {
FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ); long len = readChannel.size(); long position = readChannel.position();
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); //數據傳輸 readChannel.transferTo(position, len, writeChannel);
readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}6.3案例分析
使用 Java NIO 中的零拷貝技術的案例分析及代碼實現示例,用于將一個文件從磁盤讀取并通過網絡發送給客戶端。
傳統方式的問題
在傳統的文件傳輸方式中,當從磁盤讀取文件并通過網絡發送時,通常需要進行多次數據拷貝。首先,數據從磁盤讀取到內核緩沖區,然后從內核緩沖區拷貝到用戶空間緩沖區,接著當要通過網絡發送時,又要從用戶空間緩沖區拷貝到內核的 socket 緩沖區,最后從內核 socket 緩沖區發送到網絡。這不僅消耗了大量的 CPU 時間和內存帶寬,還增加了數據傳輸的延遲。
例如,在一個簡單的文件服務器應用中,如果使用傳統的方式處理大量的文件傳輸請求,服務器的 CPU 可能會因為頻繁的數據拷貝而負載過高,導致響應其他請求變慢,同時也會影響文件傳輸的速度和效率。
零拷貝的優勢
零拷貝技術可以避免這些不必要的數據拷貝操作。在 Java 中,可以使用FileChannel的transferTo方法來實現零拷貝。通過這種方式,數據可以直接從磁盤文件讀取到網絡通道,減少了數據在用戶空間和內核空間之間的來回拷貝,從而提高了傳輸效率,降低了 CPU 使用率和延遲。
對于大規模文件傳輸場景,如視頻文件分發、大文件下載服務等,零拷貝技術可以顯著提高系統的性能和吞吐量,能夠更快地響應客戶端請求,同時減少服務器資源的消耗。
代碼實現:
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ZeroCopyFileServer {
public static void main(String[] args) throws IOException {
// 監聽端口
int port = 8888;
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(port));
while (true) {
// 等待客戶端連接
SocketChannel socketChannel = serverSocketChannel.accept();
// 打開文件并獲取FileChannel
String filePath = "your_file_path_here"; // 替換為實際文件路徑
try (FileInputStream fileInputStream = new FileInputStream(filePath);
FileChannel fileChannel = fileInputStream.getChannel()) {
// 使用零拷貝將文件數據傳輸到網絡通道
long bytesTransferred = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("Transferred " + bytesTransferred + " bytes.");
}
// 關閉連接
socketChannel.close();
}
}
}- 首先,創建了一個ServerSocketChannel并綁定到指定端口8888,用于監聽客戶端連接。
- 在循環中,接受客戶端連接后,打開要傳輸的文件,獲取FileChannel。
- 然后,使用FileChannel的transferTo方法將文件數據直接傳輸到SocketChannel(代表與客戶端的連接通道),實現了零拷貝的數據傳輸。
- 最后,關閉與客戶端的連接,等待下一個客戶端連接。





























