[活动] 【FreeRTOS打卡第三站开启】任务状态及切换,关门时间8月20日

nmg   2020-8-18 08:49 楼主

 

活动总览:点此查看(含活动鼓励和活动学习总内容)

 

本站打卡开始和截止时间:8月18日-8月20日(3天)

打卡任务:

1、阅读cruelfox干货笔记第三篇:FreeRTOS学习笔记 (3)任务状态及切换

2、跟本帖回复思考题: 在FreeRTOS任务程序中,常使用 xTaskDelay() 函数来达到延时执行的目的。若在某一个任务里调用了 xTaskDelay(20), 想等待20个时间单位,结果这一回延时操作却超过了25个时间单位。请分析有可能是哪些原因造成的?

 

 

FreeRTOS学习笔记 (3)任务状态及切换

为了方便大家阅读,将cruelfox“FreeRTOS学习笔记 (3)任务状态及切换”复制过来了~

 

FreeRTOS 的任务具有如下几种状态:

运行 Running
就绪 Ready
阻塞 Blocked
挂起 Suspended


  除了运行状态之外的状态统称为非运行状态。因为 FreeRTOS 是为单CPU设计的系统,在任何时刻最多只能允许一个任务处在运行状态,哪怕看起来好像有多个任务同时在运行——这只是多个任务不停地切换带来的效果。当一个任务从运行状态切换到非运行状态时,执行时的现场——CPU寄存器被保存在任务的私有堆栈中;在重新回到运行状态时,再从堆栈中恢复之间保存的寄存器。这是任务调度的最基本功能。
1.jpg

  就绪的任务

  FreeRTOS 任务的就绪状态表示任务目前没有被执行,但随时可以被执行。当下一次任务切换时机到来时,FreeRTOS 将从就绪任务的列表中选择优先级最高的任务,切换成运行状态。
  任务优先级是任务的一个属性,FreeRTOS 用整型数表示优先级,最低优先级为0, 最高为 configMAX_PRIORITIES 宏定义的值减去1. 创建任务的时候需要指定一个优先级,在创建之后也可以再通过 vTaskPrioritySet() 函数更改。优先级的意义是,某一个任务处于就绪状态时,只要还存在比它优先级高的任务也处于就绪状态,它就得不到执行的机会。
  当有两个以上相同优先级的任务处于就绪状态时,它们会轮流地得到机会执行(但不保证执行时间是平均分配的),系统不会偏向其中任一个。

  任务切换的时机

  FreeRTOS 在以下情况下必然发生任务切换:
(A) 运行中的任务调用 vTaskSuspend() 将自己挂起。
(B) 运行中的任务调用了延时函数,要求等待一段时间后再执行。
(C) 运行中的任务需要等待一个未发生的同步事件。
  因为是当前任务主动暂停执行,FreeRTOS 需要将它切换到挂起或者阻塞状态,然后从就绪状态的任务列表中选择最高优先级的任务来执行。若已经没有其它需要执行的任务了,还会执行系统自带的,具有最低优先级的 Idle task. 还有一个特殊的情况是
(D) 运行中的任务调用了 taskYIELD() 主动要求切换任务。
  这时候如果就绪任务列表中有同一或更高优先级的任务,将发生任务切换,否则会返回当前任务继续执行。

  如果 FreeRTOS 配置中有 configUSE_PREEMPITON=1, 则进行抢占式任务调度。抢占的意义是在任务就绪列表中新加入的任务(包括新建任务、从挂起状态恢复的任务、从阻塞状态退出的任务),以及被修改了优先级的任务,具有比当前运行任务更高的优先级时,立即切换到那个更高优先级的任务执行。而先前运行的任务不知道自己的执行在何时被暂停,故称为“被抢占”。
  抢占式调度保证了最高优先级的任务一旦运行条件成熟,就会立即得到处理器资源。不过如果最高优先级的任务不止一个时,默认它们是不会相互抢占的,除非再配置了 configUSE_TIME_SLICING=1, 由 FreeRTOS 进行时间片管理。启用时间片管理之后,相同优先级的就绪任务轮流执行,每个任务每次最多执行一个固定的周期,基本上让处理器资源平均分配。  小结一下,在抢占式调度下任务切换的时机有:
(E) 运行中的任务被更高优先级的任务抢占。
(F) 运行中的任务执行到一个时间片末尾,被同一优先级的任务抢占。

  借用一下手册中的例子。没有抢占式调度的时候是像下图这样:Task3 没有阻塞,需要主动交出控制权才会切换到已经就绪的 Task1, Task2.
2.jpg
  而抢占式调度是下面这样:总是立即调度到更高优先级的就绪任务。
3.jpg
  启用了时间片轮流调度同优先级任务的抢占式:Task2 和 Idle task都是最低优先级的,轮流执行,在时间片末尾一定切换。
4.jpg

  任务切换的实现

  分析一下实现的细节,挑一个简单的入手: vTaskSuspend() 函数,作用是将一个任务状态调整为挂起状态。

  1. void vTaskSuspend( TaskHandle_t xTaskToSuspend )
  2. {
  3.     TCB_t *pxTCB;
  4.  
  5.     taskENTER_CRITICAL();
  6.     pxTCB = prvGetTCBFromHandle( xTaskToSuspend );
  7.     if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
  8.     {
  9.         taskRESET_READY_PRIORITY( pxTCB->uxPriority );
  10.     }
  11.     if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
  12.     {
  13.         ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
  14.     }
  15.     vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) );
  16.     taskEXIT_CRITICAL();
  17.  
  18.     if( xSchedulerRunning != pdFALSE )
  19.     {
  20.         taskENTER_CRITICAL();
  21.         prvResetNextTaskUnblockTime();
  22.         taskEXIT_CRITICAL();
  23.     }
  24.  
  25.     if( pxTCB == pxCurrentTCB )
  26.     {
  27.         if( xSchedulerRunning != pdFALSE )
  28.         {
  29.             portYIELD_WITHIN_API();
  30.         }
  31.         else
  32.         {
  33.             if( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) == uxCurrentNumberOfTasks )
  34.             {
  35.                 pxCurrentTCB = NULL;
  36.             }
  37.             else
  38.             {
  39.                 vTaskSwitchContext();
  40.             }
  41.         }
  42.     }
  43. }
复制代码


  这个函数有三大块:第一个是把该任务TCB结构中 xStateListItem 成员从所属状态列表中删除,随后插入到挂起任务列表;并把 xEventListItem 成员从所属列表中删除。第二块是如果调度器是在工作的,就执行一下 prvResetNextTaskUnblockTime(). 第三块是在如果挂起的是当前任务,就进行任务调度:portYIELD_WITHIN_API() 或者调度器没工作时 vTaskSwitchContext() 切换到没有挂起的任务。
  暂且不管 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 这两个临界区有关的操作。uxListRemove() 这个函数仅仅需要一个参数有点奇怪,需要查看 list.c 和 list.h 明白 FreeRTOS 的列表是用了双向链表数据结构实现的才能理解:从 xStateListItem 这一列表成员,能索引整个列表。当任务所属的状态列表为空时,要执行 taskRESET_READY_PRIORITY(), 重置这一级优先级的就绪状态?不好理解,得追查代码……原来每个优先级都有一个就绪任务的列表,而不是我以为的所有就绪任务用一个列表。在 tasks.c 中全局定义了以下一些列表:

  1. /* Lists for ready and blocked tasks. --------------------*/
  2. PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
  3. PRIVILEGED_DATA static List_t xDelayedTaskList1;
  4. PRIVILEGED_DATA static List_t xDelayedTaskList2;
  5. PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;
  6. PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;
  7. PRIVILEGED_DATA static List_t xPendingReadyList;
复制代码


  可以猜想,FreeRTOS 的任务必须要在某一个列表中(运行任务在就绪列表里);发生任务调度的时候,列表要作调整。
  vTastSuspend() 的第二步是重新设置 xNextTaskUnblockTime 这个全局变量,因为挂起的任务可能是处于等待延时的阻塞状态,需要排除它的影响。
  第三步是任务调度,通过调度器是用 portYIELD() 实现(否则调度器没有运行……这个稍后再来分析)。而 portYIELD() 对于 ARM Cortex-m3 平台在 portmacro.h 中是这样实现的:

  1. #define portYIELD()                                   \
  2. {                                                     \
  3.     portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;   \
  4.     __asm volatile( "dsb" );                          \
  5.     __asm volatile( "isb" );                          \
  6. }
复制代码



  这个宏做的事情是设置了 PendSV 状态位,将引起 PendSV 异常,然后在 PendSV ISR handler 中去实现任务切换。现在可以大胆猜测:FreeRTOS 用 PendSV 异常处理来进行任务切换。继续分析这个 handler, 它是 port.c 中一个汇编实现的函数:

  1. void xPortPendSVHandler( void )
  2. {
  3.     __asm volatile    (
  4.     "   mrs r0, psp                         \n"
  5.     "   isb                                 \n"
  6.     "   ldr r3, pxCurrentTCBConst           \n"
  7.     "   ldr r2, [r3]                        \n"
  8.     "   stmdb r0!, {r4-r11}                 \n"
  9.     "   str r0, [r2]                        \n"
  10.     "   stmdb sp!, {r3, r14}                \n"
  11.     "   mov r0, %0                          \n"
  12.     "   msr basepri, r0                     \n"
  13.     "   bl vTaskSwitchContext               \n"
  14.     "   mov r0, #0                          \n"
  15.     "   msr basepri, r0                     \n"
  16.     "   ldmia sp!, {r3, r14}                \n"
  17.     "   ldr r1, [r3]                        \n"
  18.     "   ldr r0, [r1]                        \n"
  19.     "   ldmia r0!, {r4-r11}                 \n"
  20.     "   msr psp, r0                         \n"
  21.     "   isb                                 \n"
  22.     "   bx r14                              \n"
  23.     "   .align 4                            \n"
  24.     "pxCurrentTCBConst: .word pxCurrentTCB  \n"
  25.     ::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY) );
  26. }
复制代码


  因为进入中断前,PSP 堆栈中保存了8个寄存器: r0, r1, r2, r3, r12, LR, PC, xPSR, 可以放心用 r0 至 r3. 先将当前任务 TCB 地址读到 r2, 将 r4 到 r11 这些寄存器保存到任务的堆栈,然后将新的任务堆栈顶(这里是r0寄存器)保存在TCB中(TCB的第一个成员就是堆栈栈顶)。接下来调用 vTaskSwitchContext() 函数切换上下文,再获取当前任务 TCB 地址到 r1, 从 TCB 中读取任务堆栈栈顶,从堆栈中弹出寄存器,设置 PSP 堆栈寄存器,最后退出异常处理。
  因为 PSP 寄存器被改了,所以进入异常之前的堆栈和退出异常后的堆栈属于不同任务的。而在一个任务看来,调用 portYIELD() 函数返回后好象什么都没有变一样。如果 vTaskSwitchContext() 函数什么都不做的话,上面的异常处理就什么实质性的操作都没干,仍然返回现在的任务。
  来看看上下文切换是怎么实现的:实际上关键性的就这一句
   taskSELECT_HIGHEST_PRIORITY_TASK();
  这又是一个宏,定义为

  1. {
  2. UBaseType_t uxTopPriority = uxTopReadyPriority;
  3.  
  4.     while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )
  5.     {
  6.         --uxTopPriority;
  7.     }
  8.  
  9.     listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );
  10.     uxTopReadyPriority = uxTopPriority;
  11. }
复制代码


  不难看懂,它从最高优先级开始向下搜索,直到找到一个就绪任务列表不空的,从中选取一个任务,设置当前 TCB 地址为它的 TCB 地址,然后保存全局就绪状态的优先级。所以 vTaskSwitchContext() 做的事情主要就是选出该执行的任务,设置当前 pxCurrentTCB 全局变量。

  时间片是如何工作的

  既然需要用到时间片,必须得有硬件定时器中断支持了。FreeRTOS 把这个中断叫做 tick interrupt(翻译为滴答中断),用什么定时器实现就跟平台相关了。查看源码之后,我确认了对于ARM Cortex-m0/m3 都是用的 Systick Timer, 是由 ARM 内核提供的定时器而不是APB总线上的定时器设备,这样充分利用了硬件,又兼容不同厂家的MCU.
  定时器中断函数还算简单

  1. void xPortSysTickHandler( void )
  2. {
  3.     portDISABLE_INTERRUPTS();
  4.     {
  5.         if( xTaskIncrementTick() != pdFALSE )
  6.         {
  7.             portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
  8.         }
  9.     }
  10.     portENABLE_INTERRUPTS();
  11. }
复制代码


  主要就是调用 xTaskIncrementTick() 进行计数,并判断是否需要任务切换。需要切换的情况除了同一优先级就绪任务的轮流运行外,还有用 xTaskDelay() 等函数申请延时到期的,还有等待事件 timeout 到期的。后两者都是 DelayedTaskList 列表里面的情况,这其中实现的细节我就不在此罗列了。

 

本站思考题貌似比上一站简单些,加油~
阅读完后,欢迎跟帖回复本站思考题:在FreeRTOS任务程序中,常使用 xTaskDelay() 函数来达到延时执行的目的。若在某一个任务里调用了 xTaskDelay(20), 想等待20个时间单位,结果这一回延时操作却超过了25个时间单位。请分析有可能是哪些原因造成的?

回复评论 (13)

在FreeRTOS任务程序中,常使用 xTaskDelay() 函数来达到延时执行的目的。若在某一个任务里调用了 xTaskDelay(20), 想等待20个时间单位,结果这一回延时操作却超过了25个时间单位。请分析有可能是哪些原因造成的?

答:我想是等待着一个信号量,而这个信号量是更低级的任务在执行,执行了超过25个时间单位才给这个信号量。或者有更高优先级的任务在这20个时间单位抢占运行了几个单位,再或者中断影响了其时间。

本帖最后由 ddllxxrr 于 2020-8-19 11:10 编辑
http://shop34182318.taobao.com/ https://shop436095304.taobao.com/?spm=a230r.7195193.1997079397.37.69fe60dfT705yr
点赞  2020-8-18 16:27

思考题: 在FreeRTOS任务程序中,常使用 xTaskDelay() 函数来达到延时执行的目的。若在某一个任务里调用了 xTaskDelay(20), 想等待20个时间单位,结果这一回延时操作却超过了25个时间单位。请分析有可能是哪些原因造成的?

答:首先这个xTaskDelay(20),20个时间单位肯定是硬吃的,毋庸置疑,主要是要分析后面这个多出来的5个时间单位被谁吃了。

1、被更高优先级抢占,就是当前任务到等到19个时间单位时,有一个高优先级任务抢占资源,运行了6个时间单位才释放资源

2、被同优先级抢占,当前任务到等到19个时间单位时,有一个同优先级任务正好在就绪态,肯定时可以接手资源运行6个时间单位再释放资源

3、被中断吃了,不管是裸奔还是,上rtos硬件中断始终优先级是最高的,正常编程过程中,我们是不会再中断中做了很多东西,但不排除有些人在中断中写了应用代码,甚至写了延时函数

点赞  2020-8-19 19:12

vTaskDelay是相对延时函数,当函数调用vTaskDelay函数后就会进入阻塞状态。当进入阻塞状态后,其他任务就会被执行。
1、在阻塞的情况时,优先级高的出现等待信号量或者消息队列,必须到达指定的timeout后才能被抢占
2、在任务执行时,发生了中断或者被高优先级任打断,那么任务的执行时间就会边长,而等执行vTaskDelay()时,相对时间不会改变,所以总时间变长
 

点赞  2020-8-19 23:04

多出来的5个延时时间,有可能是因为延时到期进入等待时,发现有更高优先级的任务抢占了。等高优先级任务退出后才获得cpu使用权

点赞  2020-8-20 07:49

running task 在使用xTaskDelay()会发生任务切换,原任务变为 blocked task,系统会切换到ready task中最高优先级来执行,那么我觉得引起延时的原因有以下几个方面

  • 没有抢占式调度时可能是因为存在硬件中断处理时间过长抢占式调度则可能是被更高优先级的任务抢占,使得CPU被占用,延时超时。
  • 另一方面是由于采用时间片轮流调度,被同优先级的抢占,但是需要到时间片末尾切换,当然还有可能是因为中断

 

点赞  2020-8-20 16:36

vTaskDelay()是相对延时函数,任务每次延时都是从调用延时函数vTaskDelay()开始算起的,延时是相对于这一时刻开始的。如果执行任务A的过程中发生中断,那么任务执行的周期就会变长。
(1)高级别的任务抢占

(2)任务A与任务B,甚至更多任务同时执行,也会造成任务A的延时函数延时

(3)任务本身在,调度时的损失时间,如阻塞,挂起至执行,也会导致延时。

点赞  2020-8-20 17:23

1、这个延时是相对时间,不是绝对时间;2、任务在就绪中,其他高优先级的任务运行了5个时间单位;

点赞  2020-8-20 17:59

多出来的5个时间单位是调用vTaskDelay()之前的代码运行了5个时间单位,因为vTaskDelay()延时计算是相对于调用的起始位置开始算的。

点赞  2020-8-20 19:05

因为执行xTaskDelay会触发任务调度 如果是抢占式:可能被高优先级任务抢占了; 如果是时间片轮转,可能延时时间超过了这个任务所占用的剩余时间片时间,被同级任务轮转了; 也可能触发任务调度,被其他调度给其他任务了(也有可能是低优先级任务) 还可能中断等事件占用了5个单位时间

点赞  2020-8-20 19:23

在延时过程中,又有优先级高的任务被执行了,多占了6个时间单位。

点赞  2020-8-20 21:30

1.存在比原本任务更高优先级的任务,在原本任务准备开始延时被就绪,且执行时间为5个时间单位;

2.存在和原本任务同等优先级的就绪任务,不同任务穿插用了 5 个时间单位;

3.硬件有问题,导致计时不准

君应有语,渺万里层云,千山暮雪,知向谁边?
点赞  2020-8-20 21:32

由于定时器用的是SysTick Timer,而非硬件Timer资源。所以还是软件实现的Timer。当有比Timer优先级更高的优先级的任务在执行的时候,就会出现比设定的延时时间长的情况。

点赞  2020-8-20 22:41

作者cruelfox相关解读:https://bbs.eeworld.com.cn/thread-1141050-1-1.html

点赞  2020-10-13 11:19
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复