1. 程式人生 > >linux-程序切換,使用者態程序,核心態程序

linux-程序切換,使用者態程序,核心態程序

一開始我並不想寫這個筆記,因為太過複雜,我一直想以簡單的方式理解核心,只從概念,避免涉及過多的程式碼。實際上,我寫筆記的時候,書已經看到很後面了,因為總要理解更多才能理解之前看似簡短實際複雜的內容。但最後發現實際上任何內容都沒有辦法跳過,即便不想看,也需要了解基本的概念,所以依舊不會拿大段程式碼,但總會拿少量程式碼。

如果不感興趣,我覺得也可以跳過,只需要知道一個概念即可。關於程序切換有更詳細的章節。。所以這裡也並沒有深入更多,只是筆記,也許以後會補充更多內容。

為了控制程序的執行,核心必須有能力掛起正在CPU上執行的程序,並恢復以前掛起的某個程序的執行。這種行為被稱為程序切換(process switch

)、任務切換(task switch)或上下文切換(content switch)。

硬體上下文

儘管每個程序都有自己的地址空間,但所有程序必須共享CPU暫存器。因此,在恢復一個程序的執行之前,核心必須確保每個暫存器裝載了掛起程序時所需要的值。

程序恢復執行前必須裝入暫存器的一組資料成為硬體上下文(hardware context)。硬體上下文是程序可執行上下文的一個自己,因為可執行上下文包含程序執行時所需要的所有資訊。在Linux中,程序硬體上下午的一部分存放在TSS段,而剩餘部分存放在核心態堆疊中。

在下面描述中,假定用prev區域性變量表示切換出的程序描述符,next表示切換進的程序描述符。因此,我們把程序切換定義為這樣的行為:儲存prev

硬體上下文,用next硬體上下文代替prev。因為程序切換經常發生,因此減少儲存和裝入硬體上下文所話費的時間是非常重要的。

早期Linux版本利用80x86體系結構所需提供的硬體支援,並通過far jmp1指令跳到next程序TSS描述符的選擇符來執行程序切換。當執行這條指令時,CPU通過自動儲存原來的硬體上下文,裝入新的硬體上下文來執行硬體上下文切換。但Linux2.6使用軟體執行程序切換,原因有:

  1. 通過一組mov指令逐步執行切換,這樣能較好地控制所裝入的資料的合法性,一面被惡意使用者偽造。far jmp指令不會有這樣的檢查。
  2. 舊方法和新方法所需時間大致相同。

程序切換值發生在核心態,在執行程序切換之前,使用者態程序使用的所有暫存器內容已儲存在核心堆疊上,這也包括ss和esp這對暫存器的內容。

任務狀態段

80x86體系結構包含了一個特殊的段型別,叫任務狀態段(Task State Segment,TSS)來存放硬體上下文,儘管Linux並不使用硬體上下文切換,但是強制它為系統中每個不同的CPU建立一個TSS,這樣做主要有兩個理由:

  1. 當80x86的一個CPU從使用者態切換到核心態時,它就從TSS中後去核心態堆疊的地址。
  2. 當用戶態程序試圖通過in或out指令訪問一個I/O埠時,CPU需要訪問存放在TSS中的I/O許可點陣圖以檢查該程序是否有訪問埠的權利。

更確切的說,當程序在使用者態執行in或out指令時,控制單元執行下列操作:

  1. 檢查eflags暫存器中的2位IOPL欄位,如果欄位的值為3,控制單元就執行I/O指令。否則,執行下一個檢查。
  2. 訪問tr暫存器以確定當前的TSS和相應的I/O許可權點陣圖。
  3. 檢查I/O指令中指定的I/O埠在I/O許可權點陣圖中對應的位,如果該位清,這條指令就執行,否則控制單元產生一個異常。

tss_struct結構描述TSS的格式,init_tss陣列為系統上每個不同的CPU存放一個TSS。在每次程序切換時,核心都更新TSS的某些欄位以便相應的CPU控制單元可以安全地檢索到它需要的資訊。因此,TSS反映了CPU上當前程序的特權級,但不必為沒有在執行的程序保留TSS。

每個TSS有它自己8位元組的任務狀態段描述符(Task State Segment Descriptor,TSSD)。這個描述符包括指向TSS起始地址的32位Base欄位,20位Limit欄位。TSSD的S標誌位被清0,以表示相應的TSS時系統段的事實。

Type欄位被置位11或9以表示這個段實際上是一個TSS。在Intel的原始設計中,系統中的每個程序都應當指向自己的TSS;Type欄位的第二個有效位叫Busy位;如果程序正由CPU執行,則該位置1,否則為0。在Linux的設計中,每個CPU只有一個TSS,因此Busy位總是為1.

由Linux建立的TSSD存放在全域性描述符表(GDT)中,GDT的基地址存放在每個CPU的gdtr暫存器中。每個CPU的tr暫存器包含相應TSS的TSSD選擇符,也包含了兩個隱藏的非程式設計欄位:TSSD的Base欄位和Limit欄位。這樣,處理器就能夠直接TSS定址而不需要從GDT中檢索TSS地址。

thread欄位

在每次程序切換時,被替換的程序的硬體上下文必須儲存在別處。不能像Intel原始設計那樣儲存在TSS中,因為Linux為每個處理器而不是為每個程序使用TSS。

因此,每個程序描述符包含一個型別為thread_structthread欄位,只要程序被切換出去,核心就把其硬體上下文儲存在這個結構中。隨後可以看到,這個資料結構包含的欄位涉及大部分CPU暫存器,但不包括eax、ebx等等這些通用暫存器。它們的值保留在核心堆疊中。

執行程序切換

程序切換可能只發生在精心定義的點:schedule()函式,這個函式很長,會在以後更長的篇幅裡講解。。這裡,只關注核心如何執行一個程序切換。

程序切換由兩步組成:

  1. 切換頁全域性目錄以安裝一個新的地址空間。
  2. 切換核心態堆疊和硬體上下文,因為硬體上下文提供了核心執行新程序所需要的所有資訊,包含CPU暫存器。

switch_to巨集

程序切換的第二步由switch_to巨集執行。它是核心中與硬體關係最為密切的例程之一,必須下很多功夫瞭解。

<include/asm-generic/system.h>

/* context switching is now performed out-of-line in switch_to.S */
extern struct task_struct *__switch_to(struct task_struct *,
        struct task_struct *);
#define switch_to(prev, next, last)\
    do {\
        ((last) = __switch_to((prev), (next)));\
    } while (0)

首先,該巨集有三個引數,prevnextlastprevnext的作用僅是區域性變數prevnext的佔位符,即它們是輸入引數,分別表示被替換程序和新程序描述符的地址在記憶體中的位置。

在任何程序切換中,涉及到的是三個程序而不是兩個。假設核心決定暫停程序A而啟用程序B,在schedule()函式中,prev指向A的描述符,而next指向B的程序描述符。switch_to巨集一旦使A暫停,A的執行流就被凍結。

隨後,當核心想再次啟用A,就必須暫停另一個程序C,因為這通常不是B,因為B有可能被其他程序比如C切換。於是就要用prev指向C而next指向A來執行另一個switch_to巨集。當A恢復它執行的流時,就會找到它原來的核心棧,於是prev區域性變數還是指向A的描述符而next指向B的描述符。此時,代表程序A執行的核心就失去了對C的任何引用。但引用對於完成程序切換是有用的,所以需要保留。

switch_to巨集的最後一個引數是輸出引數,它表示巨集把程序C的描述符地址寫在記憶體的什麼位置了,不過,這個是在恢復A執行之後完成的。在程序切換之前,巨集把第一個輸入引數prev表示的變數存入CPU的eax暫存器。在完成程序切換,A已經恢復執行時,巨集把CPU的eax暫存器的內容寫入由第三個引數last所指示的A在記憶體中的位置。因為CPU暫存器不會在切換點發生變化,所以C的描述符地址也存在記憶體的這個位置。在schedule()執行過程中,last引數指向A的區域性變數prev,所以prev被C的地址覆蓋。

__switch_to()函式

__switch_to()函式執行大多數開始於switch_to()巨集的程序切換。這個函式作用於prev_pnext_p引數,這兩個引數表示前一個程序和新程序。這個函式的呼叫不同於一般的函式呼叫。因為__switch_to()從eax和edx取引數prev_pnext_p,而不像大多數函式一樣從棧中取引數。

<arch/x86/kernel/process_32.c>

__switch_to(
    struct task_struct *prev_p,
    struct task_struct *next_p)
{
    struct thread_struct *prev = &prev_p->thread,
                 *next = &next_p->thread;
    int cpu = smp_processor_id();
    struct tss_struct *tss = &per_cpu(init_tss, cpu);
    bool preload_fpu;

    preload_fpu = tsk_used_math(next_p) && next_p->fpu_counter > 5;

    __unlazy_fpu(prev_p);

    if (preload_fpu)
        prefetch(next->xstate);

    load_sp0(tss, next);

    lazy_save_gs(prev->gs);

    load_TLS(next, cpu);

    if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
        set_iopl_mask(next->iopl);

    if (unlikely(task_thread_info(prev_p)->flags 
        & _TIF_WORK_CTXSW_PREV
        || task_thread_info(next_p)->flags
        & _TIF_WORK_CTXSW_NEXT))
        __switch_to_xtra(prev_p, next_p, tss);

    if (preload_fpu)
        clts();

    arch_end_context_switch(next_p);

    if (preload_fpu)
        __math_state_restore();
    if (prev->gs | next->gs)
        lazy_load_gs(next->gs);

    percpu_write(current_task, next_p);

    return prev_p;
}

這個函式執行步驟如下:

執行由__unlay_fpu()巨集程式碼產生的程式碼,以有選擇地儲存prev_p程序的FPU、MMX以及XMM暫存器的內容。

執行smp_processor_id()巨集獲得本地CPU的下表,即執行程式碼的CPU。該巨集從當前程序的thread_info結構的cpu欄位獲得下標並儲存到cpu區域性變數。

next_p->thread.esp0裝入對應於本地CPU的TSS的esp0欄位。其實,任何由sysenter彙編指令產生的從使用者態到核心態的特權級轉換將把這個地址拷貝到esp暫存器中。

next_p程序使用的執行緒區域性儲存(TLS)段裝載入本地CPU的全域性描述符表。

fsgs段暫存器的內容分別存放在prev_p->thread.fsprev_p->thread.gs中。esi暫存器指向prev_p->thread結構。

如果fsgs段暫存器已經被prev_pnext_p程序中的任意一個使用,則將next_p程序的thread_struct描述符中儲存的值裝入這些暫存器。

next_p->thread.debugreg陣列內容裝載dr0…dr7中的6個除錯暫存器。只有在next_p被掛起時正在使用除錯暫存器,這種操作才能進行。

如果必要,則更新TSS中的I/O點陣圖。然後終止,prev_p引數被拷貝到eax,因為預設情況下任何C函式的返回值被傳給eax暫存器。所以eax的值在呼叫__switch_to()的過程中被保護起來;這很重要,因為呼叫該函式時會假定eax總是用來存放將被替換的程序描述符地址。

組合語言指令ret把棧定儲存的返回地址裝入eip程式計數器。不過,__swtich_to()函式時通過簡單的跳轉被呼叫的。因此,ret彙編指令在棧中找到標號為1的指令地址,其中標號為1的地址是由switch_to()巨集推入堆疊的。

  1. far jmp指令既修改cs暫存器,也修改eip暫存器,而簡單的jmp之類值修改eip暫存器。 
    文章原帖地址:http://guojing.me/linux-kernel-architecture/posts/process-switch/