歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> Linux內核搶占機制 - 簡介

Linux內核搶占機制 - 簡介

日期:2017/3/1 12:08:28   编辑:Linux內核

本文主要圍繞 Linux 內核調度器 Preemption 的相關實現進行討論。其中涉及的一般操作系統和 x86 處理器和硬件概念,可能也適用於其它操作系統。

1. 背景知識

要深入理解 Preemption 必須對操作系統的 Context Switch 做一個全面的梳理。最終可以了解 Preemption 和 Context Switch 概念上的區別與聯系。

1.1 Context Switch

Context Switch (上下文切換) 指任何操作系統上下文保存和恢復執行狀態,以便於被安全地打斷和稍後被正確地恢復執行。一般操作系統中通常因以下三種方式引起上下文切換,

Task Scheduling (任務調度)

任務調度一般是由調度器代碼在內核空間完成的。
通常需要將當前 CPU 執行任務的代碼,用戶或內核棧,地址空間切換到下一個要運行任務的代碼用戶或內核棧地址空間

Interrupt (中斷) 或 Exception (異常)

中斷和異常是由硬件產生但由軟件來響應和處理的。
這個過程中,涉及到將用戶態或內核態代碼切換至中斷處理代碼。同時可能還涉及到用戶進程棧或內核棧切換到中斷棧。支持保護模式的處理器可能還涉及到保護模式的切換。x86 處理器是通過 Interrupt Gate (中斷門) 完成的。

System Call (系統調用)

系統調用是由用戶態代碼主動調用,使用戶進程陷入到內核態調用內核定義的各種系統調用服務。這個過程中,涉及到將任務的用戶態代碼和棧在同一任務上下文上切換至內核系統調用代碼和同一任務的內核棧

1.2 Preemption

Preemption (搶占) 是指操作系統允許滿足某些重要條件(例如:優先級,公平性)的任務打斷當前正在 CPU 上運行的任務而得到調度執行。並且這種打斷不需要當前正在運行的任務的配合,同時被打斷的程序可以在後來可以再次被調度恢復執行。

多任務操作系統可以按照 Cooperative Multitasking (協作多任務) 和 Preemptive Multitasking (搶占式多任務) 來劃分。本質上,搶占就是允許高優先級的任務可以立即打斷低優先級的任務而得到運行。對低 Scheduling Latency (調度延遲) 或者 Real Time (實時) 操作系統的需求來說,支持完全搶占的特性是必須的。

三種上下文切換方式中,系統調用始終發生在同一任務的上下文中,只有中斷異常任務調度機制才涉及到一個任務被令一個上下文打斷。Preemption 最終需要借助任務調度來完成任務的打斷。但是,任務調度卻和這三種上下文切換方式都密切相關,要理解 Preemption,必須對三種機制有深入的了解。

2. 任務調度

任務的調度需要內核代碼通過調用調度器核心的 schedule 函數引起。它主要完成以下工作,

完成任務調度所需的 Context Switch (上下文切換) 調度算法相關實現:選擇下一個要運行的任務,任務運行狀態和 Run Queue (運行隊列) 的維護等

本文主要關注上下文切換和引起任務調度的原因。

2.1 任務調度上下文切換

內核 schedule 函數其中一個重要的處理就是 Task Context Switch (任務上下文切換)。調度器的任務上下文切換主要做兩件事,

任務地址空間的上下文切換。

在 Linux 上通過 switch_mm 函數完成。
x86 CPU 通過裝載下一個待運行的任務的頁目錄地址 mm->pgd 到 CR3 寄存器來實現。

任務 CPU 運行狀態的上下文切換。

主要是 CPU 各寄存器的切換,包括通用寄存器,浮點寄存器和系統寄存器的上下文切換。

在 Linux x86 64位的實現裡,指令`CS:EIP` 和棧 `SS:ESP` 還有其它通用寄存器的切換由 switch_to 完成。Linux 描述任務的數據結構是 struct task_struct,其中的 thread 成員(struct thread_struct)用於保存上下文切換時任務的 CPU 狀態。

由於浮點寄存器上下文切換代價比較大,而且,很多使用場景中,被調度的任務可能根本沒有使用過 FPU (浮點運算單元),所以 Linux 和很多其它 OS 都采用了 Lazy FPU 上下文切換的設計。但隨著 Intel 今年來引入 XSAVE 特性來加速 FPU 保存和恢復,Linux 內核在 3.7 引入了non-lazy FPU 上下文切換。當內核檢測到 CPU 支持 XSAVE 指令集,就使用 non-lazy 方式。這也是Intel Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3的章節 13.4 DESIGNING OS FACILITIES FOR SAVING X87 FPU, SSE AND EXTENDED STATES ON TASK OR CONTEXT SWITCHES 裡建議的方式。

一般來說,任務調度,或者說任務上下文切換,可以分為以下兩大方式來進行,

Voluntary Context Switch (主動上下文切換) Involuntary Context Switch (強制上下文切換)

2.2 主動上下文切換

主動上下文切換就是任務主動通過直接或者間接調用 schedule 函數引起的上下文切換。引起主動上下文切換的常見時機有,

任務因為等待 IO 操作完成或者其它資源而阻塞。

任務顯式地調用 schedule 前,把任務運行態設置成TASK_UNINTERRUPTIBLE。保證任務阻塞後不能因信號到來而引起睡眠過程的中斷,從而被喚醒。Linux 內核各種同步互斥原語,如 Mutex,Semaphore,wait_queue,R/W Semaphore,及其他各種引起阻塞的內核函數。

等待資源和特定事件的發生而主動睡眠。

任務顯式地調用 schedule 前,把任務運行態被設為 TASK_INTERRUPTIBLE。保證即使等待條件不滿足也可以被任務接收到的信號所喚醒,重新進入運行態。Linux 內核各種同步互斥原語,如 Mutex,Semaphore,wait_queue,及其它各種引起睡眠的內核函數。

特殊目的,例如 debug 和 trace。

任務在顯式地用 schedule 函數前,利用 set_current_state 將任務設置成非 TASK_RUNNING 狀態。例如,設置成 TASK_STOPPED 狀態,然後調用 schedule 函數。

2.3 強制上下文切換

強制上下文切換是指並非任務自身意願調用 schedule 函數而引發的上下文切換。從定義可以看出,強制上下文切換的主要原因都和 Preemption 有關。

2.3.1 觸發 Preemption

2.3.1.1 Tick Preemption

在周期性的時鐘中斷裡,內核調度器檢查當前正在運行任務的持續運行時間是否超出具體調度算法支持的時間上限,從而決定是否剝奪當前任務的運行。一旦決定剝奪在 CPU 上任務的運行,則會給正在 CPU 上運行的當前任務設置一個請求重新調度的標志:TIF_NEED_RESCHED。

需要注意的是,TIF_NEED_RESCHED 標志置位後,並沒有立即調用 schedule 函數發生上下文切換。真正的上下文切換動作是 User Preemption 或 Kernel Preemption 的代碼完成的。

User Preemption 或 Kernel Preemption 在很多代碼路徑上放置了檢查當前任務的 TIF_NEED_RESCHED 標志,並顯式調用 schedule 的邏輯。接下來很快就會有機會調用 schedule 來觸發任務切換,這時搶占就真正的完成了。上下文切換發生時,下一個被調度的任務將由具體調度器算法來決定從運行隊列裡挑選。

例如,如果時鐘中斷剛好打斷正在用戶空間運行的進程,那麼當 Tick Preemption 的代碼將當前被打斷的用戶進程的 TIF_NEED_RESCHED 標志置位。隨後,時鐘中斷處理完成,並返回用戶空間。此時,User Preemption 的代碼會在中斷返回用戶空間時檢查 TIF_NEED_RESCHED 標志,如果置位就會調用 schedule 來完成上下文切換。

2.3.1.2 Wakeup Preemption

當原因需要喚醒另一個進程時,try_to_wake_up 的內核函數將會幫助被喚醒的進程選擇一個 CPU 的 Run Queue,然後把進程插入到 Run Queue 裡,並設置成 TASK_RUNNING 狀態。這個過程中 CPU Run Queue 的選擇和 Run Queue 插入操作都是調用具體的調度算法回調函數來實現的。

任務插入到 Run Queue 後,調度器立即將新喚醒的任務和正在 CPU 上執行的任務交給具體的調度算法去比較,決定是否剝奪當前任務的運行。與 Tick Preemption 一樣,一旦決定剝奪在 CPU 上執行的任務的運行,則會給當前任務設置一個 TIF_NEED_RESCHED 標志。而實際的 schedule 調用並不是在這時完成的。但 Wakeup Preemption 在此處真正特殊的地方在於,執行喚醒操作的任務可能把被喚醒的任務插入到本地 CPU 的 Run Queue,但還可能插入到遠程 CPU 的 Run Queue。因此,try_to_wake_up 函數的調用根據被喚醒的任務將插入 Run Queue 所屬的 CPU 和執行喚醒任務正在運行的 CPU 關系,分為如下兩種情況,

共享緩存

被喚醒任務的目標 CPU 和當前運行喚醒 CPU 共享緩存。

喚醒函數在返回過程中,只要當前任務運行到任何一處 User Preemption 或 Kernel Preemption 的代碼,這些代碼就會檢查到 TIF_NEED_RESCHED 標志,並調用 schedule 的位置,上下文切換才真正發生。實際上,如果 Kernel Preemption 是打開的,在喚醒操作結束時的 spin_unlock 或者隨後的各種可能的中斷退出路徑都有 Kernel Preemption 調用 schedule 的時機。

不共享緩存

被喚醒任務的目標 CPU 和當前運行喚醒 CPU 不共享緩存。

這種情況下,喚醒操作在設置 TIF_NEED_RESCHED 標志之後,會立即向被喚醒任務 Run Queue 所屬的 CPU 發送一個 IPI (處理器間中斷),然後才返回。以 Intel x86 架構為例,那個遠程 CPU 的 RESCHEDULE_VECTOR 被初始化來響應這個中斷,最終中斷處理函數 scheduler_ipi 在遠程 CPU 上執行。早期 Linux 內核,scheduler_ipi 其實是個空函數,因為所有中斷返回用戶空間或者內核空間都的出口位置都已經有 User Preemption 和 Kernel Preemption 的代碼在那裡,所以 schedule 一定會被調用。後來的 Linux 內核裡,又利用 scheduler_ipi 讓遠程 CPU 來做遠程喚醒的主要操作,從而減少 Run Queue 鎖競爭。所以現在的 scheduler_ipi 加入了新的代碼。

因 Wakeup Preemption 而導致的上下文切換發生時,下一個被調度的任務將由具體調度器算法來決定從運行隊列裡挑選。對於剛喚醒的任務,如果成功觸發了 Wakeup Preemption,則某些具體的調度算法會給它一個優先被調度的機會。

2.3.2 執行 Preemption

2.3.2.1 User Preemption

User Preemption 發生在如下兩種典型的狀況,

系統調用,中斷及異常在返回用戶空間前,檢查 CPU 當前正在運行的任務的 TIF_NEED_RESCHED 標志,如果置位則直接調用 schedule 函數。

任務為 TASK_RUNNING 狀態時,直接或間接地調用 schedule

舉個間接調用的例子:內核態的代碼在循環體內調用 cond_resched(),yield() 等內核 API,給其它任務得到調度的機會,防止獨占濫用 CPU。

在內核態寫邏輯上造成長時間循環的代碼,有可能造成內核死鎖或者造成超長調度延遲,尤其是當 Kernel Preemption 沒有打開時。這時可以在循環體內調用 cond_resched() 內核 API,有條件的讓出 CPU。這裡說的有條件是因為cond_resched 要檢查 TIF_NEED_RESCHED 標志,看是否有新的 Preemption 的請求。而 yield 內核 API,不檢查 TIF_NEED_RESCHED 標志,則無條件觸發任務切換,但在所在 CPU Run Queue 沒有其它任務的情況下,不會發生真正的任務切換。

2.3.2.2 Kernel Preemption

早期 Linux 內核只支持 User Preemption。2.6內核 Kernel Preemption 支持被引入。

Kernel Preemption 發生在以下幾種情況,

中斷,異常結束處理後,返回到內核空間時。

以 x86 為例,Linux 在中斷和異常處理代碼的公共代碼部分(即從具體 handler 代碼退出後),判斷是否返回內核空間,然後調用 preempt_schedule_irq 檢查 TIF_NEED_RESCHED 標志,觸發任務切換。

禁止內核搶占處理結束時

作為完全搶占內核,Linux 只允許在當前內核上下文需要禁止搶占的時候才使用 preempt_disable 禁止搶占,內核代碼在禁止搶占後,應該盡早調用 preempt_enable 使能搶占,避免引入高調度延遲。為盡快處理在禁止搶占期間 pending 的重新調度申請,內核在 preempt_enable 裡會調用 preempt_schedule 檢查 TIF_NEED_RESCHED 標志,觸發任務切換。

使用 preempt_disable 和 preempt_enable 的內核上下文有很多,典型而又為人熟知的有各種內核鎖的實現,如 Spin Lock,Mutex,Semaphore,R/W Semaphore,RCU 等。

與 User Preemption 不同的是,上述兩種 Kernel Preemption 的情況發生時,任務的運行態可能已經被設置成 TASK_RUNNING 以外的睡眠狀態,如 TASK_UNINTERRUPTIBLE。此時接下來的內核 __schedule 代碼會有特殊處理,檢查 PREEMPT_ACTIVE 對上一個被 Preempt 的任務跳過移除隊列操作,保證 Kernel Preemption 盡快被處理。而 User Preemption 則不會在當前任務在 TASK_RUNNING 以外的狀態下發生,這是因為 User Preemption 總是發生在當前任務處於 TASK_RUNNING 的特殊位置。

3. 中斷和異常

Interrupt (中斷) 通常是由硬件或者是特殊軟件指令觸發的處理器需要立即響應的信號。Exception (異常) 廣義上被歸類為中斷的一種。但狹義上,中斷和異常最大的區別是,中斷發生和處理是異步的,但異常的發生和處理是同步的。

System Call 因為利用了特殊軟件指令給處理器產生了同步的 Trap (陷阱),也可被歸類為異常的一種。但由於其設計和用途和異常處理有明顯區別,將在另一個章節做單獨介紹。本節主要介紹中斷和異常。

3.1 中斷和異常的上下文切換

很多英文技術文檔和討論裡把這種類型的打斷動作叫做 Pin,意思就是當前的任務沒有被切換走,而是被 Pin 住不能動彈了。這種打斷不像 Context Switch 那樣,涉及到地址空間的切換。而且這種打斷通暢和處理器和外圍硬件的中斷機制有關。依賴於不同操作系統的實現,可能中斷或者異常處理程序有自己的獨立內核棧,例如當前 Linux 版本在32位和64位 x86 上的實現;也可能使用任務當前的內核棧,例如早期 Linux 在32位 x86 上的實現。

以 Intel 的 x64 處理器為例,當外設產生中斷後,CPU 通過 Interrupt Gate (中斷門) 打斷了當前任務的執行。此時,不論正在執行的任務處於用戶態還是內核態,中斷門都會無條件保存當前任務執行的寄存器執行上下文。這些寄存器裡就有當前任務下一條待執行的代碼段指令寄存器 CS:RIP 和當前任務棧指針寄存器 SS:RSP。而新的中斷上下文的代碼段指令寄存器 CS:RIP 的值,早由系統啟動時由 x86 的 IDT (中斷描述符表) 相關的初始化代碼設置為所有外設中斷的公共 IRQ Routine (中斷處理例程) 函數。在 Linux 3.19 的這個公共中斷處理例程 entry_64.S 的 irq_entries_start 匯編函數裡,SAVE_ARGS_IRQ 宏定義會把存儲在 per-CPU 變量 irq_stack_ptr 裡的內核 IRQ Stack (中斷棧) 賦值給 SS:RSP。這樣一來,一個完整的中斷上下文切換就由 CPU 和中斷處理例程共同協作完成。中斷執行完畢,從中斷例程的返回過程則會利用之前保存的上下文,恢復之前被打斷的任務。

x64 的異常機制與中斷機制類似,都利用了 IDT 來完被打斷任務的 CS:RIP 和 SS:RSP 的保存,但 IDT 表裡異常的公共入口函數卻是不同的函數。而且在這個函數的匯編實現裡,切換到內核 IRQ Stack 的代碼是借助硬件裡預先被內核初始化好的 IST (中斷服務表) 裡保存的 SS:RSP 的值,這是與一般外設中斷處理的不同之處。

另外,x64 和 x32 的 IDT 機制在從內核態進入到中斷門時,硬件在是否把當前任務的 SS:RSP 寄存器壓棧的處理上有明顯差別。此外,IDT 在初始化時,IDT 描述符裡的 IST 選擇位如果非零,則意味著內核 IRQ Stack 的切換是要由內核代碼借助 IST 實現。但如果 IDT 描述符的 IST 選擇位是零,則內核的 IRQ Stack 切換由內核代碼借助 per-CPU 的內核中斷棧變量實現。

由於主題和篇幅限制,這裡不會詳細介紹中斷的上下文切換機制。了解 x86 平台中斷和異常的上下文切換機制,需要對 x86 處理器的硬件規范有所了解。Intel Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3裡的 6.14 EXCEPTION AND INTERRUPT HANDLING IN 64-BIT MODE 章節裡有硬件的詳細介紹,尤其詳細說明了32位和64位,以及中斷和異常的詳細差異。

3.2 中斷引起的任務調度

Linux 內核裡,中斷和異常因其打斷的上下文不同,在返回時可能會觸發以下類型的任務調度,

User Preemption

中斷和異常打斷了用戶態運行的任務,在返回時檢查 TIF_NEED_RESCHED 標志,決定是否調用 schedule。

Kernel Preemption

中斷和異常打斷了內核態運行的任務,在返回時調用 preempt_schedule_irq。其代碼會檢查 TIF_NEED_RESCHED 標志,決定是否調用 schedule。

User 和 Kernel Preemption 的代碼是實現在 Linux 內核所有中斷和異常處理函數通用處理代碼層的,因此,中斷異常的具體處理函數返回後就會被執行。盡管所有類型中斷都可能引發任務切換,和任務調度和搶占密切相關,但以下兩種中斷直接與調度器相關,是內核調度器設計的一部分,

Timer Interrupt (時鐘中斷) Scheduler IPI (調度器處理器間中斷)

3.2.1 時鐘中斷

Timer Interrupt (時鐘中斷) 對操作系統調度有著特殊的意義。
如前所述,周期性執行的時鐘中斷處理函數會觸發 Tick Preemption 的請求。隨後中斷在返回前,根據返回的上下文不同,可能會執行到 User Preemption 和 Kernel Preemption 的邏輯。這裡的中斷,可以是操作系統任何的中斷,例如一般外設的中斷。由於操作系統一般在具體中斷處理函數進入前和退出後有公共中斷處理邏輯,所以 Preemption 一般都實現在這裡,而具體的中斷處理函數並無 Preemption 的 Knowledge。而我們知道,外設中斷一般具有隨機性,所以,如果沒有時鐘中斷的存在,那麼 Preemption 的實現恐怕很難有時間保證了。因此,周期性的時鐘中斷在這裡發揮了重要的作用。當然,除了 Preemption,時鐘中斷還擔負了系統中很多重要的功能的處理,例如調度隊列的均衡,進程時間的更新,軟件定時器的執行等。下面從 Preemption 的角度簡單的討論一下與時鐘中斷的關系,

時鐘中斷源

內核的時鐘中斷是基於其運行硬件支持的可以周期觸發時鐘中斷的設備來實現的。因此在不同硬件平台上,其實現機制和差異比較大。早期的 Linux x86 支持 PIT 還有 HPET 做時鐘中斷中斷源。現在 Linux 默認使用 x86 處理器的 Local APIC Timer 做時鐘中斷源。Local APIC Timer 與 PIT 和 HPET 最大的不同就是,APIC timer 中斷是 Per-CPU 的,但 PIT 和 HPET 是系統全局的。因此每 CPU 的 APIC Timer 中斷更加適合 SMP 系統的 Preemption 實現。

時鐘中斷頻率

早期 Linux 和一些 Unix 服務操作系統內核將時鐘中斷頻率設置成 100HZ。這意味著時鐘中斷的執行周期是 10ms。而新 Linux 內核默認將 x86 上 Linux 內核的頻率提高到 1000HZ。這樣,在 x86 上,時鐘中斷的處理周期縮短為 1ms。一個時鐘中斷周期通常被稱作一個 Tick。通常,Unix/Linux 都會使用一個全局技術器來對系統啟動以來的時鐘中斷次數來計數。Linux 內核中的這個全局變量被叫做 Jiffies。因此 Linux 內核中一個 Tick 也被叫一個 Jiffy。

當一個 Tick 從 10ms 縮短到 1ms,系統因處理高頻時鐘中斷的開銷理論上會增大,但著也帶來的更快更低延遲的 Preemption。由於硬件性能的提高,這種改變的負面影響很有限,但好處是很明顯的。

3.2.2 調度器處理器間中斷

Scheduler IPI (調度器處理器間中斷) 最初的引入主要是為了解決 SMP 系統中,喚醒代碼觸發 Wakeup Preemption 時,需要遠程 CPU 協助產生 User Preemption 或 Kernel Preemption 而引入的機制。其具體的過程如下,

喚醒代碼經過具體調度器算法為被喚醒任務選擇 CPU。 當選擇的 CPU 是遠程的時,將處於睡眠的進程喚醒並放入到遠程 CPU 所屬的 Run Queue 喚醒代碼調用具體調度算法檢查是否觸發 Wakeup Preemption,並在返回前觸發 Scheduler IPI (調度器處理器間中斷)。 遠程 CPU 正在執行的代碼被打斷,Scheduler IPI 處理函數被執行。 Scheduler IPI 處理函數內部並無針對 Preemption 的實際處理。 Scheduler IPI 處理函數退出時會進入中斷處理的公共代碼部分,根據中斷返回上下文是用戶還是內核上下文,觸發 User Preemption 或 Kernel Preemption。

需要指出的是,新的 x86 平台和 Linux 內核裡,Timer Interrupt 和 Scheduler IPI 都屬於 CPU Local APIC 處理的中斷。在 Linux 內核裡,在 entry_64.S 裡的公共入口和返回都由 apicinterrupt 處理。而 apicinterrupt 和其它外設中斷的公共入口返回代碼共享 User Preemption 或 Kernel Preemption 的處理邏輯,即 ret_from_intr 的處理。

4. 系統調用

System Call (系統調用) 是為應用程序請求操作系統內核服務而設計的,一整套相對穩定的編程接口和服務例程。本小節主要關注系統調用的上下文切換和引起的任務調度部分。

4.1 系統調用的上下文切換

系統調用與中斷和異常最大的不同是,系統調用的發生是同步的,是應用程序通過編程接口主動觸發的,因此不存在打斷當前執行任務的作用。所以系統調用自身就是正在執行任務的一部分,只不過,它是為任務在內核空間執行代碼而已。因此,當任務調用系統調用主動陷入到內核執行系統調用代碼時,必然發生上下文切換,一般來說,這個上下文切換是由硬件來輔助完成的。

以 Intel x86 處理器為例,系統調用完成的用戶空間到內核空間的上下文切換是由叫做 Trap Gate (陷阱門) 的硬件機制來實現的。Linux 操作系統支持以下兩種方式觸發 Intel x86 的陷阱門,

int 0x80 指令,較老的不支持系統調用指令的處理器使用。 sysenter 快速系統調用指令,較新的處理器支持。

陷阱門與中斷使用的中斷門類似,但其門調用發生過程中,沒有像中斷門一樣禁止中斷。當用戶態的代碼通過 glibc 的代碼發出上述指令觸發系統調用時,其上下文切換按照如下步驟發生,

當前任務的代碼 CS:RIP 指向了陷阱門為系統調用向量早已初始化好的系統調用公共入口函數 CPU 陷阱門存自動保存用戶任務的上下文,例如,系統調用號,用戶空間的代碼 CS:RIP 和用戶棧 SS:RSP 等。具體 layout 請參考硬件手冊。 系統調用公共入口代碼保存其它寄存器上下文,最後將 SS:RSP 指向了該任務內核棧 struct thread_info 的地址,完成了任務用戶棧到內核棧的切換。 系統調用公共入口代碼做必要檢查後,調用全局的系統調用表,進入到具體系統調用的服務例程。

4.2 系統調用引起的任務調度

與中斷處理類似,具體系統調用函數退出後,公共系統調用代碼返回用戶空間時,可能會觸發 User Preemption,即檢查 TIF_NEED_RESCHED 標志,決定是否調用 schedule。系統調用不會觸發 Kernel Preemption,因為系統調用返回時,總是返回到用戶空間,這一點與中斷和異常有很大的不同。

5. 調度觸發時機總結

Linux 內核源碼 schedule 的注釋寫的非常精煉,所以就不啰嗦了,直接上源碼,

/*
 * __schedule() is the main scheduler function.
 *
 * The main means of driving the scheduler and thus entering this function are:
 *
 *   1. Explicit blocking: mutex, semaphore, waitqueue, etc.
 *
 *   2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return
 *      paths. For example, see arch/x86/entry_64.S.
 *
 *      To drive preemption between tasks, the scheduler sets the flag in timer
 *      interrupt handler scheduler_tick().
 *
 *   3. Wakeups don't really cause entry into schedule(). They add a
 *      task to the run-queue and that's it.
 *
 *      Now, if the new task added to the run-queue preempts the current
 *      task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets
 *      called on the nearest possible occasion:
 *
 *       - If the kernel is preemptible (CONFIG_PREEMPT=y):
 *
 *         - in syscall or exception context, at the next outmost
 *           preempt_enable(). (this might be as soon as the wake_up()'s
 *           spin_unlock()!)
 *
 *         - in IRQ context, return from interrupt-handler to
 *           preemptible context
 *
 *       - If the kernel is not preemptible (CONFIG_PREEMPT is not set)
 *         then at the next:
 *
 *          - cond_resched() call
 *          - explicit schedule() call
 *          - return from syscall or exception to user-space
 *          - return from interrupt-handler to user-space
 */

6. 關聯閱讀

本文主要介紹了解 Preemption 所需的基本概念,以及 Linux 內核是如何實現 User Preemption 和 Kernel Preemption 的。由於 Context Switch 與 Preemption 密切相關,所以也結合 Intel x86 處理器做了詳細分析。這些內容在很多 Linux 內核書籍也都有覆蓋,但要深入理解,還是需要結合某種處理器架構相關的知識來一起學習,否則很難深入理解。因此了解些硬件相關的知識是必要的。

Intel Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3 6.14 和 13.4 章節 x86 系統調用入門 Proper Locking Under a Preemptible Kernel Linux Kernel Stack
Copyright © Linux教程網 All Rights Reserved