RTEMS 進程切換分析(基於i386體系)

在支持多任務操作系統中,進程切換是不可避免的,以使進程能在單個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任務切換上下文的過程,雖然是基於特定的體系結構特定的操作系統,但對於任一個多任務的操作系統任務切換都大相徑庭,無非就是保存上下文、恢復上下文,而若移植操作系統,這是需要針對特定平臺進行改寫的一段代碼。

發佈了73 篇原創文章 · 獲贊 60 · 訪問量 45萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章