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