一:作业讲解
上一节课留了一个课后作业:
为什么IDLE任务的波形有大有小?
task2运行了1个tick,轮到idle任务。
idletask->hook->flagIdleTaskrun=1, 礼让,轮到task1运行,
task1从printf中间继续运行,打印完下一个字符后,才设置flagIdleTaskrun=0,
可以看到,flagIdleTaskrun等于1的时间:在idle任务里,也在task1里,
所以这个变量用来表示任务的运行时间:并不准确。
改成这样就没问题:
现在可以看到了,空闲任务运行的时间非常非常短:
二:队列和环形缓冲区
队列没什么复杂的,本质就是一个环形缓冲区,再加上任务的休眠和唤醒。
对于环形缓冲区,它有如下要点:
- 有读写位置
- 写到buff的尾部之后,要绕到头部
- 读到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. 问: 如果任务因为等待队列被挂在xTasksWaitingToReceive
和 delay_list
时去删除这个任务,到时候xTasksWaitingToReceive
怎么释放呢?
答: 删除任务之前 ,会把自己从xTasksWaitingToReceive
这个链表删除掉。
4. 问: 一个任务想去读队列,但是队列里没有数据,就休眠,这个休眠的时间可以选择吗?
答: 可以选择。
0:没有数据我就返回一个错误,即刻返回。
portMAX_DELAY:没有数据我就永远等待,能够返回一定是已经得到的数据。
其他值:没有数据我就等待一会,如果一直没有数据的话,时间到了我就返回。
所以你要去判断返回值,成功就表示得到了数据,失败就表示没有数据。
5. 问: 唤醒等待数据的链表xTasksWaitingToReceive
中的任务,要遍历一遍链表才能找到吗,还是放的时候就是按顺序的?
答: 放入链表的时候就要排序,优先级高的放在最前面、后面来等待的就放到后面去。
6. 问: 如果有多个任务想要这个数据,那么是只有一个任务才能拿到数据?
答: 是的,只有一个任务能拿到数据,也只有一个任务被唤醒。
7. 问: 老师,触发调度是通过tick中断还是唤醒后立马调度一次,这个中断是什么中断啊?
答: RTOS有些高优先级任务事关生死,高优先级的任务怎么可能等到下一个tick到来才去执行,因此FreeRTOS使用另一个中断来执行调度。
怎么触发调度、怎么调度,先不用管,
只要知道:task1唤醒了某个任务,如果那个任务的优先级更高,肯定会立刻发生调度,否则就不叫做实时操作系统了。
8. 问: 老师假如唤醒的是同等优先级的任务,是不是就是等待tick去触发调度了?
答: 是的。当前任务正在运行,你刚刚被唤醒,大家优先级都一样,不能抢占当前同优先级任务的时间。