1. 程式人生 > >一文讓你明白CPU上下文切換

一文讓你明白CPU上下文切換

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

而在每個任務執行前,CPU 都需要知道任務從哪裡載入、又從哪裡開始執行,也就是說,需要系統事先幫它設定好CPU 暫存器和程式計數器

什麼是 CPU 上下文

CPU 暫存器和程式計數器就是 CPU 上下文,因為它們都是 CPU 在執行任何任務前,必須的依賴環境。

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

什麼是 CPU 上下文切換

就是先把前一個任務的 CPU 上下文(也就是 CPU 暫存器和程式計數器)儲存起來,然後載入新任務的上下文到這些暫存器和程式計數器,最後再跳轉到程式計數器所指的新位置,執行新任務。

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

CPU 上下文切換的型別

根據任務的不同,可以分為以下三種類型

  • 程序上下文切換
  • 執行緒上下文切換
  • 中斷上下文切換

程序上下文切換

Linux 按照特權等級,把程序的執行空間分為核心空間和使用者空間,分別對應著下圖中, CPU 特權等級的 Ring 0 和 Ring 3。

  • 核心空間(Ring 0)具有最高許可權,可以直接訪問所有資源;
  • 使用者空間(Ring 3)只能訪問受限資源,不能直接訪問記憶體等硬體裝置,必須通過系統呼叫陷入到核心中,才能訪問這些特權資源。

來自極客時間

程序既可以在使用者空間執行,又可以在核心空間中執行。程序在使用者空間執行時,被稱為程序的使用者態,而陷入核心空間的時候,被稱為程序的核心態。

系統呼叫

從使用者態到核心態的轉變,需要通過系統呼叫來完成。比如,當我們檢視檔案內容時,就需要多次系統呼叫來完成:首先呼叫 open() 開啟檔案,然後呼叫 read() 讀取檔案內容,並呼叫 write() 將內容寫到標準輸出,最後再呼叫 close() 關閉檔案。

在這個過程中就發生了 CPU 上下文切換,整個過程是這樣的:
1、儲存 CPU 暫存器裡原來使用者態的指令位
2、為了執行核心態程式碼,CPU 暫存器需要更新為核心態指令的新位置。
3、跳轉到核心態執行核心任務。
4、當系統呼叫結束後,CPU 暫存器需要恢復原來儲存的使用者態,然後再切換到使用者空間,繼續執行程序。

所以,一次系統呼叫的過程,其實是發生了兩次 CPU 上下文切換。(使用者態-核心態-使用者態)

不過,需要注意的是,系統呼叫過程中,並不會涉及到虛擬記憶體等程序使用者態的資源,也不會切換程序。這跟我們通常所說的程序上下文切換是不一樣的:程序上下文切換,是指從一個程序切換到另一個程序執行;而系統呼叫過程中一直是同一個程序在執行。

所以,系統呼叫過程通常稱為特權模式切換,而不是上下文切換。系統呼叫屬於同進程內的 CPU 上下文切換。但實際上,系統呼叫過程中,CPU 的上下文切換還是無法避免的。

程序上下文切換跟系統呼叫又有什麼區別呢

首先,程序是由核心來管理和排程的,程序的切換隻能發生在核心態。所以,程序的上下文不僅包括了虛擬記憶體、棧、全域性變數等使用者空間的資源,還包括了核心堆疊、暫存器等核心空間的狀態。

因此,程序的上下文切換就比系統呼叫時多了一步:在儲存核心態資源(當前程序的核心狀態和 CPU 暫存器)之前,需要先把該程序的使用者態資源(虛擬記憶體、棧等)儲存下來;而載入了下一程序的核心態後,還需要重新整理程序的虛擬記憶體和使用者棧

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

來自極客時間

程序上下文切換潛在的效能問題

根據 Tsuna 的測試報告,每次上下文切換都需要幾十納秒到數微秒的 CPU 時間。這個時間還是相當可觀的,特別是在程序上下文切換次數較多的情況下,很容易導致 CPU 將大量時間耗費在暫存器、核心棧以及虛擬記憶體等資源的儲存和恢復上,進而大大縮短了真正執行程序的時間。這也正是導致平均負載升高的一個重要因素。

另外,我們知道, Linux 通過 TLB(Translation Lookaside Buffer)來管理虛擬記憶體到實體記憶體的對映關係。當虛擬記憶體更新後,TLB 也需要重新整理,記憶體的訪問也會隨之變慢。特別是在多處理器系統上,快取是被多個處理器共享的,重新整理快取不僅會影響當前處理器的程序,還會影響共享快取的其他處理器的程序。

發生程序上下文切換的場景

  1. 為了保證所有程序可以得到公平排程,CPU 時間被劃分為一段段的時間片,這些時間片再被輪流分配給各個程序。這樣,當某個程序的時間片耗盡了,就會被系統掛起,切換到其它正在等待 CPU 的程序執行。
  2. 程序在系統資源不足(比如記憶體不足)時,要等到資源滿足後才可以執行,這個時候程序也會被掛起,並由系統排程其他程序執行。
  3. 當程序通過睡眠函式 sleep 這樣的方法將自己主動掛起時,自然也會重新排程。
  4. 當有優先順序更高的程序執行時,為了保證高優先順序程序的執行,當前程序會被掛起,由高優先順序程序來執行
  5. 發生硬體中斷時,CPU 上的程序會被中斷掛起,轉而執行核心中的中斷服務程式。

執行緒上下文切換

執行緒與程序最大的區別在於:執行緒是排程的基本單位,而程序則是資源擁有的基本單位。說白了,所謂核心中的任務排程,實際上的排程物件是執行緒;而程序只是給執行緒提供了虛擬記憶體、全域性變數等資源。

所以,對於執行緒和程序,我們可以這麼理解:

  • 當程序只有一個執行緒時,可以認為程序就等於執行緒。
  • 當程序擁有多個執行緒時,這些執行緒會共享相同的虛擬記憶體和全域性變數等資源。這些資源在上下文切換時是不需要修改的。
  • 另外,執行緒也有自己的私有資料,比如棧和暫存器等,這些在上下文切換時也是需要儲存的。

發生執行緒上下文切換的場景

  1. 前後兩個執行緒屬於不同程序。此時,因為資源不共享,所以切換過程就跟程序上下文切換是一樣。
  2. 前後兩個執行緒屬於同一個程序。此時,因為虛擬記憶體是共享的,所以在切換時,虛擬記憶體這些資源就保持不動,只需要切換執行緒的私有資料、暫存器等不共享的資料

中斷上下文切換

為了快速響應硬體的事件,中斷處理會打斷程序的正常排程和執行,轉而呼叫中斷處理程式,響應裝置事件。而在打斷其他程序時,就需要將程序當前的狀態儲存下來,這樣在中斷結束後,程序仍然可以從原來的狀態恢復執行。

跟程序上下文不同,中斷上下文切換並不涉及到程序的使用者態。所以,即便中斷過程打斷了一個正處在使用者態的程序,也不需要儲存和恢復這個程序的虛擬記憶體、全域性變數等使用者態資源。中斷上下文,其實只包括核心態中斷服務程式執行所必需的狀態,包括 CPU 暫存器、核心堆疊、硬體中斷引數等。

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

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

本文整理自極客時間:《Linux效能優化實戰》

PS:本文原創釋出於微信公眾號「不只Java」,後臺回覆「電子書」,說不定有你想要的經典書籍呢。公眾號專注分享 Java 乾貨、讀書筆記、成長思考。