資料整理: jserv
在一個以優先權為主的排程策略中,當一個新的行程 (上圖的 Task2) 進入到「可執行」(running) 的狀態,核心的排程器會去檢查它的優先權,若該行程的優先權比目前正在執行的行程 (即上圖的 Task1) 還高,核心便會觸發搶佔 (preempt),使得正在執行的行程被打斷,而擁有更高優先權的行程則會開始執行。
- to occupy (land) in order to establish a prior right to buy.
- to acquire or appropriate before someone else; take for oneself; arrogate:
a political issue preempted by the opposition party.- to take the place of because of priorities, reconsideration, rescheduling, etc.; supplant:
The special newscast preempted the usual television program.
漢語「搶佔」沒有顯著「優先權」的意涵Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
早期 Linux 核心不支援核心搶佔,這會引發以下問題:
因此,搶佔的存在是為了讓更高優先權的任務,得以有更低的延遲 (latency),從而提高系統的即時 (real-time) 能力。
在 Linux 核心中,使用者層級的搶佔是由被搶佔者行程 A 及搶佔行程 B 共同完成。示意如下:
行程 B 有較行程 A 更高的優先權,因此我們說 Process B preempts A
這不是字面上,由行程 B 單方面奪走 CPU 的使用權,而是行程 B 在確認具有搶佔資格後,藉由行程 A 的 TIF_NEED_RESCHED
旗標,告知行程 A 其需讓出 CPU 的使用權,待行程 A 執行到搶佔點時,會檢查該旗標,來選擇自己是否有權繼續執行,若發現旗標已被設定,則觸發重新排程 (reschedule,簡稱 resched
),讓排程器選擇具有更高優先權的行程執行。整體流程如下:
TIF_NEED_RESCHED
旗標TIF_NEED_RESCHED
旗標,若該旗標已設定,則觸發重新排程,而若是核心層級的行程,除了檢查旗標,還要再檢查 preempt_count
TIF_NEED_RESCHED
?在目前行程執行完被分配的時間後,若遲遲沒讓出 CPU,則由計時器中斷觸發搶佔,其處理函式如下:
計時器中斷處理函式會取出目前 CPU 正在執行的行程的 task_struct
,並呼叫其排程器類別的 task_tick()
函式,之後呼叫的流程如下:
task_tick()
entity_tick()
update_curr()
其中 update_curr()
會更新正在執行的行程的 vruntime
,之後會由 entity_tick()
函式呼叫 check_preempt_tick()
來觸發搶佔。
Linux 核心共提供三個函式來喚醒行程,分別是:
wake_up_new_task()
: 用來喚醒新的行程,例如 fork
出來的行程wake_up_process()
: 用來喚醒處於 TASK_NORMAL
狀態的行程wake_up_state()
: 用來喚醒指定狀態的行程而最後二者都會呼叫到 try_to_wake_up()
函式,之後呼叫的流程如下如下:
try_to_wake_up()
ttwu_queue()
ttwu_do_activate()
ttwu_do_wake_up()
在 wake_up_new_task()
及 ttwu_do_wake_up()
函式中,皆呼叫 check_preempt_curr()
函式,其內容如下:
可見,若搶佔者行程及被搶佔行程的 sched_class
相同,則會呼叫相對應排程器類別的 check_preempt_curr()
,根據不同的排程器類別來決定是否搶佔,而若搶佔者行程的排程器類別的優先權高於被搶佔行程,則會無條件呼叫 resched_curr()
函式來設定 TIF_NEED_RESCHED
。
而在個別排程類別中,以下以 fair 類別為例:
在 fair 排程類別中,共有兩種情況會發生搶佔:
vruntime
差值大於 gran
時最終呼叫 reshced_curr()
函式來設定 TIF_NEED_RESCHED
旗標。
preempt_count
?對於核心層級的行程而言,搶佔不僅要檢查 TIF_NEED_RESCHED
外,還需要檢查 preempt_count
是否為 0
:只有在 preempt_count
為 0
時,才能觸發搶佔。為此,核心提供一系列的巨集來設定及檢查 preempt_count
。
在 preempt_enable()
中,會用 preempt_count_dec_and_test()
巨集來檢查 preempt_count
是否為 0
,如果是則會呼叫 __preempt_schedule()
來觸發排程。
核心程式碼較少直接使用 preempt_enable()
或 preempt_disable()
,相對更常使用 lock,而我們知道,只要不是 PREEMPT_RT
(以下簡稱 RT
) 的核心組態,spinlock 不能被打斷,其實就是在 lock 時使用 preempt_disable()
,並於 unlock 時使用 preempt_enable()
,換言之,每次使用 spinlock 結束時,會預設觸發一次搶佔。
一般的使用者行程可被搶佔的地方較為固定,主要是:
在系統呼叫結束後,準備返回使用者模式時會進行檢查,以 x86 架構為例,其系統呼叫的流程如下:
do_syscall_64()
syscall_exit_to_user_mode()
exit_to_user_mode_prepare()
exit_to_user_mode_loop()
其中 exit_to_user_mode_loop()
的函式內容如下:
可見,若 TIF_NEED_RESCHED
旗標被設定,便會觸發排程。
當中斷結束,準備返回到使用者模式時,會執行下方一系列函式:
irqentry_exit()
irqentry_exit_cond_resched()
irqentry_exit_cond_resched()
之函式內容如下:
可見若 preempt_count
為 0
且需要重新排程時,便會呼叫 preempt_scheule_irq()
函式。
在 Linux 中,許多中斷處理會拆分成以下二部分:
bh
): 由 top-half 來排程,實際執行時機要等所有等待中的 top-half 都處理後,才會開始以時間順序來看是這樣:
以處理層級來看則是:
其中 bottom-half 在 Linux 核心對應的機制有三種:
以下針對非 RT
的核心組態。
如前節所述,在中斷結束後會先判斷 preempt_count
及 TIF_NEED_RESCHED
來決定是否需要進行搶佔。
在執行軟體中斷 (softirq) 前,核心會先調整 preempt_count
使其不為 0
,待 softirq 執行完畢後,再執行以下:
核心在執行 critical section 如 spinlock 時不允許搶佔,待 critical section 執行完畢後透過 preempt_schedule()
來觸發搶佔。