終於有人把CPU上下文切換說清楚了

 

前言


多個進程競爭CPU就是一經常被我們忽視的問題。

進程CPU的時候並沒有真正運行,爲什麼還會導統的負載升高呢?你應該已經猜到了,CPU上下文切換就是罪魁禍首。

我們都知道,Linux是一個多任務操作系統,它支持遠大於CPU數量的任務同時運行。當然, 這些任務實際上並不是真的在同時運行,而是因爲系統在很短的時間內,將CPU輪流分配給它們,造成多任務同時運行的錯覺。

而在每個任務運行前,CPU 都需要知道任務從哪裏加載、又從哪裏開始運行,也就是說,需要系統事先幫它設置好CPU 寄存器和程序計數器

 

指令寄存器和程序計數器


電腦執行程序的過程就是cpu不斷執行指令的過程。cpu執行指令的過程,第一步就是取指令,並將其放入指令寄存器,然後對指令譯碼,進行一些操作,最後計算下條指令的地址,並送入程序計數器。總之,一個用來存當前指令,一個用來存下條指令的地址。cpu根據程序計數器裏的地址取指令,將取到的指令送指令寄存器。

CPU 寄存器和程序計數器就是 CPU 上下文因爲它們都是 CPU 在運行任何任務前,必須的依賴環境。

    CPU 寄存器是 CPU 內置的容量小、但速度極快的內存。
    程序計數器則是用來存儲 CPU 正在執行的指令位置、或者即將執行的下一條指令位置

 

 

CPU上下文切換


知道了什麼是CPU上下文,我想你也很容易理解CPU上下文切換。CPU上下文切換,就是先把前一個任務的CPU上下文(也就是CPU寄存器和程序計數器)保存起來,然後加載新任務 的上下文到這些寄存器和程序計數器,最後再跳轉到程序計數器所指的新位置,運行新任務。

而這些保存下來的上下文,會存儲在系統內核中,並在任務重新調度執行時再次加載進來。這樣 就能保證任務原來的狀態不受影響,讓任務看起來還是連續運行。

 

 

CPU 上下文切換的類型


我猜肯定會有人說,CPU上下文切換無非就是更新了 CPU寄存器的值嘛,但這些寄存器,本身就是爲了快速運行任務而設計的,爲什麼會影響系統的CPU性能呢?

在回答這個問題前,不知道你有沒有想過,操作系統管理的這些任務到底是什麼呢?

也許你會說,任務就是進程,或者說任務就是線程。是的,進程和線程正是最常見的任務。但是除此之外,還有沒有其他的任務呢?

不要忘了,硬件通過觸發信號,會導致中斷處理程序的調用,也是一種常見的任務。

所以,根據任務的不同,CPU的上下文切換就可以分爲幾個不同的場景,也就是進程上下文切換,線程上下文切換以及中斷上下文切換。

 

進程上下文切換


Linux中每個進程有兩個棧,分別用於用戶態和內核態的進程執行,內核棧就是用於內核態的堆棧。

運行在用戶態的程序不能訪問操作系統內核數據結構合程序。

內核態下,CPU可執行任何指令,在用戶態下CPU只能執行非特權指令。當CPU處於內核態,可以隨意進入用戶態;而當CPU處於用戶態,只能通過中斷的方式進入內核態。一般程序一開始都是運行於用戶態,當程序需要使用系統資源時,就必須通過調用軟中斷進入內核態.

Linux按照特權等級,把進程的運行空間分爲內核空間和用戶空間,分別對應着下圖中,CPU 特權等級的Ring 0Ring 3

內核空間(Ring 0)具有最高權限,可以直接訪問所有資源;

•用戶空間(Ring 3)只能訪問受限資源,不能直接訪問內存等硬件設備,必須通過系統調用陷入到內核中,才能訪問這些特權資源。

換個角度看,也就是說,進程既可以在用戶空間運行,又可以在內核空間中運行。進程在用戶空間運行時,被稱爲進程的用戶態,而陷入內核空間的時候,被稱爲進程的內核態。

從用戶態到內核態的轉變,需要通過系統調用來完成。比如,當我們查看文件內容時,就需要多次系統調用來完成:首先調用。pen()打開文件,然後調用read讀取文件內容,並調用 write()將內容寫到標準輸出,最後再調用close()關閉文件。

 那麼,系統調用的過程有沒有發生CPU上下文的切換呢?答案自然是肯定的。

CPU寄存器裏原來用戶態的指令位置,需要先保存起來。接着,爲了執行內核態代碼,CPU寄存器需要更新爲內核態指令的新位置。最後纔是跳轉到內核態運行內核任務。

而系統調用結束後,CPU寄存器需要恢復原來保存的用戶態,然後再切換到用戶空間,繼續運行進程。所以,一次系統調用的過程,其實是發生了兩次CPU上下文切換。 

不過,需要注意的是,系統調用過程中,並不會涉及到虛擬內存等進程用戶態的資源,也不會切 換進程。這跟我們通常所說的進程上下文切換是不一樣的:

進程上下文切換,是指從一個進程切換到另一個進程運行。

而系統調用過程中一直是同一個進程在運行。
所以,系統調用過程通常稱爲特權模式切換,而不是上下文切換,但實際上,系統調用過程中, CPU 的上下文切換還是無法避免的。

 

那麼,進程上下文切換跟系統調用又有什麼區別呢?

首先,你需要知道,進程是由內核來管理和調度的,進程的切換隻能發生在內核態。所以,進程的上下文不僅包括了虛擬內存、棧、全局變量等用戶空間的資源,還包括了內核堆棧、寄存器等內核空間的狀態。

因此,進程的上下文切換就比系統調用時多了一步:在保存當前進程的內核狀態和cpu寄存器之前,需要先把該進程的虛擬內存、棧等保存下來;而加載了下一進程的內核態後,還需要刷新進程的虛擬內存和用戶棧。

如下圖所示,保存上下文和恢復上下文的過程並不是“免費”的!需要內核在CPU上運行才能完成。

 

每次上下文切換都需要幾十納秒到數微秒的CPU時間。這個時間還是相當可觀的,特別是在進程上下文切換次數較多的情況下,很容易導致CPU將大量時間耗費在寄存器、內核棧以及虛擬內存等資源的保存和恢復上,進而大大縮短了真正運行進程的時間。這也正是我們所講的,導致平均負載升高的一個重要因素。 

 

另外,我們知道,Linux通過TLB (Translation Lookaside Buffer)來管理虛擬內存到物理內存的映射關係。當虛擬內存更新後,TLB也需要刷新,內存的訪問也會隨之變慢。特別是在多處理器系統上,緩存是被多個處理器共享的,刷新緩存不僅會影響當前處理器的進程,還會影響共享緩存的其他處理器的進程。

 

知道了進程上切換潛在的性能再來看,究竟什麼時候會切換進程上下文

顯然,進程切換時才需要切換上下文,換句話說,只有進程調度的時候,才需要切換下文。 Linux爲每個CPU都維護了一個就緒隊列,將活躍進程(即正在運行和正在等待CPU的進程) 按照優先級和等待CPU的時間排序,然後選擇最需要CPU的進程,也就是優先級最高和等待CPU時間最長的進程來運行。

 

進程在什麼時候纔會被調度到CPU上運行呢?

最容易想到的一個時機,就是進程執行完終止了,它之前使用的CPU會釋放出來,這個時候再從就緒隊列裏,拿一個新的進程過來運行。其實還有很多其他場景,也會觸發進程調度,在這裏逐個瞧梳理一下。

其一,爲了保證所有進程可以得到公平調度,CPU時間被劃分爲一段段的時間片,這些時間片再被輪流分配給各個進程。這樣,當某個進程的時間片耗盡了,就會被系統掛起,切換到其它正在等待CPU的進程運行。

其二,進程在系統資源不足(比如內存不足)時,要等到資源滿足後纔可以運行,這個時候進程也會被掛起,並由系統調度其他進程運行。

      其三,當進程通過睡眠函數Sleep這樣的方法將自己主動掛起時,自然也會重新調度。

其四,當有優先級更高的進程運行時,爲了保證高優先級進程的運行,當前進程會被掛起,由高優先級進程來運行。

最後,發生硬件中斷時,CPU上的進程會被中斷掛起,轉而執行內核中的中斷服務程序。

瞭解這幾個場景是非常有必要的,因爲一旦出現上下文切換的性能問題,它們就是幕後兇手。

 

線程上下文切換


說完了進程的上下文切換,我們再來看相關的問題。

線程與進程最大的區別在於,線程是調度的基本單位,而進程則是資源擁有的基本單位。說白了,所謂內核中的任務調度,實際上的調度對象是線程;而進程只是給線程提供了虛擬內存、全局變量等資源。所以,對於線程和進程,我們可以這麼理解:

•當進程只有一個線程時,可以認爲進程就等於線程。

•當進程擁有多個線程時,這些線程會共享相同的虛擬內存和全局變量等資源。這些資源在上 下文切換時是不需要修改的。

•另外,線程也有自己的私有數據,比如棧和寄存器等,這些在上下文切換時也是需要保存的。

這麼一來,線程的上下文切換其實就可以分爲兩種情況:

第一種,前後兩個線程屬於不同進程。此時,因爲資源不共享,所以切換過程就跟進程上下文切換是一樣。

第二種,前後兩個線程屬於同一個進程。此時,因爲虛擬內存是共享的,所以在切換時,虛擬內存這些資源就保持不動,只需要切換線程的私有數據,寄存器不能共享的數據。

 

到這裏你應該也發現了,雖然同爲上下文切換,但同進程內的線程切換,要比多進程間的切換消更少的資源,而這,也正是多線程代替多進程的一個優勢。

 

什麼是中斷上下文切換?


幾乎所有的體系結構,都提供了中斷機制。當硬件設備想和系統通信的時候,它首先發出一個異步的中斷信號去打斷處理器的執行,繼而打斷內核的執行。中斷通常對應着一箇中斷號,內核通過這個中斷號找到中斷服務程序,調用這個程序響應和處理中斷。當你敲擊鍵盤時,鍵盤控制器發送一箇中斷信號告知系統,鍵盤緩衝區有數據到來,內核收到這個中斷號,調用相應的中斷服務程序,該服務程序處理鍵盤數據然後通知鍵盤控制器可以繼續輸入數據了。爲了保證同步,內核可以使用中止---既可以停止所有的中斷也可以有選擇地停止某個中斷號對應的中斷,許多操作系統的中斷服務程序都不在進程上下文中執行,它們在一個與所有進程無關的、專門的中斷上下文中執行。之所以存在這樣一個專門的執行環境,爲了保證中斷服務程序能夠在第一時間響應和處理中斷請求,然後快速退出。

爲了快速響應硬件的事件,中斷處理會打斷進程的正常調度和執行,轉而調用中斷處理程序,響應設備事件。而在打斷其他進程時,就需要將進程當前的狀態保存下來,這樣在中斷結束後,進程仍然可以從原來的狀態恢復運行。

跟進程上下文不同,中斷上下文切換並不涉及到進程的用戶態。所以,即便中斷過程打斷了一個正處在用戶態的進程,也不需要保存和恢復這個進程的虛擬內存、全局變量等用戶態資源。中斷上下文,其實只包括內核態中斷服務程序執行所必需的狀態,包括CPU寄存器、內核堆棧、硬 件中斷參數等。

對同一個CPU來說,中斷處理比進程擁有更高的優先級,所以中斷上下文切換並不會與進程上下文切換同時發生。由於中斷程序會打斷正常進程的調度和運行,大部分中斷處理程序都短小精悍,以便儘可能快的執行結束。

另外,跟進程上下文切換一樣,中斷上下文切換也需要消耗CPU,切換次數過多也會耗費大量 的CPU,甚至嚴重降低系統的整體性能。所以,當你發現中斷次數過多時,就需要注意去排查它是否會給你的系統帶來嚴重的性能問題。

 

小結


總結一下,不管是哪種場景導致的上下文切換,你都應該知道:

  1. CPU上下文切換,是保證Linux系統正常工作的核心功能之一,正常情況下不需要我們特別關注。
  2. 但過多的上下文切換,會把CPU時間消耗在寄存器、內核棧以及虛擬內存等數據的保存和恢復上,從而縮短進程真正運行的時間,導致系統的整體性能大幅下降。

上下文代表着內核活動的範圍,每個處理器在任何指定時間點上的活動必然爲以下三者之一:

  • 運行於用戶空間,執行用戶進程;
  • 運行於內核空間,處於進程上下文,代表某個特定的進程執行;
  • 運行於內核空間,處於中斷上下文,與任何進程無關,處理某個特定的中斷;

以上包括所有情況,即使當CPU空閒時,內核就運行一個空進程,此時處於進程上下文,但運行於內核空間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章