深入理解CPU上下文切換
我們都知道CPU上下文切換,會增加系統(tǒng)負(fù)載。那什么是CPU上下文,為什么要切換?
什么是CPU上下文
我們都知道,Linux 是一個多任務(wù)操作系統(tǒng),它支持遠(yuǎn)大于 CPU 數(shù)量的任務(wù)同時運(yùn)行。當(dāng)然,這些任務(wù)實際上并不是真的在同時運(yùn)行,而是因為系統(tǒng)在很短的時間內(nèi),將 CPU 輪流分配給它們,造成多任務(wù)同時運(yùn)行的錯覺。
而在每個任務(wù)運(yùn)行前,CPU 都需要知道任務(wù)從哪里加載、又從哪里開始運(yùn)行,也就是說,需要系統(tǒng)事先幫它設(shè)置好 CPU 寄存器和程序計數(shù)器(Program Counter,PC)。
CPU 寄存器,是 CPU 內(nèi)置的容量小、但速度極快的內(nèi)存。而程序計數(shù)器,則是用來存儲 CPU 正在執(zhí)行的指令位置、或者即將執(zhí)行的下一條指令位置。它們都是 CPU 在運(yùn)行任何任務(wù)前,必須的依賴環(huán)境,因此也被叫做 CPU 上下文。

而這些保存下來的上下文,會存儲在系統(tǒng)內(nèi)核中,并在任務(wù)重新調(diào)度執(zhí)行時再次加載進(jìn)來。這樣就能保證任務(wù)原來的狀態(tài)不受影響,讓任務(wù)看起來還是連續(xù)運(yùn)行。
根據(jù)任務(wù)的不同,CPU的上下文切換可以分為不同的場景,也就是進(jìn)程上下文切換、線程上下文切換、中斷上下文切換。
進(jìn)程上下文切換
Linux 按照特權(quán)等級,把進(jìn)程的運(yùn)行空間分為內(nèi)核空間和用戶空間,分別對應(yīng)著下圖中, CPU 特權(quán)等級的 Ring 0 和 Ring 3。
- 內(nèi)核空間(Ring 0)具有***權(quán)限,可以直接訪問所有資源;
- 用戶空間(Ring 3)只能訪問受限資源,不能直接訪問內(nèi)存等硬件設(shè)備,必須通過系統(tǒng)調(diào)用陷入到內(nèi)核中,才能訪問這些特權(quán)資源。

換個角度看,也就是說,進(jìn)程既可以在用戶空間運(yùn)行,又可以在內(nèi)核空間中運(yùn)行。進(jìn)程在用戶空間運(yùn)行時,被稱為進(jìn)程的用戶態(tài),而陷入內(nèi)核空間的時候,被稱為進(jìn)程的內(nèi)核態(tài)。
從用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)變,需要通過系統(tǒng)調(diào)用來完成。比如,當(dāng)我們查看文件內(nèi)容時,就需要多次系統(tǒng)調(diào)用來完成:首先調(diào)用 open() 打開文件,然后調(diào)用 read() 讀取文件內(nèi)容,并調(diào)用 write() 將內(nèi)容寫到標(biāo)準(zhǔn)輸出,***再調(diào)用 close() 關(guān)閉文件。
那么,系統(tǒng)調(diào)用的過程有沒有發(fā)生 CPU 上下文的切換呢?答案自然是肯定的。
CPU 寄存器里原來用戶態(tài)的指令位置,需要先保存起來。接著,為了執(zhí)行內(nèi)核態(tài)代碼,CPU 寄存器需要更新為內(nèi)核態(tài)指令的新位置。***才是跳轉(zhuǎn)到內(nèi)核態(tài)運(yùn)行內(nèi)核任務(wù)。
而系統(tǒng)調(diào)用結(jié)束后,CPU 寄存器需要恢復(fù)原來用戶保存的狀態(tài),然后再切換到用戶空間,繼續(xù)運(yùn)行進(jìn)程。所以,一次系統(tǒng)調(diào)用的過程,其實是發(fā)生了兩次 CPU 上下文切換。
不過,需要注意的是,系統(tǒng)調(diào)用過程中,并不會涉及到虛擬內(nèi)存等進(jìn)程用戶態(tài)的資源,也不會切換進(jìn)程。這跟我們通常所說的進(jìn)程上下文切換是不一樣的:
- 進(jìn)程上下文切換,是指從一個進(jìn)程切換到另一個進(jìn)程運(yùn)行。
- 而系統(tǒng)調(diào)用過程中一直是同一個進(jìn)程在運(yùn)行。
所以,系統(tǒng)調(diào)用過程通常稱為特權(quán)模式切換,而不是上下文切換。但實際上,系統(tǒng)調(diào)用過程中,CPU 的上下文切換還是無法避免的。
那么,進(jìn)程上下文切換跟系統(tǒng)調(diào)用又有什么區(qū)別呢?
首先,你需要知道,進(jìn)程是由內(nèi)核來管理和調(diào)度的,進(jìn)程的切換只能發(fā)生在內(nèi)核態(tài)。所以,進(jìn)程的上下文不僅包括了虛擬內(nèi)存、棧、全局變量等用戶空間的資源,還包括了內(nèi)核堆棧、寄存器等內(nèi)核空間的狀態(tài)。
因此,進(jìn)程的上下文切換就比系統(tǒng)調(diào)用時多了一步:在保存當(dāng)前進(jìn)程的內(nèi)核狀態(tài)和 CPU 寄存器之前,需要先把該進(jìn)程的虛擬內(nèi)存、棧等保存下來;而加載了下一進(jìn)程的內(nèi)核態(tài)后,還需要刷新進(jìn)程的虛擬內(nèi)存和用戶棧。
如下圖所示,保存上下文和恢復(fù)上下文的過程并不是“免費”的,需要內(nèi)核在 CPU 上運(yùn)行才能完成。

根據(jù)測試報告,每次上下文切換都需要幾十納秒到數(shù)微秒的 CPU 時間。這個時間還是相當(dāng)可觀的,特別是在進(jìn)程上下文切換次數(shù)較多的情況下,很容易導(dǎo)致 CPU 將大量時間耗費在寄存器、內(nèi)核棧以及虛擬內(nèi)存等資源的保存和恢復(fù)上,進(jìn)而大大縮短了真正運(yùn)行進(jìn)程的時間。這也正是上一節(jié)中我們所講的,導(dǎo)致平均負(fù)載升高的一個重要因素。
另外,我們知道, Linux 通過 TLB(Translation Lookaside Buffer)來管理虛擬內(nèi)存到物理內(nèi)存的映射關(guān)系。當(dāng)虛擬內(nèi)存更新后,TLB 也需要刷新,內(nèi)存的訪問也會隨之變慢。特別是在多處理器系統(tǒng)上,緩存是被多個處理器共享的,刷新緩存不僅會影響當(dāng)前處理器的進(jìn)程,還會影響共享緩存的其他處理器的進(jìn)程。
知道了進(jìn)程上下文切換潛在的性能問題后,我們再來看,究竟什么時候會切換進(jìn)程上下文。
顯然,進(jìn)程切換時才需要切換上下文,換句話說,只有在進(jìn)程調(diào)度的時候,才需要切換上下文。Linux 為每個 CPU 都維護(hù)了一個就緒隊列,將活躍進(jìn)程(即正在運(yùn)行和正在等待 CPU 的進(jìn)程)按照優(yōu)先級和等待 CPU 的時間排序,然后選擇最需要 CPU 的進(jìn)程,也就是優(yōu)先級***和等待 CPU 時間最長的進(jìn)程來運(yùn)行。
那么,進(jìn)程在什么時候才會被調(diào)度到 CPU 上運(yùn)行呢?
最容易想到的一個時機(jī),就是進(jìn)程執(zhí)行完終止了,它之前使用的 CPU 會釋放出來,這個時候再從就緒隊列里,拿一個新的進(jìn)程過來運(yùn)行。其實還有很多其他場景,也會觸發(fā)進(jìn)程調(diào)度,在這里我給你逐個梳理下。
其一,為了保證所有進(jìn)程可以得到公平調(diào)度,CPU 時間被劃分為一段段的時間片,這些時間片再被輪流分配給各個進(jìn)程。這樣,當(dāng)某個進(jìn)程的時間片耗盡了,就會被系統(tǒng)掛起,切換到其它正在等待 CPU 的進(jìn)程運(yùn)行。
其二,進(jìn)程在系統(tǒng)資源不足(比如內(nèi)存不足)時,要等到資源滿足后才可以運(yùn)行,這個時候進(jìn)程也會被掛起,并由系統(tǒng)調(diào)度其他進(jìn)程運(yùn)行。
其三,當(dāng)進(jìn)程通過睡眠函數(shù) sleep 這樣的方法將自己主動掛起時,自然也會重新調(diào)度。
其四,當(dāng)有優(yōu)先級更高的進(jìn)程運(yùn)行時,為了保證高優(yōu)先級進(jìn)程的運(yùn)行,當(dāng)前進(jìn)程會被掛起,由高優(yōu)先級進(jìn)程來運(yùn)行。
***一個,發(fā)生硬件中斷時,CPU 上的進(jìn)程會被中斷掛起,轉(zhuǎn)而執(zhí)行內(nèi)核中的中斷服務(wù)程序。
了解這幾個場景是非常有必要的,因為一旦出現(xiàn)上下文切換的性能問題,它們就是幕后兇手。
線程上下文切換
說完了進(jìn)程的上下文切換,我們再來看看線程相關(guān)的問題。
線程與進(jìn)程***的區(qū)別在于,線程是調(diào)度的基本單位,而進(jìn)程則是資源擁有的基本單位。說白了,所謂內(nèi)核中的任務(wù)調(diào)度,實際上的調(diào)度對象是線程;而進(jìn)程只是給線程提供了虛擬內(nèi)存、全局變量等資源。所以,對于線程和進(jìn)程,我們可以這么理解:
- 當(dāng)進(jìn)程只有一個線程時,可以認(rèn)為進(jìn)程就等于線程。
- 當(dāng)進(jìn)程擁有多個線程時,這些線程會共享相同的虛擬內(nèi)存和全局變量等資源。這些資源在上下文切換時是不需要修改的。
- 另外,線程也有自己的私有數(shù)據(jù),比如棧和寄存器等,這些在上下文切換時也是需要保存的。
這么一來,線程的上下文切換其實就可以分為兩種情況:
***種, 前后兩個線程屬于不同進(jìn)程。此時,因為資源不共享,所以切換過程就跟進(jìn)程上下文切換是一樣。
第二種,前后兩個線程屬于同一個進(jìn)程。此時,因為虛擬內(nèi)存是共享的,所以在切換時,虛擬內(nèi)存這些資源就保持不動,只需要切換線程的私有數(shù)據(jù)、寄存器等不共享的數(shù)據(jù)。
到這里你應(yīng)該也發(fā)現(xiàn)了,雖然同為上下文切換,但同進(jìn)程內(nèi)的線程切換,要比多進(jìn)程間的切換消耗更少的資源,而這,也正是多線程代替多進(jìn)程的一個優(yōu)勢。
中斷上下文切換
為了快速響應(yīng)硬件的事件,中斷處理會打斷進(jìn)程的正常調(diào)度和執(zhí)行,轉(zhuǎn)而調(diào)用中斷處理程序,響應(yīng)設(shè)備事件。而在打斷其他進(jìn)程時,就需要將進(jìn)程當(dāng)前的狀態(tài)保存下來,這樣在中斷結(jié)束后,進(jìn)程仍然可以從原來的狀態(tài)恢復(fù)運(yùn)行。
對同一個 CPU 來說,中斷處理比進(jìn)程擁有更高的優(yōu)先級,所以中斷上下文切換并不會與進(jìn)程上下文切換同時發(fā)生。同樣道理,由于中斷會打斷正常進(jìn)程的調(diào)度和執(zhí)行,所以大部分中斷處理程序都短小精悍,以便盡可能快的執(zhí)行結(jié)束。
另外,跟進(jìn)程上下文切換一樣,中斷上下文切換也需要消耗 CPU,切換次數(shù)過多也會耗費大量的 CPU,甚至嚴(yán)重降低系統(tǒng)的整體性能。所以,當(dāng)你發(fā)現(xiàn)中斷次數(shù)過多時,就需要注意去排查它是否會給你的系統(tǒng)帶來嚴(yán)重的性能問題。