進程在競爭 CPU 的時候並無真正運行,爲何還會致使系統的負載升高呢?CPU 上下文切換就是罪魁禍首。緩存
咱們都知道,Linux 是一個多任務操做系統,它支持遠大於 CPU 數量的任務同時運行。固然,這些任務實際上並非真的在同時運行,而是由於系統在很短的時間內,將 CPU 輪流分配給它們,形成多任務同時運行的錯覺。多線程
而在每一個任務運行前,CPU 都須要知道任務從哪裏加載、又從哪裏開始運行,也就是說,須要系統事先幫它設置好CPU 寄存器和程序計數器(Program Counter,PC)。ide
CPU 寄存器,是 CPU 內置的容量小、但速度極快的內存。而程序計數器,則是用來存儲 CPU 正在執行的指令位置、或者即將執行的下一條指令位置。它們都是 CPU 在運行任何任務前,必須的依賴環境,所以也被叫作CPU 上下文。函數
知道了什麼是 CPU 上下文,我想你也很容易理解 CPU 上下文切換 。性能
CPU 上下文切換,就是先把前一個任務的 CPU 上下文(也就是 CPU 寄存器和程序計數器)保存起來,測試
而後加載新任務的上下文到這些寄存器和程序計數器,最後再跳轉到程序計數器所指的新位置,運行新任務。 操作系統
而這些保存下來的上下文,會存儲在系統內核中,並在任務從新調度執行時再次加載進來。這樣就能保證任務原來的狀態不受影響,讓任務看起來仍是連續運行。線程
根據任務的不一樣 ,CPU 的上下文切換就能夠分爲幾個不一樣的場景:對象
也就是進程上下文切換、線程上下文切換以及中斷上下文切換。 blog
進程上下文切換
Linux 按照特權等級,把進程的運行空間分爲內核空間和用戶空間,分別對應着下圖中, CPU 特權等級的 Ring 0 和 Ring 3。
內核空間(Ring 0)具備最高權限,能夠直接訪問全部資源;
用戶空間(Ring 3)只能訪問受限資源,不能直接訪問內存等硬件設備,必須經過系統調用陷入到內核中,才能訪問這些特權資源。
換個角度看,也就是說,進程既能夠在用戶空間運行,又能夠在內核空間中運行。進程在用戶空間運行時,被稱爲進程的用戶態,而陷入內核空間的時候,被稱爲進程的內核態。
從用戶態到內核態的轉變,須要經過 系統調用 來完成。好比,當咱們查看文件內容時,就須要屢次系統調用來完成:首先調用 open() 打開文件,而後調用 read() 讀取文件內容,並調用 write() 將內容寫到標準輸出,最後再調用 close() 關閉文件。
那麼,系統調用的過程有沒有發生 CPU 上下文的切換呢?答案天然是確定的。
CPU 寄存器裏原來用戶態的指令位置,須要先保存起來。接着,爲了執行內核態代碼,CPU 寄存器須要更新爲內核態指令的新位置。最後纔是跳轉到內核態運行內核任務。
而系統調用結束後,CPU 寄存器須要 恢復原來保存的用戶態,而後再切換到用戶空間,繼續運行進程。因此,一次系統調用的過程,實際上是發生了兩次 CPU 上下文切換。
不過,須要注意的是,系統調用過程當中,並不會涉及到虛擬內存等進程用戶態的資源,也不會切換進程。這跟咱們一般所說的進程上下文切換是不同的:
進程上下文切換,是指從一個進程切換到另外一個進程運行。
而系統調用過程當中一直是同一個進程在運行。
因此, 系統調用過程一般稱爲特權模式切換,而不是上下文切換 。但實際上,系統調用過程當中,CPU 的上下文切換仍是沒法避免的。
進程上下文切換跟系統調用的區別
首先,你須要知道,進程是由內核來管理和調度的,進程的切換隻能發生在內核態。因此,進程的上下文不只包括了虛擬內存、棧、全局變量等用戶空間的資源,
還包括了內核堆棧、寄存器等內核空間的狀態。
所以,進程的上下文切換就比系統調用時多了一步:在保存當前進程的內核狀態和 CPU 寄存器以前,須要先把該進程的虛擬內存、棧等保存下來;
而加載了下一進程的內核態後,還須要刷新進程的虛擬內存和用戶棧。
根據實際的測試報告,每次上下文切換都須要幾十納秒到數微秒的 CPU 時間。這個時間仍是至關可觀的,特別是在進程上下文切換次數較多的狀況下,
很容易致使 CPU 將大量時間耗費在寄存器、內核棧以及虛擬內存等資源的保存和恢復上,進而大大縮短了真正運行進程的時間。
這也正是上一節中咱們所講的,致使平均負載升高的一個重要因素。
另外,咱們知道, Linux 經過 TLB(Translation Lookaside Buffer)來管理虛擬內存到物理內存的映射關係。
當虛擬內存更新後,TLB 也須要刷新,內存的訪問也會隨之變慢。特別是在多處理器系統上,緩存是被多個處理器共享的,
刷新緩存不只會影響當前處理器的進程,還會影響共享緩存的其餘處理器的進程。
知道了進程上下文切換潛在的性能問題後,咱們再來看,究竟何時會切換進程上下文。
最容易想到的一個時機,就是進程執行完終止了,它以前使用的 CPU 會釋放出來,這個時候再從就緒隊列裏,拿一個新的進程過來運行。
其實還有不少其餘場景,也會觸發進程調度,在這裏我給你逐個梳理下:
1.其一,爲了保證全部進程能夠獲得公平調度,CPU 時間被劃分爲一段段的時間片,這些時間片再被輪流分配給各個進程。
這樣,當某個進程的時間片耗盡了,就會被系統掛起,切換到其它正在等待 CPU 的進程運行。
2.其二,進程在系統資源不足(好比內存不足)時,要等到資源知足後才能夠運行,這個時候進程也會被掛起,並由系統調度其餘進程運行。
3.其三,當進程經過睡眠函數 sleep 這樣的方法將本身主動掛起時,天然也會從新調度。
4.其四,當有優先級更高的進程運行時,爲了保證高優先級進程的運行,當前進程會被掛起,由高優先級進程來運行。
5.最後一個,發生硬件中斷時,CPU 上的進程會被中斷掛起,轉而執行內核中的中斷服務程序。
瞭解這幾個場景是很是有必要的,由於一旦出現上下文切換的性能問題,它們就是幕後兇手。
線程上下文切換
線程與進程最大的區別在於, 線程是調度的基本單位,而進程則是資源擁有的基本單位 。說白了,所謂內核中的任務調度,實際上的調度對象是線程;
而進程只是給線程提供了虛擬內存、全局變量等資源。因此,對於線程和進程,咱們能夠這麼理解:
1.當進程只有一個線程時,能夠認爲進程就等於線程。
2.當進程擁有多個線程時,這些線程會共享相同的虛擬內存和全局變量等資源。這些資源在上下文切換時是不須要修改的。
3.另外,線程也有本身的私有數據,好比棧和寄存器等,這些在上下文切換時也是須要保存的。
這麼一來,線程的上下文切換其實就能夠分爲兩種狀況:
第一種, 先後兩個線程屬於不一樣進程。此時,由於資源不共享,因此切換過程就跟進程上下文切換是同樣。
第二種,先後兩個線程屬於同一個進程。此時,由於虛擬內存是共享的,因此在切換時,虛擬內存這些資源就保持不動,只須要切換線程的私有數據、寄存器等不共享的數據。
到這裏你應該也發現了,雖然同爲上下文切換,但同進程內的線程切換,要比多進程間的切換消耗更少的資源,而這,也正是多線程代替多進程的一個優點。
中斷上下文切換
除了前面兩種上下文切換,還有一個場景也會切換 CPU 上下文,那就是中斷。
爲了快速響應硬件的事件, 中斷處理會打斷進程的正常調度和執行 ,轉而調用中斷處理程序,響應設備事件。
而在打斷其餘進程時,就須要將進程當前的狀態保存下來,這樣在中斷結束後,進程仍然能夠從原來的狀態恢復運行。
跟進程上下文不一樣,中斷上下文切換並不涉及到進程的用戶態。因此,即使中斷過程打斷了一個正處在用戶態的進程,
也不須要保存和恢復這個進程的虛擬內存、全局變量等用戶態資源。中斷上下文,其實只包括內核態中斷服務程序執行所必需的狀態,
包括 CPU 寄存器、內核堆棧、硬件中斷參數等。
對同一個 CPU 來講,中斷處理比進程擁有更高的優先級 ,因此中斷上下文切換並不會與進程上下文切換同時發生。
一樣道理,因爲中斷會打斷正常進程的調度和執行,因此大部分中斷處理程序都短小精悍,以便儘量快的執行結束。
另外,跟進程上下文切換同樣,中斷上下文切換也須要消耗 CPU,切換次數過多也會耗費大量的 CPU,甚至嚴重下降系統的總體性能。
因此,當你發現中斷次數過多時,就須要注意去排查它是否會給你的系統帶來嚴重的性能問題。
小結
1.CPU 上下文切換,是保證 Linux 系統正常工做的核心功能之一,通常狀況下不須要咱們特別關注。
2.但過多的上下文切換,會把 CPU 時間消耗在寄存器、內核棧以及虛擬內存等數據的保存和恢復上,從而縮短進程真正運行的時間,致使系統的總體性能大幅降低。