FreeRTOS有两种方法触发任务切换:
- 执行系统调用,比如普通任务可以使用taskYIELD()强制任务切换,中断服务程序中使用
portYIELD_FROM_ISR()
强制任务切换; - 系统节拍时钟中断
PendSV 中断
对于第一种任务切换方法,不管是使用taskYIELD()
还是portYIELD_FROM_ISR()
,最终都会执行宏portYIELD()
1 |
|
SysTick 中断, 系统时钟节拍
对于第二种任务切换方法,在系统节拍时钟中断服务函数中,首先会更新tick计数器的值、查看是否有任务解除阻塞,如果有任务解除阻塞的话,则使能PandSV中断
1 | // 上一章中调度器创建中讲过 xPortStartScheduler 会初始化timer |
xTaskIncrementTick
tick中断到的时候, 什么条件下要触发pendsv中断的问题:
1 | BaseType_t xTaskIncrementTick( void ) |
回到之前的问题, 什么条件下需要触发pendsv中断:
- 配置了
configUSE_PREEMPTION
和configUSE_TIME_SLICING
的情况, 和当前任务优先级相同的任务要进行时间片轮转. (不能设置任务初始的时间片?) - 只有配置了
configUSE_PREEMPTION
的情况下, 有任务到期了, 且这个任务的优先级大于等于当前正在运行的任务的优先级, 才会调度到这个任务, 不然就等着当前任务运行完, 切换到非就绪状态.- 那如果没有配置
configUSE_PREEMPTION
, 怎样完成任务切换呢, 只有任务自己调yield, 触发pendsv中断才可以切到其他任务上. (比如调delay函数, take函数等最后都可能调到yield, 触发pendsv中断)
- 那如果没有配置
- 配置了
configUSE_PREEMPTION
的情况下, 调度器被挂起的时候, 有pending的任务.
PendSV 中断处理流程分析
主要有三个点:
- 保存当前任务堆栈
- 切换任务 (vTaskSwitchContext)
- 切换到新任务后,将新任务的堆栈恢复
1
2
3// 上一章中调度器创建中讲过 xPortStartScheduler 会注册SW0的中断处理函数vPortYieldISR
/* Install the software yield handler */
pvPortInstallISR( SW0, vPortYieldISR );1
2
3
4
5
6
7
8
9
10vPortYieldISR
vPortYieldISR:
portYIELD_SAVE
jal vTaskSwitchContext
nop
portYIELD_RESTORE
.end vPortYieldISR
portYIELD_SAVE
1 | .macro portYIELD_SAVE |
这个函数除了status cause中断位相关的操作外, 主要的工作就是把当前运行的任务运行到此处时的寄存器信息和cp0 epc status等的信息保存到了当前任务的堆栈中, 并更新了当前任务TCB的pxTopofStack
cp0_epc什么时候被更新的,不要忘了, 这个是进的sw0的中断, 发生中断和异常一样, cp0_epc会被更新为进sw0 中断处理函数前的最后一行指令的位置
还是参考这个图
freertos 任务调度
vTaskSwitchContext
1 | void vTaskSwitchContext( void ) |
调度器未被挂起的情况下, 根据uxTopReadyPriority
从pxReadyTasksLists[ uxTopPriority ]
中找到最高优先级的任务, 赋值给**pxCurrentTCB**
这里关联到 prvAddTaskToReadyList
这个函数, 任务要转为就绪状态, 才能得到调度, 而要转到就绪状态,就需要调用**prvAddTaskToReadyList**
** **这个函数
这个函数做的事情:
- 更新 uxTopReadyPriority, 调用
taskRECORD_READY_PRIORITY
,( uxReadyPriorities ) |= ( 1**UL** << ( TCB->priority) )
, 通过位图的方式来表示最高优先级. - 将TCB 的
**xStateListItem**
状态列表项挂接到 pxReadyTasksLists[ TCB->priority ] 的 列表下
portYIELD_RESTORE
1 | .macro portYIELD_RESTORE |
这个函数的注意点
- 新任务的sp被更新成了pxTopofStack + 132的位置, 跟上面的流程图对应
- sp 是什么时候从system stack切回来的
- 当前任务运行着,突然来了pendsv中断,在进中断处理函数前, cp0_epc会变成进中断处理函数前最后一条指令的位置, 即函数运行到的位置, 这个cp0_epc被保存到了
TCB->pxTopofStack+124
的位置, 从别的任务切换回该任务时, 要把cp0_epc
恢复回来, 调用eret
回到任务切走前的指令位置. - eret 和 cp0_epc的指令冒险是否还有问题,
调度器的挂起和恢复
前面接触到了很多的调度器挂起/未挂起的情况, 有必要深究一下.
先看调度器挂起和恢复的函数做了什么
1 | void vTaskSuspendAll( void ) |
可以看到主要的流程都在xTaskResumeAll
中, 主要做了以下的事情:
- 处理
xPendingReadyList
, 在调度器挂起时,有可能任务从等待事件列表中移出了,(事件列表项, 等待的事件等到了,但这时调度器被挂起了, 因此移入了xPendingReadyList
中)
这种情况下,从等待事件列表中移出的任务被加入到了xPendingReadyList中. 一旦调度器恢复, 此时可以安全的将xPendingReadyList中的任务挪到ready list中, 进入就绪列表排队
如果移入ready list中的任务的优先级大于等于当前运行任务的优先级, 在配置了configUSE_PREEMPTION
的情况下,要触发pendsv中断切换任务
更新tick数, 同时模拟任务已经跑了
uxPendedTicks
个tick, 因为调度器挂起时, 不能因systick切换任务, 当前运行的任务会多跑uxPendedTicks
的tick, 为了准确计时, 要让这个任务把多跑的这些tick吐出来xPendingReadyList
中有任务时, 要重置xNextTaskUnblockTime, 因为调度器被挂起时, xNextTaskUnblockTime不再更新.在调度器被恢复后, 上次赋值的xNextTaskUnblockTime表示的任务可能已经过了timeout了, 所以需要重新赋值.
TODO: 需要关注下什么情况下任务会被加入到
xPendingReadyList
中, 看情况多数都是在调度器挂起时, 任务等待事件等到了, 被加入到的这个list?