現代操作系統都是多任務的分時操作系統,也就是說同時響應多個用戶交互或同時支持多個任務處理,因為 CPU 的速度很快而用戶交互的頻率相比會低得多。所以例如在 Linux 中,可以支持遠大於 CPU 數量的任務同時執行,對於單個 CPU 來說,其實任務並不是在同時執行,而是操作系統在很短的時間內,使得多 ...
現代操作系統都是多任務的分時操作系統,也就是說同時響應多個用戶交互或同時支持多個任務處理,因為 CPU 的速度很快而用戶交互的頻率相比會低得多。所以例如在 Linux 中,可以支持遠大於 CPU 數量的任務同時執行,對於單個 CPU 來說,其實任務並不是在同時執行,而是操作系統在很短的時間內,使得多個進程交替獲得 CPU 來執行,由於切換速度比較快,因此這會給我們一種程式同時運行的錯覺,這就是操作系統中的多道程式設計技術,對 CPU 實現了虛擬化,使得我們看起來好像有多個 CPU 來運行任務一樣,我們也稱這多個任務是併發運行的。
每個任務運行時,都有自己的寄存器狀態,主要包括:數據寄存器、地址寄存器、控制和狀態寄存器等。其中地址寄存器又包括:變址寄存器、段指針寄存器、棧指針寄存器等;控制和狀態寄存器主要包括:程式計數器、指令寄存器等,這類寄存器對用戶來說是不可見的。
除了寄存器,進程在記憶體中也會有自己的狀態,操作系統基於這種狀態來管理和控制進程,而這種狀態不允許被用戶直接訪問或修改,例如頁表、進程優先順序、進程 I/O 狀態等。
上面的寄存器狀態和記憶體狀態我們統稱為進程的上下文(context switch),而整個進程主要有 3 部分組成:
- 可執行的程式(二進位序列)
- 程式運行所需的數據(例如:變數、緩衝區等)
- 程式的執行上下文
在多道程式設計中,假如有 A 和 B 兩個進程,當前 B 進程正在執行,這時操作系統需要調度 A 執行,那麼首先需要保存 B 的上下文,然後恢復 A 原來的上下文,恢復的過程會重新設置相關寄存器的值,例如將程式計數器設置為 A 上次執行到的指令地址,這樣 A 就繼續運行。這個由進程 B 切換為進程 A 的過程就叫做進程的上下文切換。
其實上下文切換不僅是進程上下文切換,還包括:線程上下文切換和中斷上下文切換。
系統調用和上下文切換的關係
進程的狀態分為用戶態和內核態,進程在用戶態通過系統調用陷入內核態,那麼在系統調用和上下文切換時什麼關係呢?我們來看一下,首先是要保存進程在用戶態下的寄存器,但並不會保存記憶體中的資源,比如虛擬記憶體、棧、進程式控制制狀態等,然後進入內核態會將 CPU 寄存器的值更新為內核指令的位置,確保正確執行內核中的一段代碼,然後開始執行特權指令,當系統調用執行完成後,CPU 需要恢複原來保存的用戶態寄存器,並切換到用戶空間,繼續運行進程。所以我們看這個過程類似於發生了 1.5 次 CPU 的上下文切換,但是這個切換比較輕量,並不會保存進程的記憶體狀態等資源,也不會對進程進行切換,所以我們一般不說系統調用是上下文切換,但是這個過程中其實是存在和上下文切換類似的過程的,只是開銷比較小一些,所以我們總結系統調用和上下文切換的區別:
- 進程上下文切換是多個進程間的切換,而系統調用過程不涉及進程的切換。
- 系統調用只進行寄存器的保存和恢復,不涉及進程資源的保存和恢復,因此開銷更小。
進程上下文切換的開銷
通過 lmbench 測試或者第三方的報告可以發現,進程上下文切換的開銷大約在幾十納秒到幾微秒之間,如果切換過於頻繁,那麼很容易導致大量的時間都浪費在寄存器、內核棧以及進程記憶體狀態等資源的保存和恢覆上,從而縮短進程真正運行的時間,由於上下文切換主要由 CPU 完成,因此會直接導致 CPU 負載的升高。
另外,CPU 通過 TLB 來提高虛擬記憶體到物理記憶體的查找性能,通過高速緩存來加速數據的查找,所以進程的上下文切換會導致 TLB 和高速緩存重新刷新,最終導致程式運行性能降低。
雖然上下文切換有一定的性能開銷,但是合理的上下文切換是提升多個進程執行效率的關鍵,通常上下文切換會發生在下麵這些情況中:
- 為了保證所有進程都有機會被公平調度,CPU 時間會劃分為一個個的時間片,這些時間片會儘量均勻的分配給各個進程,如果某個進程的時間片耗盡,會通過進程的上下文切換選擇合適的進程獲得進程的時間片繼續運行,合理的時間片可以將上下文切換的開銷給攤銷掉。
- 進程在等待資源時,比如發起系統調用需要等待 IO 或者網路完成才可以運行,這個時候操作系統也會調度其他的進程運行。
- 進程主動進入睡眠狀態時,也會讓出 CPU 給其他進程。
- 如果有優先順序更高的進程加入,優先順序低的進程也可能被掛起,轉而運行優先順序更高的進程。
- 發生硬體中斷時,進程會被中斷掛起,從而執行內核的中斷服務程式。
如果系統的上下文切換出現了問題,那麼一定是遇到了上面情況中的某一個或多個。
上下文切換除了上面所說的進程上下文切換,還包括線程上下文切換、中斷上下文切換,下麵簡單來敘述下。
進程是資源管理的基本單位,而線程是調度的基本單位,也就是說內核中任務調度的對象實際上是線程,進程給線程提供了地址空間、全局變數等資源,每個線程擁有自己的棧和寄存器,這樣在同一個進程的多個線程之間做上下文切換時,進程的資源是不需要變化的,只需要對每個線程獨立的棧和寄存器進行切換即可,因此線程上下文切換要比進程上下文切換的開銷更小,這也是多線程相比多進程的優勢所在。但是如果兩個線程屬於不同的進程,這時候線程上下文切換和進程上下文切換的開銷是一樣的。
中斷上下文切換是為了快速響應其他硬體,中斷處理會打斷其他進程的正常調度和執行,然後開始運行中斷處理程式,從而響應設備事件,因此在打斷正常運行的進程時,就需要將進程的寄存器保存下來,這樣在中斷結束後,進程仍然可以從原來的狀態恢復運行。同樣值得註意的是,中斷上下文不涉及進程的用戶態,也就是不需要保存被打斷進程的地址空間、全局變數、用戶棧等資源,因為中斷本身是在內核態執行,僅僅需要用到 CPU 寄存器、內核空間堆棧與硬體中斷參數等,因此只需要保存進程的寄存器、內核堆棧即可,恢復時也只需要恢復進程的內核態資源,所以中斷上下文的處理也比進程上下文切換的開銷更小。
對於同一個 CPU 來說,中斷處理比進程運行本身擁有更高的優先順序,所以中斷上下文切換不會與進程上下文切換同時發生,由於中斷會打斷正常運行的進程,因此必須保證硬體中斷處理程式必須短小精悍,快速執行才可以。
總體來說,正常的上下文切換是保證系統正常運轉的必要條件,但是過多的上下文切換會導致系統性能嚴重降低,需要我們特別註意。
上下文切換常見的排查工具有:vmstat
、pidstat
等,具體用法這裡不再敘述,只是簡單的給出原理和分析思路。
vmstat
工具可以查看整個系統全局的上下文切換情況,重點關註 r、b、cs、in 這些列。
- r 是就緒隊列的長度,包括正在運行和等待 CPU 的進程數。
- b 表示處於不可中斷睡眠狀態的進程數,通常是等待 I/O 資源的進程。
- cs 表示系統每秒上下文切換的次數。
- in 表示每秒中斷的次數。
如果想看每個進程的上下文切換情況,就需要使用 pidstat
這個工具了,使用 -w
參數可以獲得進程上下文切換的情況,主要輸出有:cswch/s 和 nvcswch/s 這兩個參數,分別表示每秒自願上下文切換次數、每秒非自願上下文切換次數,這兩個次數的含義主要如下:
- 自願上下文切換表示進程由於等待資源而發生的上下文切換,比如發起 I/O、記憶體、網路等請求時,就會發生自願上下文切換。
- 非自願上下文切換則表示進程並沒有等待資源,而是由於時間片已到而被系統強制調度,進而發生的上下文切換。如果大量的進程都在爭搶 CPU 時,就會發生很多非自願上下文切換。
這兩類切換對系統性能有著截然不同的影響,也是需要我們註意的。
性能分析思路
如果系統處在正常運行狀態下,那麼使用 vmstat
看到的上下文切換次數應該不會太大,可能幾十上百最多幾千這樣子,如果飆到了幾十萬,那麼說明系統的上下文切換肯定是不正常的,除了上下文切換次數,我們還可以通過 vmstat
重點關註下麵的現象:
- 正常 r 不應該超過 CPU 個數,如果 r 太高則說明有非常多的進程在爭搶 CPU,也就是說 CPU 有可能不夠用了。這個時候可以觀察 us 和 sy 確定 CPU 的占用,或者使用
top
進一步分析。 - 通常 b 也不會特別高,如果 b 很高則說明有非常多的進程處在 I/O 等待狀態,這個時候 wa 通常會很高,說明系統存在 I/O 瓶頸。
- in 如果比較高,達到幾萬或幾十萬,說明中斷過於頻繁,需要註意排查。
然後我們想進一步找到引起問題的進程,那麼可以繼續使用 pidstat
來分析,不過 pidstat
預設只能看到進程級別的,如果是多線程應用註意添加 -t
參數來顯示線程的信息,也就是 pidstat -wt <time>
,然後就可以通過分析自願上下文切換和非自願上下文切換來確定進程本身存在的問題。
如果上面發現 wa 特別高,懷疑是硬碟的問題,則可以使用 iotop
和 dstat
進一步分析硬碟相關的瓶頸。
如果上面發現中斷特別高,則可以通過內核文件 /proc/interrupts
查看,例如下麵的命令:
watch -d cat /proc/interrupts
這樣可以查看不同類型的中斷變化情況,具體內容和含義可以查看 Linux kernel 的文檔:https://docs.kernel.org/filesystems/proc.html#kernel-data 其中 interrupts 部分的內容,我們通過統計可以發現具體中斷的原因。
我們對上下文切換和含義與性能做了比較詳細的分析,下麵我們來整理一下相關的知識點: