这个基础知识也是非常重要的,那我们要学好 FreeRTOS,这些都是必不可少的。
那么就来看一下本节有哪些内容:
首先呢就是介绍一下什么是任务调度器。接着呢就是任务它拥有哪一些状态了。那这里的内容不多,但是呢都是非常重要的。
那么首先呢就先来看一下第一部分:什么是任务调度器。
1. 任务调度简介
调度器:就是使用相关的调度算法来决定当前需要执行的哪个任务。
那我们说了,我们 RTOS 会创建很多个任务,那这个任务同一时刻只能执行一个,那要执行哪一个呢?就由这个调度算法来决定选择哪一个任务来执行。
FreeRTOS 一共支持三种任务调度方式:
- 抢占式调度。主要是针对优先级不同的任务,每个任务都有一个优先级(我们创建每一个任务都会分配一个优先级给它),优先级高的任务可以抢占优先级低的任务,然后去执行。
那这里要注意:FreeRTOS 是优先级的数值越大,它的任务优先级就越大。
- 时间片调度。主要针对优先级相同的任务,当多个任务的优先级相同时(多个任务可以挂载在同一个优先级下),任务调度器就会在每一次系统时钟节拍到的时候切换任务。
就比如说我这里创建了 3 个任务 t1、t2、t3,他们优先级是相同的。它一开始会执行 t1,执行一个系统时钟节拍,一个时钟之后,它就会切换到 t2,执行一个时钟,接着切换到 t3,执行一个时钟,接着它又回到 t1 执行一个时钟,这样轮流的执行下去。
- 协程式调度。当前执行任务将会一直运行,同时高优先级的任务不会抢占低优先级任务。
所以它的实时性其实相对来说是比较差的,这种方式呢它是比较适合以前的那些 mcu、ram 这些非常小的芯片,现在我们的 mcu 其实都比较强大,所以这种方式也慢慢的在被淘汰。
那这个调度方式呢大家只要简单的了解一下就行了。因为 FreeRTOS 他虽然现在还是支持的,但是官方已经明确表态了,他是不会再更新协程式调度这一方面的了,所以我们只需要大概了解一下。
这 3 大调度方式,FreeRTOS 都是支持的。
那我们介绍完这些了,我们主要就是来了解前两种。那么下面呢也给大家举了两个例子。
1.1 抢占式调度
首先我们来看一下抢占式调度。
那这里有这么一个图,纵坐标是优先级,横坐标是时间。
那这里有几个运行条件:
- 创建三个任务:Task1、Task2、Task3
- Task1、Task2、Task3的优先级分别为1、2、3
那这里大家注意:在FreeRTOS中任务设置的数值越大,优先级越高,所以TASK3的优先级最高。跟中断是不一样的,中断是数值越小,优先级越高。
那此时呢大家看这个图。我们怎么去描述它这么一个过程?我们看一下。
运行过程如下:
- 首先 Task1 在运行中,在这个过程中 Task2 就绪了,在抢占式调度器的作用下 Task2 会抢占 Task1 的运行,所以此时就会执行 Task2 了。
- Task1 它是先在运行,因为 Task2 和 Task3 还没就绪,就是还没准备好。
- Task2 的优先级是 2,是比 Task1 大的。
- Task2 运行过程中,Task3 就绪了,在抢占式调度器的作用下 Task3 会抢占 Task2 的运行,所以此时就会执行 Task3 了。
- Task3 运行过程中,Task3 阻塞了(系统延时或等待信号量等),此时就绪态中,优先级最高的任务 Task2 执行
按理来说,Task3 是最高优先级了。那 Task3 一直运行是不可能有低优先级的可以去抢占 Task3 的 CPU 使用权。
但是阻塞的时候,那最高优先级的任务进入阻塞了,那此时就要释放 CPU 的使用权给 Task2。所以大家注意了,当 Task1 被 Task2 抢占的时候,我们执行 Task2,那 Task1 就从运行态变成了就绪态;然后 Task2 它从运行态被抢占之后就变成了就绪态了,所以这两个目前它都是处于就绪态中,那这两个优先级谁最高,Task2 最高,所以当 Task3 被阻塞的时候,那此时优先级最高的任务就会执行,所以 Task2 它就会执行了。
- Task3 阻塞解除了(延时到了或者接收到信号量),此时 Task3 恢复到就绪态中,抢占 Task2 的运行,所以此时就会执行 Task3 了。
整个过程就是这样。
那我们这里来总结一下,抢占式有几个特点:
- 高优先级任务,优先执行
- 高优先级任务不停止,低优先级任务无法执行。
除非高优先级任务阻塞了,或者挂起了,那此时高优先级任务才会释放 CPU 的使用权给下面相对来说较高的优先级去执行。
- 被抢占的任务将会进入就绪态。
那这个是抢占式调度也比较清晰明了了。
1.2 时间片调度
接着再来看一下时间片调度。那什么是时间片,我们先来了解一下。
同等优先级任务轮流地享有相同的 CPU 时间(可设置), 叫时间片。在 FreeRTOS 中,一个时间片就等于 SysTick (滴答定时器) 中断周期。
那大家这里要注意:这里指的可设置,式子是中断周期。他不是说你可以设置一个时间片,两个时间片,三个时间片,不是这样;他只能一个时间片。在 FreeRTOS 中就是这样的;在 ucos 中你可以设置两个,三个,是可以的,在 FreeRTOS 中它是不支持的。也就是说比如我有三个任务,Task1,Task2,Task3,我执行 Task1 一次只能执行一个时间片,你不能说我执行两个时间片。那这一个时间片的大小由滴答定时器的中断周期决定,所以可设置是指滴答定时器的中断周期。在我们代码中,我们一个时间片设置是 1ms,也就是说滴答定时器它是 1ms 中断一次。
那么接着再来看一下这么一个图,纵坐标是优先级,横坐标是时间。
同样的,运行条件:
- 创建三个任务:Task1、Task2、Task3
- Task1、Task2、Task3 的优先级均为1;即 3 个任务同等优先级
那此时呢是怎么运行的呀?运行过程如下:
- 首先 Task1 运行完一个时间片后,切换至 Task2 运行
Task1 它会运行一个时间片。
-
同样的,Task2 运行完一个时间片后,切换至 Task3 运行。
-
Task3 运行过程中(还不到一个时间片),Task3 阻塞了(系统延时或等待信号量等),此时直接切换到下一个任务 Task1
Task3 按道理说也是执行一个时间片,但是天有不测风云,Task3 突然来了一个阻塞,此时 Task3 可能执行了 0.5 个时间片,还不到一个呢,就进入阻塞态了。那咋整,Task3 一直等么?不是,Task3 就直接让出 CPU 的使用权回到 Task1 运行了。
- Task1 运行完一个时间片后,切换至Task2运行
那大家肯定有疑问了,那 Task3 只运行了 0.5 个,那还有 0.5 个去哪了?就丢掉了。大家注意,它是直接就丢掉了,我不要了,它直接运行 Task1、Task2,下次运行 Task3 的时候,它还是只给它一个时间片的时间,而不是给它 1.5 个,这里大家要注意。
所以我们可以总结一下:
- 同等优先级任务,轮流执行;时间片流转。
它都是一个时间片,除非说你运行了什么阻塞态,挂起态这些。
-
一个时间片大小,取决为滴答定时器中断周期
-
注意没有用完的时间片不会再使用,下次任务 Task3 得到执行还是按照一个时间片的时钟节拍运行。
- 注意:任务中途被打断或阻塞,没有用完的时间片不会再使用,下次该任务得到执行还是按照一个时间片的时钟节拍运行。
比如说 Task3 只执行了 0.5 个,那还剩 0.5 个它不会再使用了,它直接就运行到 Task1 了。
那这些是时间片调度的一个说明。
2. 任务状态
那么接着就到这个任务状态。那任务究竟有哪些状态呢?我们来了解一下。
FreeRTOS 中任务总共存在 4 种状态:
- 运行态。正在执行的任务,该任务就处于运行态,注意在 STM32 中,同一时间仅一个任务处于运行态。
注意:RTOS 实际上同一时刻只有一个任务在运行,那这个运行的任务它就是处于运行态。
- 就绪态。如果该任务什么都准备好了,已经能够被执行,但当前还未被执行,那么该任务处于就绪态。
任务调度器选择执行优先级最高的就绪态。
- 阻塞态。如果一个任务因延时或等待外部事件(信号量/互斥量等)发生,那么这个任务就处于阻塞态
也就是说任务一直在等待某一个东西。
- 挂起态。类似暂停,把任务给暂停下来,此时任务就进入挂起态。调用函数
vTaskSuspend()
进入挂起态,需要调用解挂函数vTaskResume()
才可以进入就绪态。
注意:你挂起了要解挂,解挂之后是回到就绪态,而不是解挂之后直接到运行态。(阻塞态同理)
接着呢我们就来看一下,这 4 个任务状态它们之间的转换图:
看一下这个图。那我们说过,运行态它就是正在执行的一个任务,那我们看一下其他三种状态,它可以直接变为运行态吗?大家注意没有,只有就绪态它是可以直接变为运行态。像挂起态,它只能由运行态变为挂起,而不能由挂起态变为运行;阻塞也是同样的,它不能由阻塞态变为运行态,而只能由运行变为阻塞。
所以大家如果想要运行怎么办?你就必须就绪,就你必须什么都准备好了,你才有机会去运行。
所以总结一下:
- 仅就绪态可转变成运行态
那那么多任务那个要执行啊。取决于:
- 必须就绪态。
- 优先级最高。
优先级高了又就绪了,那比如说当前这个运行态阻塞了,那就给这个优先级高的就绪态的任务去运行。或者说当前这个就绪的任务比这个正在运行的任务它的优先级还高,那此时就会抢占 CPU 让当前正在运行的任务进入就绪态,然后这个更高优先级的任务就进入运行态。
- 其他状态的任务想运行,必须先转变成就绪态(唯一的途径)
- 挂起态,要进入就绪态,就是调用
vTaskResume()
这个函数解挂,解挂之后才能进入就绪态。- 阻塞态,要么就延时时间到了,要么就等待信号量接收到了,此时才能退出阻塞,进入就绪态。
那这是这四种任务状态之间的一个关系图,接着我们再来看一下任务状态列表 。
FreeRTOS 中无非就四种状态,运行态,就绪态、阻塞态、挂起态。这四种状态中,除了运行态,其他三种任务状态的任务都有其各自对应的任务状态列表 。
这里我们又引出了一个知识点:列表。那这个列表在 FreeRTOS 中可以说是无处不在的,非常重要,学习 FreeRTOS 这是必须要掌握的。那这个其实跟链表差不多,其实就是一个链子把任务都串起来,就形成了一个列表,其实列表和链表它们是大同小异的,差不多的一个概念。在这里我们只是简单的先来了解,在后面我们还有一个专门的一个列表专题,到时候会进行一个详细的介绍。
那么就来看一下这 3 种任务状态它们对应的一个列表:
- 就绪列表。pxReadyTasksLists[x](FreeRTOS 的一个源码复制出来的),其中 x 代表任务优先级数值。
- 如果是用软件的方式,那这个范围是无限的;
- 如果是用硬件的方式(如 STM32),那它的取值范围就是 0~31。
那在我们代码中,我们使用的是这种硬件的方式,比较高效。也就是说 x 取值范围就是 0~31。
任务调度器就在就绪列表中去搜寻,找到任务优先级最高的,然后去优先执行。
- 阻塞列表。pxDelayedTaskList
还有一个溢出阻塞,那这两个就形成一个闭环交叉,那这个在后面也会继续一个详细的介绍。这些只要大概知道就行了。
- 挂起列表。xSuspendedTaskList
当然在 FreeRTOS 中,它不单单只有这三种,还有其他一些列表,那么这里主要是分为三大类来进行大概的简单的一个介绍。
如果是就绪的任务,此时它就会把任务挂载到相应的就绪列表;如果是阻塞的任务,此时它就会把任务挂载到阻塞列表;如果是挂起的任务,此时它就会把任务挂载到挂起列表。这个列表的作用是非常大的,我们要从那么多的任务挑选一个来执行,就是通过列表来挑选的。
那我们来看一下,我们前面说了,我们的运行态就是正在运行的任务,它只能由就绪态直接变为运行态,而不能由 阻塞态/挂起态 变为运行态,所以反映到我们的列表中也是同样的道理,只有我们的任务挂载到就绪列表中,我们的任务才能被变为运行态,像挂载到阻塞列表、挂起列表是不行的,它们想要变成运行态必须先转成就绪,才有机会变成运行。所以说呢我们要挑选最高优先级任务去执行,也就是挑选任务变成运行态,从哪里挑选?从就绪列表去挑选最高优先级的任务,然后去执行,执行之后就变成运行态了。所以说我们这里说了,x 代表优先级数目就是 0~31,总共 32 个,所以我们初始化我们的列表的时候,我们的就绪列表它就有 32 个,我们初始化时它有 0~31 就 32 个。
那此时假设我创建 3 个任务,三个任务分别是 task1、task2、task3,它们的优先级分别是 31、30、29。那怎么挂载呢?那我们要注意我们新创建的任务,它是直接挂载到就绪列表中的,所以 task1 挂载到 31 这个位置,因为它的优先级是 31,所以 task1 在这里;task2 挂载到 30 这个位置;task3 挂载到 29 这个位置。那此时我们的任务调度器它就会在这个就绪列表中去搜寻了,搜寻哪一个任务它的优先级最大,那这里很明显 task1 最大,那此时他就会将 task1 转入运行态去运行了,就这么一个过程。
那大家肯定有疑问了,它怎么知道这个 31 里面有任务的?它这里会定义一个变量,32 位的一个变量,那这 32 位它分别对应就是 32 个就绪列表,当比如 31 这个列表有任务,它就会将这个 31 这个位给置 1,当没任务的时候,它就是 0,那此时就判断这个变量哪一个位是 1,就知道这个优先级的就绪列表里有没有任务。所以它就通过这 32 位的一个变量,当某个位置 1 就代表它所对应的那个优先级的就绪列表有任务存在,这样我就知道哪一个优先级最高。那这是它的就绪列表的搜寻。
接着,我们再举一个例子。那假设我们创建三个任务分别是:task1、task2、task3,优先级分别是:1、2、3,那很明显,task3 的优先级是最大的,它是最高优先级。
就绪列表,x 就是 0~31,所以初始化之后它是有 32 个列表了,但我们这里只画了三个,其实它还有很多的,只是我们这里只用到这 1、2、3,所以我们只画出了 1、2、3,其实其他都是有的,没画出来而已。
然后我们的 task3 它放在哪,它优先级是 3,放在 pxReadyTasksLists[3] 这里,task2、task1 同理。
那此时我们的任务调度器就会搜寻了,怎么搜寻啊?从上往下找,找你前面的那个变量的某一位有没有被置 1,像这里前面都没有任务的,肯定都是 0,直到走到这个 pxReadyTasksLists[3] 这里才会置 1,pxReadyTasksLists[2] 置 1,pxReadyTasksLists[1] 置 1,pxReadyTasksLists[0] 是 0,它这里没任务,只有前面三个有任务。
所以此时优先级最高是哪个,其实就是 pxReadyTasksLists[3] 这个,所以它会把 task3 转成运行态,所以 task3 它就会开始运行了。
那么 task2、task1 什么时候能运行,就当 task3 进入阻塞态或者挂起态,此时假如 task3 阻塞了,它就会把 task3 丢到阻塞列表里去了,那此时 pxReadyTasksLists[3] 里就没有了,那任务调度搜寻,从上往下找,因为 pxReadyTasksLists[3] 里没任务了,所以它是 0,所以此时 pxReadyTasksLists[2] 和 pxReadyTasksLists[1] 这两个是 1,pxReadyTasksLists[2] 的优先级更高,所以此时 task2 就会转成运行态。
同样的,假如 task2 还挂起啊,进入挂起列表,那 pxReadyTasksLists[2] 这里它又没了,那此时就绪列表里只有 task1 这一个了,pxReadyTasksLists[2] 这里也是 0,所以任务调度器只搜寻到 pxReadyTasksLists[1] 这里,所以只有 task1 它进入运行态。
除非说阻塞列表里阻塞的 task3 它恢复阻塞了,就是说它从阻塞状态给恢复出去了,比如它阻塞时间到了,它就会又重新回到就绪列表,那么此时任务调度器又搜寻,首先 pxReadyTasksLists[3] 这里它又变成 1 了,此时 task3 它就去抢占这个 task1 了。
整个过程其实就这样。所以说:调度器总是在所有处于就绪列表的任务中,选择具有最高优先级的任务来执行。
那相信大家肯定还有疑问:如果task1、task2、task3,优先级均为1呢,那任务调度器怎么处理的?相同优先级的任务会连接在同一个就绪列表上,首先 task1、task2、task3 它都会挂载到 pxReadyTasksLists[1] 这个就绪列表 1 中。那此时怎么运行呢?首先 task1 它会先运行一个时间片的时间,接着切换到 task2,运行一个时间片,接着切换到 task3,运行一个时间片,就这样又回到 task1 了,这样轮流的执行,这个就是相同优先级的一个处理情况,当然要你前面没有更高优先级的任务才行,比如说前面这个 pxReadyTasksLists[3] 列表有个 task4,那很明显,从上往下找,肯定是先找到 pxReadyTasksLists[3] 这个,那肯定是 task4 进入运行态,这下面的都没得运行,都在就绪态这里等着,除非 task4 挂起或者阻塞,那下面就能得以进行,就是这样。
那这就是我们任务状态的一个介绍了。
3. 总结
总结一下上面所学习的一个内容。
调度器作用:调度器总是在所有处于就绪列表的任务中,选择具有最高优先级的任务来执行。