一:上节回顾
在上一节课我们贴了这么一个图:
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后为什么没有继续执行被抢占的任务呢?
答: 假设:
- task1运行,它被放到的就绪队列的尾部
- task3就绪,抢占任务1
- 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中断。
流程是这样:
- Task3调用vTaskDelay(5); 当前tick计数值假设是10
- 过了1ms,tick计数值累加为11
- 再过1ms,tick计数值累加为12
- ……
- tick计数值累加为15时,就把task3从delay链表移到就绪链表,并且检查发现,task3是优先级最高的就绪任务
- 发起调度
从这个流程,你发现:对于因为调用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,移到就绪链表?
- Tick中断函数判断时间到了
- 我找到一个函数,我认为是可以的,即使时间没到,别的任务也可以把它唤醒,这个没有做过实验,我会把它作为作业留给大家。
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,指向任务一
- 创建任务二
当前tcb,指向任务二
- 创建任务三
当前tcb,指向任务三
- 启动调度,会创建空闲任务
当前tcb,还是指向任务三
- task3运行vTaskDelay后:
5.1 当前tcb,指向队列里原来的、最前面的任务1, 任务1,移到队列的最后面
- Tick中断里,轮到Task2运行:
当前tcb,指向队列里原来的、最前面的任务2,任务2,移到队列的最后面.
整个调度过程就这样的.
18. 问: 空闲函数执行一次只能清理一个任务,如果有两个任务需要清理就不可以了?
**答:**执行一次,清理所有任务.
19. 问: 韦老师,FreeRTOS里讲到的任务调度方式和RT-thread等其他RTOS一样吗?您讲过RT-thread里创建任务会有返回值,这个会不会引起任务调度方法的差异?
答: 基本是类似,
FreeRTOS里每一个Tick会判断是否切换 ,每个任务默认时间是一个Tick,RTT的任务可以指定能运行多少个Tick