【RTOS训练营】作业讲解、队列和环形缓冲区、队列——传输数据、队列——同步任务和晚课提问

一:作业讲解

上一节课留了一个课后作业:

为什么IDLE任务的波形有大有小?

task2运行了1个tick,轮到idle任务。

idletask->hook->flagIdleTaskrun=1, 礼让,轮到task1运行,

task1从printf中间继续运行,打印完下一个字符后,才设置flagIdleTaskrun=0,

可以看到,flagIdleTaskrun等于1的时间:在idle任务里,也在task1里,

所以这个变量用来表示任务的运行时间:并不准确。

改成这样就没问题:

现在可以看到了,空闲任务运行的时间非常非常短:

二:队列和环形缓冲区

队列没什么复杂的,本质就是一个环形缓冲区,再加上任务的休眠和唤醒

对于环形缓冲区,它有如下要点:

  1. 有读写位置
  2. 写到buff的尾部之后,要绕到头部
  3. 读到buff的尾部之后,要绕到头部

这里再复习下环形缓冲区。

我们假设有一个数组,一开始里面没有数据,r = w = 0。

我要往里面写数据的时候,怎么写?

buf[w] = val;
w = (w+1);
if (w == 4)
	w = 0;

刚写的位置等于4的时候,就把它复位为0。

请添加图片描述

那怎么读数据呢?

val = buf[r];
r = r + 1;
if (r == 4)
	r = 0;

我们先不管空或者满,读写数据就是那么简单。

以面向对象的思想,我们就会抽象出一个结构体:

struct queue {
 char buf[4];
 int r;
 int w;
};

队列也是这么做的,我们来看看队列结构体长什么样:

图里面“环形缓冲区”那几个文字一个指向的是“写位置”,另一个指向的是“读位置”。

以前我们使用下标来表示读写的位置:

struct queue {
 char buf[4];
 int r;
 int w;
};

现在别人直接使用指针:

int8_t * pcWriteTo;  // 写位置
int8_t * pcReadFrom; // 读位置

这两个指针指向缓冲区在某些位置,这个缓冲区什么时候创建?

这个函数就会去分配一块内存,内存的长度就是: 1*sizeof(int)

我们这个例子里面,在这个队列里他只分配了一个元素的空间,你当然可以让他分配多个元素。

在以前讲环形缓冲区的时候,有同学问过一个问题:能不能够创建一个通用的环形缓冲区?可以用来传递任意大小的数据。

可以,队列就是:你可以指定这个环形缓冲区里每一个元素多大,有多少个元素。

可以看到,它创建一个队列的时候,会去创建一个结构体Queue_t,还会去分配保存数据的空间:

三:队列——传输数据

我们可以通过队列传递数据、同步任务,实现互斥操作。

怎么传递数据?

一个任务写对队列,另外一个任务读队列。

有同学对头和尾很容易混淆,在上面那个队列里我写了两个数据,红色斜线表示有数据。

第1个数据放在头部那里,第2个数据在尾部旁边。

要读数据的时候,先读第1个数据,就是从头部读。

假设读到了一个数据,头、尾就是这样的:

这时候如果我再写一个数据,头和尾就是这样的:

对于队列操作,我们一般来说是往尾部上写数据,

但是你也可以说:我这个数据比较着急处理,我可以写到头部去。

但读数据的时候,永远是从头部读。

四:队列——同步任务

我们可以通过队列来传递数据,那么怎么通过队列来同步任务呢?

我们以前写了一个项目:

业务系统在等待按键或者网络数据,我们可以把业务子系统写成一个任务。

读取按键,我我们也可以单独写成一个任务,

读取网络数据,也可以单独写成一个任务。

业务系统:他可以去读队列,没有数据的时候他就休眠。

谁把他唤醒?谁构造了数据、谁写了队列,就有这个任务去唤醒。

对于这几条语句,大家慢慢细品,我会提出很多问题:

task2读队列,因为没有数据正在休眠,

task1写队列,

task2优先级比task1高

task2会立刻被唤醒、立刻执行

任务2在等待数据,任务1写数据后,他怎么知道要去唤醒任务2?他怎么知道任务2在等待数据?

看看队列的结构体,里面有2个链表:

一个是用来管理那些等待空闲以便写入数据的任务,

另一个用来管理那些等待数据以便读出数据的任务。

task2读队列,因为没有数据而休眠,并且会把自己放在队列的这个链表上:xTasksWaitingToReceive

task1写队列,会去队列的这个链表:xTasksWaitingToReceive,挑出一个任务把它唤醒。

任务的切换,就是把任务放在不同的链表,再来分析task2读队列时:

1.因为没有数据而休眠 ==> 从ready list放到delay list

放入delay list是因为,可能它并不想死等,还给自己规定了超时时间,

时间到了,tick中断要从delay list把它唤醒。

2.并且会把自己放在队列的这个链表上xTasksWaitingToReceive

一个任务,想去读队列,但是队列里没有数据,就休眠:会把自己放入两个链表

我们再来看看任务1,任务2读不到数据就休息了,任务1写数据后发生了什么事?

task1写队列,会去队列的这个链表:xTasksWaitingToReceive,挑出一个任务把它唤醒?

在等待数据的链表xTasksWaitingToReceive,有很多个任务,有的优先级高,有的等待时间长。

谁优先级高唤醒谁,

如果优先级都相同,谁等待时间长,就唤醒谁。

以贴砖工人和拉砖司机作比喻,对比如下:

刚举的例子是:任务二想去读数据,没有数据就休眠。

反过来也是一样的:
任务1想去写队列,队列已经满了,他也可以休眠等待。

等待什么?等待别的任务去读队列,空出一个空间。

有学员反馈有点晕,通过直播来给大家演示一下。

直播回放:本晚课笔记所在目录后面

直播总结图:

五. 晚课学员提问

1. 问: 写队列,如果这个任务的优先级最高,会立即唤醒吗?

答: 假设task2读队列,因为没有数据正在休眠,

task1写队列,

task2优先级比task1高,

task2会立刻被唤醒、立刻执行。

一定要记住,实时操作系统,事关生死,高优先级的任务一旦就绪会立刻抢占低优先级的任务。

2. 问: 假设我在中断中写队列,读队列的任务在等待。那是立刻抢占还是等中断退出再抢占?

答: 中断退出之后,最高优先级的任务才可以执行。

任务不能够抢占中断,任务只有在中断处理完才可以执行。

3. 问: 如果任务因为等待队列被挂在xTasksWaitingToReceivedelay_list 时去删除这个任务,到时候xTasksWaitingToReceive怎么释放呢?

答: 删除任务之前 ,会把自己从xTasksWaitingToReceive这个链表删除掉。

4. 问: 一个任务想去读队列,但是队列里没有数据,就休眠,这个休眠的时间可以选择吗?

答: 可以选择。

0:没有数据我就返回一个错误,即刻返回。

portMAX_DELAY:没有数据我就永远等待,能够返回一定是已经得到的数据。

其他值:没有数据我就等待一会,如果一直没有数据的话,时间到了我就返回。

所以你要去判断返回值,成功就表示得到了数据,失败就表示没有数据。

5. 问: 唤醒等待数据的链表xTasksWaitingToReceive中的任务,要遍历一遍链表才能找到吗,还是放的时候就是按顺序的?

答: 放入链表的时候就要排序,优先级高的放在最前面、后面来等待的就放到后面去。

6. 问: 如果有多个任务想要这个数据,那么是只有一个任务才能拿到数据?

答: 是的,只有一个任务能拿到数据,也只有一个任务被唤醒。

7. 问: 老师,触发调度是通过tick中断还是唤醒后立马调度一次,这个中断是什么中断啊?

答: RTOS有些高优先级任务事关生死,高优先级的任务怎么可能等到下一个tick到来才去执行,因此FreeRTOS使用另一个中断来执行调度。

怎么触发调度、怎么调度,先不用管,

只要知道:task1唤醒了某个任务,如果那个任务的优先级更高,肯定会立刻发生调度,否则就不叫做实时操作系统了。

8. 问: 老师假如唤醒的是同等优先级的任务,是不是就是等待tick去触发调度了?

答: 是的。当前任务正在运行,你刚刚被唤醒,大家优先级都一样,不能抢占当前同优先级任务的时间。