什么是任务抢占?
实时操作系统大多都是基于优先级调度的抢占式的内核,这句话每本关于RTOS的资料都有。楼主最怕这种每个字都认识,但连到一起就很不好理解的书面语言。直白点说,就是:
1 每个任务都有自己的优先级,一般都是在create_task的时候以函数入参的形式确定,有的rtos也提供API允许应用程序在task运行起来后,动态修改其优先级(
XX_Create_Task,XX_
Change_Priority
)
2 任务一般分为几种状态:就绪、执行、阻塞、挂起等等。为方便理解,就认为是两种情况,可执行和不可执行。
那task什么时候不可执行呢?例如task自己调用了suspend函数把自己挂起;或者本文用到的任务阻塞在一个消息队列上等待消息,而此时消息队列里没有内容,这种情况下任务就会处于不可执行状态
3 如果有处于可执行状态的高优先级task,那么低优先级的task只能等待到它运行结束或者转为不可执行的状态,才有可能获得CPU
4 也是最有意思的地方,高优先级的task一旦获得的可继续执行的资源,就会立刻打断低优先级任务的执行。
不过这是前提的,就是该低优先级是“允许抢占的”。
任务抢占的演示
本文以几个简单的实验来引入这些概念,并清楚的演示什么是“抢占式”内核,使用的RTOS是Nucleus。
先介绍本文的实验模型:
在main函数(系统启动后的第一个任务)里创建两个task,task_send和task_receive,和一个消息队列test_queue用于这两个task之间的通信
NU_Create_Queue(&test_queue,
"test_q",
queue_Buf,
QUEUE_SIZE,
NU_FIXED_SIZE,
1, NU_FIFO);
NU_Create_Task(&sendTask,
"",
task_send,
0,
NU_NULL,
Task_Stack,
Stack_Size,
51,
0,
NU_PREEMPT, NU_START);
NU_Create_Task(&receiveTask, "" ,task_receive, 0, NU_NULL, Task_Stack, Stack_Size, 50, 0,NU_PREEMPT, NU_START);
请特别注意标注颜色的参数。
红色参数表示为任务的优先级,数字越小代表优先级越高。这里 task_send是51,task_receive是50,那么task_receive的优先级较高
绿色参数表明了该任务是否允许被抢占,NU_PREEMPT表示允许,NU_NO_PREEMPT相反
这两个task的执行函数分别如下:
void task_receive(UNSIGNED argc, VOID *argv)
{
unsigned int *pCnt;
unsigned int len;
while (1)
{
NU_Receive_From_Queue(&test_queue, &pCnt, 1, &len, NU_SUSPEND); //task_receive等待在该队列上,如果没有消息则任务阻塞
printf("Recved a msg %d",pCnt);
}
}
void task_send(UNSIGNED argc, VOID *argv)
{
static unsigned int cnt = 0;
while(1)
{
cnt++;
printf("Before Send Msg %d",cnt);
NU_Send_To_Queue(&test_queue, (void*)cnt, 1, NU_NO_SUSPEND);
printf("After Send Msg %d",cnt);
NU_Sleep(1000);
}
}
程序很简单,其运行起来后,输出的log如下:
[0:0:1:419] Before Send Msg 1
[0:0:1:419] Recved a msg 1
[0:0:1:419] After Send Msg 1
[0:0:3:919] Before Send Msg 2
[0:0:3:920] Recved a msg 2
[0:0:3:920] After Send Msg 2
......
从log中可以清晰的看到,
task_send首先打印“Before Send Msg”,然后在
task_send发送了一个消息之后,等待在该消息队列上的
task
_receive立刻解除阻塞,并且判断当前task_receive的是系统中优先级最高的task,于是发生了一次“任务抢占”,task_receive瞬间获得CPU,将task_send推在一边开始自己执行,并输出了log,紧接着又再次阻塞在消息队列上。内核再调度task_send继续运行,打印“After Send Msg”,然后睡眠2秒,开始下次循环。
如果在task创建的时候将优先级和是否允许抢占的参数修改一下,情况就会变得不同
NU_Create_Task(&sendTask,
"",
task_send,
0,
NU_NULL,
Task_Stack,
Stack_Size,
50,
0,
NU_PREEMPT, NU_START);
NU_Create_Task(&receiveTask, "" ,task_receive, 0, NU_NULL, Task_Stack, Stack_Size, 50, 0,NU_PREEMPT, NU_START);
如上,将两个task的优先级设置为相同,再次执行,log输出情况就会变成:
[0:0:1:419] Before Send Msg 1
[0:0:1:419] After Send Msg 1
[0:0:1:419] Recved a msg 1
[0:0:3:919] Before Send Msg 2
[0:0:3:920] After Send Msg 2
[0:0:3:920] Recved a msg 2
......
task_send首先打印“
Before Send Msg
”,发送一个消息给队列,内核将消息传到队列之后,发现等待在队列上的任务优先级并不比当前执行的task要高,则不会重新执行一次任务调度,而让task_send继续执行。task_send打印“After Send Msg”之后,开始进入睡眠,主动放弃CPU,这时task_receive得到执行,输出log "Recved a msg",紧接着再次阻塞在队列上,进入下次循环。
如果创建task的时候,将入参改为下面这样,得到的实验结果和前面一次也是一样的
NU_Create_Task(&sendTask,
"",
task_send,
0,
NU_NULL,
Task_Stack,
Stack_Size,
51,
0,
NU_NO_PREEMPT, NU_START);
NU_Create_Task(&receiveTask, "" ,task_receive, 0, NU_NULL, Task_Stack, Stack_Size, 50, 0,NU_PREEMPT, NU_START);
task_send设置了不可抢占属性,这样即使发送消息以后,Nucleus内核判断到task_receive的优先级较高,但由于task_send不可被抢占,只能等到task_send主动放弃CPU的时候(NU_Sleep)才能得到执行。
任务抢占是怎么做到的?
现在来看看Nucleus的内核里是怎么实现的这套机制,源码面前,了无秘密
NU_Receive_From_Queue-->
QUC_Receive_From_Queue-->
/* Determine if there are messages in the queue. */ //判断当前消息队列里是否有消息
if (queue -> qu_messages)
{
//有消息的话
.....
}
else //没有消息,判断是否需要阻塞
{
/* Queue is empty. Determine if the task wants to suspend. */
if (suspend)
{
suspend_ptr = &suspend_block; //专门用于task suspend的一个结构,记录了被挂起任务的信息
task = (TC_TCB *) TCT_Current_Thread();
suspend_ptr -> qu_suspended_task = task;
CSC_Place_On_List((CS_NODE **) &(queue -> qu_suspension_list),&(suspend_ptr -> qu_suspend_link)); //将该信息挂到队列的qu_suspension_list链表上
}
}
NU_Send_To_Queue-->
QUC_Send_To_Queue-->
在该函数里,Nucleus会根据Msg Queue结构体里的qu_suspension_list来判断是否有消息队列阻塞,如果有,将其唤醒
/* Wakeup the waiting task and check for preemption. */
preempt = TCC_Resume_Task((NU_TASK *) suspend_ptr -> qu_suspended_task,NU_QUEUE_SUSPEND);
/* Determine if preemption needs to take place. */
if (preempt)
TCT_Control_To_System(); //它类似于Linux的schedule函数,将控制权交还给内核,由内核来决定下一个要执行的task是谁
TCC_Resume_Task函数的返回值决定了是否需要发生任务抢占,它内部最关键的代码片段如下:
/* Determine if this newly ready task is higher priority
than the current task. */
if ((INT) (task -> tc_priority) < TCD_Highest_Priority)
{
/* Update the highest priority field. */
TCD_Highest_Priority = (INT) task -> tc_priority;
/* Check to see if the task to execute is preemptable. */
else if ((TCD_Execute_Task -> tc_preemption))
{
/* Yes, the task to execute is preemptable. Replace
it with the new task. */
TCT_Set_Execute_Task(task);
/* Now, check and see if the current thread is a task.
If so, return a status that indicates a context
switch is needed. */
if ((TCD_Current_Thread) &&
(((TC_TCB *) TCD_Current_Thread) -> tc_id ==
TC_TASK_ID))
/* Yes, a context switch is needed. */
status = NU_TRUE;
}
}
return status;
Over