uClinux移植與分析(3)

admin @ 2014-03-25 , reply:0

進程切換部分代碼實現

  移植linux,修改的主要就是和平台相關的那部分代碼.linux裡面和平台相關的代碼,包括很多方面,比如boot過程,系統調用,中斷處理,設備驅動,還有部分信號(軟中斷)處理等,進程切換也有很小一部分平台相關代碼.相對其它部分,我覺得這部分平台相關代碼還是相對簡單的.

  schedule()是uClinux中實現進程調度的函數.通過一定演算法,進行調度.假設有2各進程a,b,a運行時,調用了schedule(),那麼os就要從進程就緒隊列中挑選一個合適的進程,如果沒有合適進程,則後面繼續運行a,假設找到了合適進程b,則就要從當前進程a切換到b.這個切換過程是在switch_to()中進行的.

  switch_to()出現在schedule()函數裡面。調用形式switch_to(prev, next, last);prev,next都是進程式控制制塊task_struct的指針.prev指向當前運行的進程,next指向要切換的進程.

  講一下我移植的代碼.由於代碼是彙編程序,首先介紹一下cpu結構。我用的cpu採用16位指令,32位的地址和數據。有16個通用寄存器,記作r0-r15。r0作為堆棧指針寄存器sp,r1用途不固定,r2-r6作為參數傳遞寄存器,函數調用如果有不超過5個的參數,則參數從左至右依次放在r2-r6中。同時,r2還作為函數返回值寄存器,函數的返回值都放在r2裡面。r7-r14是局部變數寄存器。r15是函數返回地址寄存器,也叫link register,存放的是function call地返回地址。

  #define switch_to(prev,next,last) {           \
(1) register void *_prev __asm__ ("r2") = (prev);   \
(2) register void *_next __asm__ ("r3") = (next);   \
(3) register void *_last;                   \
(4)       __asm__ __volatile__(             \
(5)       "jbsr " SYMBOL_NAME_STR(resume) "\n\t" \
(6)       "mfcr %0, ss4"                 \
(7)         : "=r" (_last)               \
(8)         : "r" (_prev),               \
(9)           "r" (_next)                 \
(10)         : "r2", "r2", "r3");           \
(11) (last) = _last;                       \
  }
  switch_to()所做的工作其實相當於為調用resume做一些準備。(1)-(2)的意思是將變數_prev,_next分別放在寄存器r2,r3裡面,他們的值分別等於prev和next,就是兩個task_struct的指針。這麼做是為調用resume準備好參數。第三行是聲明一個寄存器臨時變數_last。

  第(5)行是調用resume函數實現進程切換。jbsr是一條跳轉指令,字面意思是跳入到子程序(jump to subroutine),這條指令做的工作是將現將當前pc+2保存到r15中(因為是16位指令,所以+2),相當於保存函數的返回值,然後再將pc設置成彙編指令參數中給出的地址(就是跳轉,這裡就是resume的地址)。

  第(6)行是將控制寄存器ss4內容放到_last對應的寄存器中。這一行指令有一些trick,先講指令所做的操作,再講為什麼這樣做。mfcr是從控制寄存器移動到通用寄存器的指令。cpu除了有16個通用寄存器,還有16各控制寄存器。所有涉及控制寄存器的操作都要在cpu的超級用戶模式下進行。cpu模式切換通過設置第0號控制寄存器來完成。16個控制寄存器分別為cr0-cr15,其中cr0也叫psr是程序狀態寄存器。cr6-cr10
也叫ss0-ss4是用於保存狀態的寄存器。第(6)代碼就是將ss4內容放入到變數_last所對應的寄存器中。

  (7)-(10)行的意義請參考AT&T彙編。

  (11)行是一個賦值,last=_last。

  switch_to()所做的工作其實相當於為調用resume做一些準備。(1)-(2)的意思是將變數_prev,_next分別放在寄存器r2,r3裡面,他們的值分別等於prev和next,就是兩個task_struct的指針。這麼做是為調用resume準備好參數。第三行是聲明一個寄存器臨時變數_last。

  第(5)行是調用resume函數實現進程切換。jbsr是一條跳轉指令,字面意思是跳入到子程序(jump to subroutine),這條指令做的工作是將現將當前pc+2保存到r15中(因為是16位指令,所以+2),相當於保存函數的返回值,然後再將pc設置成彙編指令參數中給出的地址(就是跳轉,這裡就是resume的地址)。

  第(6)行是將控制寄存器ss4內容放到_last對應的寄存器中。這一行指令有一些trick,先講指令所做的操作,再講為什麼這樣做。mfcr是從控制寄存器移動到通用寄存器的指令。cpu除了有16個通用寄存器,還有16各控制寄存器。所有涉及控制寄存器的操作都要在cpu的超級用戶模式下進行。cpu模式切換通過設置第0號控制寄存器來完成。16個控制寄存器分別為cr0-cr15,其中cr0也叫psr是程序狀態寄存器。cr6-cr10
也叫ss0-ss4是用於保存狀態的寄存器。第(6)代碼就是將ss4內容放入到變數_last所對應的寄存器中。

  (7)-(10)行的意義請參考AT&T彙編。

  (11)行是一個賦值,last=_last。

  其實,上面並不是一個非常優化的做法。完全可以省掉_last變數,不過當初我做時,看到m68k版本用了_last變數,而又不很清楚他的作用,為防止出錯,照辦了過來。其實經過後面分析,可知這個變數其實是冗餘的。

  那麼,為什麼要有(6)和(11)行的代碼呢?回頭可以看一下schedule()的代碼,在switch_to()調用過後,schedule()中調用了schedule_tail(prev)函數。顯然prev作為參數,應該放到r2裡面,所以就有了switch_to()代碼的第(11)行。那麼為什麼prev是來自ss4呢?

  在調用resume之前,prev存放在r2中。r2中的內容屬於進程的上下文,在做進程切換時,要存放在棧中。同時切換到另一個進程時,還要將另一個進程的上下文裝入到寄存器中。在裝入新進程時,r2的值就會被衝掉。舉個例子,比如你通過fork系統調用創建了一個新進程。我們知道,fork地返回值如果是0就表示子進程,大於0就是父進程。對於子進程,這個棧里r2就是0(前面說過,r2用作放函數返回值),如果此時schedule選了一個經fork后的子進程開始執行,則切換到該子進程后,其r2顯然為0,當然就不是prev了。所以,我的實現是在進程切換時,將r2值存放在ss4中,切換完畢后,再進行區別對待。如果是兩個已經運行過的進程切換,則返回就返回到原來switch_to的地方。如果是新的fork出來的進程,則第一次調用,在resume返回時,返回的是ret_from_fork,這是另外處理的。

  (11)行是一個賦值,last=_last。

  其實,上面並不是一個非常優化的做法。完全可以省掉_last變數,不過當初我做時,看到m68k版本用了_last變數,而又不很清楚他的作用,為防止出錯,照辦了過來。其實經過後面分析,可知這個變數其實是冗餘的。

  那麼,為什麼要有(6)和(11)行的代碼呢?回頭可以看一下schedule()的代碼,在switch_to()調用過後,schedule()中調用了schedule_tail(prev)函數。顯然prev作為參數,應該放到r2裡面,所以就有了switch_to()代碼的第(11)行。那麼為什麼prev是來自ss4呢?

  在調用resume之前,prev存放在r2中。r2中的內容屬於進程的上下文,在做進程切換時,要存放在棧中。同時切換到另一個進程時,還要將另一個進程的上下文裝入到寄存器中。在裝入新進程時,r2的值就會被衝掉。舉個例子,比如你通過fork系統調用創建了一個新進程。我們知道,fork地返回值如果是0就表示子進程,大於0就是父進程。對於子進程,這個棧里r2就是0(前面說過,r2用作放函數返回值),如果此時schedule選了一個經fork后的子進程開始執行,則切換到該子進程后,其r2顯然為0,當然就不是prev了。所以,我的實現是在進程切換時,將r2值存放在ss4中,切換完畢后,再進行區別對待。如果是兩個已經運行過的進程切換,則返回就返回到原來switch_to的地方。如果是新的fork出來的進程,則第一次調用,在resume返回時,返回的是ret_from_fork,這是另外處理的。

  上面說了這麼多,可能讀者還是糊裡糊塗的,我也覺得自己沒說清楚,所以這裡的這點實現有那麼一點點trick,需要對cpu的ABI和linux的內核代碼非常熟悉才行。
    (11)ldw   r7, (r0)         /* restore r7 */
    (12)ldw   r8, (r0, 4)       /* restore r8 */
    (13)addi   r0, 8
    (14)SAVE_SWITCH_STACK
    (15)lrw   r8, TASK_THREAD   /* the position of thread in task_struct */
    (16)addu   r8, r2
    (17)mfcr   r6, ss1           /* Get current usp */
    (18)stw   r6, (r8, THREAD_USP) /* Save usp in task struct */
    (19)stw   r0, (r8, THREAD_KSP) /* Save ksp in task struct */

    (20)lrw   r8, TASK_THREAD
    (21)lrw   r7, SYMBOL_NAME(_current_task)
    (22)stw   r3, (r7)         /* Set new task */
    (23)addu   r8, r3           /* Pointer to thread in task_struct */

    /* Set up next process to run */
    (24)ldw   r0, (r8, THREAD_KSP) /* Set next ksp */
    (25)ldw   r6, (r8, THREAD_USP) /* Set next usp */
    (26)mtcr   r6, ss1
    (27)ldw   r7, (r8, THREAD_SR)   /* Set next PSR */
    (28)mtcr   r7, psr
    (29)RESTORE_SWITCH_STACK
              ----------------
              |   r11     |
              ----------------
              |   r10     |
              ----------------
              |   r9     |
              ----------------
              |   r8     |
              ----------------
              |   r7     |
              ----------------
              |   r6     |
              ----------------
              |   r5     |
              ----------------
              |   r4     |
              ----------------
              |   r3     |
              ----------------
              |   r2     |
              ----------------
        0x1effC4 |   r1     |
              ----------------
0x1f0000和0x1effc4分別是執行過(14)前後r0的值。這是個contex save的操作。

註:lrw是立即數裝入操作,addu是無符號加法,mfcr和mtcr是控制寄存器移動 操作,bclri是位清除操作,ldw是load word操作,addi是立即數加法操作。

  (15)-(19)是做的棧指針保存操作。將當前進程用戶棧和內核棧保存到進程式控制制塊相應的數據結構中。linux下除了內核線程(只有內核棧)每個進程都有2個棧,一個在用戶空間一個在內核空間。如果是內核線程,則不用關心它的用戶堆棧,反正不會用到,是什麼值都可以。如果用戶進程,則在用戶進程執行系統調用或者在用戶進程執行時發生中斷時,都需要從用戶空間進入內核空間,在進入時,原先的用戶空間棧指針就會暫時存放到ss1中。所以(17)-(18)兩行就是從ss1中取出用戶空間棧指針,存入task_struct中。(15)-(19)的操作可以總結為:
  prev->thread.usp = ss1 保存用戶棧指針
  prev->thread.ksp = r0   保存內核棧指針

  那麼有人可能會問,ss1能夠保證就是正確的當前用戶棧指針么?當然可以因為內核線程沒有用戶棧,所以這個值是什麼無所謂。而對於用戶進程,進入resume的唯一入口就是schedule,而這又都是操作系統內核代碼。用戶進程進入內核手段就有系統調用和中斷,而在系統調用和中斷處理一進來就保存了用戶堆棧到ss1,所以在運行時,只要在內核里用的都是內核棧,用戶棧指針不會變。

  (20)-(23)執行的操作相當於_current_task = next。不再詳細解釋。

  (24)-(28)執行的是裝入新進程上下文的準備工作,也就是準備裝入next了。

(24)-(25)是裝入next進程的內核和用戶棧。因為進程的上下文都保存在該進程的內核棧裡面,所以第一步就是裝入該進程的棧指針。(27)-(28)是裝入next進程在切換前的狀態信息。(26)就是更新ss1,現在要裝入新進程了,當然就要設置新的用戶棧。

  (29)是裝入next進程的上下文。next進程在棧里有一個和上圖一樣的上下文,現在就要裝入。

  (30)是函數調用返回。如果這個進程是剛fork出來的子進程,則上下文裡面r15=ref_from_fork(可以參看copy_thread函數),否則就是返回到switch_to裡面第(6)句位置。

  上面就是進程切換的部分。這部分是和平台相關的。以上是我實現的代碼,感覺效率並不是非常高,但功能是正確的。可能有些地方我沒有講得很清楚,有什麼問題歡迎提出。




[admin via 研發互助社區 ] uClinux移植與分析(3)已經有1397次圍觀

http://cocdig.com/docs/show-post-42741.html