实时操作系统的任务调度示例之抢占

什么是任务抢占?
实时操作系统大多都是基于优先级调度的抢占式的内核,这句话每本关于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

你可能感兴趣的:(RTOS)