dispatch.s——MQX-Lite调度与中断系统的基础(1)

dispatch.s——MQX-Lite调度与中断系统的基础

MQX调度器的核心文件是dispatch.s,在这里先简单的预览一下这个文件的内容。
首先,这个文件是由汇编语言编写的。使用汇编语言是实现这些代码一方面是考虑了调度器的执行效率,毕竟在RTOS应用中最浪费的程序就是调度器(调度过程对应用没有任何贡献),但是汇编还有另一个优势,就是可以直接直接调用指令集中的命令,实现特权访问,这是C语言也办不到的,例如操作系统的栈指针,而这些特权操作又是调度器必然会用到的。所以,基于上述原因,调度器核心的内容由汇编语言实现是有充分理由的。
在分析代码之前,先对代码进行精简,替换或删除部分内容。原始的代码是从CodeWarrior的PEx系统中提取出来的,但继承自MQX,因此有对CodeWarrior、IAR、Keil使用各自编译器的兼容性设计。由于目前使用的IDE是IAR,因此将相关的兼容性代码全部去掉,还原成最直接、最简单的、基于IAR的原始汇编。在研究最核心的部分时,日志系统和用户模式也不是我们关注的重点,因此也要删掉。注释部分是目前仅有的参考,必然要保留,在分析的过程中也会尽量加一些新的注释。经过精简之后,剩下的内容主要就是上下文切换和系统中断服务程序的上半部了。在这里进行先介绍一下,具体的机制将在对各自系统的专门分析中整理出来。
dispatch.s文件中主要开放的函数接口如下:
    /* 调度服务相关 */
    PUBLIC _sched_start_internal
    PUBLIC _sched_run_internal
    PUBLIC _sched_check_scheduler_internal
    PUBLIC _sched_execute_scheduler_internal
    PUBLIC _task_block /* 直接提供的任务操作API */
    /* 中断服务相关 */
    PUBLIC _int_kernel_isr        /* 系统中断服务例程 */
    PUBLIC _psp_exception_return  /* 从中断服务例程反回到调度系统 */

1. 上下文切换

操作系统中调度系统的实现要考虑两个主要的部分,调度策略和上下文切换。调度策略是存算法的内容,解决如何选出最合适的任务执行并安置其它的任务,而上下文切换负责执行任务切换,是操作系统实现任务调度的最基础的部分。

在上下文切换执行前,要由调度策略部分选出最合适执行的任务,然后进入上下文切换过程。在上下文切换过程中,先保存上文,即把当前运行任务的环境状态(各种能够描述任务运行的状态信息)保存到任务自己任务栈中,然后盖上被子就睡觉了,即保存上文。接下来,调度器接手由调度策略找出来的MrRight,从它的任务栈中恢复他临睡前的状态,然后放手让他自己继续跑,即恢复下文。有一种特殊的情况还要考虑进来,就是中断。当中断打断了重在运行的任务,处理器会自动将正在运行任务的上文保存入栈,当从中断返回是,处理器还会自动恢复任务的运行环境这是由硬件决定的,软件无法干预,只能利用。

在完整版的MQX中,上下文切换是借助了Cortex内核的SVC和PendSV中断实现的,将上下文切换安排到中断服务程序都执行完毕之后。若是目前有中断服务在处理,则将上下文切换的过程放到所有中断服务程序之后进行。其实这是一个比较麻烦的处理方式,我们可以设想,只有在中断服务函数的执行过程中触发了上下文切换才能出现这种情况,而这种情况是几乎不会发生,但是要注意有一个例外,就是时间片轮询调度的实现。在实现轮询调度时,在系统滴答的计数中断服务程序中监测时间片使用情况,当某个任务的时间片耗尽时,触发调度执行上下文切换。这个时候上下文切换不会立即执行,而是等到滴答计数中断服务程序执行完毕退出时,才开始上下文中断的过程。此时,上下文切换实际上是PensSV中断的服务程序,也就是优先级最低,执行顺序最靠后的中断服务程序。

而在MQX-Lite中,简化了这种设计,上下文切换过程并未使用SVC和PendSV中断,而是直接开始保存上下文。在启动调度时,首先检测当前是否在中断服务中(kernel_data的in_isr计数,#KD_IN_ISR),这是跟MQX相同的,但不同的是,当发现目前有中断服务程序在执行,即在中断中触发了任务调度,MQX-Lite选择了直接退出,而不是向MQX一样延后执行。这样的话,上下文切换对Cortex内核特性的依赖就减少了,同时,实现上下文的复杂性大大降低,调度效率也得到了保证,系统更便于移植到非Cortex架构的处理器上。限制也是显而易见的,中断服务程序有了特殊的要求——不能触发调度功能,事实上触发了也没有用,并且完全被忽略。直接的后果就是,不能向在MQX中那样实现时间片轮询了。这里不需要沮丧,姑且不说在RTOS中时间片轮询的作用有多大,在实际设计应用时,在application的层面上实现时间片轮询也是非常容易的。也就是说,MQX-Lite的简化设计还是利大于弊的。

从函数实现的细节上分析,_sched_start_internal函数的实现和_sched_run_internal完全相同,再也不是MQX里那种的貌似相同,这就更直接地阐释了“start”和“run”的差异,其实是一样的,就是啥也不说,直接启动调度器开始上下文切换。如此暴力的做法限制的他们的使用范围,一般只有在调度系统最初的启动或是block当前任务后前途渺茫时,才开始调用。事实上,在调度内核需要这么做的时候,会同这两个函数内部实现一样,直接调用“bsched_internal”语句,这两个函数只是提供给外部的API。

_sched_check_scheduler_internal对启动调度的处理方式没有上面两个函数那样暴力,在该函数中,将有重重检测:第一关,首先会查看当前是否有中断服务函数在执行,正如前面描述的那样,当有中断时,MQX-Lite选择了直接返回调用者。在没有中断服务函数执行时,开始第二关,再查看当前运行的任务是不是最合适的任务,如果是,则不进行切换,然后也是返回给调用者,注意,通过第一关的检测之后,这里的调用者就只能是当前正在运行的任务,在当前运行吃了某些东西把可能把自己卡住的时候,系统发现它还是最合适的那个任务,那么它就得继续跑。在第一关和第二关落选的任务是真的返回,用的命令是“bxlr”。经过了这两关的检测之后,调用“b_sched_execute_scheduler_internal”,那真是肉包子打狗一去不返了。

_sched_check_scheduler_internal:
                GET_KERNEL_DATA r0
 
                /* 查看当前是否有ISR在执行 */
                ldrh r1, [r0,#KD_IN_ISR]
                cmp r1, #0
                bne_sched_check_scheduler_internal_end  /* 若有,则直接跳出,不执行调度*/
 
                /* 准备启动调度前查看即将激活的任务是不是当前任务*/
                ldr r1, [r0,#KD_CURRENT_READY_Q]
                ldr r2, [r0,#KD_ACTIVE_PTR]
                ldr r3, [r2,#TD_MY_QUEUE]
                cmp r1, r3
 
                /* current taskis still the active task */
                beq_sched_check_scheduler_internal_end
 
                b_sched_execute_scheduler_internal /* 直接执行调度,若执行到这里不会从本函数返回 */
 
_sched_check_scheduler_internal_end:
               bx lr


前面是check,到_sched_execute_scheduler_internal就开始execute了。

首先是保存上文,这里用了一个“STORE_ALL_REGISTERS”的宏函数(psp_prv.inc),在其中保存寄存器的顺序是要与PSP_BLOCKED_STACK_STRUCT结构体相匹配的,实际上就是填充这个结构体,而这个结构体的设计依据则要参考保存于恢复中断上下文的压栈顺序。

然后是关总中断,上下文切换的过程中是不允许有任何打断的。然后就是久违的“sched_internal”了,在_sched_start_internal和_sched_run_internal中也是直接跳转到这里的,因此可以看出,sched_internal实际上不经过检测、不需要保存上文,从当前就绪任务队列向后开始寻找就绪任务然后恢复下文执行新任务的。

正如我们之前预测的那样,sched_internal的执行就比较老套了,先是在find_noempty_que部分在就绪任务队列表中查找非空的就绪任务队列,如果都是空的(连保底的idel任务都没有),那么将打开中断,使用“wfi”命令进入休眠模式,等待外部事件唤醒后关闭中断再进行find_noempty_que的寻找。

找到非空就绪任务队列之后,其对手的任务就是最合适的任务(就绪任务队列本身是以FIFO的原则排队的),然后就是switch_task。

switch_task在MQX时代可是真正的明星,在MQX-Lite中它依然星光耀眼。首先是恢复系统的全局状态寄存器SR,这玩意是专门保存起来的,没有被push到任务栈中,然后掀开被子从新任务的任务描述符中提取出任务栈,开始枯燥无味的从栈中取出临睡前藏在被窝的小果子们,依照顺序摆到房间的指定位置。这里有个比较重要的考虑,switch_task会在中断返回过程和手动触发的调度过程调用,若是从中断返回,则要转到return_from_interrupt分支上,利用(不要这么强势,更准确地说是顺从)中断返回的自动还原现场机制,有一些寄存器是要留给中断返回机制自动完成的。不要把中断返回过程想得太单纯,如果一个正在运行的任务被中断打断,等中断执行完回来的时候,返回的可不一定就是最开始被打断的那个任务了。这里面有个小把戏,就是通过在中断返回的时候人为地修改返回PC的值,从而控制中断返回后的执行内容。由处理器内核强行接入的中断返回机制虽然强硬,但还是被我们玩弄于股掌之中。这里只是简单说一下,到研究中断处理过程的时候再详细说明。

跳过中断返回这一段,在rest_of_stack_frame_restore分支上执行手动还原现场时,也是执行跟中断返回类似的过程,在move_pc_at_the_start_of_stackframe中将新任务的PC值放到栈首,然后通过pop更新到PC寄存器中,据说是因为PC寄存器专好这个调调,只能通过退栈才能更新值。

至此,上下文切换的执行流程就分析完了,以后整理材料的时候可以考虑画个流程图,有个更直观的认识。

然而,除此之外,还有一个特殊的情况需要考虑,那就是阻塞任务,前面的上下文切换把前一个任务忽悠走,新一个任务忽悠来,但还都没出了就绪任务队列表。当阻塞当前任务时,那可就把一个好端端的任务忽悠出就绪任务列表啦。

在_task_block过程中,也是先调用STORE_ALL_REGISTERS宏,保存上文,盖上被子,然后把被子圈起来,在任务的“STATE(#TD_STATE)”字段中盖个章说是被阻塞了,然后一脚踹出就绪任务队列表,之后立马转向sched_internal开始找新的XX。NND,_task_block真是个薄情XX&OO!!!

_task_block:
                // store active task registers
                STORE_ALL_REGISTERS
                //disable interrupts
                cpsid i

                GET_KERNEL_DATA r0
                ldr r3, [r0, #KD_ACTIVE_PTR]        // get active task descriptor
                str r1, [r3, #TD_STACK_PTR]         // store task SP to task descriptor td

                // set task block bit
                ldr r2, [r3, #TD_STATE]
                movs r1, #1                        // TODO change from number to define
                orrs r2, r2, r1
                str r2, [r3, #TD_STATE]
                
                // remove active task from ready queue
                /* 这个狠,将任务保存后将任务从就绪任务队列中移除 */
                ldr r2, [r3, #TD_TD_PREV]           // get ptr to ready_q structure
                ldr r1, [r3, #TD_TD_NEXT]
                str r1, [r2, #RQ_HEAD_READY_Q]
                str r2, [r1, #TD_TD_PREV]

                /* 此时从当前任务就绪队列开始向后找新的就绪任务 */
                b sched_internal   // Search for the next task in the ready queue.

写到这里先告一段落,做个小总结。这里理出来的是调度内核中上下文切换实现的主线,在实际的调用中还要考虑程序调用的上下文,比如调用的时机,参数的传递,尤其是汇编函数和C函数的调用特性(主要是C程序调汇编函数)等,这些在跟踪调度的执行细节中都要再进行细致、明确地分析。在操作系统中,中断与调度联系也是比较紧密的,MQX-Lite中断的核心——中断Socket也在dispatch文件中,请听下回分解。

(今天网页的排版真是让人崩溃。。。~^~)


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值