1. 程式人生 > >Linux程序上下文切換過程context_switch詳解--Linux程序的管理與排程(二十一)

Linux程序上下文切換過程context_switch詳解--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如何完成核心搶佔**

  1. 完成一些必要的檢查, 並設定程序狀態, 處理程序所在的就緒佇列
  2. 排程全域性的pick_next_task選擇搶佔的程序
    1. 如果當前cpu上所有的程序都是cfs排程的普通非實時程序, 則直接用cfs排程, 如果無程式可排程則排程idle程式
    2. 否則從優先順序最高的排程器類sched_class_highest(目前是stop_sched_class)開始依次遍歷所有排程器類的pick_next_task函式, 選擇最優的那個程序執行
    3. context_switch完成程序上下文切換
  3. context_switch完成程序上下文切換

即程序的搶佔或者切換工作是由context_switch完成的

那麼我們今天就詳細講解一下context_switch完成程序上下文切換的原理

2 程序上下文

2.1 程序上下文的概念

作業系統管理很多程序的執行. 有些程序是來自各種程式、系統和應用程式的單獨程序,而某些程序來自被分解為很多程序的應用或程式。當一個程序從核心中移出,另一個程序成為活動的, 這些程序之間便發生了上下文切換. 作業系統必須記錄重啟程序和啟動新程序使之活動所需要的所有資訊. 這些資訊被稱作上下文, 它描述了程序的現有狀態, 程序上下文是可執行程式程式碼是程序的重要組成部分, 實際上是程序執行活動全過程的靜態描述, 可以看作是使用者程序傳遞給核心的這些引數以及核心要儲存的那一整套的變數和暫存器值和當時的環境等

程序的上下文資訊包括, 指向可執行檔案的指標, 棧, 記憶體(資料段和堆), 程序狀態, 優先順序, 程式I/O的狀態, 授予許可權, 排程資訊, 審計資訊, 有關資源的資訊(檔案描述符和讀/寫指標), 關事件和訊號的資訊, 暫存器組(棧指標, 指令計數器)等等, 諸如此類.

處理器總處於以下三種狀態之一

  1. 核心態,運行於程序上下文,核心代表程序運行於核心空間;
  2. 核心態,運行於中斷上下文,核心代表硬體運行於核心空間;
  3. 使用者態,運行於使用者空間。

使用者空間的應用程式,通過系統呼叫,進入核心空間。這個時候使用者空間的程序要傳遞 很多變數、引數的值給核心,核心態執行的時候也要儲存使用者程序的一些暫存器值、變數等。

所謂的”程序上下文”硬體通過觸發訊號,導致核心呼叫中斷處理程式,進入核心空間。這個過程中,硬體的 一些變數和引數也要傳遞給核心,核心通過這些引數進行中斷處理。所謂的”中斷上下文”,其實也可以看作就是硬體傳遞過來的這些引數和核心需要儲存的一些其他環境(主要是當前被打斷執行的程序環境)。

LINUX完全註釋中的一段話

當一個程序在執行時,CPU的所有暫存器中的值、程序的狀態以及堆疊中的內容被稱 為該程序的上下文。當核心需要切換到另一個程序時,它需要儲存當前程序的 所有狀態,即儲存當前程序的上下文,以便在再次執行該程序時,能夠必得到切換時的狀態執行下去。在LINUX中,當前程序上下文均儲存在程序的任務資料結 構中。在發生中斷時,核心就在被中斷程序的上下文中,在核心態下執行中斷服務例程。但同時會保留所有需要用到的資源,以便中繼服務結束時能恢復被中斷程序 的執行.

2.2 上下文切換

程序被搶佔CPU時候, 作業系統儲存其上下文資訊, 同時將新的活動程序的上下文資訊載入進來, 這個過程其實就是上下文切換, 而當一個被搶佔的程序再次成為活動的, 它可以恢復自己的上下文繼續從被搶佔的位置開始執行.

上下文切換(有時也稱做程序切換或任務切換)是指CPU從一個程序或執行緒切換到另一個程序或執行緒

稍微詳細描述一下,上下文切換可以認為是核心(作業系統的核心)在 CPU 上對於程序(包括執行緒)進行以下的活動:

  1. 掛起一個程序,將這個程序在 CPU 中的狀態(上下文)儲存於記憶體中的某處,
  2. 在記憶體中檢索下一個程序的上下文並將其在 CPU 的暫存器中恢復
  3. 跳轉到程式計數器所指向的位置(即跳轉到程序被中斷時的程式碼行),以恢復該程序

因此上下文是指某一時間點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中執行如下操作

  1. 程序切換, 即esp的切換, 由於從esp可以找到程序的描述符
  2. 硬體上下文切換, 設定ip暫存器的值, 並jmp到__switch_to函式
  3. 堆疊的切換, 即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;
}