大多数RTOS实现都提供了类似此一对函数功能的对应接口,需要开发者主动地分别在中断服务函数(ISR)的开始(tos_knl_irq_enter)及结束时调用(tos_knl_irq_leave)。
为什么要设计这样一对函数,并要求开发者在ISR中主动对其进行调用呢?一言以蔽之:在中断服务函数中有任务上下文切换的诉求。
为什么在中断服务函数中会产生任务上下文切换的诉求?因为中断中可能会进行信号量post之类的动作(这可能会触发此前一直处于pend状态的,且比当前被中断打断的任务优先级更高的任务进入ready状态),那么在出中断处理函数的时候,需要第一时间切换到更高优先级任务的上下文中。
简单走读一下tos_knl_irq_enter/tos_knl_irq_leave接口的内部实现:
__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一起调用 */
return;
}
--k_irq_nest_cnt; /* 嵌套标识-- */
if (knl_is_inirq()) { /* 还在中断嵌套中,无需做下面的上下文切换逻辑 */
return;
}
/* 已出中断嵌套,并有更高优先级任务已ready */
k_next_task = readyqueue_highest_ready_task_get();
/* 发起一次在中断中的上下文切换 */
cpu_irq_context_switch();
}
tos_knl_irq_enter接口(进入ISR时调用)将一个标识中断嵌套次数的变量++,tos_knl_irq_leave(出ISR时调用)里再将这个变量–,如果此变量为0(knl_is_inirq调用返回FALSE),表明当前CPU已出ISR嵌套,那么此时从readyqueue中挑选优先级最高的任务,并调用cpu_irq_context_switch触发一次在中断中的上下文调度。大多数cortex m核上的RTOS实现,在中断中触发上下文调度都是通过悬起一个PendSV中断来做的(PendSV的设计天生就是用来在m核的RTOS上做上下文调度的)。那么在tos_knl_irq_leave调用完毕并出ISR后,会立刻来一个PendSV异常,PendSV处理函数中CPU的上下文就会切换到优先级最高的ready任务里。
如果在ISR中确实进行了信号量post之类的动作并触发了某更高优先级任务进入ready状态,且在进出ISR时没有配合调用这一对函数,会导致在出ISR后此更高优先级任务并不会立刻被得到调度,只能靠程序运行到某种状态下时,在某个调用链下调用到了knl_sched函数触发一次调度,此高优先级任务才能被真正得以执行。换句话说,如果没有在ISR中调用这一对函数,会使得系统的实时性大打折扣。
大多数依赖系统systick中断驱动多任务切换的RTOS中,在systick中断处理函数的进出点上一定要调用这一对函数。在TencentOS tiny上,systick中断的内核层面处理逻辑是tos_systick_handler,此接口在每个systick到来时检测k_systick_list链表上被delay的任务是否等待时间已到(systick_update接口的逻辑),如果某任务等待时间已到,则会调用pend_task_wakeup接口唤醒此任务使得此任务进入ready状态。换句话说,systick中断处理函数中,也是有可能触发一个此前是pend状态的任务进入ready状态的,那么根据上文分析,在出systick中断处理函数时,也是需要调用tos_knl_irq_leave来使得此任务得到一次上下文切换的机会。
总结一下,对于可能会触发某些任务进入ready状态的中断处理函数(在ISR中进行了信号量post等动作,或者是systick中断),要保证系统的实时性,需要显式地在进出ISR时调用这一对函数。
在某些RTOS实现上(比如RTX),是没有这样一对接口存在的,它是怎么保证上文所述的实时性?
osStatus_t osSemaphoreRelease (osSemaphoreId_t semaphore_id) {
if (IsIrqMode() || IsIrqMasked()) {
isrRtxSemaphoreRelease(semaphore_id); /* 中断上下文中(handler模式)的信号量post动作 */
} else {
__svcSemaphoreRelease(semaphore_id); /* 任务上下文中(thread模式)的信号量post动作 */
}
}
上述是RTX信号量post接口的实现,在中断上下文中的信号量post动作,调用的是isrRtxSemaphoreRelease接口,此接口调用osRtxPostProcess进行真正的post动作,在isr_queue_put之后,直接调用SetPendSV接口触发了一次PendSV异常:
void osRtxPostProcess (os_object_t *object) {
if (isr_queue_put(object) != 0U) {
if (osRtxInfo.kernel.blocked == 0U) {
SetPendSV();
}
}
}
也就是说,在osSemaphoreRelease的中断上下文中的信号量post路径中,最终是调用到了SetPendSV来触发一次上下文调度。
在任务上下文中(thread模式下)的信号量post路径流程,这里涉及到RTX的SVC系统调用设计,后面会单独开一个主题来写,这里不展开讨论。总之结论是,在任务上下文中的信号量post流程,在__svcSemaphoreRelease的调用链上并不存在一个真正的、硬的上下文切换接口(RTX没有类似TencentOS tiny的cpu_context_switch接口,实际上RTX的任务上下文切换接口(osRtxThreadDispatch -> osRtxThreadSwitch)只是设置了一些内核标志而并没有进行真正的上下文切换动作),RTX的内核接口都被设计成系统调用。换句话说,用户程序想要获取内核服务,想要使得任何其他任务由pend状态进入ready状态(比如进行信号量post),一定是走的SVC_Handler入口,RTX的任务上下文中的上下文切换,实际上是在SVC_Handler入口中统一实现的。而systick中断的上下文切换处理,RTX也是统一在Systick_Handler中断处理函数中进行的(具体代码可以参考irq_cm3.S)。
总结一下,RTX的内核是分态的,在中断和任务上下文中进行信号量的post等动作根据上下文情况走的路径是不同的,在中断上下文中的post路径里,最终会触发PendSV来进行上下文切换;在任务上下文中的post路径里,因为走得是系统调用,统一是在SVC_Handler里做的上下文切换。其他RTOS的实现,对于FreeRTOS来说,直接是两套不同的接口,ISR(中断上下文)中调用的是形同sem_post_fromisr一类的接口;对于TencentOS tiny这一类来说,统一调用的都是sem_post,只不过在最后触发上下文切换的knl_sched接口中,判断是否是在中断上下文中,如果是在中断上下文中则直接返回不进行真正的上下文切换(如果是在任务上下文中,通过下面的cpu_context_switch接口进行上下文的切换),而是留待到tos_knl_irq_leave接口中通过cpu_irq_context_switch来切换。不同的设计会导致流程的不同。
cortex m核因为NVIC的设计天生支持中断嵌套,以及PendSV的特性天生可以用来在中断中实现上下文切换,用户在中断向量表中只需要填入中断服务函数入口地址,这给开发者带来了便利,不需要做中断上下文保存、切换等动作,只需要埋头实现好自己的中断处理函数;但是从另外一个方面来说,这种便利又产生了一定的反向依赖(指的是某些接口依赖用户去主动调用),产生了上文所述的,为了保证实时性需要用户自己在中断处理函数出入点上显式调用这对函数的诉求。同时我们也一窥了RTX的设计,从另一个角度设计实现来解决这个问题,当然这也引入了一定的移植性问题。
cortex a核的架构中,中断向量表不再只是一个一个ISR入口函数,而是IRQ处理函数的总入口,GIC也并非天生支持中断嵌套,如何简洁、优雅、高效地设计并实现中断嵌套及中断上下文切换,有很多可以说道和玩味的地方,这也是我最近在酝酿的系列文章《arm a核架构对RTOS设计与实现的影响》。
另外多说一句,TencentOS tiny目前已经基于正点原子的alpha开发板(nxp imx6ull芯片)添加了对cortex a7核心的支持,感兴趣的话可以去官方github上获取。