最近在学习实时操作系统(RTOS),故将所学知识罗列出来,以供日后参考。
1.任务的本质
任务-说白点,即是一个永远不会返回的函数。
2.任务切换的本质
即是保存当前运行任务的状态,然后再恢复出下一个要运行的任务的状态。任务状态包括任务自己的栈,堆,数据区,代码区,内核寄存器的值。数据区和代码区由编译器自动分配,故不需要关心。堆目前并未使用,故也不需关心,那么唯一要关心的就是栈和内核寄存器了。而每一个任务我们都会分配给它一个栈空间用于保存自己的状态。
3.任务切换的实现
cortex-m3是通过触发pensv中断来进行任务切换的。故我们可以通过设置NVIC相关寄存器的值触发pendsv中断来进行任务切换。
那么问题来了,第一,我们如何切换进第一个要运行的任务?第二,如何实现两个任务之间的切换?
答案很简单,像是通过设置标志位一样,我们在初始化任务完成后,通过设置psp堆栈指针为0代表着这是第一次进入任务切换的中断函数(pendsv_handler),直接恢复该任务的堆栈值到相关寄存器中,等跳出中断后即进入了第一个要运行的任务;当再次进入任务切换的中断函数时,此时psp不为0,故应知当前进入中断是想要切换任务,则把当前任务的运行状态保存起来,然后再恢复下一个要运行的任务的状态即可。
具体实现代码如下:
__asm void PendSV_Handler ()
{
IMPORT currentTask // 使用import导入C文件中声明的全局变量
IMPORT nextTask
MRS R0, PSP // 获取当前任务的堆栈指针
CBZ R0, PendSVHandler_nosave // 判断psp是否为0,如果为0,则跳转。
STMDB R0!, {R4-R11} // 如果不为0,则先保存当前任务状态
LDR R1, =currentTask
LDR R1, [R1]
STR R0, [R1] // 重置任务栈的栈顶
PendSVHandler_nosave
LDR R0, =currentTask
LDR R1, =nextTask
LDR R1, [R1]
STR R1, [R0] // 交换指针值
LDR R0, [R1] //加载该任务的栈顶指针,用于恢复出任务状态
LDMIA R0!, {R4-R11} // 恢复{R4, R11},其余硬件自动恢复
MSR PSP, R0 // 最后,恢复真正的堆栈指针到PSP
ORR LR, LR, #0x04 // 切换到PSP,使用用户级堆栈
BX LR // 恢复到上次运行停止的位置
}
4 双任务时间片运行原理
可以通过定时器定时触发pendsv中断,实现任务的切换。对于cotex-m3芯片,一般使用内核定时器systick_handler
5 任务的延时原理与空闲任务
由于硬件定时器资源有限,而任务的数量可能很多,所以一般无法给每一个任务都配置一个硬件定时器。所以一般使用软件定时器,即在任务的结构中添加一个变量,表示该任务当前需要延时的周期数。代码如下:
typedef struct _task
{
uint32_t *stack; // 指向任务栈的栈顶指针
uint32_t delayTicks; // 任务延时计数器
}Task_t;
但是这种软延时的方法有一个明显的缺陷,便是延时精度可能并不是特别准确,具体情况见下图:
另外,当所有任务都处于延时状态时,CPU应该做什么呢?正确的做法是提供一个空闲任务。
6 临界区保护
临界区指的是一个访问共享资源的程序片段;言而简之,之所以设置临界区保护,是为了防止读-改-写过程被打断,从而导致写回变量时,覆盖了打断过程对变量的改写。具体的实现方法便是关中断,其一,中断关闭后,任务不可能被中断打断,导致共享变量的改写被覆盖;其二,关闭中断后,即关闭了任务切换,所以即便是多任务共享的资源,也只能由当前任务来访问了。
唯一的问题在于当有多层嵌套临界区时,第二层的临界区的开中断操作会使前一层的临界区保护失败,故正确的解决办法是:每次关闭中断前,先保存当前中断的开关状态,退出临界区时,再恢复进入临界区时的中断状态,即可完美解决。
具体实现代码如下:
uint32_t tTaskEnterCritical (void)
{
uint32_t primask = __get_PRIMASK();
__disable_irq(); // CPSID I
return primask;
}
void tTaskExitCritical (uint32_t status)
{
__set_PRIMASK(status);
}
7调度锁保护
设置一个变量实现调度保护,当该值大于0时,表示有任务请求禁止调度。
8位图
位图是一组连续的标志位,每一位用于标识一种状态的有无。
9多优先级任务
RTOS维护一个就绪表,每个表项 对应一个任务,对应一种优先级,就绪表指明哪些优先级的任务等待占用CPU运行,说白了其实就是通过将相应优先级对应的位图位置1来实现。
10 任务的延时队列
将所有需要延时的任务放入延时队列中,当发生定时中断时,去扫描该延时队列,依次对队列中各任务的延时时间-1.如果有减到0的,将其从延时队列中移除.
11 同优先级时间片运行
将原先位图一个位对应一个优先级的任务改为对应一个任务队列,并在每个任务中加入自己的时间片变量,用于实现同一优先级对应的多个任务按照时间片方式进行运行.具体实现如下图:
12任务的挂起
在任务的结构中增加一个挂起计数器,仅当计数器值大于0且该任务不处在延时状态时,才对任务进行挂起,如果该任务是当前正在运行的任务,则要对任务进行切换.挂起方式很简单,就是将其从就绪队列中移除.
13任务的删除
一,将任务从其所在延时队列,就绪队列中删除.
二,释放该任务所占用的资源.
设计一种机制,当发现任务要被删除时,释放掉该任务所占用的资源,包括内存空间,硬件设备等.
删除方式1,设置任务删除回调函数,由该函数释放.
删除方式2,设置删除请求标志,由任务自己决定何时删除.
网友评论