what
大多數(shù)RTOS實現(xiàn)都提供了類似此一對函數(shù)功能的對應接口,需要開發(fā)者主動地分別在中斷服務函數(shù)(ISR)的開始(tos_knl_irq_enter)及結(jié)束時調(diào)用(tos_knl_irq_leave)。
why
為什么要設計這樣一對函數(shù),并要求開發(fā)者在ISR中主動對其進行調(diào)用呢?一言以蔽之:在中斷服務函數(shù)中有任務上下文切換的訴求。
為什么在中斷服務函數(shù)中會產(chǎn)生任務上下文切換的訴求?因為中斷中可能會進行信號量post之類的動作(這可能會觸發(fā)此前一直處于pend狀態(tài)的,且比當前被中斷打斷的任務優(yōu)先級更高的任務進入ready狀態(tài)),那么在出中斷處理函數(shù)的時候,需要第一時間切換到更高優(yōu)先級任務的上下文中。
簡單走讀一下tos_knl_irq_enter/tos_knl_irq_leave接口的內(nèi)部實現(xiàn):
__API__ void tos_knl_irq_enter(void)
{
? ? ++k_irq_nest_cnt; /* 嵌套標識++ */
}
?
__API__ void tos_knl_irq_leave(void)
{
?? if (!knl_is_inirq()) { /* 上下文并不在中斷里,或者此接口沒有配合tos_knl_irq_enter一起調(diào)用 */
? ? ?? return;
?? }
?
?? --k_irq_nest_cnt; /* 嵌套標識-- */
?
?? if (knl_is_inirq()) { /* 還在中斷嵌套中,無需做下面的上下文切換邏輯 */
? ? ?? return;
?? }
?
?? if (knl_is_sched_locked()) { /* 調(diào)度鎖已鎖,無需再做下面的調(diào)度邏輯 */
? ? ?? return;
?? }
?
? ? /* 已出中斷嵌套,并有更高優(yōu)先級任務已ready */
?? k_next_task = readyqueue_highest_ready_task_get();
? ? /* 發(fā)起一次在中斷中的上下文切換 */
?? cpu_irq_context_switch();
}
tos_knl_irq_enter接口(進入ISR時調(diào)用)將一個標識中斷嵌套次數(shù)的變量++,tos_knl_irq_leave(出ISR時調(diào)用)里再將這個變量--,如果此變量為0(knl_is_inirq調(diào)用返回FALSE),表明當前CPU已出ISR嵌套,那么此時從readyqueue中挑選優(yōu)先級最高的任務,并調(diào)用cpu_irq_context_switch觸發(fā)一次在中斷中的上下文調(diào)度。大多數(shù)cortex m核上的RTOS實現(xiàn),在中斷中觸發(fā)上下文調(diào)度都是通過懸起一個PendSV中斷來做的(PendSV的設計天生就是用來在m核的RTOS上做上下文調(diào)度的)。那么在tos_knl_irq_leave調(diào)用完畢并出ISR后,會立刻來一個PendSV異常,PendSV處理函數(shù)中CPU的上下文就會切換到優(yōu)先級最高的ready任務里。
如果在ISR中確實進行了信號量post之類的動作并觸發(fā)了某更高優(yōu)先級任務進入ready狀態(tài),且在進出ISR時沒有配合調(diào)用這一對函數(shù),會導致在出ISR后此更高優(yōu)先級任務并不會立刻被得到調(diào)度,只能靠程序運行到某種狀態(tài)下時,在某個調(diào)用鏈下調(diào)用到了knl_sched函數(shù)觸發(fā)一次調(diào)度,此高優(yōu)先級任務才能被真正得以執(zhí)行。換句話說,如果沒有在ISR中調(diào)用這一對函數(shù),會使得系統(tǒng)的實時性大打折扣。
大多數(shù)依賴系統(tǒng)systick中斷驅(qū)動多任務切換的RTOS中,在systick中斷處理函數(shù)的進出點上一定要調(diào)用這一對函數(shù)。在TencentOS tiny上,systick中斷的內(nèi)核層面處理邏輯是tos_systick_handler,此接口在每個systick到來時檢測k_systick_list鏈表上被delay的任務是否等待時間已到(systick_update接口的邏輯),如果某任務等待時間已到,則會調(diào)用pend_task_wakeup接口喚醒此任務使得此任務進入ready狀態(tài)。換句話說,systick中斷處理函數(shù)中,也是有可能觸發(fā)一個此前是pend狀態(tài)的任務進入ready狀態(tài)的,那么根據(jù)上文分析,在出systick中斷處理函數(shù)時,也是需要調(diào)用tos_knl_irq_leave來使得此任務得到一次上下文切換的機會。
總結(jié)一下,對于可能會觸發(fā)某些任務進入ready狀態(tài)的中斷處理函數(shù)(在ISR中進行了信號量post等動作,或者是systick中斷),要保證系統(tǒng)的實時性,需要顯式地在進出ISR時調(diào)用這一對函數(shù)。
why not
在某些RTOS實現(xiàn)上(比如RTX),是沒有這樣一對接口存在的,它是怎么保證上文所述的實時性?
osStatus_t osSemaphoreRelease (osSemaphoreId_t semaphore_id) {
? ? if (IsIrqMode() || IsIrqMasked()) {
? ? ? ? isrRtxSemaphoreRelease(semaphore_id); /* 中斷上下文中(handler模式)的信號量post動作 */
? ? } else {
? ? ? ? __svcSemaphoreRelease(semaphore_id); /* 任務上下文中(thread模式)的信號量post動作 */
? ? }
}
上述是RTX信號量post接口的實現(xiàn),在中斷上下文中的信號量post動作,調(diào)用的是isrRtxSemaphoreRelease接口,此接口調(diào)用osRtxPostProcess進行真正的post動作,在isr_queue_put之后,直接調(diào)用SetPendSV接口觸發(fā)了一次PendSV異常:
void osRtxPostProcess (os_object_t *object) {
?? if (isr_queue_put(object) != 0U) {
? ? ?? if (osRtxInfo.kernel.blocked == 0U) {
? ? SetPendSV();
? ? ?? }
? ? }
}
也就是說,在osSemaphoreRelease的中斷上下文中的信號量post路徑中,最終是調(diào)用到了SetPendSV來觸發(fā)一次上下文調(diào)度。
在任務上下文中(thread模式下)的信號量post路徑流程,這里涉及到RTX的SVC系統(tǒng)調(diào)用設計,后面會單獨開一個主題來寫,這里不展開討論。總之結(jié)論是,在任務上下文中的信號量post流程,在__svcSemaphoreRelease的調(diào)用鏈上并不存在一個真正的、硬的上下文切換接口(RTX沒有類似TencentOS tiny的cpu_context_switch接口,實際上RTX的任務上下文切換接口(osRtxThreadDispatch -> osRtxThreadSwitch)只是設置了一些內(nèi)核標志而并沒有進行真正的上下文切換動作),RTX的內(nèi)核接口都被設計成系統(tǒng)調(diào)用。換句話說,用戶程序想要獲取內(nèi)核服務,想要使得任何其他任務由pend狀態(tài)進入ready狀態(tài)(比如進行信號量post),一定是走的SVC_Handler入口,RTX的任務上下文中的上下文切換,實際上是在SVC_Handler入口中統(tǒng)一實現(xiàn)的。而systick中斷的上下文切換處理,RTX也是統(tǒng)一在Systick_Handler中斷處理函數(shù)中進行的(具體代碼可以參考irq_cm3.S)。
總結(jié)一下,RTX的內(nèi)核是分態(tài)的,在中斷和任務上下文中進行信號量的post等動作根據(jù)上下文情況走的路徑是不同的,在中斷上下文中的post路徑里,最終會觸發(fā)PendSV來進行上下文切換;在任務上下文中的post路徑里,因為走得是系統(tǒng)調(diào)用,統(tǒng)一是在SVC_Handler里做的上下文切換。其他RTOS的實現(xiàn),對于FreeRTOS來說,直接是兩套不同的接口,ISR(中斷上下文)中調(diào)用的是形同sem_post_fromisr一類的接口;對于TencentOS tiny這一類來說,統(tǒng)一調(diào)用的都是sem_post,只不過在最后觸發(fā)上下文切換的knl_sched接口中,判斷是否是在中斷上下文中,如果是在中斷上下文中則直接返回不進行真正的上下文切換(如果是在任務上下文中,通過下面的cpu_context_switch接口進行上下文的切換),而是留待到tos_knl_irq_leave接口中通過cpu_irq_context_switch來切換。不同的設計會導致流程的不同。
what's more
cortex m核因為NVIC的設計天生支持中斷嵌套,以及PendSV的特性天生可以用來在中斷中實現(xiàn)上下文切換,用戶在中斷向量表中只需要填入中斷服務函數(shù)入口地址,這給開發(fā)者帶來了便利,不需要做中斷上下文保存、切換等動作,只需要埋頭實現(xiàn)好自己的中斷處理函數(shù);但是從另外一個方面來說,這種便利又產(chǎn)生了一定的反向依賴(指的是某些接口依賴用戶去主動調(diào)用),產(chǎn)生了上文所述的,為了保證實時性需要用戶自己在中斷處理函數(shù)出入點上顯式調(diào)用這對函數(shù)的訴求。同時我們也一窺了RTX的設計,從另一個角度設計實現(xiàn)來解決這個問題,當然這也引入了一定的移植性問題。
cortex a核的架構(gòu)中,中斷向量表不再只是一個一個ISR入口函數(shù),而是IRQ處理函數(shù)的總?cè)肟冢珿IC也并非天生支持中斷嵌套,如何簡潔、優(yōu)雅、高效地設計并實現(xiàn)中斷嵌套及中斷上下文切換,有很多可以說道和玩味的地方,這也是我最近在醞釀的系列文章《arm a核架構(gòu)對RTOS設計與實現(xiàn)的影響》,敬請期待。
另外多說一句,TencentOS tiny目前已經(jīng)基于正點原子的alpha開發(fā)板(nxp imx6ull芯片)添加了對cortex a7核心的支持,感興趣的話可以去官方github上獲取。