Preempt-RT
機制即時系統是一個有時間限制的系統,具有明確定義的固定時間限制。處理必須在定義的約束內完成,否則系統將失敗。
可以將 RTOS 分為以下兩種:
注意到 Real time != Real fast
在 RTOS 中,一個事件的發生到系統回應此事件所花的時間就稱作 Latencies (延遲)。對於某些非常重要的任務,例如核能場的設備,特定的操作控制必須在非常嚴格的時序限制內進行。開發者必須保證特定任務最壞情況執行時間 (WCET) 低於特定時間限制,以便它們可以在底層實際分配給它們的計劃時間內執行。
就如圖上所示,問號的部份就是在不重要任務中切換到重要任務的時間,而我們希望盡可能減少這段時間。
現代的作業系統和 CPU 都支持這種多工系統,它允許多個使用者同時執行多個任務。多工處理是作業系統在 CPU 機器上同時執行多個任務的能力。而要實現多工系統,必須有以下特性:
下圖是三個不同概念,分別是 Single thread、Multi-thread、Multi-processing。可以看到 thread 在 process 內共享程式碼、資料以及文件,但暫存器和堆疊是獨立的。
搶佔是暫時中斷正在執行的任務的行為,以便稍後恢復它,並執行優先級更高的任務。對處理器目前正在執行的任務的這種改變稱為上下文切換(Context switching)。
對於一般的作業系統,搶佔是有效的去分享處理器使用時間,而對 RTOS 而言則是非常關鍵的,因為可以搶佔低優先級的任務而去執行關鍵的任務(critical tasks)。而不管是 kernel space 還是 user space 中的任務都應該要是可以被搶佔的。
當任務運作在 user space 執行並被中斷時,如果中斷處理程序喚醒另一個任務,我們可以立即調度(透過 scheduler)該任務從中斷處理程序返回。
有一個特殊的情況,當任務呼叫 System call 到 kernel space 中執行時,此刻下有中斷發生,結束完中斷事件後,會先執行完在 kernel space 的系統呼叫後,才會使用 Scheduler 對任務進行排程,這就會導致在中斷結束到系統呼叫結束的這段時間是 Unbounded 。
參照 Linux 核心搶佔
硬體中斷是延遲的常見來源,硬體中斷有以下兩種:
硬體中斷是可以搶佔關鍵任務的,這部分可以參考 2.3.2. 節所示的原始 Linux 核心示意圖。也就是說對系統而言關鍵任務仍然是一個執行緒,而中斷的優先級還是高於這些一般任務。因此就會導致不管任務有多重要,仍無法 "及時" 執行。解決辦法會在後面探討。
排程器(Scheduler)是保證即時系統的關鍵,它會根據不同特性去做排程,而大多數則是根據優先級做排程,優先級高的優先執行。而相同優先級的任務則是可以以 FIFO 或是 Round-Robin 的方式處理。
在 Multi tasking 的情況下意味著會對資源並行(Concurrency)的訪問。關鍵的資源通常會透過鎖(lock)的機制來保護。而常見的所有以下兩種:
優先級反轉(Priority inversion)說明了一種特殊的情況,會使高優先級的任務被低優先級的任務搶佔,以下說明例子:
上圖流程如下:
解決優先級反轉的問題就是使用優先級繼承(Priority Inheritance),它透過將低優先級的任務(但持有資源)暫時地設置為最高優先級,保證了資源可以被釋放。同樣是剛剛的例子:
real-time 系統往往存在一些迷思,以下列出兩項說明:
Preempt_RT
其實原本的 Linux 核心就是一種 Soft RTOS,只是隨著工業上、業界上越來越多對 Hard RTOS 的需求,這也使 Linux 開始發展 Hard real-time 的機制。
但由上面介紹我們知道 Hard 和 Soft 的機制完全不一樣,若是要重新為了 Real-time 重新開發 Linux 好像不是一個理想的作法,因此 Preempt_RT
的補丁(patch)機制就出現了。它實際上是 Linux 的配置參數(config),我們可以透過設定這些參數來讓 Linux 核心有不同客製化的表現。而 Preempt_RT
的設定就是使核心變成 Hard real-time 。
Preempt_RT
的關鍵就在盡可能減少非搶佔式的程式,且最大幅度的減少因為要增加可搶佔性而增加的程式碼。PREEMPT_RT
補丁(patch) 利用 Linux 核心的 SMP 功能來添加這種額外的搶佔性,而無需完全重寫核心。從某種意義上說,我們可以粗略地將搶佔視為向系統添加新的 CPU,然後使用正常的鎖去同步任何搶佔任務。
以下是幾個 Preempt_RT
的特色:
我們知道通常要保護資源會使用自旋鎖(spinlock),若是已經有一個任務獲得自旋鎖,當另一個任務想獲取資源時,只能原地等待。但在 Preempt_RT
中,將 spinlock 變成了 sleeping spinlock ,也就是說它不再原地等待了,而是會進入睡眠模式,並排程其他任務。這也就讓原本的 spinlock (spinlock_t 和 rwlock_t) 變成可搶佔的。
這樣的機制使在 critical sections 仍然還是可以搶佔。但若是真的想要一個非搶佔的 critical sections 那可以使用 raw_spinlock_t 。這是在 Preempt_RT
中保留傳統的 spinlock 。
考慮到多核的系統,若是 critical sections 能被搶佔,那在某個 CPU 上執行的關鍵部份就有可能因為搶佔而被移到不同的 CPU 上。原本 spinlock 的工作就是保護這些 CPU 參數。如今就需要額外來處理這些搶佔。
以下有兩種方法來處理:
這邊導入了 Threaded interrupt 機制,將大部分的中斷都變成和一般的執行緒一樣,使其可以和一般任務一樣可以被中斷。但有一些中斷是不希望被搶佔的,例如:
irq0
: 每個 CPU 的定時器中斷fpu_irq
: 浮點數輔助處理器 FPU 發出的中斷lpptest
: 中斷延遲基準測試這些不可被搶佔的中斷就歸類到 SA_NODELAY
,在這邊的中斷皆是不可搶佔的。若是我們歸類太多中斷到此區的話就會大幅降低中斷和排程的延遲,也就失去了 Real-time 的效益了。
下面兩張圖顯示了原始 Linux 核心和 Preempt_RT
的差別:
原始 Linux 核心
Preempt_RT
機制
我們會在 2.5. 節更詳細的探討中斷相關議題。
透過這樣的機制,就可以達成 real-time 的精隨,也就是幾乎任何情況接可以被搶佔。下圖說明搶佔情況,紅色為不可搶佔、綠色為可搶佔。
Non-Preemptive
Preemption Points in Linux Kernel
Fully Preemptive
搶佔一段 "禁止中斷" 的區域聽上去似乎是很奇怪的,但 Preempt_RT
就是要讓多數事件是能被搶佔的。值得注意的是那要如何保護關鍵區域所擁有的資源呢?
如同 2.3.1. 節所述,spinlock 已經變成 sleeping spinlock ,也就是說若是有任務搶佔了持有 spinlock 的程式,那也會在嘗試要存取 spinlock 時進入 block 狀態並再次排程其他任務。因此我們可以理解成在 Preempt_RT
的機制下會把 spinlock 變成 mutex 。
TODO: 引用程式
如同 1.7. 和 1.8. 節提到的優先級反轉以及解決它的優先級繼承,Preempt_RT
當然也有這樣的機制。
但當信號量(Semaphore)不是作為鎖,而是做為事件機制時,不需要使用優先級繼承。例如:
這樣無法知道誰會觸發事件,優先級繼承就不適用。在這種情況下,可以使用 compat_semaphore 和 compat_rw_semaphore,這些變體不會實現優先級繼承。
現在因為 spinlock 會可能進入睡眠狀態,這代表我們不能在禁止搶佔或中斷的情況下使用 spinlock 。但若是還是很想使用 spinlock 怎麼辦?
使用延遲(Deferred)操作,將那些想要使用 spinlock 的操作往後延遲到搶佔重新啟動。
為了降低中斷處理帶來的延遲(latency),Linux 核心中將中斷切成:
當發生中斷時,馬上處理 top half 的事情,並將 bottom half 的事情推遲(Defer)到後面再做,如下圖所示:
注意到在很關鍵的中斷時,會在 top-half 階段先暫時將中斷關閉。確保執行中不被搶佔。執行結束後再將中斷開啟。
這樣的設計有助於中斷搶佔關鍵區域時所浪費的時間,對照 1.4. 節中斷搶佔關鍵任務後執行了一大段時間才結束,下圖則是使用了 Deferrable work ,使中斷可以即時處理,而又不佔用關鍵任務的時間。而這也是我們將中斷變成 threaded interrupt 的原因之一。
系統中有許多事情是需要用到定時器的,而通常 CPU 內會內建一個 Timer ,會週期性的觸發中斷,讓系統知道一個時間段到了。而這個週期性的時脈就稱作 tick 。
而通常要在 user space 設置定時器往往都和 system tick 綁在一起。若我想要設置一個頻率更大的定時器要怎麼做,簡單想就是直接把 system tick 的頻率拉高就好啦,但每次 tick 時 CPU 都要去處理中斷的事情,若是頻率太高,不但會佔用到正常程式的執行,更會使系統變慢和增加功耗。這並不是一個好的辦法。
因此結合 tickless 和 "on-demand" 技術的定時器中斷就出現了。而此時底層的硬體計時器也支援 "one-shot" 的技術。當有計時器中斷時,除了處理這個計時的中斷之外,系統還會掃描所有的 Timer ,並找到即將要超時的,將其時間值設置到硬體的 Timer 就好了。
而這樣的 tickless 的設置會使其計時中斷的間隔不是均勻的,這也提供了一個較高頻的計時器實作方式。假設有一些計時器,若是使用 tick 的話就沒辦法很精準的響應真實計時時間,而 tickless 則可以較好的實現。
Linux 核心有以下幾種搶佔模型可以做選擇:
CONFIG_PREEMPT_NONE
: 核心程式碼(中斷、異常、系統呼叫)永遠不會被搶佔。
CONFIG_PREEMPT_VOLUNTARY
: 核心可以自願的選擇搶佔點
CONFIG_PREEMPT
: 大多數核心程式碼隨時都可能被非自願地搶佔。當一個行程變成可運行時,不再需要等待核心程式碼(通常是系統呼叫)在排程之前返回。
CONFIG_PREEMPT_RT
: 幾乎所有的核心程式碼都可以隨時被非自願地搶佔。
回到根目錄輸入以下命令,就可以看到我的電腦所設置 config 類型。
本節將探討 Linux 核心的中斷以及 Threaded 後的中斷帶來的影響,會結合理論和程式碼解說。
在開始討論 Threaded Interrupt 之前,先來看看 Linux 核心如何完整的處理中斷。
完整流程:
do_IRQ
函式,負責執行中斷相關服務程式handle_irq
函式,在 irq_desc
數列中找到此 IRQ 對應到的 irq_desc
。執行對應的 headle_irq
函式,也就是會執行中斷處理程式 ISR 。為了要向核心申請 interrupt handler ,需要呼叫 request_irq
函式,而在 PREEMPT_RT
的機制下,此函式以被實作成 request_threaded_irq
,也就是中斷執行緒化。下面我們來分析此函式的執行流程。
可以在 kernel/irq/manage.c 中找到函式。
參數名稱 | 功能 |
---|---|
irq |
要註冊 handler 的 IRQ number |
handler |
上半部的函式 |
thread_fn |
下半部執行緒化函式 |
irqflags |
保存關閉中斷之前的狀態 |
對於一個即時系統而言,任務可能存在期限性,若是未完成的話,則有可能導致系統崩潰。因此排程器 (scheduler) 就非常關鍵。它負責選擇適合的任務給 CPU 執行,而在不同的情況下會有不同的選擇。
在 Linux 中,行程 (Process) 和執行緒 (Thread) 是沒有差別的,在排程器眼中,就是一個任務 (Task) 。
排程器的目標可以分為以下幾點:
根據任務的性質不同,Linux 將每個行程分為:
我們又可以將普通行程分為:
交互式行程注重的是用戶使用體驗,通常是 I/O Bound 任務,因此 Response time 比較重要,例如文字編輯器等。另一方面批處理行程則是會使 CPU 做大量的工作,因此更注重 Throughput 。
前面說到,Linux 會根據不同任務的性質去分配它的類型。在 linux/include/linux/sched.h 找到任務的結構。
為了確保任務可以優先得到 CPU 時間,Linux 核心給任務定義了從 0 ~ 139 的優先級。
Nice value 本身是一個 User space 的概念,它使使用者可以修改優先級來讓排程器優先選擇。而 Nice 值得取值範圍則是 -20 ~ 19 。注意到在這個區間,越低的值,其優先級越高。反之亦然。
它映射了普通任務的優先級範圍。在一個任務被創建時,這個值就會被決定。在 kernel/sched/core.c
裡可以看到 sched_fork
函式裡 static_prio
被設置為 NICE_TO_PRIO
。
接著在 linux/sched/prio.h
定義了 NICE_TO_PRIO
宏。
因此我們知道 static_prio
會被設置為 nice + 120
。特別注意到其值越低,優先級越高。
Normal Priority 會根據 Static priority 和排程策略來決定。若是 DEADLINE 策略會設置為 -1
,若是 Real-time 策略則會設置為 100 - 1 - rt_prio
,若是普通任務就等於 static_prio
。
特別注意到,父行程的 Normal Priority 會繼承給子行程。
prio
的值是排程器真正參考的數值,在 sched_fork
函式預設是 static_prio
,但在 set_user_nice
函式中會呼叫 effective_prio
來判斷是否為即時任務。若是普通任務則會回傳 static_prio
,反之就回傳 Real-time 的 Normal Priority 。
在 Linux 核心中會有一個排程策略,根據任務動態調整排程演算法。主要分為非即時和及時策略。
旨在注重公平分配。CFS 為每個任務分配一個虛擬運行時間,根據這個時間來決定優先級,確保所有任務都能平等獲得 CPU 資源。CFS 使用紅黑樹結構來管理和快速查找最需要運行的任務,並且隨著系統負載自動調整,達到平衡系統性能與任務公平性的目標。
SCHED_NORMAL
(原本是 SCHED_OTHER
):用於一般任務的排程策略。SCHED_BATCH
:不像一般任務那樣經常搶佔,從而允許任務運行更長時間並更好地利用緩存,但以互動性為代價。這非常適合批次作業。SCHED_IDLE
:比 Nice 值為 19 還要低優先級,但它不是真正的空閒計時器排程程序,以避免陷入優先反轉問題,從而導致死結。本章的重點不是 CFS ,詳細可以參考
sched_rt
是 Linux 核心中軟即時排程的演算法分類,主要有 FIFO 與 RR 兩個演算法。
我們希望透過一系列實驗來觀察兩個演算法的特性。以下實驗皆是在 Ubuntu Linux 環境下實作。使用 Linux 核心的 SCHED_FIFO 和 SCHED_RR (時間片預設為 100ms) 實驗。編譯器使用 GCC 並且關閉所有的優化。使用 trace-cmd 追蹤 CPU 的行為,最後使用 Kernelshark 工具視覺化 CPU 內的任務排程情況。任務的執行皆為 CPU-Bound。並將任務分為4 個不同量級,分別為Little、 Medium、 High、 Extreme。
我們先觀察 FIFO 和 RR 最基本的差異,使用 C 語言的 fork 函式產生子行程,設計相同優先級的 4 個任務。
因為是相同優先級,因此 FIFO 的策略就是一直執行直到結束,而 RR 則是每 100ms 就切換任務一次。若是先進來的任務執行時間很長,在 FIFO 的策略下就會相對不公平。而 RR 策略可能會因為多次的切換而導致有比較大的開銷。
Round Robin 策略的 Time slice 的選擇是非常關鍵的。過大的 Time slice 會導致其行為變成 FIFO 策略; 而過小的 Time slice 則會導致 CPU 太頻繁的切換任務,進而降低 CPU 使用率。本實驗要來測試 Time slice 的選擇是否會造成效能上的影響,換句話說,實驗目的就是要測試多少比例的任務可以在一個 Time slice 下執行結束可以獲得最好的取捨。
我們設計了 10 個從小到大負載的任務,先計算每個任務使用 CPU 的時間,再透過調整 Linux 核心的 sched_rr_timeslice_ms
參數修改 Time slice 。預設是 100 ms。
因為每個任務的數量平均分配的,分別使 0% ~ 100% 的任務是可以在一個 Time slice 之內完成。測試程式會執行 300 個任務,也就是一個區間大小的任務有 30 個。
實驗方法就是先用迴圈 fork
出 n
個子行程,進入 start_task
函式後使用 pause
函式使子行程進入睡眠。等到所有子行程都生成後,再使用 kill
函式向子行程發送信號並喚醒。我透過實驗觀察到子行程會等這個使用 kill
的父行程對每個子行程發送完信號後才會開始執行,因此所有的子行程就是幾乎一起進入 runqueue 的。
使用 perf 效能測量工具來觀測,取五次實驗數據做平均。觀察 Context-Switch 和 Throughput 。
策略 | Time slice (ms) | Context-Switches (次數) | Throughput (task/sec) |
---|---|---|---|
RR _0% | 1 | 17694 | 2.863 |
RR _10% | 10 | 3610 | 4.855 |
RR _20% | 30 | 1425 | 4.981 |
RR _30% | 70 | 728 | 4.893 |
RR _40% | 100 | 557 | 5.118 |
RR _50% | 130 | 456 | 5.037 |
RR _60% | 160 | 412 | 5.117 |
RR _70% | 200 | 356 | 5.146 |
RR _80% | 300 | 320 | 5.108 |
RR _90% | 400 | 111 | 4.926 |
RR _100% | 1500 | 81 | 4.957 |
FIFO | 無 | 83 | 4.612 |
透過數據可以看出,RR_0% 的 Context-Switch 次數非常多,就直接導致其 吞吐量非常低。而 RR_100% 與 FIFO 策略的數據非常相近,也就證實了當 Time slice 過高時,就失去了 Round Robin 的優勢了。有研究顯示在 80% 通常吞吐量最高,本實驗則是在 70%。
此外,我們也同時追蹤每個行程的回應時間,分別考慮平均、最大和最小。下圖顯示了回應時間的折線圖。最小回應時間是不會因為策略而改變。而平均與最大回應時間就會隨著比例的上升而增大,在 RR_100 ,也就是 FIFO 策略下平均和最大回應時間都是最長的,對應上表所呈現的效能,即使 FIFO 策略的數據不差,但回應時間的增加可能導致行程無法在給定的時間內完成。而 RR_70 在效能和回應時間的取捨下表現最好的。
最後,我們將提到 SCHED_DEADLINE
策略,它是 Linux 中最貼進及時系統的排程器。而其實際上就是實作了 EDF (Earliest Deadline First) 演算法。
在 deadline 排程器中需要定義三個參數分別是:
period
: 週期runtime
: 執行時間deadline
: 截止期限我們以一個處理影像的任務來舉例說明,假設一秒鐘需要處理 60 幀,那就代表每 16.6 (ms) 就會有一幀畫面到達,因此這個任務的 period
就是 16.6 (ms) 。再者,運行時間是任務產生輸出所需的 CPU 時間。處理一幀的時間我們需要考慮的是最壞情況執行時間 (WCET),若是 WCET 為 5 (ms) 的話,則 runtime
就是 5 (ms)。截止期限是任務必須給出結果的最長時間,如果任務需要在 10 (ms) 之內將一幀的畫面處理好,那它的 deadline
就是 10 (ms)。
由於截止期限排程器知道每個截止期限任務需要多少 CPU 時間,因此它知道系統何時可以(或不行)再接納新的任務。因此,截止期限排程器不允許使用者使系統超載,而是拒絕添加更多截止期限任務,從而保證所有截止期限任務將有 CPU 時間來完成其任務,至少有一定的延遲。
在即時調度理論中,調度程序的評估重點在於它是否能滿足所有即時任務的時間要求。即時任務必須有確定的時間行為,這意味著我們可以預測它們何時會執行。
即時任務可以分為幾種類型:
任務還有不同的截止日期設定:
根據這些任務特性,即時排程研究者開發了不同的調度方法來比較哪個算法能更有效地排程任務集。針對單處理器系統,EDF 排程算法被認為是最佳的,因為它能夠有效排程所有不超過 100% CPU 使用率的週期性或零星任務。
下面舉個例子說明:
Task | Runtime(WCET) | period | deadline |
---|---|---|---|
T1 | 1 | 4 | 4 |
T2 | 2 | 6 | 6 |
T3 | 3 | 8 | 8 |
Task | Runtime(WCET) | period | deadline |
---|---|---|---|
T4 | 2 | 8 | 8 |
對於多核心系統(multi-core system)來說,排程器需要考慮要將任務放到哪個處理器上面,我們可以將其分為:
對於多核心系統,Global、Clustered、Arbitrary 都不是 optimal。考慮到多核心系統會比單核系統複雜許多,以下考慮兩個案例。假設我有一個 4 核心的系統,我想排程 4 個大任務,其執行時間和週期都是 1 秒,在這種情況下,系統會剛剛好滿載
執行狀態如下圖所示:
既然這樣滿載的情況下都可以執行了,那負載較低的話是不是必定能執行呢? 答案是不一定的。假設和上面一樣有一個 4 核心的系統,今天我有 4 個小任務,其執行時間和期限為 1 ms,然後有一個大任務,其執行時間和週期都是 1 秒。它的 CPU 使用率是小於 4 個。
但是它如果這 5 個任務是同時進入 ready queue 的,那因為期限,4 個小任務會先佔走 4 個處理器,執行完後才會給到大任務執行,但此時大任務就必定會超過截止期限 1 ms ,就如下圖所示,而這種 CPU 使用率比較小的情況,但在多核系統上會超出期限的效應就稱為 Dhall's effect 。
這篇文章深入探討了 Linux 核心的 Preempt-RT
機制,涵蓋範圍廣泛,從 RTOS 的基本概念,到 Preempt-RT
中的 spinlock 調整、threaded interrupt、優先級繼承、延遲中斷、High Resolution Timer 以及即時排程器等主題。由此可以看出,解決即時任務期限問題涉及許多作業系統的相關挑戰。本文也將作為記錄,總結與反思這些議題。
wiki/RTLinux
Linux 核心設計: PREEMPT_RT 作為邁向硬即時作業系統的機制
Understanding Linux real-time with PREEMPT_RT training
A realtime preemption overview
Technical details of the real-time preemption
Real Time Linux Scheduling Comparison
实时Linux内核(PREEMPT_RT)的编译安装以及测试
Linux 核心設計: 中斷處理和現代架構考量
linux-insides
四種任務優先度 (prio、static_prio、rt_priority 與 normal_prio) 之間的差異
Deadline scheduling part 1 — overview and theory