1. 程式人生 > >深入理解Linux的CPU上下文切換

深入理解Linux的CPU上下文切換

如何理解Linux的上下文切換
Linux 是一個多工作業系統,它支援同時執行的任務數量遠大於 CPU 個數。其實這些任務沒有真正的同時執行,是因為系統在很短的時間內,將 CPU 輪流分配給它們,造成多工同時執行的錯覺。

而在每個任務執行前,CPU 都需要知道任務從哪裡載入、從哪裡開始執行,需要系統事先設定好 CPU 暫存器和程式計數器。CPU 暫存器是 CPU 內建的容量小、速度極快的記憶體。而程式計數器則是用來儲存 CPU 正在執行的指令位置、或即將執行的下一條指令位置。它們都是 CPU 在執行任務前必須依賴的環境,也被叫做 CPU 上下文。

上下文切換,就是先把前一個任務的 CPU 上下文儲存起來,然後載入新任務的上下文到這些暫存器和程式計數器,最後再跳到程式計數器所指的新位置,執行新任務。而這些儲存下來的上下文,會儲存在系統核心中,並在任務重新排程執行時再次載入進來。這樣就能保證任務原來的狀態不受影響,讓任務看起來還是連續執行。

根據任務的不同,CPU 的上下文切換可以分為幾個不同的場景,也就是:程序上下文切換、執行緒上下文切換、中斷上下文切換。

程序上下文切換
1、使用者空間與核心空間

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

核心空間(Ring 0)具有最高許可權,可以直接訪問所有資源。
使用者空間(Ring 3)只能訪問受限資源,不能直接訪問記憶體等硬體裝置,必須通過系統呼叫陷入核心中才能訪問這些特權資源。
程序既可以在使用者空間執行,又可以在核心空間執行。在使用者空間執行時被稱為程序的使用者態,而陷入核心空間的時候,被稱為程序的核心態。
2、系統呼叫

從使用者態到核心態的轉變,需要通過系統呼叫來完成。比如檢視檔案時,需要執行多次系統呼叫:open、read、write、close等。系統呼叫的過程如下:

首先,把 CPU 暫存器裡原來使用者態的指令位置儲存起來
為了執行核心程式碼,CPU 暫存器需要更新為核心態指令的新位置,最後跳轉到核心態執行核心任務。
系統呼叫結束後,CPU 暫存器需要恢復原來儲存的使用者態,然後再切換到使用者空間,繼續執行程序
所以,一次系統呼叫的過程,其實是發生了兩次 CPU 上下文切換。
但系統呼叫的過程中並不會涉及虛擬記憶體等程序使用者態的資源,也不會切換程序,這和平時說的程序上下文切換是不一樣的:

程序上下文切換,是指從一個程序切換到另一個程序執行
系統呼叫過程中一直是同一個程序在執行
因此,系統呼叫的過程通常稱為特權模式切換,而不是上下文切換。

3、程序上下文切換

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

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

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

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

4、程序上下文何時切換

Linux 為每個 CPU 維護了一個就緒佇列,將活躍程序按照優先順序和等待 CPU 的時間排序,然後選擇最需要 CPU 的程序,也就是優先順序最高和等待 CPU 時間最長的程序來執行。那麼,程序在什麼時候才會被排程到 CPU 上執行呢?

程序執行完終止了,它之前使用的 CPU 會釋放出來,這時再從就緒佇列中拿一個新的程序來執行
為了保證所有程序可以得到公平排程,CPU 時間被劃分為一段段的時間片,這些時間片被輪流分配給各個程序。當某個程序時間片耗盡了就會被系統掛起,切換到其它等待 CPU 的程序執行。
程序在系統資源不足時,要等待資源滿足後才可以執行,這時程序也會被掛起,並由系統排程其它程序執行。
當程序通過睡眠函式 sleep 主動掛起時,也會重新排程。
當有優先順序更高的程序執行時,為了保證高優先順序程序的執行,當前程序會被掛起,由高優先順序程序來執行。
發生硬體中斷時,CPU 上的程序會被中斷掛起,轉而執行核心中的中斷服務程式。
執行緒上下文切換
執行緒與程序最大的區別在於,執行緒是作業系統排程的最小單位,而程序是作業系統分配資源的最小單位。所謂核心排程,實際上的排程物件是執行緒,而程序只是給執行緒提供了虛擬記憶體、全域性變數等資源。對於執行緒和程序我們可以這麼理解:

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

前後兩個執行緒屬於不同程序。此時因為資源不共享,所以切換過程就跟程序上下文切換是一樣的。
前後兩個執行緒屬於同一個程序。此時虛擬記憶體是共享的,上下文切換時,虛擬記憶體這些資源保持不動,只需要切換執行緒的私有數、暫存器等不共享的資料。
可以發現同進程內的執行緒切換,要比多程序間的切換消耗更少的資源,這也正是多執行緒代替多程序的一個優勢。

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

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

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

跟程序上下文切換一樣,中斷上下文切換也需要消耗 CPU,當發現中斷次數過多時,就需要注意去排查它是否會給你的系統帶來嚴重的效能問題。

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

CPU 上下文切換是保證 Linux 系統正常工作的核心功能之一,一般情況下我們無需特別關注。
過多的上下文切換,會把 CPU 時間消耗在暫存器、核心棧、虛擬記憶體等資料的儲存和恢復上,從而縮短程序真正執行的時間,導致系統的整體效能大幅下降。
如何檢視系統的上下文切換
我們可以通過 vmstat 工具來檢視系統的上下文切換情況。vmstat 主要用來分析系統記憶體使用情況,也常用來分析 CPU 上下文切換和中斷的次數。

#每隔 5 秒輸出 1 組資料
$ vmstat 5
procs -----------memory---------- —swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 7005360 91564 818900 0 0 0 0 25 33 0 0 100 0 0
我們需要重點關注下列四項內容:

cs(context switch) 是每秒上下文切換的次數。
in(interrupt) 是每秒中斷的次數。
r(Running or Runnable) 是就緒佇列的長度,也就是正在執行和等待 CPU 的程序數。
b(Blocked) 是處於不可中斷睡眠狀態的程序數。
想要檢視每個程序的詳細情況,需要使用 pidstat,給它加上 -w 選項,就可以檢視每個程序上下文切換的情況。

#每隔 5 秒輸出 1 組資料
$ pidstat -w 5
Linux 4.15.0 (ubuntu) 09/23/18 x86_64 (2 CPU)
08:18:26 UID PID cswch/s nvcswch/s Command
08:18:31 0 1 0.20 0.00 systemd
08:18:31 0 8 5.40 0.00 rcu_sched
上述結果有兩列是我們重點關注的物件,一個是 cswch,表示每秒自願上下文切換的次數;另一個是 nvcswch,表示每秒非自願上下文切換的次數。

自願上下文切換,是指程序無法獲取所需資源,導致的上下文切換。比如,IO、記憶體等系統資源不足時,就會發生自願上下文切換。
非資源上下文切換,則是指程序由於時間片已到等原因,被系統強制排程,進而發生的上下文切換。比如說,大量程序都在搶佔 CPU 時,就容易發生非自願上下文切換。
案例分析
準備環境

sysbench 是一個多執行緒的基準測試工具,一般用來評估不同系統引數下的資料庫負載情況,本次案例把它當作一個異常程序來看,作用是模擬上下文切換過多的問題。

#預先安裝 sysbench
$ yum install sysbench -y
操作和分析

首先在第一個終端裡執行 sysbench,模擬系統多執行緒排程的瓶頸:

#以 10 個執行緒執行 5 分鐘的基準測試,模擬多執行緒切換的問題
$ sysbench --threads=10 --max-time=300 threads run
接著在第二個終端執行 vmstat,觀察上下文切換情況:

#每隔 1 秒輸出 1 組資料(需要 Ctrl+C 才結束)
$ vmstat 1
procs --------memory-------- —swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
6 0 0 6487428 118240 1292772 0 0 0 0 9019 1398830 16 84 0 0 0
8 0 0 6487428 118240 1292772 0 0 0 0 10191 1392312 16 84 0 0 0
可以發現,cs 列的上下文切換次數從之前的 35 上升到了 139 萬,觀察其他幾個指標:

r 列:就緒佇列長度為 8,遠大於 CPU 個數,所以會有大量的 CPU 競爭
us 和 sys 列:這兩列加一起上升到 100%,sys 列高達 84%,說明 CPU 主要是被核心佔用了。
in 列:中斷次數為 1 萬左右,說明中斷也是個潛在的問題。
綜合分析,由於系統的就緒佇列過長,也就是正在執行和等待 CPU 的程序數過多,導致了大量的上下文切換,而上下文切換又導致了系統 CPU 的佔用率升高。

我們可以使用 pidstat 繼續分析到底是哪個程序導致了這些問題?

#每隔 1 秒輸出 1 組資料(需要 Ctrl+C 才結束)
#-w 引數表示輸出程序切換指標,而 -u 引數則表示輸出 CPU 使用指標
$ pidstat -w -u 1
08:06:33 UID PID %usr %system %guest %wait %CPU CPU Command
08:06:34 0 10488 30.00 100.00 0.00 0.00 100.00 0 sysbench
08:06:34 0 26326 0.00 1.00 0.00 0.00 1.00 0 kworker/u4:2

08:06:33 UID PID cswch/s nvcswch/s Command
08:06:34 0 8 11.00 0.00 rcu_sched
08:06:34 0 16 1.00 0.00 ksoftirqd/1
08:06:34 0 471 1.00 0.00 hv_balloon
08:06:34 0 1230 1.00 0.00 iscsid
08:06:34 0 4089 1.00 0.00 kworker/1:5
08:06:34 0 4333 1.00 0.00 kworker/0:3
08:06:34 0 10499 1.00 224.00 pidstat
08:06:34 0 26326 236.00 0.00 kworker/u4:2
08:06:34 1000 26784 223.00 0.00 sshd
可以發現,CPU 使用率的升高是 sysbench 導致的,但上下文切換則來自其他程序,包括非自願上下文切換頻率最高的 pidstat,以及自願上下文切換頻率最高的核心執行緒 kworker 和 sshd。

預設 pidstat 顯示程序的指標資料,加上 -t 引數後,才會輸出執行緒的指標

#每隔 1 秒輸出一組資料(需要 Ctrl+C 才結束)
#-wt 引數表示輸出執行緒的上下文切換指標
$ pidstat -wt 1
08:14:05 UID TGID TID cswch/s nvcswch/s Command

08:14:05 0 10551 - 6.00 0.00 sysbench
08:14:05 0 - 10551 6.00 0.00 |__sysbench
08:14:05 0 - 10552 18911.00 103740.00 |__sysbench
08:14:05 0 - 10553 18915.00 100955.00 |__sysbench
08:14:05 0 - 10554 18827.00 103954.00 |__sysbench

雖然 sysbench 程序的上下文切換次數不多,但它的子執行緒的上下文切換次數非常多,可以判定上下文切換罪魁禍首的是 sysbench 程序。還沒完,記得我們通過 vmstat 看到的中斷次數到了 1 萬,到底是什麼型別的中斷上升了呢?

我們可以通過 /proc/interrupts 來讀取中斷的使用情況,通過執行下面的命令:

#-d 引數表示高亮顯示變化的區域
$ watch -d cat /proc/interrupts
CPU0 CPU1

RES: 2450431 5279697 Rescheduling interrupts

可以發現,變化速度最快的是重排程中斷(RES),表示喚醒空閒狀態的 CPU 來排程新的任務執行。這是多處理器系統(SMP)中,排程器用來分散任務佇列到不同 CPU 的機制,通常也被稱為處理器間中斷。根本原因還是因為過多工的排程問題,跟前邊分析結果是一致的。

每秒上下文切換多少次算正常
這個數值其實取決於系統本身的 CPU 效能。如果系統的上下文切換次數比較穩定,從數百到一萬以內,都應該算是正常的。如果當上下文切換次數超過一萬次,或者切換次數出現數量級增長時,很可能已經出現了效能問題。

這時,你還需要根據上下文切換的型別,再做具體分析,比方說:

自願上下文切換變多了,說明程序都在等待資源,有可能發生了 IO 等其他問題
非自願上下文切換變多了,說明程序都在被強制排程,也就是都在爭搶 CPU,說明 CPU 的確成了瓶頸。
中斷次數變多了,說明 CPU 被中斷處理程式佔用,還需要通過檢視 /proc/interrupts 檔案來分析具體的中斷型別。