Linux 調度系統全景指南 -上篇-

目錄

CPU

CPU 作爲計算資源,一直是雲計算廠商比拼的核心競爭力,我們的目標是合理安排好計算任務,充分提高 CPU 的利用率,預留更多空間容錯,增強系統穩定性,讓任務更快執行,降低無效功耗,節約成本,從而提高市場競爭力。

                               CPU 實現的抽象邏輯圖

  1. 首先,我們有一個自動計數器。這個自動計數器會隨着時鐘主頻不斷地自增,來作爲我們的 PC 寄存器;

  2. 在這個自動計數器的後面,我們連上一個譯碼器。譯碼器還要同時連着我們通過大量的 D 觸發器組成的內存。

  3. 自動計數器會隨着時鐘主頻不斷自增,從譯碼器當中,找到對應的計數器所表示的內存地址,然後讀取出裏面的 CPU 指令。

  4. 讀取出來的 CPU 指令會通過 CPU 時鐘的控制,寫入到一個由 D 觸發器組成的寄存器,也就是指令寄存器當中。

  5. 在指令寄存器後面,我們可以再跟一個譯碼器。這個譯碼器的作用不再是用於尋址,而是把拿到的指令解析成 opcode 和對應的操作數。

  6. 當我們拿到對應的 opcode 和操作數,對應的輸出線路就要連接 ALU,開始進行各種算術和邏輯運算。對應的計算結果,則會再寫回到 D 觸發器組成的寄存器或者內存當中。

這裏整個過程就大概是 CPU 的一條指令的執行過程。爲了加快 CPU 指令的執行速度,CPU 在發展過程中做了很多優化,例如流水線,分支預測,超標量,Hyper-threading,SIMD,多級 cache,NUMA 架構等,  這裏主要關注 Linux 的調度系統。

CPU 上下文

Linux 是一個多任務操作系統,它支持遠大於 CPU 數量的任務同時運行。當然,這些任務實際上並不是真的在同時運行,而是因爲系統在很短的時間內,將 CPU 輪流分配給它們,造成多任務同時運行的錯覺。

而在每個任務運行前,CPU 都需要知道任務從哪裏加載、又從哪裏開始運行,也就是說,需要系統事先幫它設置好 CPU 寄存器和程序計數器 (Program Counter,PC)。

CPU 寄存器,是 CPU 內置的容量小、但速度極快的內存。而程序計數器,則是用來存儲 CPU 正在執行的指令位置、或者即將執行的下一條指令位置。它們都是 CPU 在運行任何任務前,必須的依賴環境,因此也被叫做 CPU 上下文(執行環境):

而這些保存下來的上下文,會存儲在系統內核中(堆棧),並在任務重新調度執行時再次加載進來。這樣就能保證任務原來的狀態不受影響,讓任務看起來還是連續運行。

在 Linux 中,內核空間和用戶空間是兩種工作模式,操作系統運行在內核空間,而用戶態應用程序運行在用戶空間,它們代表不同的級別,而對系統資源具有不同的訪問權限。

這樣代碼(指令)執行存在不同的 CPU 上下文,而進行調度的時候,要進行相應的 CPU 上下文切換,Linux 系統存在不同堆棧來保存 CPU 上下文,系統中每個進程都會擁有屬於自己的內核棧,而系統中每個 CPU 都將爲中斷處理準備了兩個獨立的中斷棧,分別是 hardirq 棧和 softirq 棧:

Linux 系統調用 CPU 上下文切換堆棧結構:

中斷

中斷是由硬件設備產生的,而它們從物理上說就是電信號,之後,它們通過中斷控制器發送給 CPU,接着 CPU 判斷收到的中斷來自於哪個硬件設備(這定義在內核中),最後,由 CPU 發送給內核,內核來處理中斷。

硬中斷簡單處理流程:

硬中斷實現:中斷控制器 + 中斷服務程序

中斷框架設計 (x86):

X86 計算機的 CPU 爲中斷只提供了兩條外接引腳:NMI 和 INTR。其中 NMI 是不可屏蔽中斷,它通常用於電源掉電和物理存儲器奇偶校驗;INTR 是可屏蔽中斷,可以通過設置中斷屏蔽位來進行中斷屏蔽,它主要用於接受外部硬件的中斷信號,這些信號由中斷控制器傳遞給 CPU。當前 x86 SMP 架構主流都是採用多級 I/O APIC(高級可編程中斷控制器)中斷系統。

Local APIC:主要負責傳遞中斷信號到指定的處理器;

I/O APIC:主要是收集來自 I/O 裝置的 Interrupt 信號且在當那些裝置需要中斷時發送信號到本地 APIC;

中斷分類

中斷可分爲同步(synchronous)中斷和異步(asynchronous)中斷:

  1. 非屏蔽中斷 (Non-maskable interrupts, 即 NMI):就像這種中斷類型的字面意思一樣,這種中斷是不可能被 CPU 忽略或取消的。NMI 是在單獨的中斷線路上進行發送的,它通常被用於關鍵性硬件發生的錯誤,如內存錯誤,風扇故障,溫度傳感器故障等。

  2. 可屏蔽中斷(Maskable interrupts):這些中斷是可以被 CPU 忽略或延遲處理的。當緩存控制器的外部針腳被觸發的時候就會產生這種類型的中斷,而中斷屏蔽寄存器就會將這樣的中斷屏蔽掉。我們可以將一個比特位設置爲 0,來禁用在此針腳觸發的中斷。

處理流程:

相同點:

  1. 最後都是由 CPU 發送給內核,由內核去處理;

  2. 處理程序的流程設計上是相似的。

不同點:

  1. 產生源不相同,陷阱、異常是由 CPU 產生的,而中斷是由硬件設備產生的;

  2. 內核需要根據是異常,陷阱,還是中斷調用不同的處理程序;

  3. 中斷不是時鐘同步的,這意味着中斷可能隨時到來;陷阱、異常是 CPU 產生的,所以,它是時鐘同步的;

  4. 當處理中斷時,處於中斷上下文中;處理陷阱、異常時,處於進程上下文中。

中斷親和:

Linux 系統常見中斷分類

時鐘中斷

時鐘芯片產生,主要工作是處理和時間有關的所有信息,決定是否執行調度程序以及處理下半部分。和時間有關的所有信息包括系統時間、進程的時間片、延時、使用 CPU 的時間、各種定時器,進程更新後的時間片爲進程調度提供依據,然後在時鐘中斷返回時決定是否要執行調度程序。下半部分處理程序是 Linux 提供的一種機制,它使一部分工作推遲執行。時鐘中斷要絕對保證維持系統時間的準確性,“時鐘中斷” 是整個操作系統的脈搏。

NMI 中斷

外部硬件通過 CPU 的 NMI Pin 去觸發(硬件觸發),或者軟件向 CPU 系統總線上投遞一個 NMI 類型中斷(軟件觸發),NMI 中斷的主要用途有兩個:

硬件 IO 中斷

大多數硬件外設 IO 中斷,比如網卡,鍵盤,硬盤,鼠標,USB,串口等;

虛擬中斷

KVM 裏面一些中斷退出和中斷注入等,軟件模擬中斷;

查看方式:cat /proc/interrupts

Linux 系統中斷處理

由於中斷會打斷內核中進程的正常調度運行,所以要求中斷服務程序儘可能的短小精悍;但是在實際系統中,當中斷到來時,要完成工作往往需要進行大量的耗時處理。因此期望讓中斷處理程序運行得快,並想讓它完成的工作量多,這兩個目標相互制約,誕生頂 / 底半部機制。

中斷上半部分

中斷處理程序是頂半部——接受中斷,它就立即開始執行,但只有做嚴格時限的工作。能夠被允許稍後完成的工作會推遲到底半部去,此後,在合適的時機,底半部會被開終端執行。頂半部簡單快速,執行時禁止部分或者全部中斷。

中斷下半部分

底半部稍後執行,而且執行期間可以響應所有的中斷。這種設計可以使系統處於中斷屏蔽狀態的時間儘可能的短,以此來提高系統的響應能力。頂半部只有中斷處理程序機制,而底半部的實現有軟中斷,tasklet 和工作隊列等實現方式;

軟中斷

軟中斷作爲下半部機制的代表,是隨着 SMP(share memory processor)的出現應運而生的,它也是 tasklet 實現的基礎(tasklet 實際上只是在軟中斷的基礎上添加了一定的機制)。軟中斷一般是 “可延遲函數” 的總稱,有時候也包括了 tasklet(請讀者在遇到的時候根據上下文推斷是否包含 tasklet)。它的出現就是因爲要滿足上面所提出的上半部和下半部的區別,使得對時間不敏感的任務延後執行,而且可以在多個 CPU 上並行執行,使得總的系統效率可以更高。它的特性包括:產生後並不是馬上可以執行,必須要等待內核的調度才能執行。軟中斷不能被自己打斷(即單個 cpu 上軟中斷不能嵌套執行),只能被硬件中斷打斷(上半部), 可以併發運行在多個 CPU 上(即使同一類型的也可以)。所以軟中斷必須設計爲可重入的函數(允許多個 CPU 同時操作),因此也需要使用自旋鎖來保護其數據結構。

軟中斷的調度時機:

  1. do_irq 完成 I/O 中斷時調用 irq_exit。

  2. 系統使用 I/O APIC,在處理完本地時鐘中斷時。

  3. local_bh_enable,即開啓本地軟中斷時。

  4. SMP 系統中,cpu 處理完被 CALL_FUNCTION_VECTOR 處理器間中斷所觸發的函數時。

  5. ksoftirqd/n 線程被喚醒時。

軟中斷內核線程

在 Linux 中,中斷具有最高的優先級。不論在任何時刻,只要產生中斷事件,內核將立即執行相應的中斷處理程序,等到所有掛起的中斷和軟中斷處理完畢後才能執行正常的任務,因此有可能造成實時任務得不到及時的處理。中斷線程化之後,中斷將作爲內核線程運行而且被賦予不同的實時優先級,實時任務可以有比中斷線程更高的優先級。這樣,具有最高優先級的實時任務就能得到優先處理,即使在嚴重負載下仍有實時性保證。但是,並不是所有的中斷都可以被線程化,比如時鐘中斷,主要用來維護系統時間以及定時器等,其中定時器是操作系統的脈搏,一旦被線程化,就有可能被掛起,後果將不堪設想,所以不應當被線程化。

軟中斷優先在 irq_exit() 中執行,如果超過時間等條件轉爲 softirqd 線程中執行。滿足以下任一條件軟中斷在 softirqd 線程中執行:

在 irq_exit()->__do_softirq() 中運行,時間超過 2ms。

在 irq_exit()->__do_softirq() 中運行,輪詢軟中斷超過 10 次。

在 irq_exit()->__do_softirq() 中運行,本線程需要被調度。

注:調用 raise_softirq() 喚醒軟中斷時,不在中斷環境中。

TASKLET

由於軟中斷必須使用可重入函數,這就導致設計上的複雜度變高,作爲設備驅動程序的開發者來說,增加了負擔。而如果某種應用並不需要在多個 CPU 上並行執行,那麼軟中斷其實是沒有必要的。因此誕生了彌補以上兩個要求的 tasklet。它具有以下特性:

a)一種特定類型的 tasklet 只能運行在一個 CPU 上,不能並行,只能串行執行。

b)多個不同類型的 tasklet 可以並行在多個 CPU 上。

c)軟中斷是靜態分配的,在內核編譯好之後,就不能改變。但 tasklet 就靈活許多,可以在運行時改變(比如添加模塊時)。

tasklet 是在兩種軟中斷類型的基礎上實現的,因此如果不需要軟中斷的並行特性,tasklet 就是最好的選擇。也就是說 tasklet 是軟中斷的一種特殊用法,即延遲情況下的串行執行。

tasklet 有兩種,tasklet 和 hi-tasklet:

前者對應 softirq_vec[TASKLET_SOFTIRQ];

後者對應 softirq_vec[HI_SOFTIRQ]。只是後者排在 softirq_vec[] 的第一個,所以更早被執行;

# cat /proc/softirqs
CPU0      
          HI:    1   //高優先級TASKLET軟中斷
       TIMER:   12571001  //定時器軟中斷
      NET_TX:     826165  //網卡發送軟中斷
      NET_RX:    6263015  //網卡接收軟中斷
       BLOCK:    1403226  //塊設備處理軟中斷
BLOCK_IOPOLL:   0  //塊設備處理軟中斷
     TASKLET:   3752   //普通TASKLET軟中斷
       SCHED:     0  //調度軟中斷
     HRTIMER:   0  //當前已經沒有使用
         RCU:    9729155  //RCU處理軟中斷,主要是callback函數處理

工作隊列 (work queue) 是 Linux kernel 中將工作推後執行的一種機制。軟中斷運行在中斷上下文中,因此不能阻塞和睡眠,而 tasklet 使用軟中斷實現,當然也不能阻塞和睡眠,工作隊列可以把工作推後,交由一個內核線程去執行—這個下半部分總是會在進程上下文執行,因此工作隊列的優勢就在於它允許重新調度甚至睡眠。

實際上,工作隊列的本質就是將工作交給內核線程處理,因此其可以用內核線程替換。但是內核線程的創建和銷燬對編程者的要求較高,而工作隊列實現了內核線程的封裝,不易出錯,推薦使用工作隊列。

中斷上下文

中斷代碼運行於內核空間,中斷上下文即運行中斷代碼所需要 CPU 上下文環境,需要硬件傳遞過來的這些參數,內核需要保存的一些其他環境(主要是當前被打斷執行的進程或其他中斷環境),這些一般都保存在中斷棧中(x86 是獨立的,其他可能和內核棧共享,這和具體處理架構密切相關),在中斷結束後,進程仍然可以從原來的狀態恢復運行。

是否處於中斷中,在 Linux 中是通過 preempt_count 來判斷的,具體如下:

#define in_irq()     (hardirq_count()) // 在處理硬中斷中

#define in_softirq()     (softirq_count()) // 在處理軟中斷中

#define in_interrupt()   (irq_count()) // 在處理硬中斷或軟中斷中

#define in_atomic()     ((preempt_count() & ~PREEMPT_ACTIVE) != 0) // 包含以上所有情況

總結和注意的點

1.Linux kernel 的設計者制定了規則:

中斷上下文(hardirq 和 softirq context)並不參與調度(暫不考慮中斷線程化),它們是異步事件的處理機制,目標就是儘快完成處理,返回現場。因此,所有中斷上下文的優先級都是高於進程上下文的。也就是說,對於用戶進程(無論內核態還是用戶態)或者內核線程,除非 disable 了 CPU 的本地中斷,否則一旦中斷髮生,它們是沒有任何能力阻擋中斷上下文搶佔當前進程上下文的執行的。

2.Linux 將中斷處理過程分成了兩個階段,也就是上半部和下半部:

  1. 硬中斷和軟中斷(只要是中斷上下文)執行的時候都不允許內核搶佔(本文後續章節會講內核搶佔)。因爲在中斷上下文中, 唯一能打斷當前中斷 handler 的只有更高優先級的中斷,它不會被進程打斷 (這點對於 softirq,tasklet 也一樣, 因此這些 bottom half 也不能睡眠);如果在中斷上下文中睡眠,則沒有辦法喚醒它,因爲所有的 wake_up_xxx 都是針對某個進程而言的,而在中斷上下文中,沒有進程的概念,沒有相應 task_struct(這點對於 softirq 和 tasklet 一樣),因此真的睡眠了,比如調用了會導致阻塞的例程,內核幾乎會掛。

  2. 硬中斷可以被另一個優先級比自己高的硬中斷 “中斷”,不能被同級(同一種硬中斷)或低級的硬中斷 “中斷”,更不能被軟中斷 “中斷”。軟中斷可以被硬中斷 “中斷”,但是不會被另一個軟中斷 “中斷”。在一個 CPU 上,軟中斷總是串行執行。所以在單處理器上,對軟中斷的數據結構進行訪問不需要加任何同步原語。

  3. 關中斷不會丟失中斷,但是對於期間到來的多個相同的中斷會合併成一個,即只處理一次;時鐘中斷中需要更新 jieffis 計數值,如果多箇中斷合成一個,爲了減少影響 jieffis 值準確性,需要其他硬件時鐘來矯正。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s?__biz=MzIxMjE1MzU4OA==&mid=2648923275&idx=2&sn=3e75b40bf38f35dcb872a7366a19703b&chksm=8f5d9910b82a1006e23fa194132814f220c03e9239e811dc52557defdfdc2d99b391188e40d1&scene=21#wechat_redirect