队列集和队列句柄注册表:多对一与一对多

上一篇:RTOS多任务传数据:全局变量、环形buffer和队列

上一篇文章我们讨论过队列,但是单纯使用队列有时候会很受限。比如处理多个队列的输入时,要么就轮询:一旦某个队列没数据,就只能卡住等待超时或者有数据输入;要么就创建多个任务,每个任务处理一个队列。这两种方式都不够高效,所以我们需要引入队列集。

队列集里保存的并不是队列本身,而是多个队列的句柄。它可以同时监控多个队列中是不是有任意一个队列来数据了,效果类似 IO 多路复用里的 epoll。

但 FreeRTOS 的队列集不像 epoll,它能做的事情很窄,只能覆盖“可读”,无法监控“可写”。具体是这样实现的:某个成员队列添加数据成功后,内核把这个队列的句柄丢进队列集。等待集合的任务会被唤醒,唤醒后的任务知道了哪条队列有数据,就能主动获取对应的数据。

如果想实现队列集的可写检测,需要自己实现,FreeRTOS 的内核不提供。

队列集是边沿触发的

队列集是边沿触发,这意味着只有往队列里添加数据成功的那一下才会把句柄扔到队列集里。如果队列被加入队列集之前就已经有数据,不会再补通知。

这里可能会有坑:如果在队列集建立之前,队列中就有数据甚至已经满了,则可能会有数据一直得不到处理,甚至导致阻塞。

队列句柄注册表

但是队列集只解决了一个任务处理多个队列信号的问题。如果是多个任务都想用同一份信号,就可以通过队列句柄注册表来实现。

具体实现方法是:在驱动中创建一个注册表,如果有需要获取这个驱动对应信息的任务,就自己创建队列,并把队列句柄注册到这个表中。有新事件的时候,驱动自动把事件放到每一个队列句柄中。

1
static QueueHandle_t s_qs[Q_MAX];

需要注意的点:

  1. 要先注册好,再启用 ISR,不然注册表一边更新,事件一边分发,可能会出现异常。
  2. 中断中分发事件只能是非阻塞的 xQueueSendFromISR
  3. 要注意订阅者不要太多,不然中断时间会比较长。

xQueueSendFromISR

xQueueSendFromISR 这个 API 值得单独展开讲,这里涉及一系列机制,我们从下往上说。

xQueueSendFromISR 成功写入后,也会从队列的 receiver list 中取出等待任务挂到就绪链表。此时任务是 Ready,不是 Running。

调度器

一个任务进入 Running,需要唤醒切换两个步骤。唤醒只是把任务挂到就绪链表,此时 CPU 可能还在别的任务手里。要切换到了这个任务,才算是真的进入了 Running 状态。

xQueueSend 会切任务

在任务 A 中调用 xQueueSend 的时候,会:

  1. 关临界区,把数据放到队列里
  2. 如果队列叫醒了更高优先级的任务 B,就把 B 加入到就绪链表
  3. xQueueSend 内部会调用 taskYIELD()
  4. 调度器会比较就绪链表内的多个任务优先级,发现存在更高优先级的任务,于是保存 A 并切换任务

所以会有一种感觉:一旦 Send 唤醒了更高优先级的任务,当前任务会立即被抢走执行权。

在 ISR 里不能马上进行任务切换

如果在 ISR 里写完一个队列马上就进行切换了,后面的其他队列可能永远都没有办法被 Send。这相当于 ISR 都没有被执行完就消失了,ISR 的中断标志可能还没清除。

所以约定:ISR 里只改内核数据结构(队列、就绪链表),不在这里换当前运行任务。

因此,xQueueSendFromISR 里不会进行 taskYIELD()。如果发现队列唤醒了更高优先级的任务,只会把 *pxHigherPriorityTaskWoken = pdTRUE

yield 是中断回去之后进行一次任务调度

ISR 完整跑完,然后用硬件的“中断返回”回到线程模式时,正好可以在此刻插入一次任务切换:还没回到被中断打断的任务,先看看就绪链表里谁的优先级最高。

这里调用 portYIELD_FROM_ISR(woken) 的含义是:

  • woken == false:没有更高优先级的任务被唤醒,继续跑被打断的任务
  • woken == true:有更高优先级的任务被唤醒,请求一次调度

如果忘了 yield,其实任务也已经就绪了,只是没有发出调度请求。CPU 会回到被打断的任务继续跑,直到下一次调度才会被切换到更高优先级的任务。