FreeRTOS任务调度

为了满足处理器多任务并发进行的需求,需要通过系统调度来合理安排各个任务占有CPU的时间。任务管理和调度是RTOS的核心功能。

一般系统中,任务可以分为Running态和非Running态,而非Running态可以细分。很容易理解,Running态就是占用CPU的任务,而非Running态就是其他任务。

FreeRTOS中,任务状态可以分为Running,Suspend,Ready,Blocked。任务在被创建之后,就会被默认设置为Ready态。调度器通过不同的任务优先级和时间将任务在各个状态之间切换。各个状态之间的转换顺序如下:

1.任务状态

 

 FreeRTOS中的调度方式包括以下

抢占式调度:当有新的任务就绪(ready),且优先级大于等于当前任务的优先级时,当前任务就会被抢占;需要用户自己通过configUSE_PREEMPTION配置。

时间片调度:同处于ready态的最高优先级的任务会轮流运行固定的时间片;通过configUSE_TIME_SLICING配置,默认开启。

 2.任务创建

FreeRTOS任务创建有两个接口:

 1 xTaskCreate           2 xTaskCreateStatic 

 任务创建的时候,会同时配置TCB(Task Control Block),用来存储该任务的优先级,任务等。

任务被创建后,默认将任务设置为就绪态,等待系统调度

3.状态链表

FreeRTOS中不同状态的任务分别用不同的状态链表来管理:

  1.运行态

 1 TCB_t * volatile pxCurrentTCB 

这个比较特殊,因为运行态的任务永远只有一个,所以用pxCurrentTCB指向任务的TCB即可

  2.就绪态:

 1 List_t pxReadyTasksLists[ configMAX_PRIORITIES ] 

configMAX_PRIORITIES对应最大的任务优先级,从0开始,随着数字的增大优先级提高。pxReadyTasksLists是一个链表数组,每个就绪的任务都会被添加到该数组对应优先级的链表中

 

  3.阻塞态:

1 List_t xDelayedTaskList1
2 List_t xDelayedTaskList2
3 List_t * volatile pxDelayedTaskList;
4 List_t * volatile pxOverflowDelayedTaskList;

当任务在等待某个事件或者主动delay的时候,就会被添加到延迟链表中,并根据即将溢出的时间(链表中的值为xTickCount+ xTicksToDelay)先后顺序排列。当延迟的任务延迟溢出时就会被添加到延迟溢出链表。当系统时间溢出时,两个链表交换,此时延迟链表为空。

  4.挂起态:

 1 List_t xSuspendedTaskList; 

挂起的任务将会被添加到这个链表中,并不再参与系统调度直到有任务调用恢复接口将该任务恢复。

  5.

还有个特殊的待定链表

 1 List_t xPendingReadyList 

当调度器被挂起时,被唤醒的任务就会先被添加到该链表中,直到调度器恢复时,再从该链表添加到就绪链表中,并且判断是否需要进行任务抢占。

该链表的作用就在于暂存调度器被挂起时就绪的任务

4.调度器启动过程

 1 void vTaskStartScheduler( void ) 

  1. 创建IDLE任务,并且把任务优先级设置为0和特权位
  2. 创建Timer任务,任务优先级一般高于用户的最高优先级
  3. 禁止中断,保持调度器启动(xPortStartScheduler)过程中不被中断
  4. 调用xPortStartScheduler:

配置tick中断以及使能tick timer,设置SVC异常,上下文切换和运行第一个任务

5.任务切换时机

FreeRTOS通过触发SWI中断,在SWI中断处理函数中进行任务切换

有以下方式触发:

  1. 同等级任务时间片用完,在SysTick 中断中触发触发PendSV
  2. 用户主动调用taskYIELD(),触发PendSV
  3. 高优先任务恢复就绪或任务调用事件阻塞接口时抢占触发PendSV

最终都是通过调用移植层提供的 portYIELD() 宏触发 PendSV 异常,手工往NVIC的 PendSV 寄存器中写 1。如果还有其他高优先级的中断,则等待其他高优先级中断处理完才执行 PendSV 异常处理进行任务切换。

 
  1. 同等级任务时间片用完,在SysTick 中断中触发触发PendSV

 1 //FreeRTOS/Source/portable/ARMv8M/non_secure/port.c
 2 void SysTick_Handler( void ) /* PRIVILEGED_FUNCTION */
 3 {
 4   uint32_t ulPreviousMask;
 5 
 6    ulPreviousMask = portSET_INTERRUPT_MASK_FROM_ISR();//屏蔽中断
 7    {
 8         /* 增加系统tick. */
 9        if( xTaskIncrementTick() != pdFALSE )
10        {
11            /* 触发PendSV,等中断处理函数结束后再判断判断是否进行任务切换. */
12            *( portNVIC_INT_CTRL ) = portNVIC_PENDSVSET;
13        }
14    }
15    portCLEAR_INTERRUPT_MASK_FROM_ISR( ulPreviousMask );//解除中断屏蔽,恢复屏蔽前状态
16 }

tick的中断处理函数先屏蔽中断,有些实现中会屏蔽全局中断,而有些只是屏蔽低优先级的中断,为了保证tick简单可靠,屏蔽tick优先级以上所有中断比较合适。函数退出前会解除中断屏蔽,恢复屏蔽前状态,这里不一定就是完全解除中断屏蔽了,因为这个tick也可能是抢占了低优先级的中断来执行的。 等待tick中断处理函数退出后,当没有比PendSV更高优先级的中断,就会进入PendSV中断进行任务切换。 

也有

  2.用户主动调用taskYIELD(),触发PendSV 

//FreeRTOS/Source/include/task.h 
#define taskYIELD() portYIELD()

//FreeRTOS/Source/portable/ARMv8M/non_secure/portable/GCC/ARM_CM23/portmacro.h
#define portYIELD()    vPortYield()

taskYIELD()在中是一个宏,vPortYield()也是触发PendSV

   3.高优先任务恢复就绪或任务调用事件阻塞接口时抢占触发PendSV

高优先任务恢复就绪(如信号量,队列等阻塞、挂起状态下退出),任务调用事件阻塞接口(如等待信号量,sleep等)

上述接口调用过程中都会调用到 xTaskResumeAll() 

 1 void vTaskDelay( const TickType_t xTicksToDelay )
 2 {
 3   BaseType_t xAlreadyYielded = pdFALSE;
 4 
 5     /* A delay time of zero just forces a reschedule. */
 6     if( xTicksToDelay > ( TickType_t ) 0U )
 7     {
 8         configASSERT( uxSchedulerSuspended == 0 );
 9         vTaskSuspendAll();
10         {
11             traceTASK_DELAY();
12 
13             /* A task that is removed from the event list while the
14             scheduler is suspended will not get placed in the ready
15             list or removed from the blocked list until the scheduler
16             is resumed.
17             This task cannot be in an event list as it is the currently
18             executing task. */
19             prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
20         }
21         xAlreadyYielded = xTaskResumeAll();
22     }
23     else
24     {
25         mtCOVERAGE_TEST_MARKER();
26     }
27     /* Force a reschedule if xTaskResumeAll has not already done so, we may
28     have put ourselves to sleep. */
29     if( xAlreadyYielded == pdFALSE )
30     {
31         portYIELD_WITHIN_API();
32     }
33     else
34     {
35         mtCOVERAGE_TEST_MARKER();
36     }
37 }

   以 vTaskDelay 为例,当sleep的时间大于0的时候,就会把当前task从 pxReadyTasksLists 移除并添加到 pxDelayedTaskList 链表中,然后调用 xTaskResumeAll()。xTaskResumeAll() 中会判断在调度器挂起期间是否有有新的任务 ready,如果有,就会将这些 task 从 xPendingReadyList 中移除并添加到 pxReadyTasksLists, 并判断是否需要进行任务抢占。

6. 任务切换过程

 一般来说都是在 PendSV_Handler() 中进行保存上一个任务的现场,任务切换,恢复新的任务的现场的操作。这个一般都是汇编来实现,并且每一款处理器都不一样。这里不赘述了,值得一提的就是所有的 xPortPendSVHandler() 都是通过 tasks.c 中的 vTaskSwitchContext() 来进行任务切换的。 

 1 //FreeRTOS/Source/tasks.c
 2 //已经仅为该函数部分代码
 3 void vTaskSwitchContext( void )
 4 {
 5     //...
 6     traceTASK_SWITCHED_OUT();//FreeRTOS中 rtos trace 点,记录原本正在执行的任务的切出
 7     //...
 8     /* Check for stack overflow, if configured. */
 9     taskCHECK_FOR_STACK_OVERFLOW();
10 
11     /* Before the currently running task is switched out, save its errno. */
12     #if( configUSE_POSIX_ERRNO == 1 )
13     {
14         pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
15     }
16     #endif
17 
18     /* Select a new task to run using either the generic C or port
19     optimised asm code. */
20     taskSELECT_HIGHEST_PRIORITY_TASK(); //在 pxReadyTasksLists 中选择优先级最高的 task,软件实现O(1)
21     traceTASK_SWITCHED_IN();//FreeRTOS中的rtos trace点,记录新任务切入
22 
23     //...
24 } 

有趣的是,可以在该函数的两个 trace 点中添加自定义的 debug 工具,比如检测运行task时间过长,记录任务的切换流程等

参考文档:

https://blog.csdn.net/qq_18150497/article/details/52824082

https://juejin.im/post/5da5bdd05188254f4d2b7f67

https://blog.csdn.net/pfysw/article/details/80964603

猜你喜欢

转载自www.cnblogs.com/zhn11212023/p/13115799.html
今日推荐