高速網(wǎng)絡(luò)的未來:零拷貝Zero-Copy架構(gòu)
在當今高速發(fā)展的信息技術(shù)領(lǐng)域,追求極致的性能和效率是永恒的主題。而當我們深入探索計算機系統(tǒng)的內(nèi)部奧秘時,一個令人矚目的概念 —— 零拷貝(Zero-Copy)架構(gòu),逐漸走入我們的視野。想象一下,在數(shù)據(jù)如洪流般在系統(tǒng)中穿梭的場景下,傳統(tǒng)的數(shù)據(jù)傳輸方式往往伴隨著頻繁的數(shù)據(jù)復制操作,這不僅消耗了大量的時間和系統(tǒng)資源,還成為了性能提升的瓶頸。而零拷貝架構(gòu)宛如一位神奇的魔術(shù)師,以其獨特的方式打破了這些束縛,為我們展現(xiàn)出一個全新的數(shù)據(jù)處理境界。
那么,零拷貝架構(gòu)究竟有著怎樣的魔力?它是如何實現(xiàn)高效的數(shù)據(jù)傳輸,為系統(tǒng)性能帶來質(zhì)的飛躍?讓我們一同踏上這場充滿驚喜與挑戰(zhàn)的探索之旅,揭開零拷貝架構(gòu)的神秘面紗,領(lǐng)略其在現(xiàn)代計算機系統(tǒng)中所綻放的璀璨光芒。
一、零拷貝架構(gòu)簡介
零拷貝指的是在 I/O 過程中,用戶空間和內(nèi)核空間不需要進行 CPU 數(shù)據(jù)拷貝。傳統(tǒng) I/O 與零拷貝架構(gòu)在數(shù)據(jù)拷貝次數(shù)和上下文切換次數(shù)上存在明顯差異。
零拷貝(zero-copy)基本思想是:數(shù)據(jù)報從網(wǎng)絡(luò)設(shè)備到用戶程序空間傳遞的過程中,減少數(shù)據(jù)拷貝次數(shù),減少系統(tǒng)調(diào)用,實現(xiàn)CPU的零參與,徹底消除 CPU在這方面的負載。實現(xiàn)零拷貝用到的最主要技術(shù)是DMA數(shù)據(jù)傳輸技術(shù)和內(nèi)存區(qū)域映射技術(shù)。如圖下所示,傳統(tǒng)的網(wǎng)絡(luò)數(shù)據(jù)報處理,需要經(jīng)過網(wǎng)絡(luò)設(shè)備到操作系統(tǒng)內(nèi)存空間,系統(tǒng)內(nèi)存空間到用戶應用程序空間這兩次拷貝,同時還需要經(jīng)歷用戶向系統(tǒng)發(fā)出的系統(tǒng)調(diào)用。
而零拷貝技術(shù)則首先利用DMA技術(shù)將網(wǎng)絡(luò)數(shù)據(jù)報直接傳遞到系統(tǒng)內(nèi)核預先分配的地址空間中,避免CPU的參與;同時,將系統(tǒng)內(nèi)核中存儲數(shù)據(jù)報的內(nèi)存區(qū)域映射到檢測程序的應用程序空間(還有一種方式是在用戶空間建立一緩存,并將其映射到內(nèi)核空間,類似于linux系統(tǒng)下的kiobuf技術(shù)),檢測程序直接對這塊內(nèi)存進行訪問,從而減少了系統(tǒng)內(nèi)核向用戶空間的內(nèi)存拷貝,同時減少了系統(tǒng)調(diào)用的開銷,實現(xiàn)了真正的“零拷貝”。
圖片
在傳統(tǒng)的 I/O 操作中,讀取文件并通過 Socket 發(fā)送,需要經(jīng)過多次數(shù)據(jù)拷貝和上下文切換。具體來說,包括 4 次上下文切換、2 次 CPU 數(shù)據(jù)拷貝和 2 次 DMA 控制器數(shù)據(jù)拷貝。而 Linux 零拷貝架構(gòu)旨在減少這些數(shù)據(jù)拷貝和上下文切換的次數(shù),從而提高系統(tǒng)性能。在 Linux 操作系統(tǒng)層面上,有多種實現(xiàn)零拷貝的方案,如內(nèi)存映射(mmap)、sendfile、splice、tee 等。
內(nèi)存映射(mmap)是指用戶空間和內(nèi)核空間的虛擬內(nèi)存地址同時映射到同一塊物理內(nèi)存,用戶態(tài)進程可以直接操作物理內(nèi)存,避免用戶空間和內(nèi)核空間之間的數(shù)據(jù)拷貝。其執(zhí)行流程如下:用戶進程通過系統(tǒng)調(diào)用 mmap 函數(shù)進入內(nèi)核態(tài),發(fā)生第 1 次上下文切換,并建立內(nèi)核緩沖區(qū);發(fā)生缺頁中斷,CPU 通知 DMA 讀取數(shù)據(jù);
DMA 拷貝數(shù)據(jù)到物理內(nèi)存,并建立內(nèi)核緩沖區(qū)和物理內(nèi)存的映射關(guān)系;建立用戶空間的進程緩沖區(qū)和同一塊物理內(nèi)存的映射關(guān)系,由內(nèi)核態(tài)轉(zhuǎn)變?yōu)橛脩魬B(tài),發(fā)生第 2 次上下文切換;用戶進程進行邏輯處理后,通過系統(tǒng)調(diào)用 Socket send,用戶態(tài)進入內(nèi)核態(tài),發(fā)生第 3 次上下文切換;系統(tǒng)調(diào)用 Send 創(chuàng)建網(wǎng)絡(luò)緩沖區(qū),并拷貝內(nèi)核讀緩沖區(qū)數(shù)據(jù);DMA 控制器將網(wǎng)絡(luò)緩沖區(qū)的數(shù)據(jù)發(fā)送網(wǎng)卡,并返回,由內(nèi)核態(tài)進入用戶態(tài),發(fā)生第 4 次上下文切換。
總結(jié)來看,mmap 避免了內(nèi)核空間和用戶空間的 2 次 CPU 拷貝,但增加了 1 次內(nèi)核空間的 CPU 拷貝,整體上相當于只減少了 1 次 CPU 拷貝。針對大文件比較適合 mmap,小文件則會造成較多的內(nèi)存碎片,得不償失。當 mmap 一個文件時,如果文件被另一個進程截獲可能會因為非法訪問導致進程被 SIGBUS 信號終止。
sendfile 是在 linux2.1 引入的,它只需要 2 次上下文切換和 1 次內(nèi)核 CPU 拷貝、2 次 DMA 拷貝。函數(shù)原型為 ssize_t sendfile (int out_fd, int in_fd, off_t *offset, size_t count);out_fd 為文件描述符,in_fd 為網(wǎng)絡(luò)緩沖區(qū)描述符,offset 偏移量(默認 NULL),count 文件大小。
其內(nèi)部執(zhí)行流程是:用戶進程系統(tǒng)調(diào)用 senfile,由用戶態(tài)進入內(nèi)核態(tài),發(fā)生第 1 次上下文切換;CPU 通知 DMA 控制器把文件數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū);內(nèi)核空間自動調(diào)用網(wǎng)絡(luò)發(fā)送功能并拷貝數(shù)據(jù)到網(wǎng)絡(luò)緩沖區(qū);CPU 通知 DMA 控制器發(fā)送數(shù)據(jù);sendfile 系統(tǒng)調(diào)用結(jié)束并返回,進程由內(nèi)核態(tài)進入用戶態(tài),發(fā)生第 2 次上下文切換??偨Y(jié)來看,數(shù)據(jù)處理完全是由內(nèi)核操作,減少了 2 次上下文切換,整個過程 2 次上下文切換、1 次 CPU 拷貝,2 次 DMA 拷貝。雖然可以設(shè)置偏移量,但不能對數(shù)據(jù)進行任何的修改。
Linux2.4 對 sendfile 進行了優(yōu)化,為 DMA 控制器引入了 gather 功能,即 sendfile+DMA gather。在不拷貝數(shù)據(jù)到網(wǎng)絡(luò)緩沖區(qū)的情況下,將待發(fā)送數(shù)據(jù)的內(nèi)存地址和偏移量等描述信息存在網(wǎng)絡(luò)緩沖區(qū),DMA 根據(jù)描述信息從內(nèi)核的讀緩沖區(qū)截取數(shù)據(jù)并發(fā)送。
其流程是:用戶進程系統(tǒng)調(diào)用 senfile,由用戶態(tài)進入內(nèi)核態(tài),發(fā)生第 1 次上下文切換;CPU 通知 DMA 控制器把文件數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū);把內(nèi)核緩沖區(qū)地址和 sendfile 的相關(guān)參數(shù)作為數(shù)據(jù)描述信息存在網(wǎng)絡(luò)緩沖區(qū)中;CPU 通知 DMA 控制器,DMA 根據(jù)網(wǎng)絡(luò)緩沖區(qū)中的數(shù)據(jù)描述截取數(shù)據(jù)并發(fā)送;sendfile 系統(tǒng)調(diào)用結(jié)束并返回,進程由內(nèi)核態(tài)進入用戶態(tài),發(fā)生第 2 次上下文切換??偨Y(jié)來看,需要硬件支持,如 DMA;整個過程 2 次上下文切換,0 次 CPU 拷貝,2 次 DMA 拷貝,實現(xiàn)真正意義上的零拷貝。依然不能修改數(shù)據(jù)。
splice 是鑒于 Sendfile 的缺點,在 Linux2.6.17 中引入的。它在讀緩沖區(qū)和網(wǎng)絡(luò)操作緩沖區(qū)之間建立管道避免 CPU 拷貝:先將文件讀入到內(nèi)核緩沖區(qū),然后再與內(nèi)核網(wǎng)絡(luò)緩沖區(qū)建立管道。其函數(shù)原型為 ssize_t splice (int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags)。執(zhí)行流程如下:用戶進程系統(tǒng)調(diào)用 splice,由用戶態(tài)進入內(nèi)核態(tài),發(fā)生第 1 次上下文切換;CPU 通知 DMA 控制器把文件數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū);建立內(nèi)核緩沖區(qū)和網(wǎng)絡(luò)緩沖區(qū)的管道;CPU 通知 DMA 控制器,DMA 從管道讀取數(shù)據(jù)并發(fā)送??偨Y(jié)來看,依然不能修改數(shù)據(jù)。tee與splice 類同,但 fd_in 和 fd_out 都必須是管道。
⑴什么是零拷貝?
簡單一點來說,零拷貝就是一種避免 CPU 將數(shù)據(jù)從一塊存儲拷貝到另外一塊存儲的技術(shù)。針對操作系統(tǒng)中的設(shè)備驅(qū)動程序、文件系統(tǒng)以及網(wǎng)絡(luò)協(xié)議堆棧而出現(xiàn)的各種零拷貝技術(shù)極大地提升了特定應用程序的性能,并且使得這些應用程序可以更加有效地利用系統(tǒng)資源。這種性能的提升就是通過在數(shù)據(jù)拷貝進行的同時,允許 CPU 執(zhí)行其他的任務來實現(xiàn)的。
零拷貝技術(shù)可以減少數(shù)據(jù)拷貝和共享總線操作的次數(shù),消除傳輸數(shù)據(jù)在存儲器之間不必要的中間拷貝次數(shù),從而有效地提高數(shù)據(jù)傳輸效率。而且,零拷貝技術(shù)減少了用戶應用程序地址空間和操作系統(tǒng)內(nèi)核地址空間之間因為上下文切換而帶來的開銷。進行大量的數(shù)據(jù)拷貝操作其實是一件簡單的任務,從操作系統(tǒng)的角度來說,如果 CPU 一直被占用著去執(zhí)行這項簡單的任務,那么這將會是很浪費資源的;如果有其他比較簡單的系統(tǒng)部件可以代勞這件事情,從而使得 CPU 解脫出來可以做別的事情,那么系統(tǒng)資源的利用則會更加有效。
⑵避免數(shù)據(jù)拷貝
- 避免操作系統(tǒng)內(nèi)核緩沖區(qū)之間進行數(shù)據(jù)拷貝操作。
- 避免操作系統(tǒng)內(nèi)核和用戶應用程序地址空間這兩者之間進行數(shù)據(jù)拷貝操作。
- 用戶應用程序可以避開操作系統(tǒng)直接訪問硬件存儲。
- 數(shù)據(jù)傳輸盡量讓 DMA 來做。
⑶將多種操作結(jié)合在一起
- 避免不必要的系統(tǒng)調(diào)用和上下文切換。
- 需要拷貝的數(shù)據(jù)可以先被緩存起來。
- 對數(shù)據(jù)進行處理盡量讓硬件來做。
前文提到過,對于高速網(wǎng)絡(luò)來說,零拷貝技術(shù)是非常重要的。這是因為高速網(wǎng)絡(luò)的網(wǎng)絡(luò)鏈接能力與 CPU 的處理能力接近,甚至會超過 CPU 的處理能力。
如果是這樣的話,那么 CPU 就有可能需要花費幾乎所有的時間去拷貝要傳輸?shù)臄?shù)據(jù),而沒有能力再去做別的事情,這就產(chǎn)生了性能瓶頸,限制了通訊速率,從而降低了網(wǎng)絡(luò)連接的能力。一般來說,一個 CPU 時鐘周期可以處理一位的數(shù)據(jù)。舉例來說,一個 1 GHz 的處理器可以對 1Gbit/s 的網(wǎng)絡(luò)鏈接進行傳統(tǒng)的數(shù)據(jù)拷貝操作,但是如果是 10 Gbit/s 的網(wǎng)絡(luò),那么對于相同的處理器來說,零拷貝技術(shù)就變得非常重要了。
對于超過 1 Gbit/s 的網(wǎng)絡(luò)鏈接來說,零拷貝技術(shù)在超級計算機集群以及大型的商業(yè)數(shù)據(jù)中心中都有所應用。然而,隨著信息技術(shù)的發(fā)展,1 Gbit/s,10 Gbit/s 以及 100 Gbit/s 的網(wǎng)絡(luò)會越來越普及,那么零拷貝技術(shù)也會變得越來越普及,這是因為網(wǎng)絡(luò)鏈接的處理能力比 CPU 的處理能力的增長要快得多。傳統(tǒng)的數(shù)據(jù)拷貝受限于傳統(tǒng)的操作系統(tǒng)或者通信協(xié)議,這就限制了數(shù)據(jù)傳輸性能。零拷貝技術(shù)通過減少數(shù)據(jù)拷貝次數(shù),簡化協(xié)議處理的層次,在應用程序和網(wǎng)絡(luò)之間提供更快的數(shù)據(jù)傳輸方法,從而可以有效地降低通信延遲,提高網(wǎng)絡(luò)吞吐率。零拷貝技術(shù)是實現(xiàn)主機或者路由器等設(shè)備高速網(wǎng)絡(luò)接口的主要技術(shù)之一。
現(xiàn)代的 CPU 和存儲體系結(jié)構(gòu)提供了很多相關(guān)的功能來減少或避免 I/O 操作過程中產(chǎn)生的不必要的 CPU 數(shù)據(jù)拷貝操作,但是,CPU 和存儲體系結(jié)構(gòu)的這種優(yōu)勢經(jīng)常被過高估計。存儲體系結(jié)構(gòu)的復雜性以及網(wǎng)絡(luò)協(xié)議中必需的數(shù)據(jù)傳輸可能會產(chǎn)生問題,有時甚至會導致零拷貝這種技術(shù)的優(yōu)點完全喪失。在下一章中,我們會介紹幾種 Linux 操作系統(tǒng)中出現(xiàn)的零拷貝技術(shù),簡單描述一下它們的實現(xiàn)方法,并對它們的弱點進行分析。
二、零拷貝分類與優(yōu)勢
2.1零拷貝技術(shù)分類
零拷貝技術(shù)的發(fā)展很多樣化,現(xiàn)有的零拷貝技術(shù)種類也非常多,而當前并沒有一個適合于所有場景的零拷貝技術(shù)的出現(xiàn)。對于 Linux 來說,現(xiàn)存的零拷貝技術(shù)也比較多,這些零拷貝技術(shù)大部分存在于不同的 Linux 內(nèi)核版本,有些舊的技術(shù)在不同的 Linux 內(nèi)核版本間得到了很大的發(fā)展或者已經(jīng)漸漸被新的技術(shù)所代替。本文針對這些零拷貝技術(shù)所適用的不同場景對它們進行了劃分。概括起來,Linux 中的零拷貝技術(shù)主要有下面這幾種:
- 直接 I/O:對于這種數(shù)據(jù)傳輸方式來說,應用程序可以直接訪問硬件存儲,操作系統(tǒng)內(nèi)核只是輔助數(shù)據(jù)傳輸:這類零拷貝技術(shù)針對的是操作系統(tǒng)內(nèi)核并不需要對數(shù)據(jù)進行直接處理的情況,數(shù)據(jù)可以在應用程序地址空間的緩沖區(qū)和磁盤之間直接進行傳輸,完全不需要 Linux 操作系統(tǒng)內(nèi)核提供的頁緩存的支持。
- 在數(shù)據(jù)傳輸?shù)倪^程中,避免數(shù)據(jù)在操作系統(tǒng)內(nèi)核地址空間的緩沖區(qū)和用戶應用程序地址空間的緩沖區(qū)之間進行拷貝。有的時候,應用程序在數(shù)據(jù)進行傳輸?shù)倪^程中不需要對數(shù)據(jù)進行訪問,那么,將數(shù)據(jù)從 Linux 的頁緩存拷貝到用戶進程的緩沖區(qū)中就可以完全避免,傳輸?shù)臄?shù)據(jù)在頁緩存中就可以得到處理。在某些特殊的情況下,這種零拷貝技術(shù)可以獲得較好的性能。Linux 中提供類似的系統(tǒng)調(diào)用主要有 mmap(),sendfile() 以及 splice()。
- 對數(shù)據(jù)在 Linux 的頁緩存和用戶進程的緩沖區(qū)之間的傳輸過程進行優(yōu)化。該零拷貝技術(shù)側(cè)重于靈活地處理數(shù)據(jù)在用戶進程的緩沖區(qū)和操作系統(tǒng)的頁緩存之間的拷貝操作。這種方法延續(xù)了傳統(tǒng)的通信方式,但是更加靈活。在Linux中,該方法主要利用了寫時復制技術(shù)。
前兩類方法的目的主要是為了避免應用程序地址空間和操作系統(tǒng)內(nèi)核地址空間這兩者之間的緩沖區(qū)拷貝操作。這兩類零拷貝技術(shù)通常適用在某些特殊的情況下,比如要傳送的數(shù)據(jù)不需要經(jīng)過操作系統(tǒng)內(nèi)核的處理或者不需要經(jīng)過應用程序的處理。第三類方法則繼承了傳統(tǒng)的應用程序地址空間和操作系統(tǒng)內(nèi)核地址空間之間數(shù)據(jù)傳輸?shù)母拍?,進而針對數(shù)據(jù)傳輸本身進行優(yōu)化。
我們知道,硬件和軟件之間的數(shù)據(jù)傳輸可以通過使用 DMA 來進行,DMA 進行數(shù)據(jù)傳輸?shù)倪^程中幾乎不需要CPU 參與,這樣就可以把 CPU 解放出來去做更多其他的事情,但是當數(shù)據(jù)需要在用戶地址空間的緩沖區(qū)和Linux 操作系統(tǒng)內(nèi)核的頁緩存之間進行傳輸?shù)臅r候,并沒有類似 DMA 這種工具可以使用,CPU 需要全程參與到這種數(shù)據(jù)拷貝操作中,所以這第三類方法的目的是可以有效地改善數(shù)據(jù)在用戶地址空間和操作系統(tǒng)內(nèi)核地址空間之間傳遞的效率。
2.2Linux 零拷貝架構(gòu)的優(yōu)勢
⑴減少了 CPU 拷貝,提升了 I/O 性能。
Linux 零拷貝架構(gòu)通過多種方式減少了 CPU 拷貝操作,從而顯著提升了 I/O 性能。在傳統(tǒng)的 I/O 操作中,數(shù)據(jù)需要在用戶空間和內(nèi)核空間之間進行多次拷貝,這不僅占用了大量的 CPU 資源,還增加了數(shù)據(jù)傳輸?shù)臅r間。而零拷貝架構(gòu)則避免了這些不必要的拷貝操作,直接將數(shù)據(jù)從源地址傳輸?shù)侥繕说刂?,大大提高了?shù)據(jù)傳輸?shù)男省?/p>
例如,內(nèi)存映射(mmap)雖然增加了一次內(nèi)核空間的 CPU 拷貝,但整體上相當于只減少了一次 CPU 拷貝。對于大文件來說,mmap 可以有效地減少數(shù)據(jù)拷貝的次數(shù),提高 I/O 性能。而 sendfile 在 Linux 2.1 引入后,只需要 2 次上下文切換和 1 次內(nèi)核 CPU 拷貝、2 次 DMA 拷貝,相比傳統(tǒng)的 I/O 操作,大大減少了 CPU 拷貝的次數(shù)。在 Linux 2.4 對 sendfile 進行優(yōu)化后,引入了 DMA gather 功能,實現(xiàn)了真正意義上的零拷貝,整個過程只需要 2 次上下文切換和 0 次 CPU 拷貝、2 次 DMA 拷貝。
這些零拷貝技術(shù)的實現(xiàn),使得數(shù)據(jù)傳輸更加高效,減少了 CPU 的負擔,從而提升了系統(tǒng)的整體性能。
⑵降低了用戶態(tài)和內(nèi)核態(tài)切換次數(shù),提高系統(tǒng)效率。
除了減少 CPU 拷貝外,Linux 零拷貝架構(gòu)還降低了用戶態(tài)和內(nèi)核態(tài)的切換次數(shù),進一步提高了系統(tǒng)效率。在傳統(tǒng)的 I/O 操作中,讀取文件并通過 Socket 發(fā)送需要經(jīng)過多次用戶態(tài)和內(nèi)核態(tài)的切換,這不僅增加了系統(tǒng)的開銷,還降低了系統(tǒng)的響應速度。
而零拷貝架構(gòu)通過優(yōu)化數(shù)據(jù)傳輸?shù)牧鞒?,減少了系統(tǒng)調(diào)用的次數(shù),從而降低了用戶態(tài)和內(nèi)核態(tài)的切換次數(shù)。例如,sendfile 只需要 2 次上下文切換,相比傳統(tǒng)的 I/O 操作減少了 2 次切換次數(shù)。這使得系統(tǒng)能夠更加高效地處理數(shù)據(jù)傳輸任務,提高了系統(tǒng)的整體效率。
此外,零拷貝技術(shù)還可以減少內(nèi)存帶寬的占用,提高系統(tǒng)的并發(fā)處理能力。在實際應用中,可以根據(jù)具體的業(yè)務需求選擇合適的零拷貝技術(shù),以達到最佳的性能優(yōu)化效果。
三、零拷貝的定義
Zero-copy, 就是在操作數(shù)據(jù)時, 不需要將數(shù)據(jù) buffer 從一個內(nèi)存區(qū)域拷貝到另一個內(nèi)存區(qū)域. 因為少了一次內(nèi)存的拷貝, 因此 CPU 的效率就得到的提升;在 OS 層面上的 Zero-copy 通常指避免在 用戶態(tài)(User-space) 與 內(nèi)核態(tài)(Kernel-space) 之間來回拷貝數(shù)據(jù)。
Netty 中的 Zero-copy 與 OS 的 Zero-copy 不太一樣, Netty的 Zero-coyp 完全是在用戶態(tài)(Java 層面)的, 它的 Zero-copy 的更多的是偏向于優(yōu)化數(shù)據(jù)操作。
3.1Netty的“零拷貝”
主要體現(xiàn)在如下三個方面:
- Netty的接收和發(fā)送ByteBuffer采用DIRECT BUFFERS,使用堆外直接內(nèi)存進行Socket讀寫,不需要進行字節(jié)緩沖區(qū)的二次拷貝。如果使用傳統(tǒng)的堆內(nèi)存(HEAP BUFFERS)進行Socket讀寫,JVM會將堆內(nèi)存Buffer拷貝一份到直接內(nèi)存中,然后才寫入Socket中。相比于堆外直接內(nèi)存,消息在發(fā)送過程中多了一次緩沖區(qū)的內(nèi)存拷貝。
- Netty提供了組合Buffer對象,可以聚合多個ByteBuffer對象,用戶可以像操作一個Buffer那樣方便得對組合Buffer進行操作,避免了傳統(tǒng)通過內(nèi)存拷貝的方式將幾個小Buffer合并成一個大的Buffer。
- Netty的文件傳輸采用了transferTo方法,它可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標Channel,避免了傳統(tǒng)通過循環(huán)write方式導致的內(nèi)存拷貝問題。
3.2傳統(tǒng) IO 方式
在 java 開發(fā)中,從某臺機器將一份數(shù)據(jù)通過網(wǎng)絡(luò)傳輸?shù)搅硗庖慌_機器,大致的代碼如下:
Socket socket = new Socket(HOST, PORT);
InputStream inputStream = new FileInputStream(FILE_PATH);
OutputStream outputStream = new DataOutputStream(socket.getOutputStream());
byte[] buffer = new byte[4096];
while (inputStream.read(buffer) >= 0) {
outputStream.write(buffer);
}
outputStream.close();
socket.close();
inputStream.close();
看起來代碼很簡單,但如果我們深入到操作系統(tǒng)層面,就會發(fā)現(xiàn)實際的微觀操作更復雜。具體操作如下圖:
圖片
- 用戶進程向OS發(fā)出read()系統(tǒng)調(diào)用,觸發(fā)上下文切換,從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài)。
- CPU發(fā)起IO請求,通過直接內(nèi)存訪問(DMA)從磁盤讀取文件內(nèi)容,復制到內(nèi)核緩沖區(qū)PageCache中
- 將內(nèi)核緩沖區(qū)數(shù)據(jù),拷貝到用戶空間緩沖區(qū),觸發(fā)上下文切換,從內(nèi)核態(tài)轉(zhuǎn)換到用戶態(tài)。
- 用戶進程向OS發(fā)起write系統(tǒng)調(diào)用,觸發(fā)上下文切換,從用戶態(tài)切換到內(nèi)核態(tài)。
- 將數(shù)據(jù)從用戶緩沖區(qū)拷貝到內(nèi)核中與目的地Socket關(guān)聯(lián)的緩沖區(qū)。
- 數(shù)據(jù)最終經(jīng)由Socket通過DMA傳送到硬件(網(wǎng)卡)緩沖區(qū),write()系統(tǒng)調(diào)用返回,并從內(nèi)核態(tài)切換回用戶態(tài)。
圖片
四、零拷貝(Zero-copy)
4.1數(shù)據(jù)拷貝基礎(chǔ)過程
在Linux系統(tǒng)內(nèi)部緩存和內(nèi)存容量都是有限的,更多的數(shù)據(jù)都是存儲在磁盤中。對于Web服務器來說,經(jīng)常需要從磁盤中讀取數(shù)據(jù)到內(nèi)存,然后再通過網(wǎng)卡傳輸給用戶:
圖片
上述數(shù)據(jù)流轉(zhuǎn)只是大框,接下來看看幾種模式。
⑴僅CPU方式
- 當應用程序需要讀取磁盤數(shù)據(jù)時,調(diào)用read()從用戶態(tài)陷入內(nèi)核態(tài),read()這個系統(tǒng)調(diào)用最終由CPU來完成;
- CPU向磁盤發(fā)起I/O請求,磁盤收到之后開始準備數(shù)據(jù);
- 磁盤將數(shù)據(jù)放到磁盤緩沖區(qū)之后,向CPU發(fā)起I/O中斷,報告CPU數(shù)據(jù)已經(jīng)Ready了;
- CPU收到磁盤控制器的I/O中斷之后,開始拷貝數(shù)據(jù),完成之后read()返回,再從內(nèi)核態(tài)切換到用戶態(tài);
圖片
⑵CPU&DMA方式
CPU的時間寶貴,讓它做雜活就是浪費資源。
直接內(nèi)存訪問(Direct Memory Access),是一種硬件設(shè)備繞開CPU獨立直接訪問內(nèi)存的機制。所以DMA在一定程度上解放了CPU,把之前CPU的雜活讓硬件直接自己做了,提高了CPU效率。
目前支持DMA的硬件包括:網(wǎng)卡、聲卡、顯卡、磁盤控制器等。
圖片
有了DMA的參與之后的流程發(fā)生了一些變化:
圖片
主要的變化是,CPU不再和磁盤直接交互,而是DMA和磁盤交互并且將數(shù)據(jù)從磁盤緩沖區(qū)拷貝到內(nèi)核緩沖區(qū),之后的過程類似。
【敲黑板】無論從僅CPU方式和DMA&CPU方式,都存在多次冗余數(shù)據(jù)拷貝和內(nèi)核態(tài)&用戶態(tài)的切換?!?/p>
我們繼續(xù)思考Web服務器讀取本地磁盤文件數(shù)據(jù)再通過網(wǎng)絡(luò)傳輸給用戶的詳細過程。
4.2普通模式數(shù)據(jù)交互
一次完成的數(shù)據(jù)交互包括幾個部分:系統(tǒng)調(diào)用syscall、CPU、DMA、網(wǎng)卡、磁盤等。
圖片
系統(tǒng)調(diào)用syscall是應用程序和內(nèi)核交互的橋梁,每次進行調(diào)用/返回就會產(chǎn)生兩次切換:
- 調(diào)用syscall 從用戶態(tài)切換到內(nèi)核態(tài)
- syscall返回 從內(nèi)核態(tài)切換到用戶態(tài)
圖片
來看下完整的數(shù)據(jù)拷貝過程簡圖:
圖片
讀數(shù)據(jù)過程:
- 應用程序要讀取磁盤數(shù)據(jù),調(diào)用read()函數(shù)從而實現(xiàn)用戶態(tài)切換內(nèi)核態(tài),這是第1次狀態(tài)切換;
- DMA控制器將數(shù)據(jù)從磁盤拷貝到內(nèi)核緩沖區(qū),這是第1次DMA拷貝;
- CPU將數(shù)據(jù)從內(nèi)核緩沖區(qū)復制到用戶緩沖區(qū),這是第1次CPU拷貝;
- CPU完成拷貝之后,read()函數(shù)返回實現(xiàn)用戶態(tài)切換用戶態(tài),這是第2次狀態(tài)切換;
寫數(shù)據(jù)過程:
- 應用程序要向網(wǎng)卡寫數(shù)據(jù),調(diào)用write()函數(shù)實現(xiàn)用戶態(tài)切換內(nèi)核態(tài),這是第1次切換;
- CPU將用戶緩沖區(qū)數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū),這是第1次CPU拷貝;
- DMA控制器將數(shù)據(jù)從內(nèi)核緩沖區(qū)復制到socket緩沖區(qū),這是第1次DMA拷貝;
- 完成拷貝之后,write()函數(shù)返回實現(xiàn)內(nèi)核態(tài)切換用戶態(tài),這是第2次切換;
綜上所述:
- 讀過程涉及2次空間切換、1次DMA拷貝、1次CPU拷貝;
- 寫過程涉及2次空間切換、1次DMA拷貝、1次CPU拷貝;
可見傳統(tǒng)模式下,涉及多次空間切換和數(shù)據(jù)冗余拷貝,效率并不高,接下來就該零拷貝技術(shù)出場了。
4.3零拷貝技術(shù)
(1)出現(xiàn)原因
我們可以看到,如果應用程序不對數(shù)據(jù)做修改,從內(nèi)核緩沖區(qū)到用戶緩沖區(qū),再從用戶緩沖區(qū)到內(nèi)核緩沖區(qū)。兩次數(shù)據(jù)拷貝都需要CPU的參與,并且涉及用戶態(tài)與內(nèi)核態(tài)的多次切換,加重了CPU負擔。
我們需要降低冗余數(shù)據(jù)拷貝、解放CPU,這也就是零拷貝Zero-Copy技術(shù)。
(2)解決思路
目前來看,零拷貝技術(shù)的幾個實現(xiàn)手段包括:mmap+write、sendfile、sendfile+DMA收集、splice等。
圖片
(3)mmap方式
mmap函數(shù)的原型為void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);。其中,addr是開始映射的地址,屬于進程的邏輯地址;length從開始映射地址,映射的長度,一般是一個頁大小,4KB;prot是期望的內(nèi)存保護標志,不能與文件打開的標志沖突,比如文件只可讀,這里就不能可寫,有PROT_EXEC(頁內(nèi)容可以被執(zhí)行)、PROT_READ(頁內(nèi)容可以被讀?。?、PROT_WRITE(頁可以被寫入)、PROT_NONE(頁不可訪問)幾種選項;flags指定映射對象的類型,映射選項和映射頁是否可以共享,如MAX_FIXED(如果參數(shù)start所指的地址無法成功建立映射時,則放棄映射)、MAP_SHARED(與其他映射這個文件的進程共享映射內(nèi)存,可能存在并發(fā)修改)、MAP_PRIVATE(對映射區(qū)域的寫入操作會產(chǎn)生一個映射文件的復制,類似于寫時復制,對此區(qū)域作的任何修改都不會寫回原來的文件內(nèi)容);fd是文件描述符;offset是文件映射的偏移量,已經(jīng)映射了多少。
mmap是Linux提供的一種內(nèi)存映射文件的機制,它實現(xiàn)了將內(nèi)核中讀緩沖區(qū)地址與用戶空間緩沖區(qū)地址進行映射,從而實現(xiàn)內(nèi)核緩沖區(qū)與用戶緩沖區(qū)的共享。這樣就減少了一次用戶態(tài)和內(nèi)核態(tài)的CPU拷貝,但是在內(nèi)核空間內(nèi)仍然有一次CPU拷貝。
mmap+write 工作流程:
- 第一,調(diào)用mmap函數(shù)將文件和進程虛擬地址空間映射。
- 第二,將磁盤數(shù)據(jù)讀取到頁高速緩存。
- 第三,調(diào)用write函數(shù)將頁高速緩存數(shù)據(jù)直接寫入套接字緩沖區(qū)。
- 第四,將套接字緩沖區(qū)的數(shù)據(jù)寫入網(wǎng)卡。
mmap+write 數(shù)據(jù)傳輸流程:
- 用戶進程調(diào)用mmap函數(shù),向內(nèi)核發(fā)起調(diào)用,CPU 從用戶態(tài)切換到內(nèi)核態(tài)。
- 建立文件物理地址和虛擬內(nèi)存映射區(qū)域的映射,或者說是內(nèi)核緩沖區(qū) (頁高速緩存) 和虛擬內(nèi)存映射區(qū)域的映射。
- CPU 向磁盤 DMA 控制器發(fā)送讀取指定位置和大小的指令,DMA 控制器將數(shù)據(jù)從磁盤拷貝到內(nèi)核緩沖區(qū)。
- mmap系統(tǒng)調(diào)用結(jié)束返回,CPU 從內(nèi)核態(tài)切換到用戶態(tài)。
- 用戶進程調(diào)用write函數(shù),向內(nèi)核發(fā)起調(diào)用,CPU 從用戶態(tài)切換到內(nèi)核態(tài)。
- CPU 將頁高速緩存中的數(shù)據(jù)拷貝到套接字緩沖區(qū)。
- CPU 向磁盤 DMA 控制器發(fā)送 DMA 寫指令,DMA 控制器從套接字緩沖區(qū)調(diào)用協(xié)議棧處理,最后把數(shù)據(jù)拷貝到網(wǎng)卡。
- write系統(tǒng)調(diào)用結(jié)束返回,CPU 從內(nèi)核態(tài)切換到用戶態(tài)。
圖片
mmap對大文件傳輸有一定優(yōu)勢,但是小文件可能出現(xiàn)碎片,并且在多個進程同時操作文件時可能產(chǎn)生引發(fā)coredump的signal。
(4)sendfile方式
sendfile函數(shù)原型為ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);。其中,out_fd是寫入的文件描述符;in_fd是寫入文件描述符;offset是從哪個位置開始讀?。籧ount是讀取多少數(shù)據(jù)。
- mmap+write方式有一定改進,但是由系統(tǒng)調(diào)用引起的狀態(tài)切換并沒有減少。
- sendfile系統(tǒng)調(diào)用是在 Linux 內(nèi)核2.1版本中被引入,它建立了兩個文件之間的傳輸通道。
sendfile 工作原理:
- 第一,調(diào)用sendfile函數(shù)。
- 第二,從磁盤讀取數(shù)據(jù),拷貝到內(nèi)核緩沖區(qū)。
- 第三,CPU 將內(nèi)核緩沖區(qū)數(shù)據(jù)拷貝到套接字緩沖區(qū)。
- 第四,套接字緩沖區(qū)數(shù)據(jù)拷貝到網(wǎng)卡。
sendfile 數(shù)據(jù)傳輸流程:
- 用戶進程調(diào)用sendfile函數(shù),向內(nèi)核發(fā)起調(diào)用,CPU 從用戶態(tài)切換內(nèi)核態(tài)。
- CPU 向磁盤 DMA 控制器發(fā)送讀取數(shù)據(jù)的指令,DMA 控制器讀取磁盤數(shù)據(jù),拷貝到頁高速緩存。
- 然后 CPU 將頁高速緩存的數(shù)據(jù)拷貝到套接字緩沖區(qū)。
- CPU 向網(wǎng)卡 DMA 引擎發(fā)送讀取指令,從套接字緩沖區(qū)調(diào)用協(xié)議棧處理,然后數(shù)據(jù)拷貝到網(wǎng)卡。
- sendfile調(diào)用結(jié)束,CPU 從內(nèi)核態(tài)切換到用戶態(tài)。
sendfile + DMA scatter / gather copy
什么是 block DMA:
block DMA 就是要求傳輸數(shù)據(jù)塊的源物理地址和目標物理地址都是連續(xù)的,每次只能傳輸一個數(shù)據(jù)塊,傳輸完成后,中斷機構(gòu)觸發(fā)中斷。
什么是 SG-DMA:
- 第一,SG-DMA 是 scatter /gather 的縮寫,scatter 可以將一個源位置連續(xù)的數(shù)據(jù)塊傳輸?shù)侥康牡仉x散的存儲;gather 可以將源位置分散的數(shù)據(jù)塊傳輸?shù)侥康牡剡B續(xù)的存儲。
- 第二,SG-DMA 會預先維護一個物理上不連續(xù)的塊描述符的鏈表,描述符中包含有數(shù)據(jù)的起始地址和長度。傳輸時只需要遍歷鏈表,按序傳輸數(shù)據(jù),全部完成后發(fā)起一次中斷即可,效率比 Block DMA 要高。
sendfile + DMA scatter /gather copy 的核心思想:
sendfile 設(shè)計的時候并不是針對處理大文件的,如果需要處理大文件的話,需要調(diào)用另外一個接口sendfile64()。它的核心思想就是基于 DMA scatter /gather 來實現(xiàn)的。在 Linux 2.4 內(nèi)核版本中,對sendfile系統(tǒng)方法做了優(yōu)化升級,引入 SG-DMA 技術(shù),需要 DMA 控制器支持。
其實就是對 DMA 拷貝加入了 scatter/gather 操作,它可以直接從內(nèi)核空間緩沖區(qū)中將數(shù)據(jù)讀取到網(wǎng)卡,多省去一次 CPU 拷貝。實現(xiàn)了真正意義上的零拷貝,整個過程 2 次上下文切換,0 次 CPU 拷貝,2 次 DMA 拷貝。
sendfile方式只使用一個函數(shù)就可以完成之前的read+write 和 mmap+write的功能,這樣就少了2次狀態(tài)切換,由于數(shù)據(jù)不經(jīng)過用戶緩沖區(qū),因此該數(shù)據(jù)無法被修改。
splice 系統(tǒng)調(diào)用可以在內(nèi)核緩沖區(qū)和socket緩沖區(qū)之間建立管道來傳輸數(shù)據(jù),避免了兩者之間的 CPU 拷貝操作。
splice也有一些局限,它的兩個文件描述符參數(shù)中有一個必須是管道設(shè)備。
以下使用 FileChannel.transferTo 方法,實現(xiàn) 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();
相比傳統(tǒng)方式,零拷貝的執(zhí)行流程如下圖:
圖片
可以看到,相比傳統(tǒng)方式,零拷貝不走數(shù)據(jù)緩沖區(qū)減少了一些不必要的操作。
4.4零拷貝的應用
零拷貝在很多框架中得到了廣泛使用,常見的比如 Netty、Kafka 等等。
在 kafka 中使用了很多設(shè)計思想,比如分區(qū)并行、順序?qū)懭搿㈨摼彺?、高效序列化、零拷貝等等?/p>
上邊博客分析了 Kafka 的大概架構(gòu),知道了 kafka 中的文件都是以.log 文件存儲,每個日志文件對應兩個索引文件.index 與.timeindex。kafka 在傳輸數(shù)據(jù)時利用索引,使用 fileChannel.transferTo (position, count, socketChannel) 指定數(shù)據(jù)位置與大小實現(xiàn)零拷貝。
kafka 底層傳輸源碼:(TransportLayer)
/**
* Transfers bytes from `fileChannel` to this `TransportLayer`.
*
* This method will delegate to {@link FileChannel#transferTo(long, long, java.nio.channels.WritableByteChannel)},
* but it will unwrap the destination channel, if possible, in order to benefit from zero copy. This is required
* because the fast path of `transferTo` is only executed if the destination buffer inherits from an internal JDK
* class.
*
* @param fileChannel The source channel
* @param position The position within the file at which the transfer is to begin; must be non-negative
* @param count The maximum number of bytes to be transferred; must be non-negative
* @return The number of bytes, possibly zero, that were actually transferred
* @see FileChannel#transferTo(long, long, java.nio.channels.WritableByteChannel)
*/
long transferFrom(FileChannel fileChannel, long position, long count) throws IOException;
實現(xiàn)類(PlaintextTransportLayer):
@OverRide
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel.transferTo(position, count, socketChannel);
}
該方法的功能是將 FileChannel 中的數(shù)據(jù)傳輸?shù)?TransportLayer,也就是 SocketChannel。在實現(xiàn)類 PlaintextTransportLayer 的對應方法中,就是直接調(diào)用了 FileChannel.transferTo () 方法。
五、零拷貝架構(gòu)應用場景
5.1文件下載服務中的應用
在文件下載服務中,傳統(tǒng)的 I/O 操作會導致多次數(shù)據(jù)拷貝和上下文切換,降低了系統(tǒng)性能。而 Linux 零拷貝架構(gòu)可以有效地解決這個問題。例如,在文件下載服務中,可以使用內(nèi)存映射(mmap)、sendfile 等零拷貝技術(shù),直接將文件從磁盤讀取到內(nèi)核緩沖區(qū),然后再通過網(wǎng)絡(luò)發(fā)送到客戶端,避免了用戶空間和內(nèi)核空間之間的數(shù)據(jù)拷貝,提高了文件下載的速度。
此外,零拷貝技術(shù)還可以減少內(nèi)存帶寬的占用,提高系統(tǒng)的并發(fā)處理能力。在文件下載服務中,可以同時處理多個客戶端的請求,提高系統(tǒng)的吞吐量。
5.2Kafka 中的應用
Apache Kafka 是一款開源的、分布式的、高吞吐量的流平臺,廣泛用于實時數(shù)據(jù)流的處理。Kafka 的高性能得益于其服務端和客戶端的架構(gòu)設(shè)計,以及關(guān)鍵的設(shè)計和優(yōu)化技術(shù),如服務端的順序?qū)懘疟P、零拷貝,客戶端的批量發(fā)送等。
在 Kafka 中,使用了 Linux 的零拷貝技術(shù) ——sendfile 系統(tǒng)調(diào)用來將消息從頁面緩存發(fā)送到網(wǎng)絡(luò)套接字。這樣,數(shù)據(jù)可以在內(nèi)核空間內(nèi)直接傳輸,避免了在用戶空間和內(nèi)核空間之間來回拷貝數(shù)據(jù),大大提高了數(shù)據(jù)傳輸?shù)男省?/p>
此外,Kafka 的客戶端也充分考慮了性能優(yōu)化。一個重要的優(yōu)化技術(shù)是批量發(fā)送,即客戶端將多條消息打包成一個批次,然后一次性發(fā)送到服務器。這種方式減少了網(wǎng)絡(luò)交互的開銷,提高了整體的吞吐量。
五、零拷貝架構(gòu)的實例
6.1數(shù)據(jù)采集設(shè)備中的應用
在數(shù)據(jù)采集設(shè)備中,傳統(tǒng)的 I/O 操作會導致數(shù)據(jù)在設(shè)備驅(qū)動、應用層和網(wǎng)卡之間進行多次拷貝,消耗大量的 CPU 資源,降低系統(tǒng)效率。而采用 Linux 零拷貝技術(shù)可以有效地解決這個問題。
例如,實際應用中有一數(shù)據(jù)采集設(shè)備 A,產(chǎn)生大量數(shù)據(jù)流。應用層讀取通過該設(shè)備驅(qū)動獲得數(shù)據(jù),處理后再通過網(wǎng)卡分發(fā)出去。在傳統(tǒng)操作中,數(shù)據(jù)要由 A 驅(qū)動 copy 到應用層,再由應用層處理后再次走網(wǎng)卡驅(qū)動,cpu 除了要進行大量的應用處理(計算)還要 copy,效率很低,接收端會出現(xiàn)數(shù)據(jù)卡的現(xiàn)象。
采用 zero-copy 技術(shù)后,具體實現(xiàn)如下:內(nèi)核端定義共享內(nèi)存大小和相關(guān)變量,通過一系列操作獲取內(nèi)核緩沖區(qū)的虛擬地址和物理地址,并將物理地址傳給應用層。應用層通過獲得的物理地址打開內(nèi)存映射,獲取共享內(nèi)存指針,直接讀取其中的內(nèi)容。這樣就避免了數(shù)據(jù)在設(shè)備驅(qū)動和應用層之間的拷貝,提高了系統(tǒng)效率。
6.2Java 中的零拷貝方式
Java 中主要有兩種方式實現(xiàn)零拷貝:使用 FileChannel 的 transferTo 和 transferFrom 方法,以及使用 MappedByteBuffer。
使用 FileChannel 的 transferTo 和 transferFrom 方法:FileChannel 類提供了 transferTo 和 transferFrom 方法,可以在兩個通道之間直接傳輸數(shù)據(jù),而無需將數(shù)據(jù)拷貝到用戶空間。例如,一個使用 FileChannel 的 transferTo 方法實現(xiàn)文件復制的示例中,通過將數(shù)據(jù)從源通道直接傳輸?shù)侥繕送ǖ?,避免了?shù)據(jù)在內(nèi)核空間和用戶空間之間的拷貝。
使用 MappedByteBuffer:MappedByteBuffer 允許我們將文件的某個區(qū)域直接映射到內(nèi)存中,從而可以像操作內(nèi)存一樣來操作文件。這種方式同樣減少了數(shù)據(jù)拷貝的次數(shù)。例如,一個使用 MappedByteBuffer 實現(xiàn)文件讀取和寫入的示例中。