理解平均負(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)程上下文切換像棘、線程上下文切換以及中斷上下文切換稽亏。
進(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ò)程通常稱為特權(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)行才能完成。
根據(jù) Tsuna 的測(cè)試報(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í)間。這也正是上一節(jié)中我們所講的州藕,導(dǎo)致平均負(fù)載升高的一個(gè)重要因素束世。
另外,我們知道床玻, Linux 通過(guò) TLB(Translation Lookaside Buffer)來(lái)管理虛擬內(nèi)存到物理內(nèi)存的映射關(guān)系毁涉。當(dāng)虛擬內(nèi)存更新后,TLB 也需要刷新锈死,內(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)資源油啤。中斷上下文典徘,其實(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)題。
小結(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)的整體性能大幅下降证杭。
- 郵箱:ithelei@sina.cn
- 技術(shù)討論群:687856230
- GoodLuck
- 03