來(lái)源:古時(shí)的風(fēng)箏
我們總會(huì)在各種地方看到零拷貝,那零拷貝到底是個(gè)什么東西。
接下來(lái),讓我們來(lái)理一理啊。
拷貝說的是計(jì)算機(jī)里的 I/O 操作,也就是數(shù)據(jù)的讀寫操作。計(jì)算機(jī)可是一個(gè)復(fù)雜的家伙,包括軟件和硬件兩大部分,軟件主要指操作系統(tǒng)、驅(qū)動(dòng)程序和應(yīng)用程序。硬件那就多了,CPU、內(nèi)存、硬盤等等一大堆東西。
(資料圖片僅供參考)
這么復(fù)雜的設(shè)備要進(jìn)行讀寫操作,其中繁瑣和復(fù)雜程度可想而知。
傳統(tǒng)I/O的讀寫過程如果要了解零拷貝,那就必須要知道一般情況下,計(jì)算機(jī)是如何讀寫數(shù)據(jù)的,我把這種情況稱為傳統(tǒng) I/O。
數(shù)據(jù)讀寫的發(fā)起者是計(jì)算機(jī)中的應(yīng)用程序,比如我們常用的瀏覽器、辦公軟件、音視頻軟件等。
而數(shù)據(jù)的來(lái)源呢,一般是硬盤、外部存儲(chǔ)設(shè)備或者是網(wǎng)絡(luò)套接字(也就是網(wǎng)絡(luò)上的數(shù)據(jù)通過網(wǎng)口+網(wǎng)卡的處理)。
過程本來(lái)是很復(fù)雜的,所以大學(xué)課程里要通過《操作系統(tǒng)》、《計(jì)算機(jī)組成原理》來(lái)專門講計(jì)算機(jī)的軟硬件。
簡(jiǎn)化版讀操作流程那么細(xì)的沒辦法講來(lái),所以,我們把這個(gè)讀寫過程簡(jiǎn)化一下,忽略大多數(shù)細(xì)節(jié),只講流程。
圖片
上圖是應(yīng)用程序進(jìn)行一次讀操作的過程。
應(yīng)用程序先發(fā)起讀操作,準(zhǔn)備讀取數(shù)據(jù)了;內(nèi)核將數(shù)據(jù)從硬盤或外部存儲(chǔ)讀取到內(nèi)核緩沖區(qū);內(nèi)核將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū);應(yīng)用程序讀取用戶緩沖區(qū)的數(shù)據(jù)進(jìn)行處理加工;詳細(xì)的讀寫操作流程下面是一個(gè)更詳細(xì)的 I/O 讀寫過程。這個(gè)圖可好用極了,我會(huì)借助這個(gè)圖來(lái)厘清 I/O 操作的一些基礎(chǔ)但非常重要的概念。
圖片
先看一下這個(gè)圖,上面紅粉色部分是讀操作,下面藍(lán)色部分是寫操作。
如果一下子看著有點(diǎn)兒迷糊的話,沒關(guān)系,看看下面幾個(gè)概念就清楚了。
應(yīng)用程序就是安裝在操作系統(tǒng)上的各種應(yīng)用。
系統(tǒng)內(nèi)核系統(tǒng)內(nèi)核是一些列計(jì)算機(jī)的核心資源的集合,不僅包括CPU、總線這些硬件設(shè)備,也包括進(jìn)程管理、文件管理、內(nèi)存管理、設(shè)備驅(qū)動(dòng)、系統(tǒng)調(diào)用等一些列功能。
外部存儲(chǔ)外部存儲(chǔ)就是指硬盤、U盤等外部存儲(chǔ)介質(zhì)。
內(nèi)核態(tài)內(nèi)核態(tài)是操作系統(tǒng)內(nèi)核運(yùn)行的模式,當(dāng)操作系統(tǒng)內(nèi)核執(zhí)行特權(quán)指令時(shí),處于內(nèi)核態(tài)。在內(nèi)核態(tài)下,操作系統(tǒng)內(nèi)核擁有最高權(quán)限,可以訪問計(jì)算機(jī)的所有硬件資源和敏感數(shù)據(jù),執(zhí)行特權(quán)指令,控制系統(tǒng)的整體運(yùn)行。內(nèi)核態(tài)提供了操作系統(tǒng)管理和控制計(jì)算機(jī)硬件的能力,它負(fù)責(zé)處理系統(tǒng)調(diào)用、中斷、硬件異常等核心任務(wù)。用戶態(tài)這里的用戶可以理解為應(yīng)用程序,這個(gè)用戶是對(duì)于計(jì)算機(jī)的內(nèi)核而言的,對(duì)于內(nèi)核來(lái)說,系統(tǒng)上的各種應(yīng)用程序會(huì)發(fā)出指令來(lái)調(diào)用內(nèi)核的資源,這時(shí)候,應(yīng)用程序就是內(nèi)核的用戶。
用戶態(tài)是應(yīng)用程序運(yùn)行的模式,當(dāng)應(yīng)用程序執(zhí)行普通的指令時(shí),處于用戶態(tài)。在用戶態(tài)下,應(yīng)用程序只能訪問自己的內(nèi)存空間和受限的硬件資源,無(wú)法直接訪問操作系統(tǒng)的敏感數(shù)據(jù)或控制計(jì)算機(jī)的硬件設(shè)備。用戶態(tài)提供了一種安全的運(yùn)行環(huán)境,確保應(yīng)用程序之間相互隔離,防止惡意程序?qū)ο到y(tǒng)造成影響。模式切換計(jì)算機(jī)為了安全性考慮,區(qū)分了內(nèi)核態(tài)和用戶態(tài),應(yīng)用程序不能直接調(diào)用內(nèi)核資源,必須要切換到內(nèi)核態(tài)之后,讓內(nèi)核來(lái)調(diào)用,內(nèi)核調(diào)用完資源,再返回給應(yīng)用程序,這個(gè)時(shí)候,系統(tǒng)在切換會(huì)用戶態(tài),應(yīng)用程序在用戶態(tài)下才能處理數(shù)據(jù)。
上述過程其實(shí)一次讀和一次寫都分別發(fā)生了兩次模式切換。
圖片
內(nèi)核緩沖區(qū)內(nèi)核緩沖區(qū)指內(nèi)存中專門用來(lái)給內(nèi)核直接使用的內(nèi)存空間。可以把它理解為應(yīng)用程序和外部存儲(chǔ)進(jìn)行數(shù)據(jù)交互的一個(gè)中間介質(zhì)。
應(yīng)用程序想要讀外部數(shù)據(jù),要從這里讀。應(yīng)用程序想要寫入外部存儲(chǔ),要通過內(nèi)核緩沖區(qū)。
用戶緩沖區(qū)用戶緩沖區(qū)可以理解為應(yīng)用程序可以直接讀寫的內(nèi)存空間。因?yàn)閼?yīng)用程序沒法直接到內(nèi)核讀寫數(shù)據(jù), 所以應(yīng)用程序想要處理數(shù)據(jù),必須先通過用戶緩沖區(qū)。
磁盤緩沖區(qū)磁盤緩沖區(qū)是計(jì)算機(jī)內(nèi)存中用于暫存從磁盤讀取的數(shù)據(jù)或?qū)?shù)據(jù)寫入磁盤之前的臨時(shí)存儲(chǔ)區(qū)域。它是一種優(yōu)化磁盤 I/O 操作的機(jī)制,通過利用內(nèi)存的快速訪問速度,減少對(duì)慢速磁盤的頻繁訪問,提高數(shù)據(jù)讀取和寫入的性能和效率。
PageCachePageCache 是 Linux 內(nèi)核對(duì)文件系統(tǒng)進(jìn)行緩存的一種機(jī)制。它使用空閑內(nèi)存來(lái)緩存從文件系統(tǒng)讀取的數(shù)據(jù)塊,加速文件的讀取和寫入操作。當(dāng)應(yīng)用程序或進(jìn)程讀取文件時(shí),數(shù)據(jù)會(huì)首先從文件系統(tǒng)讀取到 PageCache 中。如果之后再次讀取相同的數(shù)據(jù),就可以直接從 PageCache 中獲取,避免了再次訪問文件系統(tǒng)。同樣,當(dāng)應(yīng)用程序或進(jìn)程將數(shù)據(jù)寫入文件時(shí),數(shù)據(jù)會(huì)先暫存到 PageCache 中,然后由 Linux 內(nèi)核異步地將數(shù)據(jù)寫入磁盤,從而提高寫入操作的效率。再說數(shù)據(jù)讀寫操作流程上面弄明白了這幾個(gè)概念后,再回過頭看一下那個(gè)流程圖,是不是就清楚多了。
讀操作首先應(yīng)用程序向內(nèi)核發(fā)起讀請(qǐng)求,這時(shí)候進(jìn)行一次模式切換了,從用戶態(tài)切換到內(nèi)核態(tài);內(nèi)核向外部存儲(chǔ)或網(wǎng)絡(luò)套接字發(fā)起讀操作;將數(shù)據(jù)寫入磁盤緩沖區(qū);系統(tǒng)內(nèi)核將數(shù)據(jù)從磁盤緩沖區(qū)拷貝到內(nèi)核緩沖區(qū),順便再將一份(或者一部分)拷貝到 PageCache;內(nèi)核將數(shù)據(jù)拷貝到用戶緩沖區(qū),供應(yīng)用程序處理。此時(shí)又進(jìn)行一次模態(tài)切換,從內(nèi)核態(tài)切換回用戶態(tài);寫操作應(yīng)用程序向內(nèi)核發(fā)起寫請(qǐng)求,這時(shí)候進(jìn)行一次模式切換了,從用戶態(tài)切換到內(nèi)核態(tài);內(nèi)核將要寫入的數(shù)據(jù)從用戶緩沖區(qū)拷貝到 PageCache,同時(shí)將數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū);然后內(nèi)核將數(shù)據(jù)寫入到磁盤緩沖區(qū),從而寫入磁盤,或者直接寫入網(wǎng)絡(luò)套接字。瓶頸在哪里但是傳統(tǒng)I/O有它的瓶頸,這才是零拷貝技術(shù)出現(xiàn)的緣由。瓶頸是啥呢,當(dāng)然是性能問題,太慢了。尤其是在高并發(fā)場(chǎng)景下,I/O性能經(jīng)常會(huì)卡脖子。
那是什么地方耗時(shí)了呢?
數(shù)據(jù)拷貝在傳統(tǒng) I/O 中,數(shù)據(jù)的傳輸通常涉及多次數(shù)據(jù)拷貝。數(shù)據(jù)需要從應(yīng)用程序的用戶緩沖區(qū)復(fù)制到內(nèi)核緩沖區(qū),然后再?gòu)膬?nèi)核緩沖區(qū)復(fù)制到設(shè)備或網(wǎng)絡(luò)緩沖區(qū)。這些數(shù)據(jù)拷貝過程導(dǎo)致了多次內(nèi)存訪問和數(shù)據(jù)復(fù)制,消耗了大量的 CPU 時(shí)間和內(nèi)存帶寬。
用戶態(tài)和內(nèi)核態(tài)的切換由于數(shù)據(jù)要經(jīng)過內(nèi)核緩沖區(qū),導(dǎo)致數(shù)據(jù)在用戶態(tài)和內(nèi)核態(tài)之間來(lái)回切換,切換過程中會(huì)有上下文的切換,如此一來(lái),大大增加了處理數(shù)據(jù)的復(fù)雜性和時(shí)間開銷。
每一次操作耗費(fèi)的時(shí)間雖然很小,但是當(dāng)并發(fā)量高了以后,積少成多,也是不小的開銷。所以要提高性能、減少開銷就要從以上兩個(gè)問題下手了。
這時(shí)候,零拷貝技術(shù)就出來(lái)解決問題了。
什么是零拷貝問題出來(lái)數(shù)據(jù)拷貝和模態(tài)切換上。
但既然是 I/O 操作,不可能沒有數(shù)據(jù)拷貝的,只能減少拷貝的次數(shù),還有就是盡量將數(shù)據(jù)存儲(chǔ)在離應(yīng)用程序(用戶緩沖區(qū))更近的地方。
而區(qū)分用戶態(tài)和內(nèi)核態(tài)有其他更重要的原因,不可能單純?yōu)榱?I/O 效率就改變這種設(shè)計(jì)吧。那也只能盡量減少切換的次數(shù)。
零拷貝的理想狀態(tài)就是操作數(shù)據(jù)不用拷貝,但是顯示情況下并不一定真的就是一次復(fù)制操作都沒有,而是盡量減少拷貝操作的次數(shù)。
要實(shí)現(xiàn)零拷貝,應(yīng)該從下面這三個(gè)方面入手:
盡量減少數(shù)據(jù)在各個(gè)存儲(chǔ)區(qū)域的復(fù)制操作,例如從磁盤緩沖區(qū)到內(nèi)核緩沖區(qū)等;盡量減少用戶態(tài)和內(nèi)核態(tài)的切換次數(shù)及上下文切換;使用一些優(yōu)化手段,例如對(duì)需要操作的數(shù)據(jù)先緩存起來(lái),內(nèi)核中的 PageCache 就是這個(gè)作用;實(shí)現(xiàn)零拷貝方案直接內(nèi)存訪問(DMA)DMA 是一種硬件特性,允許外設(shè)(如網(wǎng)絡(luò)適配器、磁盤控制器等)直接訪問系統(tǒng)內(nèi)存,而無(wú)需通過 CPU 的介入。在數(shù)據(jù)傳輸時(shí),DMA 可以直接將數(shù)據(jù)從內(nèi)存?zhèn)鬏數(shù)酵庠O(shè),或者從外設(shè)傳輸數(shù)據(jù)到內(nèi)存,避免了數(shù)據(jù)在用戶態(tài)和內(nèi)核態(tài)之間的多次拷貝。
圖片
DMA1
如上圖所示,內(nèi)核將數(shù)據(jù)讀取的大部分?jǐn)?shù)據(jù)讀取操作都交個(gè)了 DMA 控制器,而空出來(lái)的資源就可以去處理其他的任務(wù)了。
sendfile一些操作系統(tǒng)(例如 Linux)提供了特殊的系統(tǒng)調(diào)用,如 sendfile,在網(wǎng)絡(luò)傳輸文件時(shí)實(shí)現(xiàn)零拷貝。通過 sendfile,應(yīng)用程序可以直接將文件數(shù)據(jù)從文件系統(tǒng)傳輸?shù)骄W(wǎng)絡(luò)套接字或者目標(biāo)文件,而無(wú)需經(jīng)過用戶緩沖區(qū)和內(nèi)核緩沖區(qū)。
如果不用sendfile,如果將A文件寫入B文件。
需要先將A文件的數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū),再?gòu)膬?nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū);然后內(nèi)核再將用戶緩沖區(qū)的數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū),之后才能寫入到B文件;而用了sendfile,用戶緩沖區(qū)和內(nèi)核緩沖區(qū)的拷貝都不用了,節(jié)省了一大部分的開銷。
共享內(nèi)存使用共享內(nèi)存技術(shù),應(yīng)用程序和內(nèi)核可以共享同一塊內(nèi)存區(qū)域,避免在用戶態(tài)和內(nèi)核態(tài)之間進(jìn)行數(shù)據(jù)拷貝。應(yīng)用程序可以直接將數(shù)據(jù)寫入共享內(nèi)存,然后內(nèi)核可以直接從共享內(nèi)存中讀取數(shù)據(jù)進(jìn)行傳輸,或者反之。
圖片
通過共享一塊兒內(nèi)存區(qū)域,實(shí)現(xiàn)數(shù)據(jù)的共享。就像程序中的引用對(duì)象一樣,實(shí)際上就是一個(gè)指針、一個(gè)地址。
內(nèi)存映射文件(Memory-mapped Files)內(nèi)存映射文件直接將磁盤文件映射到應(yīng)用程序的地址空間,使得應(yīng)用程序可以直接在內(nèi)存中讀取和寫入文件數(shù)據(jù),這樣一來(lái),對(duì)映射內(nèi)容的修改就是直接的反應(yīng)到實(shí)際的文件中。
當(dāng)文件數(shù)據(jù)需要傳輸時(shí),內(nèi)核可以直接從內(nèi)存映射區(qū)域讀取數(shù)據(jù)進(jìn)行傳輸,避免了數(shù)據(jù)在用戶態(tài)和內(nèi)核態(tài)之間的額外拷貝。
雖然看上去感覺和共享內(nèi)存沒什么差別,但是兩者的實(shí)現(xiàn)方式完全不同,一個(gè)是共享地址,一個(gè)是映射文件內(nèi)容。
Java 實(shí)現(xiàn)零拷貝的方式Java 標(biāo)準(zhǔn)的 IO 庫(kù)是沒有零拷貝方式的實(shí)現(xiàn)的,標(biāo)準(zhǔn)IO就相當(dāng)于上面所說的傳統(tǒng)模式。只是在 Java 推出的 NIO 中,才包含了一套新的 I/O 類,如ByteBuffer和Channel,它們可以在一定程度上實(shí)現(xiàn)零拷貝。
ByteBuffer:可以直接操作字節(jié)數(shù)據(jù),避免了數(shù)據(jù)在用戶態(tài)和內(nèi)核態(tài)之間的復(fù)制。
Channel:支持直接將數(shù)據(jù)從文件通道或網(wǎng)絡(luò)通道傳輸?shù)搅硪粋€(gè)通道,實(shí)現(xiàn)文件和網(wǎng)絡(luò)的零拷貝傳輸。
借助這兩種對(duì)象,結(jié)合 NIO 中的API,我們就能在 Java 中實(shí)現(xiàn)零拷貝了。
首先我們先用傳統(tǒng) IO 寫一個(gè)方法,用來(lái)和后面的 NIO 作對(duì)比,這個(gè)程序的目的很簡(jiǎn)單,就是將一個(gè)100M左右的PDF文件從一個(gè)目錄拷貝到另一個(gè)目錄。
public static void ioCopy() { try { File sourceFile = new File(SOURCE_FILE_PATH); File targetFile = new File(TARGET_FILE_PATH); try (FileInputStream fis = new FileInputStream(sourceFile); FileOutputStream fos = new FileOutputStream(targetFile)) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } } System.out.println("傳輸 " + formatFileSize(sourceFile.length()) + " 字節(jié)到目標(biāo)文件"); } catch (IOException e) { e.printStackTrace(); }}
下面是這個(gè)拷貝程序的執(zhí)行結(jié)果,109.92M,耗時(shí)1.29秒。
FileChannel.transferTo() 和 transferFrom()“
傳輸 109.92 M 字節(jié)到目標(biāo)文件 耗時(shí): 1.290 秒
”
FileChannel 是一個(gè)用于文件讀寫、映射和操作的通道,同時(shí)它在并發(fā)環(huán)境下是線程安全的,基于 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 getChannel() 方法可以創(chuàng)建并打開一個(gè)文件通道。FileChannel 定義了 transferFrom() 和 transferTo() 兩個(gè)抽象方法,它通過在通道和通道之間建立連接實(shí)現(xiàn)數(shù)據(jù)傳輸?shù)摹?/p>
這兩個(gè)方法首選用 sendfile 方式,只要當(dāng)前操作系統(tǒng)支持,就用 sendfile,例如Linux或MacOS。如果系統(tǒng)不支持,例如windows,則采用內(nèi)存映射文件的方式實(shí)現(xiàn)。
transferTo()下面是一個(gè) transferTo 的例子,仍然是拷貝那個(gè)100M左右的 PDF,我的系統(tǒng)是 MacOS。
public static void nioTransferTo() { try { File sourceFile = new File(SOURCE_FILE_PATH); File targetFile = new File(TARGET_FILE_PATH); try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel(); FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) { long transferredBytes = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel); System.out.println("傳輸 " + formatFileSize(transferredBytes) + " 字節(jié)到目標(biāo)文件"); } } catch (IOException e) { e.printStackTrace(); }}
只耗時(shí)0.536秒,快了一倍。
傳輸 109.92 M 字節(jié)到目標(biāo)文件 耗時(shí): 0.536 秒
transferFrom()下面是一個(gè) transferFrom 的例子,仍然是拷貝那個(gè)100M左右的 PDF,我的系統(tǒng)是 MacOS。
public static void nioTransferFrom() { try { File sourceFile = new File(SOURCE_FILE_PATH); File targetFile = new File(TARGET_FILE_PATH); try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel(); FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) { long transferredBytes = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size()); System.out.println("傳輸 " + formatFileSize(transferredBytes) + " 字節(jié)到目標(biāo)文件"); } } catch (IOException e) { e.printStackTrace(); }}
執(zhí)行時(shí)間
傳輸 109.92 M 字節(jié)到目標(biāo)文件 耗時(shí): 0.603 秒
Memory-Mapped FilesJava 的 NIO 也支持內(nèi)存映射文件(Memory-mapped Files),通過FileChannel.map()實(shí)現(xiàn)。
下面是一個(gè)FileChannel.map()的例子,仍然是拷貝那個(gè)100M左右的 PDF,我的系統(tǒng)是 MacOS。
public static void nioMap(){ try { File sourceFile = new File(SOURCE_FILE_PATH); File targetFile = new File(TARGET_FILE_PATH); try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel(); FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) { long fileSize = sourceChannel.size(); MappedByteBuffer buffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize); targetChannel.write(buffer); System.out.println("傳輸 " + formatFileSize(fileSize) + " 字節(jié)到目標(biāo)文件"); } } catch (IOException e) { e.printStackTrace(); } }
執(zhí)行時(shí)間:
傳輸 109.92 M 字節(jié)到目標(biāo)文件 耗時(shí): 0.663 秒
關(guān)鍵詞:
大眾朗逸多久換一次機(jī)油比較好(大眾朗逸多久換一次機(jī)油?)
大眾朗逸(參數(shù)|詢價(jià))5000公里或者半年換一次機(jī)油,機(jī)油的作用是:緩解
金輝集團(tuán):“21金輝02”將于7月31日進(jìn)行回售兌付 發(fā)行總額8.5億元
本期債券簡(jiǎn)稱為“21金輝02”,債券代碼為188473,發(fā)行總額為8 50億元,
金嶺礦業(yè):7月11日融資買入181.08萬(wàn)元,融資融券余額1.57億元
7月11日,金嶺礦業(yè)(000655)融資買入181 08萬(wàn)元,融資償還174 77萬(wàn)元
臺(tái)軍緊盯:38架次解放軍軍機(jī)持續(xù)在臺(tái)海周邊活動(dòng)
38架次解放軍軍機(jī)持續(xù)在臺(tái)海周邊活動(dòng)
篩選:北京那個(gè)醫(yī)院割包皮好{口碑榜}北京看男科去哪家醫(yī)院好
篩選:北京那個(gè)醫(yī)院割包皮好{口碑榜}北京看男科去哪家醫(yī)院好得少精
關(guān)于我們 加入我們 聯(lián)系我們 商務(wù)合作 粵ICP備2022077823號(hào)
創(chuàng)氪網(wǎng) www.www931.net 版權(quán)所有 技術(shù)支持:廣州中創(chuàng)互聯(lián)網(wǎng)信息服務(wù)有限公司
投稿投訴聯(lián)系郵箱:317 493 128 @qq.com