1 前景回顧 1.1 Linux的調度器組成 2個調度器 可以用兩種方法來激活調度 一種是直接的, 比如進程打算睡眠或出於其他原因放棄CPU 另一種是通過周期性的機制, 以固定的頻率運行, 不時的檢測是否有必要 因此當前linux的調度程式由兩個調度器組成:主調度器,周期性調度器(兩者又統稱為通用調 ...
1 前景回顧
1.1 Linux的調度器組成
2個調度器
可以用兩種方法來激活調度
- 一種是直接的, 比如進程打算睡眠或出於其他原因放棄CPU
- 另一種是通過周期性的機制, 以固定的頻率運行, 不時的檢測是否有必要
因此當前linux的調度程式由兩個調度器組成:主調度器,周期性調度器(兩者又統稱為通用調度器(generic scheduler)或核心調度器(core scheduler))
並且每個調度器包括兩個內容:調度框架(其實質就是兩個函數框架)及調度器類
6種調度策略
linux內核目前實現了6中調度策略(即調度演算法), 用於對不同類型的進程進行調度, 或者支持某些特殊的功能
- SCHED_NORMAL和SCHED_BATCH調度普通的非實時進程
- SCHED_FIFO和SCHED_RR和SCHED_DEADLINE則採用不同的調度策略調度實時進程
- SCHED_IDLE則在系統空閑時調用idle進程.
5個調度器類
而依據其調度策略的不同實現了5個調度器類, 一個調度器類可以用一種種或者多種調度策略調度某一類進程, 也可以用於特殊情況或者調度特殊功能的進程.
其所屬進程的優先順序順序為
stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
3個調度實體
調度器不限於調度進程, 還可以調度更大的實體, 比如實現組調度.
這種一般性要求調度器不直接操作進程, 而是處理可調度實體, 因此需要一個通用的數據結構描述這個調度實體,即seched_entity結構, 其實際上就代表了一個調度對象,可以為一個進程,也可以為一個進程組.
linux中針對當前可調度的實時和非實時進程, 定義了類型為seched_entity的3個調度實體
- sched_dl_entity 採用EDF演算法調度的實時調度實體
- sched_rt_entity 採用Roound-Robin或者FIFO演算法調度的實時調度實體
- sched_entity 採用CFS演算法調度的普通非實時進程的調度實體
1.2 調度工作
周期性調度器通過調用各個調度器類的task_tick函數完成周期性調度工作
- 如果當前進程是完全公平隊列中的進程, 則首先根據當前就緒隊列中的進程數算出一個延遲時間間隔,大概每個進程分配2ms時間,然後按照該進程在隊列中的總權重中占得比例,算出它該執行的時間X,如果該進程執行物理時間超過了X,則激發延遲調度;如果沒有超過X,但是紅黑樹就緒隊列中下一個進程優先順序更高,即curr->vruntime-leftmost->vruntime > X,也將延遲調度
- 如果當前進程是實時調度類中的進程:則如果該進程是SCHED_RR,則遞減時間片[為HZ/10],到期,插入到隊列尾部,並激發延遲調度,如果是SCHED_FIFO,則什麼也不做,直到該進程執行完成
延遲調度的真正調度過程在:schedule中實現,會按照調度類順序和優先順序挑選出一個最高優先順序的進程執行
而對於主調度器則直接關閉內核搶占後, 通過調用schedule來完成進程的調度
可見不管是周期性調度器還是主調度器, 內核中的許多地方, 如果要將CPU分配給與當前活動進程不同的另外一個進程(即搶占),都會直接或者調用調度函數, 包括schedule或者其子函數__schedule, 其中schedule在關閉內核搶占後調用__schedule完成了搶占.
而__schedule則執行瞭如下操作
**__schedule如何完成內核搶占**
- 完成一些必要的檢查, 並設置進程狀態, 處理進程所在的就緒隊列
- 調度全局的pick_next_task選擇搶占的進程
- 如果當前cpu上所有的進程都是cfs調度的普通非實時進程, 則直接用cfs調度, 如果無程式可調度則調度idle程式
- 否則從優先順序最高的調度器類sched_class_highest(目前是stop_sched_class)開始依次遍歷所有調度器類的pick_next_task函數, 選擇最優的那個進程執行
- context_switch完成進程上下文切換
- context_switch完成進程上下文切換
即進程的搶占或者切換工作是由context_switch完成的
那麼我們今天就詳細講解一下context_switch完成進程上下文切換的原理
2 進程上下文
2.1 進程上下文的概念
操作系統管理很多進程的執行. 有些進程是來自各種程式、系統和應用程式的單獨進程,而某些進程來自被分解為很多進程的應用或程式。當一個進程從內核中移出,另一個進程成為活動的, 這些進程之間便發生了上下文切換. 操作系統必須記錄重啟進程和啟動新進程使之活動所需要的所有信息. 這些信息被稱作上下文, 它描述了進程的現有狀態, 進程上下文是可執行程式代碼是進程的重要組成部分, 實際上是進程執行活動全過程的靜態描述, 可以看作是用戶進程傳遞給內核的這些參數以及內核要保存的那一整套的變數和寄存器值和當時的環境等
進程的上下文信息包括, 指向可執行文件的指針, 棧, 記憶體(數據段和堆), 進程狀態, 優先順序, 程式I/O的狀態, 授予許可權, 調度信息, 審計信息, 有關資源的信息(文件描述符和讀/寫指針), 關事件和信號的信息, 寄存器組(棧指針, 指令計數器)等等, 諸如此類.
處理器總處於以下三種狀態之一
- 內核態,運行於進程上下文,內核代表進程運行於內核空間;
- 內核態,運行於中斷上下文,內核代表硬體運行於內核空間;
- 用戶態,運行於用戶空間。
用戶空間的應用程式,通過系統調用,進入內核空間。這個時候用戶空間的進程要傳遞 很多變數、參數的值給內核,內核態運行的時候也要保存用戶進程的一些寄存器值、變數等。
所謂的”進程上下文”硬體通過觸發信號,導致內核調用中斷處理程式,進入內核空間。這個過程中,硬體的 一些變數和參數也要傳遞給內核,內核通過這些參數進行中斷處理。所謂的”中斷上下文”,其實也可以看作就是硬體傳遞過來的這些參數和內核需要保存的一些其他環境(主要是當前被打斷執行的進程環境)。
LINUX完全註釋中的一段話
當一個進程在執行時,CPU的所有寄存器中的值、進程的狀態以及堆棧中的內容被稱 為該進程的上下文。當內核需要切換到另一個進程時,它需要保存當前進程的 所有狀態,即保存當前進程的上下文,以便在再次執行該進程時,能夠必得到切換時的狀態執行下去。在LINUX中,當前進程上下文均保存在進程的任務數據結 構中。在發生中斷時,內核就在被中斷進程的上下文中,在內核態下執行中斷服務常式。但同時會保留所有需要用到的資源,以便中繼服務結束時能恢復被中斷進程 的執行.
2.2 上下文切換
進程被搶占CPU時候, 操作系統保存其上下文信息, 同時將新的活動進程的上下文信息載入進來, 這個過程其實就是上下文切換, 而當一個被搶占的進程再次成為活動的, 它可以恢復自己的上下文繼續從被搶占的位置開始執行.
上下文切換(有時也稱做進程切換或任務切換)是指CPU從一個進程或線程切換到另一個進程或線程
稍微詳細描述一下,上下文切換可以認為是內核(操作系統的核心)在 CPU 上對於進程(包括線程)進行以下的活動:
- 掛起一個進程,將這個進程在 CPU 中的狀態(上下文)存儲於記憶體中的某處,
- 在記憶體中檢索下一個進程的上下文並將其在 CPU 的寄存器中恢復
- 跳轉到程式計數器所指向的位置(即跳轉到進程被中斷時的代碼行),以恢復該進程
因此上下文是指某一時間點CPU寄存器和程式計數器的內容, 廣義上還包括記憶體中進程的虛擬地址映射信息.
上下文切換隻能發生在內核態中, 上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味著消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。
Linux相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少.
3 context_switch進程上下文切換
linux中進程調度時, 內核在選擇新進程之後進行搶占時, 通過context_switch完成進程上下文切換.
註意 進程調度與搶占的區別
進程調度不一定發生搶占, 但是搶占時卻一定發生了調度
在進程發生調度時, 只有當前內核發生當前進程因為主動或者被動需要放棄CPU時, 內核才會選擇一個與當前活動進程不同的進程來搶占CPU
context_switch其實是一個分配器, 他會調用所需的特定體繫結構的方法
調用switch_mm(), 把虛擬記憶體從一個進程映射切換到新進程中
switch_mm更換通過task_struct->mm描述的記憶體管理上下文, 該工作的細節取決於處理器, 主要包括載入頁表, 刷出地址轉換後備緩衝器(部分或者全部), 向記憶體管理單元(MMU)提供新的信息
調用switch_to(),從上一個進程的處理器狀態切換到新進程的處理器狀態。這包括保存、恢復棧信息和寄存器信息
switch_to切換處理器寄存器的呢內容和內核棧(虛擬地址空間的用戶部分已經通過switch_mm變更, 其中也包括了用戶狀態下的棧, 因此switch_to不需要變更用戶棧, 只需變更內核棧), 此段代碼嚴重依賴於體繫結構, 且代碼通常都是用彙編語言編寫.
context_switch函數建立next進程的地址空間。進程描述符的active_mm欄位指向進程所使用的記憶體描述符,而mm欄位指向進程所擁有的記憶體描述符。對於一般的進程,這兩個欄位有相同的地址,但是,內核線程沒有它自己的地址空間而且它的 mm欄位總是被設置為 NULL
context_switch( )函數保證:如果next是一個內核線程, 它使用prev所使用的地址空間
由於不同架構下地址映射的機制有所區別, 而寄存器等信息弊病也是依賴於架構的, 因此switch_mm和switch_to兩個函數均是體繫結構相關的
3.1 context_switch完全註釋
context_switch定義在kernel/sched/core.c#L2711, 如下所示
/*
* context_switch - switch to the new MM and the new thread's register state.
*/
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
/* 完成進程切換的準備工作 */
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev);
/* 如果next是內核線程,則線程使用prev所使用的地址空間
* schedule( )函數把該線程設置為懶惰TLB模式
* 內核線程並不擁有自己的頁表集(task_struct->mm = NULL)
* 它使用一個普通進程的頁表集
* 不過,沒有必要使一個用戶態線性地址對應的TLB表項無效
* 因為內核線程不訪問用戶態地址空間。
*/
if (!mm) /* 內核線程無虛擬地址空間, mm = NULL*/
{
/* 內核線程的active_mm為上一個進程的mm
* 註意此時如果prev也是內核線程,
* 則oldmm為NULL, 即next->active_mm也為NULL */
next->active_mm = oldmm;
/* 增加mm的引用計數 */
atomic_inc(&oldmm->mm_count);
/* 通知底層體繫結構不需要切換虛擬地址空間的用戶部分
* 這種加速上下文切換的技術稱為惰性TBL */
enter_lazy_tlb(oldmm, next);
}
else /* 不是內核線程, 則需要切切換虛擬地址空間 */
switch_mm(oldmm, mm, next);
/* 如果prev是內核線程或正在退出的進程
* 就重新設置prev->active_mm
* 然後把指向prev記憶體描述符的指針保存到運行隊列的prev_mm欄位中
*/
if (!prev->mm)
{
/* 將prev的active_mm賦值和為空 */
prev->active_mm = NULL;
/* 更新運行隊列的prev_mm成員 */
rq->prev_mm = oldmm;
}
/*
* Since the runqueue lock will be released by the next
* task (which is an invalid locking op but in the case
* of the scheduler it's an obvious special-case), so we
* do an early lockdep release here:
*/
lockdep_unpin_lock(&rq->lock);
spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
/* Here we just switch the register state and the stack.
* 切換進程的執行環境, 包括堆棧和寄存器
* 同時返回上一個執行的程式
* 相當於prev = witch_to(prev, next) */
switch_to(prev, next, prev);
/* switch_to之後的代碼只有在
* 當前進程再次被選擇運行(恢復執行)時才會運行
* 而此時當前進程恢復執行時的上一個進程可能跟參數傳入時的prev不同
* 甚至可能是系統中任意一個隨機的進程
* 因此switch_to通過第三個參數將此進程返回
*/
/* 路障同步, 一般用編譯器指令實現
* 確保了switch_to和finish_task_switch的執行順序
* 不會因為任何可能的優化而改變 */
barrier();
/* 進程切換之後的處理工作 */
return finish_task_switch(prev);
}
3.2 prepare_arch_switch切換前的準備工作
在進程切換之前, 首先執行調用每個體繫結構都必須定義的prepare_task_switch掛鉤, 這使得內核執行特定於體繫結構的代碼, 為切換做事先準備. 大多數支持的體繫結構都不需要該選項
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next); /* 完成進程切換的準備工作 */
prepare_task_switch函數定義在kernel/sched/core.c, line 2558, 如下所示
/**
* prepare_task_switch - prepare to switch tasks
* @rq: the runqueue preparing to switch
* @prev: the current task that is being switched out
* @next: the task we are going to switch to.
*
* This is called with the rq lock held and interrupts off. It must
* be paired with a subsequent finish_task_switch after the context
* switch.
*
* prepare_task_switch sets up locking and calls architecture specific
* hooks.
*/
static inline void
prepare_task_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
sched_info_switch(rq, prev, next);
perf_event_task_sched_out(prev, next);
fire_sched_out_preempt_notifiers(prev, next);
prepare_lock_switch(rq, next);
prepare_arch_switch(next);
}
3.3 next是內核線程時的處理
由於用戶空間進程的寄存器內容在進入核心態時保存在內核棧中, 在上下文切換期間無需顯式操作. 而因為每個進程首先都是從核心態開始執行(在調度期間控制權傳遞給新進程), 在返回用戶空間時, 會使用內核棧上保存的值自動恢復寄存器數據.
另外需要註意, 內核線程沒有自身的用戶空間上下文, 其task_struct->mm為NULL, 參見Linux內核線程kernel thread詳解–Linux進程的管理與調度(十), 從當前進程”借來”的地址空間記錄在active_mm中
/* 如果next是內核線程,則線程使用prev所使用的地址空間
* schedule( )函數把該線程設置為懶惰TLB模式
* 內核線程並不擁有自己的頁表集(task_struct->mm = NULL)
* 它使用一個普通進程的頁表集
* 不過,沒有必要使一個用戶態線性地址對應的TLB表項無效
* 因為內核線程不訪問用戶態地址空間。
*/
if (!mm) /* 內核線程無虛擬地址空間, mm = NULL*/
{
/* 內核線程的active_mm為上一個進程的mm
* 註意此時如果prev也是內核線程,
* 則oldmm為NULL, 即next->active_mm也為NULL */
next->active_mm = oldmm;
/* 增加mm的引用計數 */
atomic_inc(&oldmm->mm_count);
/* 通知底層體繫結構不需要切換虛擬地址空間的用戶部分
* 這種加速上下文切換的技術稱為惰性TBL */
enter_lazy_tlb(oldmm, next);
}
else /* 不是內核線程, 則需要切切換虛擬地址空間 */
switch_mm(oldmm, mm, next);
qizhongenter_lazy_tlb通知底層體繫結構不需要切換虛擬地址空間的用戶空間部分, 這種加速上下文切換的技術稱之為惰性TLB
3.6 switch_to完成進程切換
3.6.1 switch_to函數
最後用switch_to完成了進程的切換, 該函數切換了寄存器狀態和棧, 新進程在該調用後開始執行, 而switch_to之後的代碼只有在當前進程下一次被選擇運行時才會執行
執行環境的切換是在switch_to()中完成的, switch_to完成最終的進程切換,它保存原進程的所有寄存器信息,恢復新進程的所有寄存器信息,並執行新的進程
該函數往往通過巨集來實現, 其原型聲明如下
/*
* Saving eflags is important. It switches not only IOPL between tasks,
* it also protects other tasks from NT leaking through sysenter etc.
*/
#define switch_to(prev, next, last)
體繫結構 | switch_to實現 |
---|---|
x86 | arch/x86/include/asm/switch_to.h中兩種實現 定義CONFIG_X86_32巨集 未定義CONFIG_X86_32巨集 |
arm | arch/arm/include/asm/switch_to.h, line 25 |
通用 | include/asm-generic/switch_to.h, line 25 |
內核在switch_to中執行如下操作
- 進程切換, 即esp的切換, 由於從esp可以找到進程的描述符
- 硬體上下文切換, 設置ip寄存器的值, 並jmp到__switch_to函數
- 堆棧的切換, 即ebp的切換, ebp是棧底指針, 它確定了當前用戶空間屬於哪個進程
__switch_to函數
體繫結構 | __switch_to實現 |
---|---|
x86 | arch/x86/kernel/process_32.c, line 242 |
x86_64 | arch/x86/kernel/process_64.c, line 277 |
arm64 | arch/arm64/kernel/process.c, line 329 |
3.6.2 為什麼switch_to需要3個參數
調度過程可能選擇了一個新的進程, 而清理工作則是針對此前的活動進程, 請註意, 這不是發起上下文切換的那個進程, 而是系統中隨機的某個其他進程, 內核必須想辦法使得進程能夠與context_switch常式通信, 這就可以通過switch_to巨集實現. 因此switch_to函數通過3個參數提供2個變數.
在新進程被選中時, 底層的進程切換冽程必須將此前執行的進程提供給context_switch, 由於控制流會回到陔函數的中間, 這無法用普通的函數返回值來做到, 因此提供了3個參數的巨集
我們考慮這個樣一個例子, 假定多個進程A, B, C…在系統上運行, 在某個時間點, 內核決定從進程A切換到進程B, 此時prev = A, next = B, 即執行了switch_to(A, B), 而後當被搶占的進程A再次被選擇執行的時候, 系統可能進行了多次進程切換/搶占(至少會經歷一次即再次從B到A),假設A再次被選擇執行時時當前活動進程是C, 即此時prev = C. next = A.
在每個switch_to被調用的時候, prev和next指針位於各個進程的內核棧中, prev指向了當前運行的進程, 而next指向了將要運行的下一個進程, 那麼為了執行從prev到next的切換, switcth_to使用前兩個參數prev和next就夠了.
在進程A被選中再次執行的時候, 會出現一個問題, 此時控制權即將回到A, switch_to函數返回, 內核開始執行switch_to之後的點, 此時內核棧準確的恢復到切換之前的狀態, 即進程A上次被切換出去時的狀態, prev = A, next = B. 此時, 內核無法知道實際上在進程A之前運行的是進程C.
因此, 在新進程被選中執行時, 內核恢復到進程被切換出去的點繼續執行, 此時內核只知道誰之前將新進程搶占了, 但是卻不知道新進程再次執行是搶占了誰, 因此底層的進程切換機制必須將此前執行的進程(即新進程搶占的那個進程)提供給context_switch. 由於控制流會回到函數的該中間, 因此無法通過普通函數的返回值來完成. 因此使用了一個3個參數, 但是邏輯效果是相同的, 仿佛是switch_to是帶有兩個參數的函數, 而且返回了一個指向此前運行的進程的指針.
switch_to(prev, next, last);
即
prev = last = switch_to(prev, next);
其中返回的prev值並不是做參數的prev值, 而是prev被再次調度的時候搶占掉的那個進程last.
在上個例子中, 進程A提供給switch_to的參數是prev = A, next = B, 然後控制權從A交給了B, 但是恢復執行的時候是通過prev = C, next = A完成了再次調度, 而後內核恢復了進程A被切換之前的內核棧信息, 即prev = A, next = B. 內核為了通知調度機制A搶占了C的處理器, 就通過last參數傳遞迴來, prev = last = C.
內核實現該行為特性的方式依賴於底層的體繫結構, 但內核顯然可以通過考慮兩個進程的內核棧來重建所需要的信息
3.6.3 switch_to函數註釋
switch_mm()進行用戶空間的切換, 更確切地說, 是切換地址轉換表(pgd), 由於pgd包括內核虛擬地址空間和用戶虛擬地址空間地址映射, linux內核把進程的整個虛擬地址空間分成兩個部分, 一部分是內核虛擬地址空間, 另外一部分是內核虛擬地址空間, 各個進程的虛擬地址空間各不相同, 但是卻共用了同樣的內核地址空間, 這樣在進程切換的時候, 就只需要切換虛擬地址空間的用戶空間部分.
每個進程都有其自身的頁目錄表pgd
進程本身尚未切換, 而存儲管理機制的頁目錄指針cr3卻已經切換了,這樣不會造成問題嗎?不會的,因為這個時候CPU在系統空間運行,而所有進程的頁目錄表中與系統空間對應的目錄項都指向相同的頁表,所以,不管切換到哪一個進程的頁目錄表都一樣,受影響的只是用戶空間,系統空間的映射則永遠不變
我們下麵來分析一下子, x86_32位下的switch_to函數, 其定義在arch/x86/include/asm/switch_to.h, line 27
先對flags寄存器和ebp壓入舊進程內核棧,並將確定舊進程恢復執行的下一跳地址,並將舊進程ip,esp保存到task_struct->thread_info中,這樣舊進程保存完畢;然後用新進程的thread_info->esp恢復新進程的內核堆棧,用thread->info的ip恢復新進程地址執行。
關鍵點:內核寄存器[eflags、ebp保存到內核棧;內核棧esp地址、ip地址保存到thread_info中,task_struct在生命期中始終是全局的,所以肯定能根據該結構恢復出其所有執行場景來]
/*
* Saving eflags is important. It switches not only IOPL between tasks,
* it also protects other tasks from NT leaking through sysenter etc.
*/
#define switch_to(prev, next, last) \
do { \
/* \
* Context-switching clobbers all registers, so we clobber \
* them explicitly, via unused output variables. \
* (EAX and EBP is not listed because EBP is saved/restored \
* explicitly for wchan access and EAX is the return value of \
* __switch_to()) \
*/ \
unsigned long ebx, ecx, edx, esi, edi; \
\
asm volatile("pushfl\n\t" /* save flags 保存就的ebp、和flags寄存器到舊進程的內核棧中*/ \
"pushl %%ebp\n\t" /* save EBP */ \
"movl %%esp,%[prev_sp]\n\t" /* save ESP 將舊進程esp保存到thread_info結構中 */ \
"movl %[next_sp],%%esp\n\t" /* restore ESP 用新進程esp填寫esp寄存器,此時內核棧已切換 */ \
"movl $1f,%[prev_ip]\n\t" /* save EIP 將該進程恢復執行時的下條地址保存到舊進程的thread中*/ \
"pushl %[next_ip]\n\t" /* restore EIP 將新進程的ip值壓入到新進程的內核棧中 */ \
__switch_canary \
"jmp __switch_to\n" /* regparm call */ \
"1:\t" \
"popl %%ebp\n\t" /* restore EBP 該進程執行,恢復ebp寄存器*/ \
"popfl\n" /* restore flags 恢復flags寄存器*/ \
\
/* output parameters */ \
: [prev_sp] "=m" (prev->thread.sp), \
[prev_ip] "=m" (prev->thread.ip), \
"=a" (last), \
\
/* clobbered output registers: */ \
"=b" (ebx), "=c" (ecx), "=d" (edx), \
"=S" (esi), "=D" (edi) \
\
__switch_canary_oparam \
\
/* input parameters: */ \
: [next_sp] "m" (next->thread.sp), \
[next_ip] "m" (next->thread.ip), \
\
/* regparm parameters for __switch_to(): */ \
[prev] "a" (prev), \
[next] "d" (next) \
\
__switch_canary_iparam \
\
: /* reloaded segment registers */ \
"memory"); \
} while (0)
3.7 barrier路障同步
switch_to完成了進程的切換, 新進程在該調用後開始執行, 而switch_to之後的代碼只有在當前進程下一次被選擇運行時才會執行.
/* switch_to之後的代碼只有在
* 當前進程再次被選擇運行(恢復執行)時才會運行
* 而此時當前進程恢復執行時的上一個進程可能跟參數傳入時的prev不同
* 甚至可能是系統中任意一個隨機的進程
* 因此switch_to通過第三個參數將此進程返回
*/
/* 路障同步, 一般用編譯器指令實現
* 確保了switch_to和finish_task_switch的執行順序
* 不會因為任何可能的優化而改變 */
barrier();
/* 進程切換之後的處理工作 */
return finish_task_switch(prev);
而為了程式編譯後指令的執行順序不會因為編譯器的優化而改變, 因此內核提供了路障同步barrier來保證程式的執行順序.
barrier往往通過編譯器指令來實現, 內核中多處都實現了barrier, 形式如下
// http://lxr.free-electrons.com/source/include/linux/compiler-gcc.h?v=4.6#L15
/* Copied from linux/compiler-gcc.h since we can't include it directly
* 採用內斂彙編實現
* __asm__用於指示編譯器在此插入彙編語句
* __volatile__用於告訴編譯器,嚴禁將此處的彙編語句與其它的語句重組合優化。
* 即:原原本本按原來的樣子處理這這裡的彙編。
* memory強制gcc編譯器假設RAM所有記憶體單元均被彙編指令修改,這樣cpu中的registers和cache中已緩存的記憶體單元中的數據將作廢。cpu將不得不在需要的時候重新讀取記憶體中的數據。這就阻止了cpu又將registers,cache中的數據用於去優化指令,而避免去訪問記憶體。
* "":::表示這是個空指令。barrier()不用在此插入一條串列化彙編指令。在後文將討論什麼叫串列化指令。
*/
#define barrier() __asm__ __volatile__("": : :"memory")
關於記憶體屏障的詳細信息, 可以參見 Linux內核同步機制之(三):memory barrier
3.8 finish_task_switch完成清理工作
finish_task_switch完成一些清理工作, 使得能夠正確的釋放鎖, 但我們不會詳細討論這些. 他會向各個體繫結構提供了另一個掛鉤上下切換過程的可能性, 當然這隻在少數電腦上需要.
註:A進程切換到B, A被切換, 而當A再次被選擇執行, C再次切換到A,此時A執行,但是系統為了告知調度器A再次執行前的進程是C, 通過switch_to的last參數返回的prev指向C,在A調度時候需要把調用A的進程的信息清除掉
由於從C切換到A時候, A內核棧中保存的實際上是A切換出時的狀態信息, 即prev=A, next=B,但是在A執行時, 其位於context_switch上下文中, 該函數的last參數返回的prev應該是切換到A的進程C, A負責對C進程信息進行切換後處理,比如,如果切換到A後,A發現C進程已經處於TASK_DEAD狀態,則將釋放C進程的TASK_STRUCT結構
函數定義在kernel/sched/core.c, line 2715中, 如下所示
/**
* finish_task_switch - clean up after a task-switch
* @prev: the thread we just switched away from.
*
* finish_task_switch must be called after the context switch, paired
* with a prepare_task_switch call before the context switch.
* finish_task_switch will reconcile locking set up by prepare_task_switch,
* and do any other architecture-specific cleanup actions.
*
* Note that we may have delayed dropping an mm in context_switch(). If
* so, we finish that here outside of the runqueue lock. (Doing it
* with the lock held can cause deadlocks; see schedule() for
* details.)
*
* The context switch have flipped the stack from under us and restored the
* local variables which were saved when this task called schedule() in the
* past. prev == current is still correct but we need to recalculate this_rq
* because prev may have moved to another CPU.
*/
static struct rq *finish_task_switch(struct task_struct *prev)
__releases(rq->lock)
{
struct rq *rq = this_rq();
struct mm_struct *mm = rq->prev_mm;
long prev_state;
/*
* The previous task will have left us with a preempt_count of 2
* because it left us after:
*
* schedule()
* preempt_disable(); // 1
* __schedule()
* raw_spin_lock_irq(&rq->lock) // 2
*
* Also, see FORK_PREEMPT_COUNT.
*/
if (WARN_ONCE(preempt_count() != 2*PREEMPT_DISABLE_OFFSET,
"corrupted preempt_count: %s/%d/0x%x\n",
current->comm, current->pid, preempt_count()))
preempt_count_set(FORK_PREEMPT_COUNT);
rq->prev_mm = NULL;
/*
* A task struct has one reference for the use as "current".
* If a task dies, then it sets TASK_DEAD in tsk->state and calls
* schedule one last time. The schedule call will never return, and
* the scheduled task must drop that reference.
*
* We must observe prev->state before clearing prev->on_cpu (in
* finish_lock_switch), otherwise a concurrent wakeup can get prev
* running on another CPU and we could rave with its RUNNING -> DEAD
* transition, resulting in a double drop.
*/
prev_state = prev->state;
vtime_task_switch(prev);
perf_event_task_sched_in(prev, current);
finish_lock_switch(rq, prev);
finish_arch_post_lock_switch();
fire_sched_in_preempt_notifiers(current);
if (mm)
mmdrop(mm);
if (unlikely(prev_state == TASK_DEAD)) /* 如果上一個進程已經終止,釋放其task_struct 結構 */
{
if (prev->sched_class->task_dead)
prev->sched_class->task_dead(prev);
/*
* Remove function-return probe instances associated with this
* task and put them back on the free list.
*/
kprobe_flush_task(prev);
put_task_struct(prev);
}
tick_nohz_task_switch();
return rq;
}