新聞中心
一、前置知識(shí)
1.1 內(nèi)存分段
現(xiàn)代計(jì)算機(jī)在加載操作系統(tǒng)、正常啟動(dòng)后,其內(nèi)存會(huì)主要分成兩大段:

創(chuàng)新互聯(lián)是一家專注于網(wǎng)站制作、成都做網(wǎng)站與策劃設(shè)計(jì),站前網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)做網(wǎng)站,專注于網(wǎng)站建設(shè)十多年,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:站前等地區(qū)。站前做網(wǎng)站價(jià)格咨詢:13518219792
- 內(nèi)核段
- 用戶段
內(nèi)核段:
操作系統(tǒng)本質(zhì)上是一個(gè)計(jì)算機(jī)的管理程序,該程序相關(guān)的所有資源,被存放在內(nèi)核段中。
用戶段:
用戶段用來(lái)存放各個(gè)進(jìn)程的數(shù)據(jù)和指令。
根據(jù)所訪問(wèn)的內(nèi)存段的不同,CPU會(huì)處于不同的態(tài),訪問(wèn)用戶段的時(shí)候處于用戶態(tài),訪問(wèn)內(nèi)核段的時(shí)候處于內(nèi)核態(tài)。
1.2 CPU的態(tài)
1.2.1 CPU的工作過(guò)程
CPU要執(zhí)行的指令的地址存在寄存器中,指令存放在內(nèi)存中,而CPU本質(zhì)上就是一個(gè)去內(nèi)存中根據(jù)地址取指令,然后執(zhí)行指令的硬件。
舉一個(gè)例子:
例如PC寄存器中存放50,CPU讀到存放的50,發(fā)出一條取址指令,去取出地址為50的內(nèi)存單元中的指令,再傳回給CPU。
1.2.2 寄存器
眾所周知,為了配平CPU和內(nèi)存之間速率的差距,CPU和內(nèi)存之間存在著一個(gè)由寄存器組成的中間層,寄存器種會(huì)存放著CPU接下來(lái)要執(zhí)行的指令,以及后續(xù)可能要執(zhí)行到的指令以及可能要用到的數(shù)據(jù)。只有預(yù)先裝載進(jìn)去這部分可能要用到的東西才能抹平CPU和內(nèi)存之間的速率差距,不然每次都要去內(nèi)存取內(nèi)容,可能是會(huì)拉低CPU的效率的。
但該預(yù)先裝載哪些內(nèi)容進(jìn)寄存器中呢?這里遵循了程序的局部性原理。
程序的局部性原理:
程序在執(zhí)行的時(shí)候呈現(xiàn)出局部性規(guī)律,在一段時(shí)間內(nèi),整個(gè)程序的執(zhí)行僅限于程序中的某一個(gè)部分,相應(yīng)的,執(zhí)行所訪問(wèn)的存儲(chǔ)空間也局限于某個(gè)內(nèi)存區(qū)域。局部性又分為時(shí)間局部性和空間局部性。時(shí)間局部性指的是,如果程序中的某條指令一旦執(zhí)行,則不久后可能會(huì)被再次執(zhí)行,執(zhí)行指令時(shí)訪問(wèn)的數(shù)據(jù)單元在不久后會(huì)被再次訪問(wèn)??臻g局部性指的是,一旦訪問(wèn)了某個(gè)存儲(chǔ)單元,不久后,其附近的存儲(chǔ)單元也將被訪問(wèn)。
1.2.3 CPU的上下文
為了抹平內(nèi)存和CPU之間的速率差,給CPU配備了寄存器。寄存器中存儲(chǔ)著當(dāng)前執(zhí)行的指令、數(shù)據(jù)、以及下一條指令在內(nèi)存中的地址等等事關(guān)程序正常運(yùn)行的關(guān)鍵信息。所以寄存器中存儲(chǔ)的內(nèi)容合稱為CPU的上下文。
1.2.4 系統(tǒng)調(diào)用
系統(tǒng)中將一些對(duì)系統(tǒng)級(jí)別資源的調(diào)用封裝成了一個(gè)個(gè)函數(shù),稱為系統(tǒng)調(diào)用,常見的系統(tǒng)調(diào)用有很多,比如IO操作就是個(gè)系統(tǒng)調(diào)用。
1.2.5 CPU的態(tài)
操作系統(tǒng)在啟動(dòng)后,內(nèi)存被分為兩部分(兩段):
- 內(nèi)核段從0地址開始編址,存放操作系統(tǒng)程序,里面包含系統(tǒng)調(diào)用。
- 用戶段,從內(nèi)核段之后開始編址,存放用戶程,也就是各個(gè)進(jìn)程的數(shù)據(jù)和指令。
由于內(nèi)核段存放的是系統(tǒng)相關(guān)的內(nèi)容,基于安全的考慮,肯定是不允許被CPU隨意訪問(wèn)的,需要特權(quán)才行。因此將CPU的權(quán)限設(shè)計(jì)為了兩種狀態(tài):
- 用戶態(tài),只能訪問(wèn)用戶段
- 內(nèi)核態(tài),能訪問(wèn)用戶段和內(nèi)核段
所謂的態(tài)就是能訪問(wèn)用戶段的上下文以及能訪問(wèn)內(nèi)核段的上下文。當(dāng)我們調(diào)用系統(tǒng)調(diào)用的時(shí)候會(huì)引起上下文的切換,也就是CPU態(tài)的切換。上下文切換的意思是,先把前一個(gè)任務(wù)的 CPU 上下文保存起來(lái),然后加載新任務(wù)的上下文到這些寄存器和程序計(jì)數(shù)器,最后再跳轉(zhuǎn)到程序計(jì)數(shù)器所指的新位置,運(yùn)行新任務(wù)。之所以會(huì)切換上下文,這是因?yàn)榧拇嫫骷虞d數(shù)據(jù)和指令的時(shí)候遵循了程序的局部性原理。CPU訪問(wèn)用戶段時(shí),寄存器里預(yù)加載的是用戶段的資源;CPU訪問(wèn)內(nèi)核段時(shí),寄存器里預(yù)加載的是內(nèi)核段的資源。
所以CPU進(jìn)行態(tài)切換的時(shí)候,上下文一定會(huì)完全換一套的。總的來(lái)說(shuō)為了保證多數(shù)情況下程序執(zhí)行的效率,“局部性原理”是必須存在的,為了內(nèi)核的安全,CPU態(tài)的劃分是必須存在的。所以,CPU上下文切換是不得不接受的一種代價(jià)。
CPU的上下文切換是種耗時(shí)的操作:
寄存器保存和恢復(fù):在上下文切換過(guò)程中,需要保存當(dāng)前任務(wù)的寄存器狀態(tài),并恢復(fù)下一個(gè)任務(wù)的寄存器狀態(tài)。寄存器保存和恢復(fù)涉及將寄存器的值從CPU保存到內(nèi)存(或者棧)中,以及從內(nèi)存中恢復(fù)到CPU中。這涉及到數(shù)據(jù)的讀寫和復(fù)制操作,會(huì)引入一定的延遲和開銷。
內(nèi)存刷新和緩存失效:上下文切換可能涉及刷新CPU緩存和內(nèi)存管理單元(MMU)的操作。當(dāng)切換到一個(gè)新的任務(wù)時(shí),之前的任務(wù)的緩存內(nèi)容可能需要刷新,新任務(wù)的頁(yè)表和內(nèi)存映射需要加載和設(shè)置,這些操作可能導(dǎo)致緩存失效和內(nèi)存訪問(wèn)延遲。
上下文數(shù)據(jù)復(fù)制:在上下文切換過(guò)程中,需要將當(dāng)前任務(wù)的上下文數(shù)據(jù)保存到內(nèi)存中,同時(shí)從內(nèi)存中加載下一個(gè)任務(wù)的上下文數(shù)據(jù)。這包括寄存器狀態(tài)、程序計(jì)數(shù)器、標(biāo)志位和其他與任務(wù)執(zhí)行相關(guān)的數(shù)據(jù)。數(shù)據(jù)的復(fù)制和加載需要占用CPU和內(nèi)存帶寬,并引入一定的延遲。
任務(wù)切換開銷:上下文切換不僅僅涉及寄存器和內(nèi)存的操作,還包括任務(wù)切換本身的開銷。這包括切換內(nèi)核棧、更新任務(wù)控制塊(TCB)、更新調(diào)度器數(shù)據(jù)結(jié)構(gòu)等操作。這些操作可能需要修改內(nèi)核數(shù)據(jù)結(jié)構(gòu),增加了上下文切換的開銷。
總的來(lái)說(shuō)CPU上下文切換很耗時(shí),我們常見的就是IO操作、進(jìn)程切換這些都會(huì)引起CPU上下文切換。
1.3 計(jì)算機(jī)IO的過(guò)程
在程序執(zhí)行時(shí)有很多高耗時(shí)操作,比如IO操作就是。當(dāng)計(jì)算機(jī)執(zhí)行IO操作的時(shí)候,IO設(shè)備的速度肯定是遠(yuǎn)遠(yuǎn)落后于CPU的速度的,IO沒(méi)有完成,后續(xù)依賴的數(shù)據(jù)沒(méi)到位,程序也沒(méi)辦法繼續(xù)向下執(zhí)行,于是CPU就只好賦閑,傻傻的等IO執(zhí)行完成,再繼續(xù)向下運(yùn)行程序,無(wú)疑這會(huì)造成CPU資源的浪費(fèi),使得計(jì)算機(jī)的工作效率變得很低。
于是現(xiàn)代操作系統(tǒng)中將CPU劃分成了很多時(shí)間片,不同時(shí)間片可以去運(yùn)行不同的程序,比如:
- 這一秒運(yùn)行的A程序,
- 下一秒運(yùn)行的B程序,
- 再下一秒再運(yùn)行A程序。
這樣間插執(zhí)行就會(huì)避免傻等帶來(lái)的CPU資源的浪費(fèi),如果IO耗時(shí)2秒,那么CPU至少還有1秒被其它程序使用到了。
后來(lái)操作系統(tǒng)用了更激進(jìn)的方式來(lái)處理IO指令,讓CPU的時(shí)間一絲一毫都不被浪費(fèi),這種處理方式就是遇見IO指令,直接啟動(dòng)IO后,CPU直接轉(zhuǎn)去執(zhí)行其它任務(wù),當(dāng)IO完成后發(fā)送一個(gè)中斷信號(hào)給CPU,讓CPU中斷當(dāng)前的任務(wù),轉(zhuǎn)過(guò)來(lái)繼續(xù)執(zhí)行IO后的程序
1.4 IO與內(nèi)存
計(jì)算機(jī)進(jìn)行IO的時(shí)候,本質(zhì)上會(huì)為每一個(gè)IO設(shè)備在內(nèi)存中分配一塊空間,向這塊空間里進(jìn)行讀寫,即可完成IO。為什么給IO設(shè)備分配的內(nèi)存會(huì)是在內(nèi)核段里喃?主要是基于兩點(diǎn)進(jìn)行考慮的:
- 安全性
- 特權(quán)操作
1.4.1 安全性:
I/O 操作通常需要與計(jì)算機(jī)的外部設(shè)備(如磁盤、網(wǎng)絡(luò)設(shè)備等)進(jìn)行交互,如果允許各個(gè)進(jìn)程自己私自與外部設(shè)備進(jìn)行交互,IO的內(nèi)存放在各個(gè)進(jìn)程內(nèi)部,太散了,不是很好進(jìn)行安全控制,相反,如果將IO的內(nèi)存放在內(nèi)核段,就很便于集中管理,可以附加一些安全機(jī)制上去。
1.4.2 特權(quán)操作:
首先IO指令本身就是特權(quán)指令,會(huì)讓CPU進(jìn)入內(nèi)核態(tài),其次進(jìn)行IO的時(shí)候會(huì)用到中斷信號(hào),也涉及到特權(quán)指令,也要求CPU處于內(nèi)核態(tài),所以如果IO內(nèi)存是在內(nèi)核段中,讓CPU提前進(jìn)入內(nèi)核狀態(tài),也避免了后面來(lái)回切狀態(tài)造成的時(shí)間浪費(fèi)。
整個(gè)IO在內(nèi)存中的流轉(zhuǎn)過(guò)程如下:
讀的時(shí)候磁盤拷貝到內(nèi)核段、內(nèi)核段拷貝到用戶段,
寫的時(shí)候用戶段拷貝到內(nèi)核段、內(nèi)核段拷貝到磁盤。
一共四次復(fù)制。
特別說(shuō)明:
我知道其它很多地方這里將圖畫成了這個(gè)樣子:
這是因?yàn)樗枥L的這次IO是從磁盤上讀出來(lái)然后寫到網(wǎng)絡(luò)上去,網(wǎng)卡和磁盤可以理解為兩個(gè)不同的IO設(shè)備,所以他們?cè)趦?nèi)核段中的IO內(nèi)存,地址是不同的。但是如果僅僅是對(duì)磁盤的一次本地IO,那么進(jìn)行IO的內(nèi)核段地址會(huì)是同一個(gè),在同一個(gè)地址內(nèi)進(jìn)行讀寫。這里為了涵蓋多種情況,所以博主沒(méi)有將它分開,讀者悉知。
二、零拷貝
零拷貝(Zero-copy)是一種優(yōu)化技術(shù),并不是一次拷貝都不做,而是旨在減少數(shù)據(jù)在系統(tǒng)內(nèi)部的復(fù)制操作,從而提高數(shù)據(jù)傳輸?shù)男?。它的主要目?biāo)是減少內(nèi)存到內(nèi)存之間的數(shù)據(jù)拷貝。零拷貝有兩種實(shí)現(xiàn)方式:
- MMap
- SendFile
2.1.MMap
通過(guò)上文我們知道一次IO,數(shù)據(jù)會(huì)進(jìn)行四次拷貝,MMap這種方式在將內(nèi)核段中的數(shù)據(jù)拷貝到用戶段的這次拷貝中,拷貝的不是數(shù)據(jù),而是數(shù)據(jù)的映射,這樣在用戶段中進(jìn)行數(shù)據(jù)處理完后,就不必再?gòu)挠脩舳慰截惢貎?nèi)核段,從而減少了一次拷貝。
之所以能實(shí)現(xiàn)這樣的效果是得益于操作系統(tǒng)底層有兩種讀操作:
讀取數(shù)據(jù):常見的系統(tǒng)調(diào)用如 read()(用于文件描述符)或 recv()(用于套接字)用于從文件或套接字中讀取數(shù)據(jù)。這些系統(tǒng)調(diào)用從相應(yīng)的輸入源(如磁盤、網(wǎng)絡(luò)等)讀取數(shù)據(jù),并將其復(fù)制到應(yīng)用程序提供的緩沖區(qū)中。這種方式涉及了數(shù)據(jù)的復(fù)制,因?yàn)閿?shù)據(jù)需要從內(nèi)核態(tài)復(fù)制到用戶態(tài)緩沖區(qū)中。
讀取映射:另一種方式是通過(guò)內(nèi)存映射(Memory Mapping)來(lái)實(shí)現(xiàn)讀取操作。通過(guò)將文件或設(shè)備的數(shù)據(jù)映射到進(jìn)程的內(nèi)存區(qū)域中,應(yīng)用程序可以直接訪問(wèn)內(nèi)存映射區(qū)域中的數(shù)據(jù),而無(wú)需使用傳統(tǒng)的 read() 系統(tǒng)調(diào)用。在這種情況下,應(yīng)用程序可以通過(guò)直接讀取內(nèi)存映射區(qū)域中的數(shù)據(jù)來(lái)獲取文件或設(shè)備的內(nèi)容,避免了中間的數(shù)據(jù)復(fù)制。
特別說(shuō)明:
還是和上文類似,畫圖的問(wèn)題。這里為了涵蓋,本地IO和網(wǎng)絡(luò)IO兩種情況,內(nèi)核段沒(méi)拆成幾個(gè)設(shè)備的不同地址空間,但是如果是從磁盤中讀,然后向網(wǎng)絡(luò)中寫,是跨了IO設(shè)備的,所以中間有個(gè)內(nèi)核段地址間的復(fù)制過(guò)程,如下圖:
2.2.SendFile
SendFile更狠,直接就不走用戶段,直接就是從內(nèi)核段的一個(gè)內(nèi)存地址復(fù)制到另一個(gè)內(nèi)存地址,主要是拿來(lái)進(jìn)行網(wǎng)絡(luò)傳輸?shù)?,從本地磁盤讀數(shù)據(jù),讀到一個(gè)地址里,然后將這個(gè)地址里的數(shù)據(jù)復(fù)制給另一個(gè)IO設(shè)備的地址,這個(gè)地址就可以是網(wǎng)絡(luò)IO的地址。很明顯sendFile有一個(gè)弊病,就是沒(méi)走用戶段的話,數(shù)據(jù)沒(méi)辦法處理,所以其只是一種用于實(shí)現(xiàn)數(shù)據(jù)傳輸?shù)?"零拷貝" 技術(shù),而不能直接進(jìn)行數(shù)據(jù)處理。并且SendFile還存在大小限制。
三、JAVA中的零拷貝
零拷貝需要進(jìn)行系統(tǒng)調(diào)用才能實(shí)現(xiàn),很明顯要我們手寫實(shí)現(xiàn)零拷貝是很底層、很麻煩的,好在JAVA在NIO中封裝了mmap、SendFile兩種零拷貝的API,當(dāng)我們想在JAVA中使用零拷貝時(shí),直接調(diào)API即可。
很多同學(xué)在NIO中老是搞不明白channel和buffer的關(guān),容易暈,這里博主一句話總結(jié)一下:
JavaNlO中 的Channel就相當(dāng)于操作系統(tǒng)中的內(nèi)核緩沖區(qū),而Buffer就相當(dāng)于操作系統(tǒng)中的用戶緩沖區(qū)。
mmap:
MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r")
.getChannel()
.map(FileChannel.MapMode.READ_ONLY, 0, len);SendFile:
sendFile進(jìn)行網(wǎng)絡(luò)傳輸:
FileChannel sourceChannel = new RandomAccessFile(sourceFile, "rw").getChannel();
SocketChannel socketChannel = SocketChannel.open(sa);
sourceChannel.transferTo(0, sourceChannel.size(), socketChannel);sendFile進(jìn)行文件拷貝:
try (FileChannel srcChannel = new FileInputStream(src).getChannel();
FileChannel targetChannel = new FileInputStream(target).getChannel()) {
srcChannel.transferTo(0, srcChannel.size(), targetChannel );
} catch (IOException e) {
e.printStackTrace();
} 網(wǎng)站名稱:對(duì)IO概念模糊:計(jì)算機(jī)IO過(guò)程與零拷貝
URL標(biāo)題:http://fisionsoft.com.cn/article/cojceog.html


咨詢
建站咨詢
