現代計算機在加載操作系統、正常啟動后,其內存會主要分成兩大段:
內核段:
操作系統本質上是一個計算機的管理程序,該程序相關的所有資源,被存放在內核段中。
用戶段:
用戶段用來存放各個進程的數據和指令。
根據所訪問的內存段的不同,CPU會處于不同的態,訪問用戶段的時候處于用戶態,訪問內核段的時候處于內核態。
CPU要執行的指令的地址存在寄存器中,指令存放在內存中,而CPU本質上就是一個去內存中根據地址取指令,然后執行指令的硬件。
舉一個例子:
例如PC寄存器中存放50,CPU讀到存放的50,發出一條取址指令,去取出地址為50的內存單元中的指令,再傳回給CPU。
眾所周知,為了配平CPU和內存之間速率的差距,CPU和內存之間存在著一個由寄存器組成的中間層,寄存器種會存放著CPU接下來要執行的指令,以及后續可能要執行到的指令以及可能要用到的數據。只有預先裝載進去這部分可能要用到的東西才能抹平CPU和內存之間的速率差距,不然每次都要去內存取內容,可能是會拉低CPU的效率的。
但該預先裝載哪些內容進寄存器中呢?這里遵循了程序的局部性原理。
程序的局部性原理:
程序在執行的時候呈現出局部性規律,在一段時間內,整個程序的執行僅限于程序中的某一個部分,相應的,執行所訪問的存儲空間也局限于某個內存區域。局部性又分為時間局部性和空間局部性。時間局部性指的是,如果程序中的某條指令一旦執行,則不久后可能會被再次執行,執行指令時訪問的數據單元在不久后會被再次訪問。空間局部性指的是,一旦訪問了某個存儲單元,不久后,其附近的存儲單元也將被訪問。
為了抹平內存和CPU之間的速率差,給CPU配備了寄存器。寄存器中存儲著當前執行的指令、數據、以及下一條指令在內存中的地址等等事關程序正常運行的關鍵信息。所以寄存器中存儲的內容合稱為CPU的上下文。
系統中將一些對系統級別資源的調用封裝成了一個個函數,稱為系統調用,常見的系統調用有很多,比如IO操作就是個系統調用。
操作系統在啟動后,內存被分為兩部分(兩段):
由于內核段存放的是系統相關的內容,基于安全的考慮,肯定是不允許被CPU隨意訪問的,需要特權才行。因此將CPU的權限設計為了兩種狀態:
所謂的態就是能訪問用戶段的上下文以及能訪問內核段的上下文。當我們調用系統調用的時候會引起上下文的切換,也就是CPU態的切換。上下文切換的意思是,先把前一個任務的 CPU 上下文保存起來,然后加載新任務的上下文到這些寄存器和程序計數器,最后再跳轉到程序計數器所指的新位置,運行新任務。之所以會切換上下文,這是因為寄存器加載數據和指令的時候遵循了程序的局部性原理。CPU訪問用戶段時,寄存器里預加載的是用戶段的資源;CPU訪問內核段時,寄存器里預加載的是內核段的資源。
所以CPU進行態切換的時候,上下文一定會完全換一套的。總的來說為了保證多數情況下程序執行的效率,“局部性原理”是必須存在的,為了內核的安全,CPU態的劃分是必須存在的。所以,CPU上下文切換是不得不接受的一種代價。
CPU的上下文切換是種耗時的操作:
寄存器保存和恢復:在上下文切換過程中,需要保存當前任務的寄存器狀態,并恢復下一個任務的寄存器狀態。寄存器保存和恢復涉及將寄存器的值從CPU保存到內存(或者棧)中,以及從內存中恢復到CPU中。這涉及到數據的讀寫和復制操作,會引入一定的延遲和開銷。
內存刷新和緩存失效:上下文切換可能涉及刷新CPU緩存和內存管理單元(MMU)的操作。當切換到一個新的任務時,之前的任務的緩存內容可能需要刷新,新任務的頁表和內存映射需要加載和設置,這些操作可能導致緩存失效和內存訪問延遲。
上下文數據復制:在上下文切換過程中,需要將當前任務的上下文數據保存到內存中,同時從內存中加載下一個任務的上下文數據。這包括寄存器狀態、程序計數器、標志位和其他與任務執行相關的數據。數據的復制和加載需要占用CPU和內存帶寬,并引入一定的延遲。
任務切換開銷:上下文切換不僅僅涉及寄存器和內存的操作,還包括任務切換本身的開銷。這包括切換內核棧、更新任務控制塊(TCB)、更新調度器數據結構等操作。這些操作可能需要修改內核數據結構,增加了上下文切換的開銷。
總的來說CPU上下文切換很耗時,我們常見的就是IO操作、進程切換這些都會引起CPU上下文切換。
在程序執行時有很多高耗時操作,比如IO操作就是。當計算機執行IO操作的時候,IO設備的速度肯定是遠遠落后于CPU的速度的,IO沒有完成,后續依賴的數據沒到位,程序也沒辦法繼續向下執行,于是CPU就只好賦閑,傻傻的等IO執行完成,再繼續向下運行程序,無疑這會造成CPU資源的浪費,使得計算機的工作效率變得很低。
于是現代操作系統中將CPU劃分成了很多時間片,不同時間片可以去運行不同的程序,比如:
這樣間插執行就會避免傻等帶來的CPU資源的浪費,如果IO耗時2秒,那么CPU至少還有1秒被其它程序使用到了。
后來操作系統用了更激進的方式來處理IO指令,讓CPU的時間一絲一毫都不被浪費,這種處理方式就是遇見IO指令,直接啟動IO后,CPU直接轉去執行其它任務,當IO完成后發送一個中斷信號給CPU,讓CPU中斷當前的任務,轉過來繼續執行IO后的程序
計算機進行IO的時候,本質上會為每一個IO設備在內存中分配一塊空間,向這塊空間里進行讀寫,即可完成IO。為什么給IO設備分配的內存會是在內核段里喃?主要是基于兩點進行考慮的:
I/O 操作通常需要與計算機的外部設備(如磁盤、網絡設備等)進行交互,如果允許各個進程自己私自與外部設備進行交互,IO的內存放在各個進程內部,太散了,不是很好進行安全控制,相反,如果將IO的內存放在內核段,就很便于集中管理,可以附加一些安全機制上去。
首先IO指令本身就是特權指令,會讓CPU進入內核態,其次進行IO的時候會用到中斷信號,也涉及到特權指令,也要求CPU處于內核態,所以如果IO內存是在內核段中,讓CPU提前進入內核狀態,也避免了后面來回切狀態造成的時間浪費。
整個IO在內存中的流轉過程如下:
讀的時候磁盤拷貝到內核段、內核段拷貝到用戶段,
寫的時候用戶段拷貝到內核段、內核段拷貝到磁盤。
一共四次復制。
特別說明:
我知道其它很多地方這里將圖畫成了這個樣子:
這是因為他描繪的這次IO是從磁盤上讀出來然后寫到網絡上去,網卡和磁盤可以理解為兩個不同的IO設備,所以他們在內核段中的IO內存,地址是不同的。但是如果僅僅是對磁盤的一次本地IO,那么進行IO的內核段地址會是同一個,在同一個地址內進行讀寫。這里為了涵蓋多種情況,所以博主沒有將它分開,讀者悉知。
零拷貝(Zero-copy)是一種優化技術,并不是一次拷貝都不做,而是旨在減少數據在系統內部的復制操作,從而提高數據傳輸的效率。它的主要目標是減少內存到內存之間的數據拷貝。零拷貝有兩種實現方式:
通過上文我們知道一次IO,數據會進行四次拷貝,MMap這種方式在將內核段中的數據拷貝到用戶段的這次拷貝中,拷貝的不是數據,而是數據的映射,這樣在用戶段中進行數據處理完后,就不必再從用戶段拷貝回內核段,從而減少了一次拷貝。
之所以能實現這樣的效果是得益于操作系統底層有兩種讀操作:
讀取數據:常見的系統調用如 read()(用于文件描述符)或 recv()(用于套接字)用于從文件或套接字中讀取數據。這些系統調用從相應的輸入源(如磁盤、網絡等)讀取數據,并將其復制到應用程序提供的緩沖區中。這種方式涉及了數據的復制,因為數據需要從內核態復制到用戶態緩沖區中。
讀取映射:另一種方式是通過內存映射(Memory Mapping)來實現讀取操作。通過將文件或設備的數據映射到進程的內存區域中,應用程序可以直接訪問內存映射區域中的數據,而無需使用傳統的 read() 系統調用。在這種情況下,應用程序可以通過直接讀取內存映射區域中的數據來獲取文件或設備的內容,避免了中間的數據復制。
特別說明:
還是和上文類似,畫圖的問題。這里為了涵蓋,本地IO和網絡IO兩種情況,內核段沒拆成幾個設備的不同地址空間,但是如果是從磁盤中讀,然后向網絡中寫,是跨了IO設備的,所以中間有個內核段地址間的復制過程,如下圖:
SendFile更狠,直接就不走用戶段,直接就是從內核段的一個內存地址復制到另一個內存地址,主要是拿來進行網絡傳輸的,從本地磁盤讀數據,讀到一個地址里,然后將這個地址里的數據復制給另一個IO設備的地址,這個地址就可以是網絡IO的地址。很明顯sendFile有一個弊病,就是沒走用戶段的話,數據沒辦法處理,所以其只是一種用于實現數據傳輸的 "零拷貝" 技術,而不能直接進行數據處理。并且SendFile還存在大小限制。
零拷貝需要進行系統調用才能實現,很明顯要我們手寫實現零拷貝是很底層、很麻煩的,好在JAVA在NIO中封裝了mmap、SendFile兩種零拷貝的API,當我們想在JAVA中使用零拷貝時,直接調API即可。
很多同學在NIO中老是搞不明白channel和buffer的關,容易暈,這里博主一句話總結一下:
JavaNlO中 的Channel就相當于操作系統中的內核緩沖區,而Buffer就相當于操作系統中的用戶緩沖區。
mmap:
MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r") .getChannel() .map(FileChannel.MapMode.READ_ONLY, 0, len);
SendFile:
sendFile進行網絡傳輸:
FileChannel sourceChannel = new RandomAccessFile(sourceFile, "rw").getChannel();SocketChannel socketChannel = SocketChannel.open(sa);sourceChannel.transferTo(0, sourceChannel.size(), socketChannel);
sendFile進行文件拷貝:
try (FileChannel srcChannel = new FileInputStream(src).getChannel(); FileChannel targetChannel = new FileInputStream(target).getChannel()) { srcChannel.transferTo(0, srcChannel.size(), targetChannel ); } catch (IOException e) { e.printStackTrace(); }
本文鏈接:http://www.tebozhan.com/showinfo-26-12196-0.html對IO概念模糊:計算機IO過程與零拷貝
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 深入了解快速排序:原理、性能分析與 Java 實現
下一篇: 三個殺手級VS Code插件