【RTOS训练营】上节回顾、空闲任务、定时器任务、执行顺序、调度策略和晚课提问

一:上节回顾

在上一节课我们贴了这么一个图:

FreeRTOS里面有很多个链表,这些链表分为三类:就绪列表、暂停列表、Delay链表。

对于就绪列表,每一个优先级都有一个链表,比如我们有32个优先级,那么就有32个就绪链表。

就绪链表里面存放的是:就绪状态的任务、运行状态的任务。

同一时间,对于单核CPU,只能够有一个运行状态的任务。

对于这一段代码,系统里面有几个任务?

答案是:4个或者5个

第4个是空闲任务,第5个是定时器任务。

二:空闲任务

如果我们配置了支持定时器,那么就会有一个定时器任务,看看代码:

再提一个问题,能不能够去掉空闲任务?

答案是:不能。

空闲任务通常为自杀任务释放内存,但是如果编写的程序,所有的任务都不自杀。

假设有任务1,任务2,假设他们都进入到了暂停状态。任务是暂停了,那CPU还在运行, CPU运行谁的代码?

所以总得有一些代码让CPU来运行,总得有一些函数来运行,这个函数就是空闲任务的函数。

从这个角度来看, CPU总得去做点事情。

当所有我们自己创建的任务都不再运行,一定有一个任务在运行:这就是空闲任务。

从这个角度来说,空闲任务只有两种状态:就绪态,运行态。

空闲任务有什么作用?回收。

在使用vTaskDelete来删除别的任务后,就会自己清理。

怎么清除呢?释放栈、释放TCB。

去创建一个任务的时候,会为他分配栈,分配TCB结构。

去删除一个任务的时候,要去释放栈、释放TCB结构体。

上面讲的是使用vTaskDelete来删除别的任务,那对于自杀的任务,能否自己清理呢?

答案是:不能。运行程序需要栈,但释放自己的栈需要运行代码,运行代码又需要栈。

我们假设可以,要运行某个函数A,函数A用到栈,函数A要去释放栈,自相矛盾。

那么对于自杀的任务,他的清理工作,就有空闲任务来执行,怎么清理呢?

上面贴的图就是空闲任务的函数,函数名取得比较奇怪。

我们把那个宏展开,这就是一个名为 prvIdelTask的函数。

清理自杀的任务,这就是空闲任务的主要工作。

在视频里面我们有一个实验,故意不让空闲任务执行,然后不断地创建、删除任务,最后发现内存耗尽。

原因就是空闲任务不能够执行,他就不能够去释放自杀的任务。

我们再来看看空闲任务的其他作用,直接看代码就可以知道:

比如说你想统计一下系统的CPU占用率、内存占用情况,可以去提供上图里面的那个函数。

这个函数就是空闲任务的钩子函数,函数内容需要自己写。

三:定时器任务

再来看看第5个任务是怎样的:

在配置了这个内核确定说使用定时器的时候,他才会去帮你创建定时器任务。

定时器任务我们暂时用不到,先不细讲,对应配置项:configUSE_TIMERS

四:执行顺序

我们假设有4个任务:1、2、3、空闲任务。他们怎么执行呢?谁先运行呢?

首先任务3的优先级最高,他先运行。

如果任务三,不休眠的话,作为最高优先级的任务,他将会一直运行。

这跟Linux不一样,在Linux系统中,最高优先级的任务也会让路。

在FreeRTOS里,最高优先级的任务:优先执行,他不放弃的话,别的任务都没有机会执行。

即使时间片轮转打开,他也只是在同等优先级的任务里面轮流执行。时间片轮转,只适用于同等优先级的多个任务。

五:调度策略

上面讲的是默认的调度算法:可抢占、时间片轮转。

我们先概括介绍下调度策略:

从3个角度统一理解多种调度算法:

  • 可否抢占?高优先级的任务能否优先执行(配置项: configUSE_PREEMPTION)

    • 可以:被称作"可抢占调度"(Pre-emptive),高优先级的就绪任务马上执行,下面再细化。
    • 不可以:不能抢就只能协商了,被称作"合作调度模式"(Co-operative Scheduling)
      • 当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让出CPU资源。
      • 其他同优先级的任务也只能等待:更高优先级的任务都不能抢占,平级的更应该老实点
  • 可抢占的前提下,同优先级的任务是否轮流执行(配置项:configUSE_TIME_SLICING)

    • 轮流执行:被称为"时间片轮转"(Time Slicing),同优先级的任务轮流执行,你执行一个时间片、我再执行一个时间片
    • 不轮流执行:英文为"without Time Slicing",当前任务会一直执行,直到主动放弃、或者被高优先级任务抢占
  • 在"可抢占"+"时间片轮转"的前提下,进一步细化:空闲任务是否让步于用户任务(配置项:configIDLE_SHOULD_YIELD)

    • 空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务
    • 空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊

我们来举例说明:

是否可抢占,配置项为:configUSE_PREEMPTION

可抢占的意思,就是高优先级的任务就绪之后,可以去抢占低优先级的任务。

只要高优先级的任务就绪,他就可以马上、抢占别人。

那反过来,如果不允许抢占呢?

我们看一个对比图:

task1、task2在task3阻塞的时候为啥运行时间不一样呢,它们不是均分时间片吗?

大家沿着123来看:

①:task3运行vTaskDelay的时刻,有可能是在1ms边界的附近,也可能在1ms很远的地方

②:假设任务3休眠之后任务1运行,任务1能够运行的时间并不是足额的1ms

③:1ms的tick中断发生后,轮到任务2运行

大家可以看到,业务1运行的时间,是随机的。下图用红色的圆圈,绿色的圆圈框出了一些波形,大家感受一下。

前面讨论了抢占,可抢占,是默认配置。

不允许抢占,用的很少,基本没人这样去用它。

那如果不允许抢占的话,会发生什么事情?

在任务一运行的过程中,即使任务三休眠时间到了,因为他不能够抢占,他的优先级再高,也只能够等。

在代码上是怎么体现出来的呢?

看看这个图,这是可抢占的情况,如果我没有配置configUSE_PREEMPTION,这个图的代码就没有效果。

如果不抢占的话, 为什么大家不轮流执行呢?

这应该是FreeRTOS根本没考虑到这一点,我们来看看代码:

我认为,这是FreeRTOS的设计缺陷,它根本就没有考虑:不抢占的实用性。

六: 晚课学员提问

1. 问: 空闲任务是否可以空操作?

答: 可以是空操作,空操作就是:NOP汇编指令,那也得执行指令。

2. 问: 空闲任务应该是最低优先级的吧?不是最低的话,比他低的都不会执行?

答: 是最低的,但是其他的任务可以跟他并列最低。

3. 问: 如果高优先级的任务再主动放弃的过程中,又来了一个一个触发他运行的事件怎么办?

答: 高优先级的任务可以马上再次运行。

4. 问: 老师,高优先级的任务就绪以后自己会触发一个调度吗?还是通过硬件中断触发一个调度,然后再执行?

答: 自己触发一个调度?这句话有逻辑错误。之前是休眠状态,休眠的任务怎么可以触发调度呢?

休眠,意味着不执行,你都不执行了,你怎么能够触发调度。所以:是别人发起调度。

这个别人是谁?task3调用vTaskDelay休眠一段时间,Tick中断发现你的时间到了,会触发调度,是tick中断来触发调度。

如果换另外一种方法进入休眠呢:假设task3在等待某个事件,谁来把他唤醒?事件的源头把它唤醒。

高优先级的任务就绪以后自己会触发一个调度吗?不会,由中断或者别的任务来触发调度。

5. 问: 老师,task3,delay后为什么没有继续执行被抢占的任务呢?

答: 假设:

  1. task1运行,它被放到的就绪队列的尾部
  2. task3就绪,抢占任务1
  3. task3再次休眠,从就绪队列的头部取出一个任务来执行,是task2

所以,task3抢占了task1,任务3再次休眠时,不一定是task1继续运行。

6. 问: 调度的时间占用任务的时间片吗?

答: 占用,调度也是需要花时间,会占用一些时间。

7. 问: 老师,假设高优先级的任务正在执行,这个时候tick时间到了,这个时候还要触发调度?

答: 高优先级的任务正在执行,可能高优先级的任务有多个。

所以,tick中断,他要去判断:有没有同优先级的其他任务?有的话就触发调度。

没有的话, 整个系统你最大, 当然就不用触发调度了。

8. 问: task1 里对两个全局变量a b 进行累加,a++ b++,那么一段时间后a 和b的值可能不同是吧。a++ 执行后,可能被高优先级任务抢占,b++没执行。

答: 是的。

9. 问: 某个任务被高优先级打断,剩下的就得不到执行了,感觉不太合理吧。会给设计带来困难啊。

答: 所以我们编写程序的时候,高优先级的任务,处理完紧急的事情之后就要休眠,不要让高优先级的任务一直执行。

高优先级的任务休眠之后,低优先级的任务可以再次运行:从被中断的地方再次运行。

只不过,低优先级能够再次运行的时间,取决于高优先级的任务什么时候休眠。

10. 问: 假设tick设置100ms,任务3目前已经从阻塞或暂停态恢复就绪态,此时tick未进入中断发生调度,那任务3是怎么进行调度的(它是抢占最高的),还有delay它是怎么被运行的(就是他要把task3从阻塞恢复成就绪态,也就是说谁把他恢复的),一切的一切是因为delay与tick绑定在一起,ms级别延时是吗?还有此时它是怎么抢占的,是谁把他调度的,一切的一切都是和tick绑定在一起的吗?抢占的意义还存在吗(delay是1ms,tick也是1ms,我怎么知道是否抢占,还不是利用tick吗?我没看源码分析,所以有这些问题)

答: 这个问题有一些概念错误。

假设tick设置100ms,任务3目前已经从阻塞或暂停态恢复就绪态,此时tick未进入中断发生调度

task3调用vTaskDelay,他能够恢复为就绪态,必定是发生了tick中断,tick计数值累加了。

还有delay它是怎么被运行的(就是他要把task3从阻塞恢复成就绪态,也就是说谁把他恢复的),一切的一切是因为delay与tick绑定在一起,ms级别延时是吗?

没错, vTaskDelay的基准就是Tick中断。

流程是这样:

  1. Task3调用vTaskDelay(5); 当前tick计数值假设是10
  2. 过了1ms,tick计数值累加为11
  3. 再过1ms,tick计数值累加为12
  4. ……
  5. tick计数值累加为15时,就把task3从delay链表移到就绪链表,并且检查发现,task3是优先级最高的就绪任务
  6. 发起调度

从这个流程,你发现:对于因为调用vTaskDelay而进入休眠的任务,是被tick中断导致的调度唤醒的。

11. 问: 请教一个问题 ,引起调度是不是有以下情况:

1.当前任务主动执行了 delay 或者supend的操作

2.TICK中断会触发一次调度

答: 有很多种情况,比如说队列操作:

一开始队列为空,task1去读队列,因为没有数据进入休眠;

task2写队列,还会去唤醒等待队列数据的task1

12. 问: 老师,这个时候把task3休眠,那么休眠的时间是从这个tick起点开始?

答:

我们在中间调用vTaskDelay,那么他什么时候被唤醒?

中间时刻+5 吗 ?不是的,我们的最小时间精度就是Tick,对于中间的时间,他没有办法记录。

那么他什么时候被唤醒?左边的Tick + 5。

13. 问: 老师,如果task3由于调用vTaskDelay后进入休眠。休眠时间还没有到的话,能不能用其他方式把他唤醒成就绪状态?

答: 一个任务调用vTaskDelay后,就被放入了delay list。我们怎样才能够把它从delay list,移到就绪链表?

  1. Tick中断函数判断时间到了
  2. 我找到一个函数,我认为是可以的,即使时间没到,别的任务也可以把它唤醒,这个没有做过实验,我会把它作为作业留给大家。

14. 问: 一个任务执行到A位置被打断了,未来某个时刻该任务还会被执行,接着从A位置执行,那这个A位置保存在这个任务的栈里?

在栈某个什么位置,这个位置有什么说法,为什么能找到他?

答:

大家沿着12345来看,假设任务1,调用函数A,A调用B, B调用C。

123:分别在栈里面画出了函数ABC的栈空间,

在函数C的运行过程中,假设是在X位置,被切换出去了。

X的值保存在PC寄存器里,PC寄存器的值保存在图中4的位置, 所有的寄存器都会保存起来。

并且,栈的当前位置SP也会记录在TCB结构体里。

以后,task1能够再次运行时,从TCB终找到栈,回复各个寄存器,也就回复了PC寄存器,也就从X位置继续运行了。

15. 问: 老师,X的值不是保存在C的栈里面吗?

答: 不是,在函数C里,你当前运行的什么位置,根本不是保存在函数C的栈里。

函数C的栈,保存的是C的局部变量等。

16. 问: 老师,这些宏配置的抢站或不抢占,轮转或不轮转,礼让或不礼让,这些宏配置在程序运行中还可以更改配置状态么?

答: 宏开关是用来决定某一段代码是不是要启用它,一旦编译程序之后,得到的可执行程序就没有办法再去改宏开关了。

一旦改宏开关,就要重新编译程序,重新烧写程序.

17. 问: 老师,当前任务是链表头的任务么,这个TCB指针是指向哪里的呢,能用图像的方法表示下任务是如何在链表中替换的么?

答:

1.创建任务一

当前tcb,指向任务一

  1. 创建任务二

当前tcb,指向任务二

  1. 创建任务三

当前tcb,指向任务三

  1. 启动调度,会创建空闲任务

当前tcb,还是指向任务三

  1. task3运行vTaskDelay后:

5.1 当前tcb,指向队列里原来的、最前面的任务1, 任务1,移到队列的最后面

  1. Tick中断里,轮到Task2运行:

当前tcb,指向队列里原来的、最前面的任务2,任务2,移到队列的最后面.

整个调度过程就这样的.

18. 问: 空闲函数执行一次只能清理一个任务,如果有两个任务需要清理就不可以了?

**答:**执行一次,清理所有任务.

19. 问: 韦老师,FreeRTOS里讲到的任务调度方式和RT-thread等其他RTOS一样吗?您讲过RT-thread里创建任务会有返回值,这个会不会引起任务调度方法的差异?

答: 基本是类似,
FreeRTOS里每一个Tick会判断是否切换 ,每个任务默认时间是一个Tick,RTT的任务可以指定能运行多少个Tick