上一節(jié),我給你講了要怎么理解平均負(fù)載( Load Average),并用三個(gè)案例展示了不同場(chǎng)景下平均負(fù)載升高的分析方法。這其中,多個(gè)進(jìn)程競(jìng)爭(zhēng) CPU 就是一個(gè)經(jīng)常被我們忽視的問(wèn)題唁盏。
我想你一定很好奇,進(jìn)程在競(jìng)爭(zhēng) CPU 的時(shí)候并沒(méi)有真正運(yùn)行检眯,為什么還會(huì)導(dǎo)致系統(tǒng)的負(fù)載升高呢厘擂?看到今天的主題,你應(yīng)該已經(jīng)猜到了锰瘸,CPU 上下文切換就是罪魁禍?zhǔn)住?/p>
我們都知道刽严,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)行的錯(cuò)覺(jué)。
而在每個(gè)任務(wù)運(yùn)行前佩谣,CPU 都需要知道任務(wù)從哪里加載把还、又從哪里開(kāi)始運(yùn)行,也就是說(shuō),需要系統(tǒng)事先幫它設(shè)置好 CPU 寄存器和程序計(jì)數(shù)器(Program Counter吊履,PC)安皱。
CPU 寄存器,是 CPU 內(nèi)置的容量小艇炎、但速度極快的內(nèi)存酌伊。而程序計(jì)數(shù)器,則是用來(lái)存儲(chǔ) CPU 正在執(zhí)行的指令位置缀踪、或者即將執(zhí)行的下一條指令位置居砖。它們都是 CPU 在運(yùn)行任何任務(wù)前,必須的依賴環(huán)境驴娃,因此也被叫做 CPU 上下文讥裤。
知道了什么是 CPU 上下文藻雌,我想你也很容易理解 CPU 上下文切換。CPU 上下文切換舷胜,就是先把前一個(gè)任務(wù)的 CPU 上下文(也就是 CPU 寄存器和程序計(jì)數(shù)器)保存起來(lái)摩泪,然后加載新任務(wù)的上下文到這些寄存器和程序計(jì)數(shù)器寞射,最后再跳轉(zhuǎn)到程序計(jì)數(shù)器所指的新位置厌蔽,運(yùn)行新任務(wù)密末。
而這些保存下來(lái)的上下文,會(huì)存儲(chǔ)在系統(tǒng)內(nèi)核中婆硬,并在任務(wù)重新調(diào)度執(zhí)行時(shí)再次加載進(jìn)來(lái)狠轻。這樣就能保證任務(wù)原來(lái)的狀態(tài)不受影響奸例,讓任務(wù)看起來(lái)還是連續(xù)運(yùn)行彬犯。
我猜肯定會(huì)有人說(shuō),CPU 上下文切換無(wú)非就是更新了 CPU 寄存器的值嘛查吊,但這些寄存器谐区,本身就是為了快速運(yùn)行任務(wù)而設(shè)計(jì)的,為什么會(huì)影響系統(tǒng)的 CPU 性能呢逻卖?
在回答這個(gè)問(wèn)題前宋列,不知道你有沒(méi)有想過(guò),操作系統(tǒng)管理的這些“任務(wù)”到底是什么呢评也?
也許你會(huì)說(shuō)炼杖,任務(wù)就是進(jìn)程,或者說(shuō)任務(wù)就是線程盗迟。是的坤邪,進(jìn)程和線程正是最常見(jiàn)的任務(wù)。但是除此之外罚缕,還有沒(méi)有其他的任務(wù)呢艇纺?
不要忘了,硬件通過(guò)觸發(fā)信號(hào),會(huì)導(dǎo)致中斷處理程序的調(diào)用黔衡,也是一種常見(jiàn)的任務(wù)蚓聘。
所以,根據(jù)任務(wù)的不同盟劫,CPU 的上下文切換就可以分為幾個(gè)不同的場(chǎng)景夜牡,也就是進(jìn)程上下文切換、線程上下文切換以及中斷上下文切換侣签。
這節(jié)課我就帶你來(lái)看看氯材,怎么理解這幾個(gè)不同的上下文切換,以及它們?yōu)槭裁磿?huì)引發(fā) CPU 性能相關(guān)問(wèn)題硝岗。
進(jìn)程上下文切換
Linux 按照特權(quán)等級(jí)氢哮,把進(jìn)程的運(yùn)行空間分為內(nèi)核空間和用戶空間,分別對(duì)應(yīng)著下圖中型檀, CPU 特權(quán)等級(jí)的 Ring 0 和 Ring 3冗尤。
- 內(nèi)核空間(Ring 0)具有最高權(quán)限,可以直接訪問(wèn)所有資源胀溺;
- 用戶空間(Ring 3)只能訪問(wèn)受限資源裂七,不能直接訪問(wèn)內(nèi)存等硬件設(shè)備,必須通過(guò)系統(tǒng)調(diào)用陷入到內(nèi)核中仓坞,才能訪問(wèn)這些特權(quán)資源背零。
換個(gè)角度看,也就是說(shuō)无埃,進(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)變壳繁,需要通過(guò)系統(tǒng)調(diào)用來(lái)完成。比如荔棉,當(dāng)我們查看文件內(nèi)容時(shí)闹炉,就需要多次系統(tǒng)調(diào)用來(lái)完成:首先調(diào)用 open() 打開(kāi)文件,然后調(diào)用 read() 讀取文件內(nèi)容润樱,并調(diào)用 write() 將內(nèi)容寫(xiě)到標(biāo)準(zhǔn)輸出渣触,最后再調(diào)用 close() 關(guān)閉文件。
那么祥国,系統(tǒng)調(diào)用的過(guò)程有沒(méi)有發(fā)生 CPU 上下文的切換呢昵观?答案自然是肯定的晾腔。
CPU 寄存器里原來(lái)用戶態(tài)的指令位置,需要先保存起來(lái)啊犬。接著灼擂,為了執(zhí)行內(nèi)核態(tài)代碼,CPU 寄存器需要更新為內(nèi)核態(tài)指令的新位置觉至。最后才是跳轉(zhuǎn)到內(nèi)核態(tài)運(yùn)行內(nèi)核任務(wù)剔应。
而系統(tǒng)調(diào)用結(jié)束后,CPU 寄存器需要恢復(fù)原來(lái)保存的用戶態(tài)语御,然后再切換到用戶空間峻贮,繼續(xù)運(yùn)行進(jìn)程。所以应闯,一次系統(tǒng)調(diào)用的過(guò)程纤控,其實(shí)是發(fā)生了兩次 CPU 上下文切換。
不過(guò)碉纺,需要注意的是船万,系統(tǒng)調(diào)用過(guò)程中,并不會(huì)涉及到虛擬內(nèi)存等進(jìn)程用戶態(tài)的資源骨田,也不會(huì)切換進(jìn)程耿导。這跟我們通常所說(shuō)的進(jìn)程上下文切換是不一樣的:
- 進(jìn)程上下文切換,是指從一個(gè)進(jìn)程切換到另一個(gè)進(jìn)程運(yùn)行态贤。而系統(tǒng)調(diào)用過(guò)程中一直是同一個(gè)進(jìn)程在運(yùn)行舱呻。
- 而系統(tǒng)調(diào)用過(guò)程中一直是同一個(gè)進(jìn)程在運(yùn)行。
所以悠汽,系統(tǒng)調(diào)用過(guò)程通常稱為特權(quán)模式切換箱吕,而不是上下文切換。但實(shí)際上介粘,系統(tǒng)調(diào)用過(guò)程中殖氏,CPU 的上下文切換還是無(wú)法避免的晚树。
那么姻采,進(jìn)程上下文切換跟系統(tǒng)調(diào)用又有什么區(qū)別呢?
首先爵憎,你需要知道慨亲,進(jìn)程是由內(nèi)核來(lá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)存、棧等保存下來(lái)片橡;而加載了下一進(jìn)程的內(nèi)核態(tài)后妈经,還需要刷新進(jìn)程的虛擬內(nèi)存和用戶棧。
如下圖所示捧书,保存上下文和恢復(fù)上下文的過(guò)程并不是“免費(fèi)”的吹泡,需要內(nèi)核在 CPU 上運(yùn)行才能完成。
每次上下文切換都需要幾十納秒到數(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í)間。這也正是上一節(jié)中我們所講的歪泳,導(dǎo)致平均負(fù)載升高的一個(gè)重要因素萝勤。
另外,我們知道呐伞, Linux 通過(guò) TLB(Translation Lookaside Buffer)來(lái)管理虛擬內(nèi)存到物理內(nèi)存的映射關(guān)系敌卓。當(dāng)虛擬內(nèi)存更新后,TLB (TLB是mmu里頁(yè)表的緩存伶氢,刷新是加快映射速度趟径。)也需要刷新,內(nèi)存的訪問(wèn)也會(huì)隨之變慢癣防。特別是在多處理器系統(tǒng)上蜗巧,緩存是被多個(gè)處理器共享的,刷新緩存不僅會(huì)影響當(dāng)前處理器的進(jìn)程蕾盯,還會(huì)影響共享緩存的其他處理器的進(jìn)程幕屹。
知道了進(jìn)程上下文切換潛在的性能問(wèn)題后,我們?cè)賮?lái)看级遭,究竟什么時(shí)候會(huì)切換進(jìn)程上下文望拖。
顯然,進(jìn)程切換時(shí)才需要切換上下文挫鸽,換句話說(shuō)说敏,只有在進(jìn)程調(diào)度的時(shí)候,才需要切換上下文丢郊。Linux 為每個(gè) CPU 都維護(hù)了一個(gè)就緒隊(duì)列盔沫,將活躍進(jìn)程(即正在運(yùn)行和正在等待 CPU 的進(jìn)程)按照優(yōu)先級(jí)和等待 CPU 的時(shí)間排序医咨,然后選擇最需要 CPU 的進(jìn)程,也就是優(yōu)先級(jí)最高和等待 CPU 時(shí)間最長(zhǎng)的進(jìn)程來(lái)運(yùn)行架诞。
那么腋逆,進(jìn)程在什么時(shí)候才會(huì)被調(diào)度到 CPU 上運(yùn)行呢?
最容易想到的一個(gè)時(shí)機(jī)侈贷,就是進(jìn)程執(zhí)行完終止了惩歉,它之前使用的 CPU 會(huì)釋放出來(lái),這個(gè)時(shí)候再?gòu)木途w隊(duì)列里俏蛮,拿一個(gè)新的進(jìn)程過(guò)來(lái)運(yùn)行撑蚌。其實(shí)還有很多其他場(chǎng)景,也會(huì)觸發(fā)進(jìn)程調(diào)度搏屑,在這里我給你逐個(gè)梳理下争涌。
其一,為了保證所有進(jìn)程可以得到公平調(diào)度辣恋,CPU 時(shí)間被劃分為一段段的時(shí)間片亮垫,這些時(shí)間片再被輪流分配給各個(gè)進(jìn)程。這樣伟骨,當(dāng)某個(gè)進(jìn)程的時(shí)間片耗盡了饮潦,就會(huì)被系統(tǒng)掛起,切換到其它正在等待 CPU 的進(jìn)程運(yùn)行携狭。
其二继蜡,進(jìn)程在系統(tǒng)資源不足(比如內(nèi)存不足)時(shí),要等到資源滿足后才可以運(yùn)行逛腿,這個(gè)時(shí)候進(jìn)程也會(huì)被掛起稀并,并由系統(tǒng)調(diào)度其他進(jìn)程運(yùn)行。
其三单默,當(dāng)進(jìn)程通過(guò)睡眠函數(shù) sleep 這樣的方法將自己主動(dòng)掛起時(shí)碘举,自然也會(huì)重新調(diào)度。
其四搁廓,當(dāng)有優(yōu)先級(jí)更高的進(jìn)程運(yùn)行時(shí)引颈,為了保證高優(yōu)先級(jí)進(jìn)程的運(yùn)行,當(dāng)前進(jìn)程會(huì)被掛起枚抵,由高優(yōu)先級(jí)進(jìn)程來(lái)運(yùn)行线欲。
最后一個(gè),發(fā)生硬件中斷時(shí)汽摹,CPU 上的進(jìn)程會(huì)被中斷掛起,轉(zhuǎn)而執(zhí)行內(nèi)核中的中斷服務(wù)程序苦锨。
了解這幾個(gè)場(chǎng)景是非常有必要的逼泣,因?yàn)橐坏┏霈F(xiàn)上下文切換的性能問(wèn)題趴泌,它們就是幕后兇手。
線程上下文切換
說(shuō)完了進(jìn)程的上下文切換拉庶,我們?cè)賮?lái)看看線程相關(guān)的問(wèn)題嗜憔。
線程與進(jìn)程最大的區(qū)別在于,線程是調(diào)度的基本單位氏仗,而進(jìn)程則是資源擁有的基本單位吉捶。說(shuō)白了,所謂內(nèi)核中的任務(wù)調(diào)度皆尔,實(shí)際上的調(diào)度對(duì)象是線程呐舔;而進(jìn)程只是給線程提供了虛擬內(nèi)存、全局變量等資源慷蠕。所以珊拼,對(duì)于線程和進(jìn)程,我們可以這么理解:
- 當(dāng)進(jìn)程只有一個(gè)線程時(shí)流炕,可以認(rèn)為進(jìn)程就等于線程澎现。
- 當(dāng)進(jìn)程擁有多個(gè)線程時(shí),這些線程會(huì)共享相同的虛擬內(nèi)存和全局變量等資源每辟。這些資源在上下文切換時(shí)是不需要修改的剑辫。
- 另外,線程也有自己的私有數(shù)據(jù)渠欺,比如棧和寄存器等揭斧,這些在上下文切換時(shí)也是需要保存的。
這么一來(lái)峻堰,線程的上下文切換其實(shí)就可以分為兩種情況:
- 第一種讹开, 前后兩個(gè)線程屬于不同進(jìn)程。此時(shí)捐名,因?yàn)橘Y源不共享旦万,所以切換過(guò)程就跟進(jìn)程上下文切換是一樣。
- 第二種镶蹋,前后兩個(gè)線程屬于同一個(gè)進(jìn)程成艘。此時(shí),因?yàn)樘摂M內(nèi)存是共享的贺归,所以在切換時(shí)淆两,虛擬內(nèi)存這些資源就保持不動(dòng),只需要切換線程的私有數(shù)據(jù)拂酣、寄存器等不共享的數(shù)據(jù)秋冰。
到這里你應(yīng)該也發(fā)現(xiàn)了,雖然同為上下文切換婶熬,但同進(jìn)程內(nèi)的線程切換剑勾,要比多進(jìn)程間的切換消耗更少的資源埃撵,而這,也正是多線程代替多進(jìn)程的一個(gè)優(yōu)勢(shì)虽另。
中斷上下文切換
除了前面兩種上下文切換暂刘,還有一個(gè)場(chǎng)景也會(huì)切換 CPU 上下文,那就是中斷捂刺。
為了快速響應(yīng)硬件的事件谣拣,中斷處理會(huì)打斷進(jìn)程的正常調(diào)度和執(zhí)行,轉(zhuǎn)而調(diào)用中斷處理程序族展,響應(yīng)設(shè)備事件森缠。而在打斷其他進(jìn)程時(shí),就需要將進(jìn)程當(dāng)前的狀態(tài)保存下來(lái)苛谷,這樣在中斷結(jié)束后辅鲸,進(jìn)程仍然可以從原來(lái)的狀態(tài)恢復(fù)運(yùn)行。
跟進(jìn)程上下文不同腹殿,中斷上下文切換并不涉及到進(jìn)程的用戶態(tài)独悴。所以,即便中斷過(guò)程打斷了一個(gè)正處在用戶態(tài)的進(jìn)程锣尉,也不需要保存和恢復(fù)這個(gè)進(jìn)程的虛擬內(nèi)存刻炒、全局變量等用戶態(tài)資源。(不是不保存進(jìn)程上下文自沧,而是不需要刷新虛擬內(nèi)存這些用戶空間的數(shù)據(jù))中斷上下文坟奥,其實(shí)只包括內(nèi)核態(tài)中斷服務(wù)程序執(zhí)行所必需的狀態(tài),包括 CPU 寄存器拇厢、內(nèi)核堆棧爱谁、硬件中斷參數(shù)等。
對(duì)同一個(gè) CPU 來(lái)說(shuō)孝偎,中斷處理比進(jìn)程擁有更高的優(yōu)先級(jí)访敌,所以中斷上下文切換并不會(huì)與進(jìn)程上下文切換同時(shí)發(fā)生。同樣道理衣盾,由于中斷會(huì)打斷正常進(jìn)程的調(diào)度和執(zhí)行寺旺,所以大部分中斷處理程序都短小精悍,以便盡可能快的執(zhí)行結(jié)束势决。
另外阻塑,跟進(jìn)程上下文切換一樣,中斷上下文切換也需要消耗 CPU果复,切換次數(shù)過(guò)多也會(huì)耗費(fèi)大量的 CPU陈莽,甚至嚴(yán)重降低系統(tǒng)的整體性能。所以,當(dāng)你發(fā)現(xiàn)中斷次數(shù)過(guò)多時(shí)传透,就需要注意去排查它是否會(huì)給你的系統(tǒng)帶來(lái)嚴(yán)重的性能問(wèn)題耘沼。
中斷上下文和進(jìn)程上下文區(qū)分极颓?
首先朱盐,這兩個(gè)上下文都處于內(nèi)核空間。
其次菠隆,兩者的區(qū)別在于兵琳,進(jìn)程上下文與當(dāng)前執(zhí)行進(jìn)程密切相關(guān),而中斷上下文在邏輯上與進(jìn)程沒(méi)有關(guān)系骇径。
進(jìn)程上下文主要是異常處理程序和內(nèi)核線程躯肌。內(nèi)核之所以進(jìn)入進(jìn)程上下文是因?yàn)檫M(jìn)程自身的一些工作需要在內(nèi)核中做。例如破衔,系統(tǒng)調(diào)用是為當(dāng)前進(jìn)程服務(wù)的清女,異常通常是處理進(jìn)程導(dǎo)致的錯(cuò)誤狀態(tài)等。所以在進(jìn)程上下文中引用current是有意義的晰筛。
內(nèi)核進(jìn)入中斷上下文是因?yàn)橹袛嘈盘?hào)而導(dǎo)致的中斷處理或軟中斷嫡丙。而中斷信號(hào)的發(fā)生是隨機(jī)的,中斷處理程序及軟中斷并不能事先預(yù)測(cè)發(fā)生中斷時(shí)當(dāng)前運(yùn)行的是哪個(gè)進(jìn)程读第,所以在中斷上下文中引用current是可以的曙博,但沒(méi)有意義。事實(shí)上怜瞒,對(duì)于A進(jìn)程希望等待的中斷信號(hào)父泳,可能在B進(jìn)程執(zhí)行期間發(fā)生。例如吴汪,A進(jìn)程啟動(dòng)寫(xiě)磁盤(pán)操作惠窄,A進(jìn)程睡眠后現(xiàn)在時(shí)B進(jìn)程在運(yùn)行,當(dāng)磁盤(pán)寫(xiě)完后磁盤(pán)中斷信號(hào)打斷的是B進(jìn)程漾橙,在中斷處理時(shí)會(huì)喚醒A進(jìn)程杆融。
處理器總處于以下?tīng)顟B(tài)中的一種:
1、內(nèi)核態(tài)近刘,運(yùn)行于進(jìn)程上下文擒贸,內(nèi)核代表進(jìn)程運(yùn)行于內(nèi)核空間;
2觉渴、內(nèi)核態(tài)介劫,運(yùn)行于中斷上下文,內(nèi)核代表硬件運(yùn)行于內(nèi)核空間案淋;
3座韵、用戶態(tài),運(yùn)行于用戶空間。
當(dāng)一個(gè)進(jìn)程在執(zhí)行時(shí),CPU的所有寄存器中的值誉碴、進(jìn)程的狀態(tài)以及堆棧中的內(nèi)容被稱為該進(jìn)程的上下文宦棺。當(dāng)內(nèi)核需要切換到另一個(gè)進(jìn)程時(shí),它需要保存當(dāng)前進(jìn)程的所有狀態(tài)黔帕,即保存當(dāng)前進(jìn)程的上下文代咸,以便在再次執(zhí)行該進(jìn)程時(shí),能夠必得到切換時(shí)的狀態(tài)執(zhí)行下去成黄。在LINUX中呐芥,當(dāng)前進(jìn)程上下文均保存在進(jìn)程的任務(wù)數(shù)據(jù)結(jié)構(gòu)中。在發(fā)生中斷時(shí),內(nèi)核就在被中斷進(jìn)程的上下文中奋岁,在內(nèi)核態(tài)下執(zhí)行中斷服務(wù)例程思瘟。但同時(shí)會(huì)保留所有需要用到的資源,以便中繼服務(wù)結(jié)束時(shí)能恢復(fù)被中斷進(jìn)程的執(zhí)行闻伶。
小結(jié)
總結(jié)一下滨攻,不管是哪種場(chǎng)景導(dǎo)致的上下文切換,你都應(yīng)該知道:
- CPU 上下文切換蓝翰,是保證 Linux 系統(tǒng)正常工作的核心功能之一光绕,一般情況下不需要我們特別關(guān)注。
- 但過(guò)多的上下文切換霎箍,會(huì)把 CPU 時(shí)間消耗在寄存器奇钞、內(nèi)核棧以及虛擬內(nèi)存等數(shù)據(jù)的保存和恢復(fù)上,從而縮短進(jìn)程真正運(yùn)行的時(shí)間漂坏,導(dǎo)致系統(tǒng)的整體性能大幅下降景埃。
FAQ
因?yàn)閮?nèi)核態(tài)是cpu的一種特權(quán)模式,在這種模式下顶别,內(nèi)核可以訪問(wèn)系統(tǒng)資源(包括內(nèi)存谷徙、cpu和其他IO設(shè)備),所以cpu上下文切換(包括進(jìn)程上下文切換驯绎、線程上下文切換和中斷切換)均是發(fā)生在內(nèi)核態(tài)完慧。
對(duì)于應(yīng)用程序,在最開(kāi)始都是運(yùn)行在用戶態(tài)剩失,當(dāng)程序中需要調(diào)用或者使用系統(tǒng)資源時(shí)(包括內(nèi)存屈尼、cpu和其他io設(shè)備),由于受到安全性限制拴孤,他們只能能通過(guò)系統(tǒng)調(diào)用來(lái)運(yùn)行部分內(nèi)核的代碼實(shí)現(xiàn)對(duì)系統(tǒng)資源的調(diào)用使用脾歧,這個(gè)過(guò)程就是從用戶態(tài)陷入內(nèi)核態(tài)的過(guò)程,在系統(tǒng)調(diào)用過(guò)程中一直是在同一個(gè)進(jìn)程中實(shí)現(xiàn)演熟。