cpu上下文與切換
程序在競争 CPU 的時候并沒有真正運作,為什麼還會導緻系統的負載升高呢?CPU 上下文切換就是罪魁禍首。
我們都知道,Linux 是一個多任務作業系統,它支援遠大于 CPU 數量的任務同時運作。當然,這些任務實際上并不是真的在同時運作,而是因為系統在很短的時間内,将 CPU 輪流配置設定給它們,造成多任務同時運作的錯覺。
而在每個任務運作前,CPU 都需要知道任務從哪裡加載、又從哪裡開始運作,也就是說, 需要系統事先幫它設定好 CPU 寄存器和程式計數器(Program Counter,PC)。CPU 寄存器,是 CPU 内置的容量小、但速度極快的記憶體。而程式計數
器,則是用來存儲CPU 正在執行的指令位置、或者即将執行的下一條指令位置。它們都是 CPU 在運作任何任務前,必須的依賴環境,是以也被叫做 CPU 上下文。
CPU 上下文切換。CPU 上下文切換,就是先把前一個任務的 CPU 上下文(也就是 CPU 寄存器和程式計數器)儲存起來,然後加載新任務的上下文到這些寄存器和程式計數器,最後再跳轉到程式計數器所指的新位置, 運作新任務。
而這些儲存下來的上下文,會存儲在系統核心中,并在任務重新排程執行時再次加載進來。這樣就能保證任務原來的狀态不受影響,讓任務看起來還是連續運作。
我猜肯定會有人說,CPU 上下文切換無非就是更新了 CPU 寄存器的值嘛,但這些寄存器,本身就是為了快速運作任務而設計的,為什麼會影響系統的 CPU 性能呢?
在回答這個問題前,不知道你有沒有想過,作業系統管理的這些“任務”到底是什麼呢?
也許你會說,任務就是程序,或者說任務就是線程。是的,程序和線程正是最常見的任務。但是除此之外,還有沒有其他的任務呢?
不要忘了,硬體通過觸發信号,會導緻中斷處理程式的調用,也是一種常見的任務。
是以,根據任務的不同,CPU 的上下文切換就可以分為幾個不同的場景,也就是程序上下文切換、線程上下文切換以及中斷上下文切換。
這節課我就帶你來看看,怎麼了解這幾個不同的上下文切換,以及它們為什麼會引發 CPU 性能相關問題。
程序上下文切換
Linux 按照特權等級,把程序的運作空間分為核心空間和使用者空間,分别對應着下圖中, CPU 特權等級的 Ring 0 和 Ring 3。
核心空間(Ring 0)具有最高權限,可以直接通路所有資源;
使用者空間(Ring 3)隻能通路受限資源,不能直接通路記憶體等硬體裝置,必須通過系統調用陷入到核心中,才能通路這些特權資源。
換個角度看,也就是說,程序既可以在使用者空間運作,又可以在核心空間中運作。程序在使用者空間運作時,被稱為程序的使用者态,而陷入核心空間的時候,被稱為程序的核心态。從使用者态到核心态的轉變,需要通過系統調用來完成。比如,當我們檢視檔案内容時,就需要多次系統調用來完成:首先調用 open() 打開檔案,然後調用 read() 讀取檔案内容, 并調用 write() 将内容寫到标準輸出,最後再調用 close() 關閉檔案。
那麼,系統調用的過程有沒有發生 CPU 上下文的切換呢?答案自然是肯定的。
CPU 寄存器裡原來使用者态的指令位置,需要先儲存起來。接着,為了執行核心态代碼, CPU 寄存器需要更新為核心态指令的新位置。最後才是跳轉到核心态運作核心任務。而系統調用結束後,CPU 寄存器需要恢複原來儲存的使用者态,然後再切換到使用者空間,繼續運作程序。是以,一次系統調用的過程,其實是發生了兩次 CPU 上下文切換。不過,需要注意的是,系統調用過程中,并不會涉及到虛拟記憶體等程序使用者态的資源,也不會切換程序。這跟我們通常所說的程序上下文切換是不一樣的:程序上下文切換,是指從一個程序切換到另一個程序運作。而系統調用過程中一直是同一個程序在運作。
是以,系統調用過程通常稱為特權模式切換,而不是上下文切換。但實際上,系統調用過程中,CPU 的上下文切換還是無法避免的。
那麼,程序上下文切換跟系統調用又有什麼差別呢?首先,你需要知道,程序是由核心來管理和排程的,程序的切換隻能發生在核心态。是以,程序的上下文不僅包括了虛拟記憶體、棧、全局變量等使用者空間的資源,還包括了核心堆棧、寄存器等核心空間的狀态。是以,程序的上下文切換就比系統調用時多了一步:在儲存目前程序的核心狀态和 CPU 寄存器之前,需要先把該程序的虛拟記憶體、棧等儲存下來;而加載了下一程序的核心态後, 還需要重新整理程序的虛拟記憶體和使用者棧。如下圖所示,儲存上下文和恢複上下文的過程并不是“免費”的,需要核心在 CPU 上運作才能完成。
根據 Tsuna 的測試報告,每次上下文切換都需要幾十納秒到數微秒的 CPU 時間。這個時間還是相當可觀的,特别是在程序上下文切換次數較多的情況下,很容易導緻 CPU 将大量時間耗費在寄存器、核心棧以及虛拟記憶體等資源的儲存和恢複上,進而大大縮短了真正運作程序的時間。這也正是上一節中我們所講的,導緻平均負載升高的一個重要因素。
另外,我們知道, Linux 通過 TLB(Translation Lookaside Buffer)來管理虛拟記憶體到實體記憶體的映射關系。當虛拟記憶體更新後,TLB 也需要重新整理,記憶體的通路也會随之變慢。特别是在多處理器系統上,緩存是被多個處理器共享的,重新整理緩存不僅會影響目前處理器的程序,還會影響共享緩存的其他處理器的程序。知道了程序上下文切換潛在的性能問題後,我們再來看,究竟什麼時候會切換程序上下文。
顯然,程序切換時才需要切換上下文,換句話說,隻有在程序排程的時候,才需要切換上下文。Linux 為每個 CPU 都維護了一個就緒隊列,将活躍程序(即正在運作和正在等待CPU 的程序)按照優先級和等待 CPU 的時間排序,然後選擇最需要 CPU 的程序,也就是優先級最高和等待 CPU 時間最長的程序來運作。
那麼,程序在什麼時候才會被排程到 CPU 上運作呢?最容易想到的一個時機,就是程序執行完終止了,它之前使用的 CPU 會釋放出來,這個時候再從就緒隊列裡,拿一個新的程序過來運作。其實還有很多其他場景,也會觸發程序調 度,在這裡我給你逐個梳理下。
其一,為了保證所有程序可以得到公平排程,CPU 時間被劃分為一段段的時間片,這些時間片再被輪流配置設定給各個程序。這樣,當某個程序的時間片耗盡了,就會被系統挂起,切 換到其它正在等待 CPU 的程序運作。
其二,程序在系統資源不足(比如記憶體不足)時,要等到資源滿足後才可以運作,這個時候程序也會被挂起,并由系統排程其他程序運作。
其三,當程序通過睡眠函數 sleep 這樣的方法将自己主動挂起時,自然也會重新排程。
其四,當有優先級更高的程序運作時,為了保證高優先級程序的運作,目前程序會被挂起,由高優先級程序來運作。
最後一個,發生硬體中斷時,CPU 上的程序會被中斷挂起,轉而執行核心中的中斷服務程式。
線程上下文切換
線程與程序最大的差別在于,線程是排程的基本機關,而程序則是資源擁有的基本機關。說白了,所謂核心中的任務排程,實際上的排程對象是線程;而程序隻是給線程提供了虛拟記憶體、全局變量等資源。是以,對于線程和程序,我們可以這麼了解:
當程序隻有一個線程時,可以認為程序就等于線程。
當程序擁有多個線程時,這些線程會共享相同的虛拟記憶體和全局變量等資源。這些資源在上下文切換時是不需要修改的。
另外,線程也有自己的私有資料,比如棧和寄存器等,這些在上下文切換時也是需要儲存的。
這麼一來,線程的上下文切換其實就可以分為兩種情況:
第一種, 前後兩個線程屬于不同程序。此時,因為資源不共享,是以切換過程就跟程序上下文切換是一樣。
第二種,前後兩個線程屬于同一個程序。此時,因為虛拟記憶體是共享的,是以在切換時, 虛拟記憶體這些資源就保持不動,隻需要切換線程的私有資料、寄存器等不共享的資料。
到這裡你應該也發現了,雖然同為上下文切換,但同程序内的線程切換,要比多程序間的切換消耗更少的資源,而這,也正是多線程代替多程序的一個優勢。
中斷上下文切換
除了前面兩種上下文切換,還有一個場景也會切換 CPU 上下文,那就是中斷。
為了快速響應硬體的事件,中斷處理會打斷程序的正常排程和執行,轉而調用中斷處理程式,響應裝置事件。而在打斷其他程序時,就需要将程序目前的狀态儲存下來,這樣在中斷結束後,程序仍然可以從原來的狀态恢複運作。跟程序上下文不同,中斷上下文切換并不涉及到程序的使用者态。是以,即便中斷過程打斷了一個正處在使用者态的程序,也不需要儲存和恢複這個程序的虛拟記憶體、全局變量等使用者 态資源。中斷上下文,其實隻包括核心态中斷服務程式執行所必需的狀态,包括 CPU 寄存器、核心堆棧、硬體中斷參數等。
對同一個 CPU 來說,中斷處理比程序擁有更高的優先級,是以中斷上下文切換并不會與程序上下文切換同時發生。同樣道理,由于中斷會打斷正常程序的排程和執行,是以大部分中斷處理程式都短小精悍,以便盡可能快的執行結束。另外,跟程序上下文切換一樣,中斷上下文切換也需要消耗 CPU,切換次數過多也會耗費大量的 CPU,甚至嚴重降低系統的整體性能。是以,當你發現中斷次數過多時,就需要注意去排查它是否會給你的系統帶來嚴重的性能問題。
小結
總結一下,不管是哪種場景導緻的上下文切換,你都應該知道: