进程用户态 上下文切换需要保存哪些_漫话性能:CPU上下文切换
序言
表弟:CPU平均負(fù)載?zhuanlan.zhihu.com我們理解了平均負(fù)載( Load Average),并用三個(gè)案例展示了不同場景下平均負(fù)載升高的分析方法。這其中,多個(gè)進(jìn)程(或線程)競爭 CPU 就是一個(gè)經(jīng)常被我們忽視的問題。
進(jìn)程在競爭 CPU 的時(shí)候并沒有真正運(yùn)行,為什么還會導(dǎo)致系統(tǒng)的負(fù)載升高呢?CPU 上下文切換就是罪魁禍?zhǔn)住N覀兌贾?#xff0c;Linux 是一個(gè)多任務(wù)操作系統(tǒng),它支持遠(yuǎn)大于 CPU 數(shù)量的任務(wù)同時(shí)運(yùn)行。當(dāng)然,這些任務(wù)實(shí)際上并不是真的在同時(shí)運(yùn)行,而是因?yàn)橄到y(tǒng)在很短的時(shí)間內(nèi),將 CPU 輪流分配給它們,造成多任務(wù)同時(shí)運(yùn)行的錯覺。
而在每個(gè)任務(wù)運(yùn)行前,CPU 都需要知道任務(wù)從哪里加載、又從哪里開始運(yùn)行,也就是說,需要系統(tǒng)事先幫它設(shè)置好 CPU 寄存器和程序計(jì)數(shù)器(Program Counter,PC)。
CPU上下文
什么是CPU上下文呢?說直白點(diǎn)就是CPU的寄存器信息,以ARM為例,CPU寄存器信息如下所示:
CPU 寄存器,是 CPU 內(nèi)置的容量小、但速度極快的內(nèi)存。而程序計(jì)數(shù)器,則是用來存儲 CPU 正在執(zhí)行的指令位置、或者即將執(zhí)行的下一條指令位置。它們都是 CPU 在運(yùn)行任何任務(wù)前,必須的依賴環(huán)境,因此也被叫做 CPU 上下文。
CPU 上下文切換,就是先把前一個(gè)任務(wù)的 CPU 上下文(也就是 CPU 寄存器和程序計(jì)數(shù)器)保存起來,然后加載新任務(wù)的上下文到這些寄存器和程序計(jì)數(shù)器,最后再跳轉(zhuǎn)到程序計(jì)數(shù)器所指的新位置,運(yùn)行新任務(wù)。而這些保存下來的上下文,會存儲在系統(tǒng)內(nèi)核中,并在任務(wù)重新調(diào)度執(zhí)行時(shí)再次加載進(jìn)來。這樣就能保證任務(wù)原來的狀態(tài)不受影響,讓任務(wù)看起來還是連續(xù)運(yùn)行。
進(jìn)程上下文切換
進(jìn)程上下文切換是指從一個(gè)進(jìn)程切換到另一個(gè)進(jìn)程運(yùn)行。而系統(tǒng)調(diào)用過程中一直是同一個(gè)進(jìn)程在運(yùn)行。所以,系統(tǒng)調(diào)用過程通常稱為特權(quán)模式切換,而不是上下文切換。但實(shí)際上,系統(tǒng)調(diào)用過程中,CPU 的上下文切換還是無法避免的。
特權(quán)模式
內(nèi)核把虛擬地址空間劃分為兩個(gè)部分,因此能夠保護(hù)各個(gè)系統(tǒng)進(jìn)程,使之彼此隔離。所有的現(xiàn)代CPU 都提供了幾種特權(quán)級別,進(jìn)程可以駐留在某一特權(quán)級別。每個(gè)特權(quán)級別都有各種限制,例如對執(zhí)行 某些匯編語言指令或訪問虛擬地址空間某一特定部分的限制。IA-32體系結(jié)構(gòu)使用4種特權(quán)級別構(gòu)成的系統(tǒng), 各級別可以看作是環(huán)。內(nèi)環(huán)能夠訪問更多的功能,外環(huán)則較少,如圖所示:
盡管英特爾處理器區(qū)分4種特權(quán)級別,但Linux只使用兩種不同的狀態(tài):核心態(tài)和用戶狀態(tài)。 兩種狀態(tài)的關(guān)鍵差別在于對高于TASK_SIZE的內(nèi)存區(qū)域的訪問。簡而言之,在用戶狀態(tài)禁止訪問內(nèi)核空間。 用戶進(jìn)程不能操作或讀取內(nèi)核空間中的數(shù)據(jù),也無法執(zhí)行內(nèi)核空間中的代碼。這是內(nèi)核的專用領(lǐng)域。 這種機(jī)制可防止進(jìn)程無意間修改彼此的數(shù)據(jù)而造成相互干擾。從用戶狀態(tài)到核心態(tài)的切換通過系統(tǒng)調(diào)用的特定轉(zhuǎn)換手段完成。
換個(gè)角度看,也就是說,進(jìn)程既可以在用戶空間運(yùn)行,又可以在內(nèi)核空間中運(yùn)行。進(jìn)程在用戶空間運(yùn)行時(shí),被稱為進(jìn)程的用戶態(tài),而陷入內(nèi)核空間的時(shí)候,被稱為進(jìn)程的內(nèi)核態(tài)。從用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)變,需要通過系統(tǒng)調(diào)用來完成。
系統(tǒng)調(diào)用的過程有沒有發(fā)生 CPU 上下文的切換呢?如下文章有詳細(xì)分析:
novelinux/system_calls?github.com不過,需要注意的是,系統(tǒng)調(diào)用過程中,并不會涉及到虛擬內(nèi)存等進(jìn)程用戶態(tài)的資源,也不會切換進(jìn)程。這跟我們通常所說的進(jìn)程上下文切換是不一樣的。
進(jìn)程上下文切換跟系統(tǒng)調(diào)用又有什么區(qū)別呢?
首先,進(jìn)程是由內(nèi)核來管理和調(diào)度的,進(jìn)程的切換只能發(fā)生在內(nèi)核態(tài)。所以,進(jìn)程的上下文不僅包括了虛擬內(nèi)存、棧、全局變量等用戶空間的資源,還包括了內(nèi)核堆棧、寄存器等內(nèi)核空間的狀態(tài)。因此,進(jìn)程的上下文切換就比系統(tǒng)調(diào)用時(shí)多了一步:在保存當(dāng)前進(jìn)程的內(nèi)核狀態(tài)和 CPU 寄存器之前,需要先把該進(jìn)程的虛擬內(nèi)存、棧等保存下來;而加載了下一進(jìn)程的內(nèi)核態(tài)后,還需要刷新進(jìn)程的虛擬內(nèi)存和用戶棧。如下圖所示,保存上下文和恢復(fù)上下文的過程并不是“免費(fèi)”的,需要內(nèi)核在 CPU 上運(yùn)行才能完成。
根據(jù)https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html 的測試報(bào)告,每次上下文切換都需要幾十納秒到數(shù)微秒的 CPU 時(shí)間。這個(gè)時(shí)間還是相當(dāng)可觀的,特別是在進(jìn)程上下文切換次數(shù)較多的情況下,很容易導(dǎo)致 CPU 將大量時(shí)間耗費(fèi)在寄存器、內(nèi)核棧以及虛擬內(nèi)存等資源的保存和恢復(fù)上,進(jìn)而大大縮短了真正運(yùn)行進(jìn)程的時(shí)間。這也正是導(dǎo)致平均負(fù)載升高的一個(gè)重要因素。
我們知道, Linux 通過 TLB(Translation Lookaside Buffer)來管理虛擬內(nèi)存到物理內(nèi)存的映射關(guān)系。當(dāng)虛擬內(nèi)存更新后,TLB 也需要刷新,內(nèi)存的訪問也會隨之變慢。特別是在多處理器系統(tǒng)上,緩存是被多個(gè)處理器共享的,刷新緩存不僅會影響當(dāng)前處理器的進(jìn)程,還會影響共享緩存的其他處理器的進(jìn)程。知道了進(jìn)程上下文切換潛在的性能問題后,我們再來看,究竟什么時(shí)候會切換進(jìn)程上下文。顯然,進(jìn)程切換時(shí)才需要切換上下文,換句話說,只有在進(jìn)程調(diào)度的時(shí)候,才需要切換上下文。
Linux 為每個(gè) CPU 都維護(hù)了一個(gè)就緒隊(duì)列,將活躍進(jìn)程(即正在運(yùn)行和正在等待 CPU 的進(jìn)程)按照優(yōu)先級和等待 CPU 的時(shí)間排序,然后選擇最需要 CPU 的進(jìn)程,也就是優(yōu)先級最高和等待 CPU 時(shí)間最長的進(jìn)程來運(yùn)行。那么,進(jìn)程在什么時(shí)候才會被調(diào)度到 CPU 上運(yùn)行呢?最容易想到的一個(gè)時(shí)機(jī),就是進(jìn)程執(zhí)行完終止了,它之前使用的 CPU 會釋放出來,這個(gè)時(shí)候再從就緒隊(duì)列里,拿一個(gè)新的進(jìn)程過來運(yùn)行。其實(shí)還有很多其他場景,也會觸發(fā)進(jìn)程調(diào)度:
- 為了保證所有進(jìn)程可以得到公平調(diào)度,CPU 時(shí)間被劃分為一段段的時(shí)間片,這些時(shí)間片再被輪流分配給各個(gè)進(jìn)程。這樣,當(dāng)某個(gè)進(jìn)程的時(shí)間片耗盡了,就會被系統(tǒng)掛起,切換到其它正在等待 CPU 的進(jìn)程運(yùn)行。
- 進(jìn)程在系統(tǒng)資源不足(比如內(nèi)存不足)時(shí),要等到資源滿足后才可以運(yùn)行,這個(gè)時(shí)候進(jìn)程也會被掛起,并由系統(tǒng)調(diào)度其他進(jìn)程運(yùn)行。
- 當(dāng)進(jìn)程通過睡眠函數(shù) sleep 這樣的方法將自己主動掛起時(shí),自然也會重新調(diào)度。
- 當(dāng)有優(yōu)先級更高的進(jìn)程運(yùn)行時(shí),為了保證高優(yōu)先級進(jìn)程的運(yùn)行,當(dāng)前進(jìn)程會被掛起,由高優(yōu)先級進(jìn)程來運(yùn)行。
- 發(fā)生硬件中斷時(shí),CPU 上的進(jìn)程會被中斷掛起,轉(zhuǎn)而執(zhí)行內(nèi)核中的中斷服務(wù)程序。
線程上下文切換
線程與進(jìn)程最大的區(qū)別在于,線程是調(diào)度的基本單位,而進(jìn)程則是資源擁有的基本單位。說白了,所謂內(nèi)核中的任務(wù)調(diào)度,實(shí)際上的調(diào)度對象是線程;而進(jìn)程只是給線程提供了虛擬內(nèi)存、全局變量等資源。
所以,對于線程和進(jìn)程,我們可以這么理解:當(dāng)進(jìn)程只有一個(gè)線程時(shí),可以認(rèn)為進(jìn)程就等于線程。當(dāng)進(jìn)程擁有多個(gè)線程時(shí),這些線程會共享相同的虛擬內(nèi)存和全局變量等資源。這些資源在上下文切換時(shí)是不需要修改的。另外,線程也有自己的私有數(shù)據(jù),比如棧和寄存器等,這些在上下文切換時(shí)也是需要保存的。
線程的上下文切換其實(shí)就可以分為兩種情況:
- 第一種, 前后兩個(gè)線程屬于不同進(jìn)程。此時(shí),因?yàn)橘Y源不共享,所以切換過程就跟進(jìn)程上下文切換是一樣。
- 第二種,前后兩個(gè)線程屬于同一個(gè)進(jìn)程。此時(shí),因?yàn)樘摂M內(nèi)存是共享的,所以在切換時(shí),虛擬內(nèi)存這些資源就保持不動,只需要切換線程的私有數(shù)據(jù)、寄存器等不共享的數(shù)據(jù)。
雖然同為上下文切換,但同進(jìn)程內(nèi)的線程切換,要比多進(jìn)程間的切換消耗更少的資源,而這,也正是多線程代替多進(jìn)程的一個(gè)優(yōu)勢。
中斷上下文切換
為了快速響應(yīng)硬件的事件,中斷處理會打斷進(jìn)程的正常調(diào)度和執(zhí)行,轉(zhuǎn)而調(diào)用中斷處理程序,響應(yīng)設(shè)備事件。而在打斷其他進(jìn)程時(shí),就需要將進(jìn)程當(dāng)前的狀態(tài)保存下來,這樣在中斷結(jié)束后,進(jìn)程仍然可以從原來的狀態(tài)恢復(fù)運(yùn)行。
跟進(jìn)程上下文不同,中斷上下文切換并不涉及到進(jìn)程的用戶態(tài)。所以,即便中斷過程打斷了一個(gè)正處在用戶態(tài)的進(jìn)程,也不需要保存和恢復(fù)這個(gè)進(jìn)程的虛擬內(nèi)存、全局變量等用戶態(tài)資源。中斷上下文,其實(shí)只包括內(nèi)核態(tài)中斷服務(wù)程序執(zhí)行所必需的狀態(tài),包括 CPU 寄存器、內(nèi)核堆棧、硬件中斷參數(shù)等。
對同一個(gè) CPU 來說,中斷處理比進(jìn)程擁有更高的優(yōu)先級,所以中斷上下文切換并不會與進(jìn)程上下文切換同時(shí)發(fā)生。同樣道理,由于中斷會打斷正常進(jìn)程的調(diào)度和執(zhí)行,所以大部分中斷處理程序都短小精悍,以便盡可能快的執(zhí)行結(jié)束。
另外,跟進(jìn)程上下文切換一樣,中斷上下文切換也需要消耗 CPU,切換次數(shù)過多也會耗費(fèi)大量的 CPU,甚至嚴(yán)重降低系統(tǒng)的整體性能。所以,當(dāng)你發(fā)現(xiàn)中斷次數(shù)過多時(shí),就需要注意去排查它是否會給你的系統(tǒng)帶來嚴(yán)重的性能問題。
查看系統(tǒng)的上下文切換
vmstat
vmstat 是一個(gè)常用的系統(tǒng)性能分析工具,主要用來分析系統(tǒng)的內(nèi)存使用情況,也常用來分析 CPU 上下文切換和中斷的次數(shù)。比如,下面就是一個(gè) vmstat 的使用示例:
- cs(context switch)是每秒上下文切換的次數(shù)。
- in(interrupt)則是每秒中斷的次數(shù)。
- r(Running or Runnable)是就緒隊(duì)列的長度,也就是正在運(yùn)行和等待 CPU 的進(jìn)程數(shù)。
- b(Blocked)則是處于不可中斷睡眠狀態(tài)的進(jìn)程數(shù)。
pidstat
vmstat 只給出了系統(tǒng)總體的上下文切換情況,要想查看每個(gè)進(jìn)程的詳細(xì)情況,就需要使用我們前面提到過的 pidstat 了。給它加上 -w 選項(xiàng),你就可以查看每個(gè)進(jìn)程上下文切換的情況了。
這個(gè)結(jié)果中有兩列內(nèi)容是我們的重點(diǎn)關(guān)注對象。一個(gè)是 cswch ,表示每秒自愿上下文切換(voluntary context switches)的次數(shù),另一個(gè)則是 nvcswch ,表示每秒非自愿上下文切換(non voluntary context switches)的次數(shù)。
- 自愿上下文切換,是指進(jìn)程無法獲取所需資源,導(dǎo)致的上下文切換。比如說, I/O、內(nèi)存等系統(tǒng)資源不足時(shí),就會發(fā)生自愿上下文切換。
- 而非自愿上下文切換,則是指進(jìn)程由于時(shí)間片已到等原因,被系統(tǒng)強(qiáng)制調(diào)度,進(jìn)而發(fā)生的上下文切換。比如說,大量進(jìn)程都在爭搶 CPU 時(shí),就容易發(fā)生非自愿上下文切換。
案例分析
先用 vmstat 看一下空閑系統(tǒng)的上下文切換次數(shù):
這里你可以看到,現(xiàn)在的上下文切換次數(shù) cs 是3,而中斷次數(shù) in 是 8,r 和 b 都是 0。因?yàn)檫@會兒并沒有運(yùn)行其他任務(wù),所以它們就是空閑系統(tǒng)的上下文切換次數(shù)。
首先,在第一個(gè)終端里運(yùn)行 sysbench ,模擬系統(tǒng)多線程調(diào)度的瓶頸:
sysbench --test=cpu --num-threads=10 --max-time=300 run接著,在第二個(gè)終端運(yùn)行 vmstat ,觀察上下文切換情況
vmstat 1我們發(fā)現(xiàn),cs 列的上下文切換次數(shù)從之前的 0驟然上升到了 3000多。同時(shí),注意觀察其他幾個(gè)指標(biāo):
- r 列:就緒隊(duì)列的長度已經(jīng)到了 8,等于系統(tǒng) CPU 的個(gè)數(shù) 8,所以肯定會有大量的 CPU 競爭。
- us(user)和 sy(system)列:這兩列的 CPU 使用率加起來上升到了 100%,其中系統(tǒng) CPU 使用率,也就是 sy 列高達(dá) 100%,說明 CPU 主要是被內(nèi)核占用了。
- in 列:中斷次數(shù)也上升到了 1000多,說明中斷處理也是個(gè)潛在的問題。
綜合這幾個(gè)指標(biāo),我們可以知道,系統(tǒng)的就緒隊(duì)列過長,也就是正在運(yùn)行和等待 CPU 的進(jìn)程數(shù)過多,導(dǎo)致了大量的上下文切換,而上下文切換又導(dǎo)致了系統(tǒng) CPU 的占用率升高。那么到底是什么進(jìn)程導(dǎo)致了這些問題呢?
pidstat -w -u 1從 pidstat 的輸出你可以發(fā)現(xiàn),CPU 使用率的升高果然是 sysbench 導(dǎo)致的,它的 CPU 使用率已經(jīng)達(dá)到了 100%。但上下文切換則是來自其他進(jìn)程,包括非自愿上下文切換頻率最高的 pidstat ,以及自愿上下文切換頻率最高的內(nèi)核線程 kworker 和 sshd。
不過,細(xì)心的你肯定也發(fā)現(xiàn)了一個(gè)怪異的事兒:pidstat 輸出的上下文切換次數(shù),加起來也就幾百,比 vmstat 的 3000明顯小了太多。這是怎么回事呢?難道是工具本身出了錯嗎?別著急,在懷疑工具之前,我們再來回想一下,前面講到的幾種上下文切換場景。其中有一點(diǎn)提到, Linux 調(diào)度的基本單位實(shí)際上是線程,而我們的場景 sysbench 模擬的也是線程的調(diào)度問題,那么,是不是 pidstat 忽略了線程的數(shù)據(jù)呢?通過運(yùn)行 man pidstat ,你會發(fā)現(xiàn),pidstat 默認(rèn)顯示進(jìn)程的指標(biāo)數(shù)據(jù),加上 -t 參數(shù)后,才會輸出線程的指標(biāo)。所以,我們可以在第三個(gè)終端里, Ctrl+C 停止剛才的 pidstat 命令,再加上 -t 參數(shù),重試一下看看:
pidstat -wt 1現(xiàn)在你就能看到了,雖然 sysbench 進(jìn)程(也就是主線程)的上下文切換次數(shù)看起來并不多,但它的子線程的上下文切換次數(shù)卻有很多。看來,上下文切換罪魁禍?zhǔn)?#xff0c;還是過多的 sysbench 線程。
我們已經(jīng)找到了上下文切換次數(shù)增多的根源。不知道你還記不記得,前面在觀察系統(tǒng)指標(biāo)時(shí),除了上下文切換頻率驟然升高,還有一個(gè)指標(biāo)也有很大的變化。是的,正是中斷次數(shù)。中斷次數(shù)也上升到了 2000多,但到底是什么類型的中斷上升了,現(xiàn)在還不清楚。我們接下來繼續(xù)抽絲剝繭找源頭。既然是中斷,我們都知道,它只發(fā)生在內(nèi)核態(tài),而 pidstat 只是一個(gè)進(jìn)程的性能分析工具,并不提供任何關(guān)于中斷的詳細(xì)信息,怎樣才能知道中斷發(fā)生的類型呢?沒錯,那就是從 /proc/interrupts 這個(gè)只讀文件中讀取。/proc 實(shí)際上是 Linux 的一個(gè)虛擬文件系統(tǒng),用于內(nèi)核空間與用戶空間之間的通信。/proc/interrupts 就是這種通信機(jī)制的一部分,提供了一個(gè)只讀的中斷使用情況。
觀察一段時(shí)間,你可以發(fā)現(xiàn),變化速度最快的是重調(diào)度中斷(RES),這個(gè)中斷類型表示,喚醒空閑狀態(tài)的 CPU 來調(diào)度新的任務(wù)運(yùn)行。這是多處理器系統(tǒng)(SMP)中,調(diào)度器用來分散任務(wù)到不同 CPU 的機(jī)制,通常也被稱為處理器間中斷(Inter-Processor Interrupts,IPI)。所以,這里的中斷升高還是因?yàn)檫^多任務(wù)的調(diào)度問題,跟前面上下文切換次數(shù)的分析結(jié)果是一致的。
通過這個(gè)案例,我們發(fā)現(xiàn)了多工具、多方面指標(biāo)對比觀測的好處。如果最開始時(shí),我們只用了 pidstat 觀測,這些很嚴(yán)重的上下文切換線程,壓根兒就發(fā)現(xiàn)不了了。現(xiàn)在再回到最初的問題,每秒上下文切換多少次才算正常呢?這個(gè)數(shù)值其實(shí)取決于系統(tǒng)本身的 CPU 性能。
小結(jié)
如果系統(tǒng)的上下文切換次數(shù)比較穩(wěn)定,那么從數(shù)百到一萬以內(nèi),都應(yīng)該算是正常的。但當(dāng)上下文切換次數(shù)超過一萬次,或者切換次數(shù)出現(xiàn)數(shù)量級的增長時(shí),就很可能已經(jīng)出現(xiàn)了性能問題。這時(shí),還需要根據(jù)上下文切換的類型,再做具體分析。比方說:
- 自愿上下文切換變多了,說明進(jìn)程都在等待資源,有可能發(fā)生了 I/O 等其他問題;
- 非自愿上下文切換變多了,說明進(jìn)程都在被強(qiáng)制調(diào)度,也就是都在爭搶 CPU,說明 CPU 的確成了瓶頸;
- 中斷次數(shù)變多了,說明 CPU 被中斷處理程序占用,還需要通過查看 /proc/interrupts 文件來分析具體的中斷類型。
總結(jié)
以上是生活随笔為你收集整理的进程用户态 上下文切换需要保存哪些_漫话性能:CPU上下文切换的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python第三周笔记_Python第三
- 下一篇: python文本菜单程序_python3