队列传递的是具体的数据。信号量不传数据,只维护一个整数。
信号量有两个操作:Take 和 Give。
- Take 从信号量中拿走一个计数,也就是
cnt--,可以阻塞等到cnt非 0; - Give 放一个计数到信号量中,也就是
cnt++,到计数上限会立刻返回不等待。
信号量和队列的差别在于,队列还会把数据拷入环形buffer,信号量只会改计数,没有 payload。 也就是说,信号量是把队列中传输数据的那部分结构删除后剩下的:
- 去掉
r、w和环形buffer,因为没有数据 - 去掉 sender list,因为 Give 失败就返回
- 留下
cnt和 receiver list - 增加计数上限
max,相当于队列的长度
cnt 为 0 时候的阻塞和读空队列一样:把自己挂进等待链表;Give 时 cnt++,从链表头取出一个任务放回就绪链表,它再 cnt--,然后继续跑。
计数信号量和二值信号量
信号量就是一个整数加一条等待链表,创建信号量时要定两个数:上限 max 和计数的初值。
两种信号量机制完全一样,只是 max 不同:
max > 1:计数信号量,计数可以在 0 到max之间。max = 1:二值信号量,计数只能是 0 或 1。
不存在第二套机制。分成两个名字,是因为它们对应两种不同的用法。
用法A:限制同时使用某个资源的任务数
假设有 N 份资源,初值设为 N,表示全部可用。每个任务用资源之前 Take,用完之后 Give。这样,任何时刻处在 Take 和 Give 之间的任务最多只有 N 个。N > 1 时用计数信号量;N = 1 时就是"同一时刻只允许一个任务",也就是锁。
用法B:通知某件事发生了
初值设为 0,表示还没发生。事件发生的一方 Give,等待的一方 Take:没有事件的时候睡觉,有事件就被叫醒。 如果等待方还没来得及处理,事件又发生了好几次:二值信号量只能记录“至少发生过一次”。
Give到上限为什么不阻塞
只有同时满足两个条件,阻塞等待才是有意义的:
- 当前状态是协议允许的状态,只是暂时的
- 放弃这次操作会丢失信息
队列满了的时候,两条都满足:
- 生产者暂时比消费者快是正常现象
- 数据一旦丢弃就找不回来
所以写队列可以选择阻塞等待。
但如果信号量的计数到了 max,计数会代表什么呢?
如果用计数代表资源:每次 Give 都对应一次 Take。计数到了
max,说明所有资源都已经归还了。这时候再 Give,找不到对应的 Take,是调用方的逻辑错误。错误应该报告,而阻塞等待会把错误藏起来。如果用计数代表事件:计数到了
max,说明待处理的事件已存满,等待方一定会被叫醒。对二值信号量,多出来的这次 Give 没有带来新信息。对计数信号量,会丢掉一次计数,这时候应该把max设大一点,而不是让发信号的一方停下来等。
两种情况下,Give 阻塞等待都换不回任何东西!
此外,Give 很常在中断里调用,而中断不能阻塞。所以 FreeRTOS 把 Give 的等待时间固定为 0,信号量里也就不需要发送者等待链表。
Take唤醒的优先级
多个任务都在等待信号量的时候,等待链表会按照任务的优先级排序(通常是 xTaskCreate 时设定的优先级),优先级相同就先到先得。
这个排序是在插入时候实现的。当任务因为 Take 不到而休眠时,内核会按照它当时的优先级将它插入等待链表中合适的位置:
- 优先级高的排在前面;
- 优先级相同的,排在已有同级任务的后面,所以同级之间先来先得。
Give 时只叫醒链表最前面的那一个,所以高优先级任务先拿到。
这条规则不是信号量独有的,队列也是这样,因为信号量的底层就是队列。
Give在中断里调用,会发生什么?
Give 可以在中断里调用,这时候在 Take 中阻塞的任务会不会马上被唤醒取决于两件事:
- 被叫醒的任务优先级是否高于被中断打断的那个任务
- 中断结尾有没有请求切换
这个机制和前面队列说到的 xQueueSendFromISR 完全一样,因为信号量的底层就是队列。
中断里要用 xSemaphoreGiveFromISR(sem, &woken),不能用普通的 xSemaphoreGive。
它只做两件事:计数 +1,把等待链表最前面的任务移动到就绪链表(没任务等待就跳过)。
被叫醒的任务这时候只是可以跑了,并没有在跑。中断还没执行完,内核不会在中断里换任务。
退出中断的时候:
- 如果被叫醒的任务优先级更高,并且结尾调用了
portYIELD_FROM_ISR(woken),被叫醒的任务就会马上开跑; - 如果被叫醒的任务优先级更高,但是没有调用 yield,被打断的任务会继续跑、被叫醒的任务会在下一次调度的时候跑;
- 如果被叫醒的任务优先级相同或者更低,被打断的任务会继续跑。
woken 由 GiveFromISR 填写,只有被叫醒的任务优先级高于被打断任务时才会设为 pdTRUE,所以标准写法是:
| |
这样写,需要立刻切换时就会切,不需要时就回到原来的任务。