在支持多任務操作系統中,進程切換是不可避免的,以使進程能在單個CPU上併發執行。進程的調度涉及到的東西較多,例如調度的時機、調度的策略等等,在這裏我們只討論RTEMS任務調度中進程切換的細節,通過分析以明白操作系統如何做到使一個CPU的使用權如何從一個任務上切換到另一個任務。
下面假設兩個任務TASK1和TASK2,當前正在執行的任務executing = TASK1,需要切換到的任務 heir = TASK2,下面爲進程調度進行上下文切換的代碼(最精簡的一個函數,除去多核的配置、其他一些擴展函數、可配置的浮點上下文保存恢復等代碼):
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
void _Thread_Dispatch(void )
{ Thread_Control *executing; Thread_Control *heir; ISR_Level level; /* * Now determine if we need to perform a dispatch on the current CPU. */ executing = _Thread_Executing; _ISR_Disable( level ); while ( _Thread_Dispatch_necessary ==true ) { heir = _Thread_Heir; _Thread_Dispatch_necessary = false; _Thread_Executing = heir; /* * When the heir and executing are the same, then we are being * requested to do the post switch dispatching. This is normally * done to dispatch signals. */ if ( heir == executing ) goto post_switch; /* * Since heir and executing are not the same, we need to do a real * context switch. */ _ISR_Enable( level ); _Context_Switch( &executing->Registers, &heir->Registers ); executing = _Thread_Executing; _ISR_Disable( level ); } post_switch: _ISR_Enable( level ); } |
此函數執行之前,有兩個全局變量_Thread_Executing 和 _Heir_Executing 分別指向 Task1 和 Task2 的進程控制塊TCB,下面對此函數進行分析:
首先:切換之後全局變量 _Thread_Executing 應該指向 Task2(第15行);
其次,如果切換之前和切換之後是同一個任務,就無需進行上下文切換(第22行);
若不同,則必須進行上下文切換,使CPU的控制權轉到Task2上。所謂的上下文切換,就是保存Task1進程執行的上下文(主要是一些重要的寄存器),並且恢復Task2進程被切換出去之前執行的上下文。
進程控制塊TCB中有個字段Context_Control Registers 是用來保存/恢復上下文的,該結構體在i386體系下定義爲:
typedef struct
{
uint32_t eflags; /* extended flags register */
void *esp;
/* extended stack pointer register */
void *ebp;
/* extended base pointer register */
uint32_t ebx; /* extended bx register */
uint32_t esi; /* extended source index register */
uint32_t edi; /* extended destination index flags register */
} Context_Control;
也即意味着兩個進程進行切換時,需要保存的寄存器有EFLAGS、ESP、EBP、EBX、ESI、EDI等,第32行_Context_Switch 函數完成:切換之前將這些寄存器的值保存在需要切換的進程(此處爲Task1)TCB的Registers中,切換時這些寄存器的值從即將切換的進程(此處爲Task2)TCB的Registers中恢復。這是一個與體系結構相關的操作,需要使用匯編去完成,我們看看_Context_Switch( &executing->Registers,
&heir->Registers ) 的彙編實現:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
/*
* Format of i386 Register structure */ .set REG_EFLAGS, 0 .set REG_ESP, REG_EFLAGS + 4 .set REG_EBP, REG_ESP + 4 .set REG_EBX, REG_EBP + 4 .set REG_ESI, REG_EBX + 4 .set REG_EDI, REG_ESI + 4 .set SIZE_REGS, REG_EDI + 4 BEGIN_CODE /* * void _CPU_Context_switch( run_context, heir_context ) * * This routine performs a normal non-FP context. */ .p2align 1 PUBLIC (_CPU_Context_switch) .set RUNCONTEXT_ARG, 4 /* save context argument */ .set HEIRCONTEXT_ARG, 8 /* restore context argument */ SYM (_CPU_Context_switch): movl RUNCONTEXT_ARG(esp),eax /*eax = running threads context */ pushf /*push eflags */ popl REG_EFLAGS(eax) /* save eflags */ movl esp,REG_ESP(eax) /* save stack pointer */ movl ebp,REG_EBP(eax) /* save base pointer */ movl ebx,REG_EBX(eax) /* saveebx */ movl esi,REG_ESI(eax) /* save source register */ movl edi,REG_EDI(eax) /* save destination register */ movl HEIRCONTEXT_ARG(esp),eax /*eax = heir threads context */ restore: pushl REG_EFLAGS(eax) /*push eflags */ popf /* restore eflags */ movl REG_ESP(eax),esp /* restore stack pointer */ movl REG_EBP(eax),ebp /* restore base pointer */ movl REG_EBX(eax),ebx /* restoreebx */ movl REG_ESI(eax),esi /* restore source register */ movl REG_EDI(eax),edi /* restore destination register */ ret |
在進入此彙編代碼之前,Task1 棧爲:
解釋一下,C語言通過堆棧傳參慣例,參數從右到左依次壓棧,然後將下一條程序指針壓棧,因此在進入_CPU_Context_Switch函數之後Task1棧如上圖所示。函數體完成的功能:
第一步:將Task1上下文保存在Task1->Registers結構體中(第28~35行)。
28: 將Task1->Register的指針(esp+4)保存在eax寄存器中;
29:將eflags寄存器壓棧(pushl);
30:再出棧,存放在(&Task1->Register)->eflags中;
31~35:分別將esp、ebp、ebx、esi、edi寄存器的值保存在Task1->Register結構的相關字段中。
到此爲止,完成了保存Task1進程的上下文,Task1切換出去之前棧還是如上圖所示。
同理可以想象Task2被其他進程切換走之後一定也具有類似的棧結構,Task2->Registers中保存的是切換出去之前的它的上下文。
第二步:從Task2->Register結構中恢復Task2的上下文(第37~46行)。
37:將Task2->Registers的指針(esp+8)保存在eax寄存器中;
40:將Task2->Registers.eflags(即Task2切換出去之前保存的eflags寄存器)壓棧;
41:出棧popf,此時eflags寄存器恢復爲Task2上下文的eflags;
42:恢復esp,此步最爲關鍵,因爲此步之後esp寄存器將由Task1棧頂轉移到Task2棧頂,此後操作將在Task2的棧上進行;
43~46:依次恢復esp、ebp、ebx、esi、edi寄存器。
至此,完成了恢復Task2進程的上下文。
第三步:從_CPU_Context_Switch 函數返回。
47:ret操作,彈出棧頂到eip中,注意esp此時已經指向的是Task2的棧,但是上面我們說過Task2棧與Task1棧切換之前的結構是類似的,因此Task2棧頂保存的仍然是_Context_Switch 下一條語句的代碼。
從彙編函數中返回之後,又回到_Thread_Dispatch 函數體中,只不過與之前不同的是,此時處理器運行在Task2的上下文環境中。
還有一個問題需要我們去探索:如果Task2是第一次被調度執行,即Task2之前沒有被切換出去,不曾執行到_Thread_Dispatch 中的 _Context_Switch 切換出去,也就是Task2棧並不是我們上面討論的那樣,同樣我們也不希望Task2第一次被調度執行的第一條代碼不是_Context_Switch 的下一條語句,因爲Task2棧上並不存在_Thread_Dispatch函數的棧幀,如果這樣,肯定會出現不可預期的錯誤。
所以,我們在創建任務時,就需要先初始化好Task2的棧,使得其第一次調度時,執行的是我們需要它需要執行的代碼。
在rtems_task_start –> _Thread_Start –> _Thread_Load_environment –> _Context_initialize函數
宏 會在任務加入就緒隊列的時候進行上下文初始化。函數調用如下:
1
2 3 4 5 6 7 8 |
|
_Context_Initialize(
&the_thread->Registers, //任務上下文結構指針 the_thread->Start.Initial_stack.area, //指向新建的一個任務棧區域的基址 the_thread->Start.Initial_stack.size, //任務棧的大小 the_thread->Start.isr_level, //中斷級別 _Thread_Handler, //任務執行時第一次要執行的代碼 is_fp ); #define _Context_Initialize(_the_context, _stack, _size, _isr, _entry, _is_fp) \
_CPU_Context_Initialize( _the_context, _stack, _size, _isr, _entry, _is_fp ) |
我們看看這個函數 宏 實現的 彙編 代碼:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
#define _CPU_Context_Initialize( _the_context, _stack_base, _size, \
_isr, _entry_point, _is_fp ) \ do { \ uint32_t _stack; \ \ if ( (_isr) ) (_the_context)->eflags = CPU_EFLAGS_INTERRUPTS_OFF; \ else (_the_context)->eflags = CPU_EFLAGS_INTERRUPTS_ON; \ \ _stack = ((uint32_t)(_stack_base)) + (_size); \ _stack &= ~ (CPU_STACK_ALIGNMENT - 1); \ _stack -= 2*sizeof(proc_ptr*); \ *((proc_ptr *)(_stack)) = (_entry_point); \ (_the_context)->ebp = (void *)0; \ (_the_context)->esp = (void *) _stack; \ } while (0) |
第6&7行通過參數isr來決定應初始化elfags寄存器的值;
第9&10行根據棧基址以及棧的大小計算出初始時棧頂的位置(需要對齊);
第11行將棧頂的位置往下移兩個指針大小;
第12行將程序入口指針_entry_point寫入棧頂;
第13&14行分別初始化ebp、esp寄存器。
初始任務棧的示意圖如下:
至此,任務初始上下文初始化完成,主要初始化了eflags、esp、ebp等寄存器,保存在TCB的registers結構體中。
當一個從未執行的任務第一次被調度執行時,回到上下文切換函數_Context_Switch中,保存和恢復和上面一樣,只不過在第三步ret操作時,會返回棧頂位置的值當作程序指針,和上面區別的是:這種情形eip不是跳到_Context_Switch的下一句,而是我們初始化保存在棧頂的值_entry_point。返回之後,就會執行所_entry_point指向的函數_Thread_Handle,在這個函數會執行到我們創建的任務體中。
結束語:上面討論的是基於i386體系下RTEMS任務切換上下文的過程,雖然是基於特定的體系結構特定的操作系統,但對於任一個多任務的操作系統任務切換都大相徑庭,無非就是保存上下文、恢復上下文,而若移植操作系統,這是需要針對特定平臺進行改寫的一段代碼。