聊聊Linux中CPU上下文切換

2022-10-08 12:05:24

作者:小牛呼嚕嚕 | https://xiaoniuhululu.com
計算機內功、JAVA底層、面試相關資料等更多精彩文章在公眾號「小牛呼嚕嚕 」

什麼是CPU上下文

Linux是一個多工的作業系統,多工作業系統是指多個程序執行在一個 CPU 中互不打擾,看起來像同時執行一樣。多工的作業系統這樣就可以支援遠大於CPU數量的任務"同時執行"。但是我們都知道這其實是一個錯覺,真正是系統在很短的時間內將CPU輪流執行各個任務,給使用者造成多工"同時執行"的感覺。
其中有一個問題,在每次執行任務之前,CPU必須要知道從哪裡載入任務,以及載入後從哪裡開始執行,作業系統通過CPU中暫存器和程式計數器配合,來儲存和恢復相應進度的資訊

CPU 暫存器:CPU 暫存器是 CPU 內建的速度極快的記憶體;
程式計數器:程式計數器會儲存 CPU 正在執行的指令位置,或者即將執行的指令位置。

在任務排程的過程中, 而這些資訊都儲存在CPU的暫存器中,其中即將執行的下一條指令的地址被儲存在程式計數器上。我們將這些資訊稱為CPU的上下文,也叫硬體上下文
當某一程序自願放棄它的 CPU 時間或者系統分配的時間片用完時,就會發生CPU上下文切換。

CPU上下文切換

作業系統OS在切換執行任務時,將上一任務的上下文儲存起來,然後載入新任務的上下文到CPU暫存器,最後再跳轉到程式計數器所指的新位置上執行新任務的這一動作,被稱為CPU上下文切換

CPU上下文切換的步驟:

  1. 將前一個 CPU 的上下文(也就是 CPU 暫存器和程式計數器裡邊的內容)儲存起來;
  2. 然後載入新任務的上下文到暫存器和程式計數器;
  3. 最後跳轉到程式計數器所指的新位置,執行新任務。
  4. 被儲存起來的上下文會儲存到系統核心中,等待任務重新排程執行時再次載入進來。

CPU 的上下文切換分三種:程序上下文切換、執行緒上下文切換、中斷上下文切換

上一任務的CPU上下文儲存在哪?

我們知道因為CPU過於昂貴,其效能與其他儲存裝置有數量級的差距,為了充分壓榨其效能,計算機將CPU的時間進行分片,讓各個程式在CPU上輪轉執行,被剝奪執行權的程式,等後面CPU繼續執行它的時候,這時需要一個資料結構來儲存相關資訊,以便之後恢復繼續執行,這個其實就是程序。
CPU上下文會被儲存在程序的核心空間(kernel space)上。OS在給每個程序分配虛擬記憶體空間時,會分配一個核心空間,這部分記憶體只能由核心程式碼存取。OS在切換CPU上下文前,會先將當前CPU的通用暫存器、PC等程序現場資訊儲存在程序的核心空間上,待下次切換時,再取出重新裝載到CPU上,以恢復任務的執行。

程序上下文切換

核心空間和使用者空間

我們知道為了限制不同的指令的存取能力,提升安全,Linux 按照特權等級,把程序的執行空間分為核心空間使用者空間 。程序既可以在使用者空間執行,又可以在核心空間中執行。程序在使用者空間執行時,被稱為程序的使用者態,而陷入核心空間的時候,被稱為程序的核心態

  1. 核心空間(Ring 0):具有最高許可權,可以直接存取所有資源(讀取檔案)

常見的核心操作:分配記憶體、IO操作、建立子程序……

  1. 使用者空間(Ring 3):只能存取受限資源,不能直接存取記憶體等硬體裝置,必須通過系統呼叫進入到核心中,才能存取這些特權資源

常見的使用者態空間程式:資料庫、web伺服器、shell指令碼、Java程式或者其他常見語言的程式……

我們一起看下Linux整體架構圖:

top命令檢視CPU資源

在linux系統使用top命令檢視cpu時,能看到使用者態和核心態佔用的cpu資源

其中各項資料表示內容:

us 使用者空間佔用CPU百分比
sy 核心空間佔用CPU百分比
ni 使用者程序空間內改變過優先順序的程序佔用CPU百分比
id 空閒CPU百分比
wa 等待輸入輸出的CPU時間百分比
hi 硬體中斷
si 軟體中斷
st 實時

系統呼叫

對於一個程序來說,比如web服務的程序,一般是執行在使用者態的,但是當需要存取記憶體、磁碟等硬體裝置的時候需要先進入到核心態中,也就是從使用者態到核心態的轉變,而這種轉變需要藉助系統呼叫來實現。系統呼叫是核心向用戶程序提供服務的唯一方法。
比如檢視檔案時,需要執行多次系統呼叫:open()開啟檔案,read()讀取檔案內容,write()將檔案內容輸出到控制檯,最後close()關閉檔案等。
系統呼叫的過程如下:

  1. 把 CPU 暫存器裡原來使用者態的指令位置儲存起來;
  2. 為了執行核心程式碼,CPU 暫存器需要更新為核心態指令的新位置,最後跳轉到核心態執行核心任務;
  3. 系統呼叫結束後,CPU 暫存器需要恢復原來儲存的使用者態,然後再切換到使用者空間,繼續執行程序;

我們可以發現一次系統呼叫的過程,其實是發生了兩次 CPU 上下文切換(使用者態-核心態-使用者態)。
需要注意的是:系統呼叫過程中,不涉及虛擬記憶體等程序使用者態的資源,也不會切換程序,也就是系統呼叫過程中一直是同一個程序在執行。系統呼叫過程也通常稱為特權模式切換

程序上下文切換 和 系統呼叫的區別?

  1. 程序上下文切換是指,從一個程序切換到另一個程序;系統呼叫過程一直是同一個程序在執行,屬於程序之內的上下文切換

需要注意的是:程序是由核心來管理和排程的,程序的切換隻能發生在核心態,儲存上下文和恢復上下文的過程並不免費,需要消耗一定資源

  1. 程序的上下文不僅包括了虛擬記憶體、棧、全域性變數等使用者空間的資源,還包括了核心堆疊、暫存器等核心空間的狀態。而系統呼叫這裡沒有涉及到虛擬記憶體等這些程序使用者態的資源

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

程序切換的常見場景

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

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

執行緒上下文切換

對作業系統來說,程序是資源分配的基本單位,而執行緒則是任務排程的基本單位。核心中的任務排程實際是在排程執行緒,程序只是給執行緒提供虛擬記憶體、全域性變數等資源。執行緒上下文切換時,共用相同的虛擬記憶體和全域性變數等資源不需要修改。而執行緒自己的私有資料,如棧和暫存器等,上下文切換時需要儲存。
關於程序和執行緒的區別:

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

因此執行緒上下文切換有兩種情況:

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

中斷上下文切換

上下文切換有時也因硬體中斷而觸發。硬體中斷是指硬體裝置(如鍵盤、滑鼠、偵錯解調器、系統時鐘)給核心傳送的一個訊號,該訊號表示一個事件(如按鍵、滑鼠移動、從網路連線接收到資料)發生了。
為了快速響應硬體的事件,中斷處理會打斷程序的正常排程和執行,然後呼叫中斷處理程式,響應裝置事件。在打斷其他程序時,需要先將程序當前的狀態儲存下來,等中斷結束後,程序仍然可以恢復回來。

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

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

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

上下文切換的消耗

儲存上下文恢復上下文的過程並不是免費的,需要核心在 CPU 上執行才能完成。據測試,每次上下文切換都需要幾十納秒到數微妙的 CPU 時間。特別是在程序上下文切換次數較多的情況下,很容易導致 CPU 將大量時間消耗在暫存器、核心棧、虛擬記憶體等資源的儲存和恢復上,從而大大縮短了真正執行程序的時間。

Linux相比與其他作業系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
Linux 通過 TLB 來管理虛擬記憶體到實體記憶體的對映關係。當虛擬記憶體更新後,TLB 也需要重新整理,記憶體的存取也會隨之變慢。特別是多處理器系統上,快取是被多個處理器共用的,重新整理快取不僅會影響當前處理器的程序,還會影響共用快取的其它處理器的程序。所以過多的上下文切換對系統來說意味著會消耗大量的 CPU 時間。

根據Tsuna的測試報告,每次上下文切換都需要幾十納秒到數微妙的CPU時間,這個時間還是相當可觀的。
不管是哪種場景導致的上下文切換,你都應該知道:

  1. CPU上下文切換,是保證Linux系統正常工作的核心功能,一般情況下不需要開發人員特別關注。
  2. 但過多的上下文切換,會把CPU時間消耗在暫存器、核心棧以及虛擬記憶體等資料的儲存和恢復上,從而縮短程序真正執行的時間,耗費大量的 CPU,甚至嚴重降低系統的整體效能。

補充:vmstat命令檢視整體CPU上下文切換情況

上面已經介紹到CPU上下文切換分為程序上下文切換、執行緒上下文切換、中斷上下文切換,那麼過多的上下文切換會把CPU的時間消耗在暫存器、核心棧以及虛擬記憶體等資料的儲存和恢復上,縮短程序真正執行的時間,成為系統效能大幅下降的一個因素
所以我們可以使用vmstat這個工具來查詢系統的上下文切換情況,vmstat是一個常用的系統效能分析工具,可以用來分析CPU上下文切換和中斷的次數
執行如下的命令:vmstat 5 (每隔5s輸出一組資料)

該命令輸出資訊中,各個欄位以及含義:
procs:procs 中有 r 和 b 列,它報告程序統計資訊。在上面的輸出中,在執行佇列(r)中有兩個程序在等待 CPU 並有零個休眠程序(b)。通常,它不應該超過處理器(或核心)的數量,如果你發現異常,最好使用 top 命令進一步地排除故障。

  • r:等待執行的程序數。
  • b:休眠狀態下的程序數。

memory: memory 下有報告記憶體統計的 swpd、free、buff 和 cache 列。你可以用 free -m 命令看到同樣的資訊。在上面的記憶體統計中,統計資料以千位元組表示,這有點難以理解,最好新增 M 引數來看到以兆位元組為單位的統計資料。

  • swpd:使用的虛擬記憶體量。
  • free:空閒記憶體量。
  • buff:用作緩衝區的記憶體量。
  • cache:用作快取記憶體的記憶體量。
  • inact:非活動記憶體的數量。
  • active:活動記憶體量。

swap:swap 有 si 和 so 列,用於報告交換記憶體統計資訊。你可以用 free -m 命令看到相同的資訊。

  • si:從磁碟交換的記憶體量(換入,從 swap 移到實際記憶體的記憶體)。
  • so:交換到磁碟的記憶體量(換出,從實際記憶體移動到 swap 的記憶體)。

I/O:I/O 有 bi 和 bo 列,它以「塊讀取」和「塊寫入」的單位來報告每秒磁碟讀取和寫入的塊的統計資訊。如果你發現有巨大的 I/O 讀寫,最好使用 iotop 和 iostat 命令來檢視。

  • bi:從塊裝置接收的塊數。
  • bo:傳送到塊裝置的塊數。

system:system 有 in 和 cs 列,它報告每秒的系統操作。

  • in:每秒的系統中斷數,包括時鐘中斷。
  • cs:系統為了處理所以任務而上下文切換的數量。

CPU:CPU 有 us、sy、id 和 wa 列,報告(所用的) CPU 資源佔總 CPU 時間的百分比。如果你發現異常,最好使用 top 和 free 命令。

  • us:處理器在非核心程式消耗的時間。
  • sy:處理器在核心相關任務上消耗的時間。
  • id:處理器的空閒時間。
  • wa:處理器在等待IO操作完成以繼續處理任務上的時間。

補充:pidstat命令檢視程序的CPU上下文切換情況

筆者的環境是:Centos7

執行如下的命令:pidstat,檢視程序的CPU上下文切換情況
如果沒有安裝,yum install sysstat安裝即可

在結果中你能看到如下內容:

  • PID - 被監控的任務的程序號
  • %usr - 當在使用者層執行(應用程式)時這個任務的cpu使用率,和 nice 優先順序無關。注意這個欄位計算的cpu時間不包括在虛擬處理器中花去的時間。
  • %system - 這個任務在系統層使用時的cpu使用率。
  • %guest - 任務花費在虛擬機器器上的cpu使用率(執行在虛擬處理器)。
  • %CPU - 任務總的cpu使用率。在SMP環境(多處理器)中,如果在命令列中輸入-I引數的話,cpu使用率會除以你的cpu數量。
  • CPU - 正在執行這個任務的處理器編號。
  • Command - 這個任務的命令名稱。

參考資料:
《Linux核心設計與實現》
《Linux效能優化實戰》
http://ifeve.com/context-switch-definition
https://www.it610.com/article/1289356670568308736.htm


本篇文章到這裡就結束啦,很感謝你能看到最後,如果覺得文章對你有幫助,別忘記關注我!更多精彩的文章