任務環境切換新解(MIPS處理器)
來源:互聯網
上載者:User
在前一篇博文 即時作業系統核心的任務調度點裡總結了RTOS裡的任務調度時機,當作業系統核心決定要運行另一個任務的時候,它將會將當前任務的上下文環境,通常是指CPU寄存器,儲存到當期任務的堆棧上,並且恢複新任務的上下文環境使之繼續運行,這個過程就稱為環境切換。 上面這段話,幾乎在每一本講嵌入式軟體的教材、資料裡都會有,但是能再講深一點的卻不多。樓主在從事嵌入式行業的前N年裡也是一直停留在這句話的認識層面,但始終對這個神秘的過程非常好奇而不得要領。為什麼各大經典教材並不會去詳細描述該過程呢。樓主以為,原因有以下幾條: 1 環境切換是一個和作業系統核心緊密相關的過程,RTOS的廠家在銷售其作業系統核心時都已將這些代碼寫好調試通過,所以能真正瞭解到其編寫和調試過程的人並不多。而且因為這部分內容非常穩定,一旦成功運行起來,幾乎很少需要修改。不需要修改,誰會去看呢。 2 環境切換是一個處理器相關的過程,所謂的上下文一般指的也就是CPU寄存器,而每種處理器的寄存器各不相同,切換的動作也就互不相同,本文以MIPS為例進行講解。 3 嵌入式應用程式開發人員大多使用C語言進行開發,並不會關注到這些具體的切換過程,在他們的眼中,這是一個很自然的過程。而環境切換過程因為要直接操作CPU寄存器,所以必須使用彙編或者將彙編嵌入在C語言代碼裡來實現,這會讓很多人“望彙編生畏”,不知道這些寄存器move過來move過去的到底在做什麼。更麻煩的是,幾乎不能通過列印等普通調試手段來瞭解環境切換,只能通過硬體調試器(如JTAG)來單步跟蹤CPU指令,並逐步查看記憶體的變化,才有可能真正理解。
對於嵌入式架構師、系統軟體工程師來說,不應該有任何技術上的死角。樓主在這篇文章以一個常見的運行軌跡(如圖1所示),以情景分析的方式介紹多次任務環境切換的過程,希望對感興趣的看官有一點協助或者啟發。
圖1 從圖中可以看出,CPU啟動並執行過程為:高優先順序task-->低優先順序task-->中斷-->高優先順序task, (後面把高優先順序task都縮寫為HighTask,低優先順序task都縮寫為LowTask)。 假設系統裡只有2個優先順序不同的task,從某一時刻起,HighTask正在運行,它在運行了一段時間後,進入阻塞,將CPU讓給LowTask,這是第一次環境切換;LowTask在執行中被中斷打斷,這是第二次;中斷運行一段時間,並且會通過發送訊息或者訊號等形式觸發HighTask繼續運行,這是第三次;HighTask醒來運行一段時間,再次將CPU讓給LowTask,這是第四次。我們一個一個的來看。 每個task在建立的時候,系統核心裡都會為其建立一個堆棧(有的系統是應用自己申請堆棧空間),用來儲存函數棧幀和調用中的臨時變數等資料。第一次環境切換發生之前,假設HighTask在讓出CPU之前的函數調用過程為:FuncA-->FuncB-->XXX_WaitMsg-->XXX_ContextSwitch,它的堆棧分布情況如圖2所示:
圖2 這是一個向下增長的堆棧,從高地址往低走依次是FuncA、FuncB、XXX_WaitMsg、XXX_ContextSwitch的函數棧幀,HighTask是調用XXX_WaitMsg主動讓出CPU,這個函數裡會例行公事地把當期task從就緒態鏈表摘除,掛到阻塞態鏈表上,尋找下一個要啟動並執行任務(其實就是要找到它的任務控制塊結構以及裡面的SP指標,關於任務調度演算法請參考 ucosii即時作業系統的任務調度),再調用XXX_ContextSwitch來切換上下文,XXX_ContextSwitch的實現一般如下 { 移動當前任務的堆棧指標,以騰出一段記憶體 儲存當前任務的上下文 找到下一個要運行任務的堆棧指標 恢複下一個任務的上下文,其最後一個步驟是將儲存的PC值配給CPU寄存器 } 這裡需要引入兩個“原創概念”:完整上下文和部分上下文。完整上下文就是指CPU裡所有使用到的可變的寄存器內容;考慮一下這個case,HighTask最後是執行XXX_ContextSwitch函數,非常從容地讓出CPU,它知道被切換出去之前執行到的之後一條指令在哪裡,並也知道執行到這條指令這裡,有哪些寄存器的值是有意義的,這些就是需要儲存的用在被重新調度時以恢複現場的“部分上下文”。在樓主經曆過的一個MIPS處理器的切換,只需要儲存s0,s1,和ra3個寄存器,另外需要增加一個flag欄位,用來表明本次儲存的是部分還是完整的上下文,儲存完之後堆棧結構如圖3所示:
圖3 這一步做完後,有一個很重要的動作,就是要更新當前任務的堆棧指標到一個全域的資料結構,這個資料結構裡儲存了所有task的堆棧指標。有了這個資料結構,我們就能隨時擷取到任意一個task的堆棧指標,以恢複其運行現場。
HighTask的上下文儲存說完了,接著到了LowTask的上下文恢複,因為並不清楚LowTask的上下文是部分還是完整上下文,這一步我們跳過。LowTask開始執行,在運行一段時間後,系統來了一個中斷,因為中斷的發生是隨時的、不可預期的,所以這裡的上下文儲存是一個完整的上下文。假設LowTask的函數調用過程是FuncC-->FuncD,被打斷前,它的堆棧應該如圖4所示:
圖4 中斷到來後,MIPS CPU會將PC轉到一個固定地址的異常向量表去執行,它的流程如下: { 減小當前任務的堆棧指標,以騰出一段記憶體 儲存當前任務的完整上下文 記錄當前任務的堆棧指標到gp寄存器 將CPU的SP寄存器設為中斷堆棧地址(一般是一個全域變數) 執行中斷響應函數,俗稱Irq (這一步裡可能會觸發更HighTask,需要將gp寄存器的值修改為HighTask的sp) 從gp寄存器裡取出下一個要啟動並執行任務堆棧指標 恢複任務上下文,其最後一個步驟是將儲存的PC值配給CPU寄存器 } MIPS處理器需要儲存的完整上下文資訊幾乎包括了所有的CPU寄存器,如:at,v0,v1,a0~a3,t0~t9,s0~s8,ra,epc(exception PC,記錄了LowTask被中斷前即將執行的指令地址),以及乘法器的結果(用指令mflo和mfhi擷取),再加上表明本次是完整內容相關的flag。儲存完後LowTask的堆棧記憶體結構如圖5所示:
圖5 可以看出,完整上下文儲存和恢複比部分上下文明顯要耗時的多。 在異常處理的後半段,從gp寄存器裡取出的sp指標是HighTask的堆棧,首先需要判斷其flag,參考圖3,是部分上下文,然後將s0,s1從堆棧裡取出放入寄存器裡,再取出ra的值,執行下面的指令就可以回到HighTask睡眠前的現場繼續執行了(執行jr指令前,需要移動SP指標,指向XXX_ContextSwitch的棧幀)。 jr ra 好,現在HighTask繼續執行,一段時間後,又要重複以前的故事了,HighTask主動讓出CPU,做一次部分內容相關的儲存,並找到LowTask的SP,恢複其上下文。一樣的,先找到flag,LowTask的上下文是被中斷打斷時的完整上下文,再一個一個的將這些寄存器值從堆棧裡取出歸還到CPU寄存器裡,最後一步回到LowTask繼續執行的指令是 jr k0(k0寄存器儲存了epc,原因參考 MIPS的32個通用寄存器 )