<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Semaphore on Klin's Notebook 🍉</title><link>https://klinlike.github.io/tags/semaphore/</link><description>Recent content in Semaphore on Klin's Notebook 🍉</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><copyright>Klin</copyright><lastBuildDate>Thu, 08 Oct 2026 20:17:19 +0800</lastBuildDate><atom:link href="https://klinlike.github.io/tags/semaphore/index.xml" rel="self" type="application/rss+xml"/><item><title>从队列到信号量：计数、二值与中断里的 Give</title><link>https://klinlike.github.io/posts/2026-10-08-freertos-semaphore/</link><pubDate>Thu, 08 Oct 2026 20:17:19 +0800</pubDate><guid>https://klinlike.github.io/posts/2026-10-08-freertos-semaphore/</guid><description>&lt;blockquote&gt;
&lt;p&gt;前文：&lt;a class="link" href="https://klinlike.github.io/posts/2026-09-17-rtos-task-ipc/" &gt;RTOS多任务传数据：全局变量、环形buffer和队列&lt;/a&gt;、&lt;a class="link" href="https://klinlike.github.io/posts/2026-09-22-queue-set-isr/" &gt;队列集和队列句柄注册表：多对一与一对多&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;队列传递的是具体的数据。信号量不传数据，只维护一个整数。&lt;/p&gt;
&lt;p&gt;信号量有两个操作：Take 和 Give。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Take&lt;/strong&gt; 从信号量中拿走一个计数，也就是 &lt;code&gt;cnt--&lt;/code&gt;，可以阻塞等到 &lt;code&gt;cnt&lt;/code&gt; 非 0；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Give&lt;/strong&gt; 放一个计数到信号量中，也就是 &lt;code&gt;cnt++&lt;/code&gt;，到计数上限会立刻返回不等待。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;信号量和队列的差别在于，队列还会把数据拷入环形buffer，信号量只会改计数，没有 payload。
也就是说，信号量是把队列中传输数据的那部分结构删除后剩下的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;去掉 &lt;code&gt;r&lt;/code&gt;、&lt;code&gt;w&lt;/code&gt; 和环形buffer，因为没有数据&lt;/li&gt;
&lt;li&gt;去掉 sender list，因为 Give 失败就返回&lt;/li&gt;
&lt;li&gt;留下 &lt;code&gt;cnt&lt;/code&gt; 和 receiver list&lt;/li&gt;
&lt;li&gt;增加计数上限 &lt;code&gt;max&lt;/code&gt;，相当于队列的长度&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;cnt&lt;/code&gt; 为 0 时候的阻塞和读空队列一样：把自己挂进等待链表；Give 时 &lt;code&gt;cnt++&lt;/code&gt;，从链表头取出一个任务放回就绪链表，它再 &lt;code&gt;cnt--&lt;/code&gt;，然后继续跑。&lt;/p&gt;
&lt;h2 id="计数信号量和二值信号量"&gt;计数信号量和二值信号量
&lt;/h2&gt;&lt;p&gt;信号量就是一个整数加一条等待链表，创建信号量时要定两个数：上限 &lt;code&gt;max&lt;/code&gt; 和计数的初值。&lt;/p&gt;
&lt;p&gt;两种信号量机制完全一样，只是 &lt;code&gt;max&lt;/code&gt; 不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;max &amp;gt; 1&lt;/code&gt;：计数信号量，计数可以在 0 到 &lt;code&gt;max&lt;/code&gt; 之间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max = 1&lt;/code&gt;：二值信号量，计数只能是 0 或 1。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不存在第二套机制。分成两个名字，是因为它们对应两种不同的用法。&lt;/p&gt;
&lt;h3 id="用法a限制同时使用某个资源的任务数"&gt;用法A：限制同时使用某个资源的任务数
&lt;/h3&gt;&lt;p&gt;假设有 N 份资源，初值设为 N，表示全部可用。每个任务用资源之前 Take，用完之后 Give。这样，任何时刻处在 Take 和 Give 之间的任务最多只有 N 个。N &amp;gt; 1 时用计数信号量；N = 1 时就是&amp;quot;同一时刻只允许一个任务&amp;quot;，也就是锁。&lt;/p&gt;
&lt;h3 id="用法b通知某件事发生了"&gt;用法B：通知某件事发生了
&lt;/h3&gt;&lt;p&gt;初值设为 0，表示还没发生。事件发生的一方 Give，等待的一方 Take：没有事件的时候睡觉，有事件就被叫醒。
如果等待方还没来得及处理，事件又发生了好几次：二值信号量只能记录“至少发生过一次”。&lt;/p&gt;
&lt;h2 id="give到上限为什么不阻塞"&gt;Give到上限为什么不阻塞
&lt;/h2&gt;&lt;p&gt;只有同时满足两个条件，阻塞等待才是有意义的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当前状态是协议允许的状态，只是暂时的&lt;/li&gt;
&lt;li&gt;放弃这次操作会丢失信息&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;队列满了的时候，两条都满足：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;生产者暂时比消费者快是正常现象&lt;/li&gt;
&lt;li&gt;数据一旦丢弃就找不回来&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以写队列可以选择阻塞等待。&lt;/p&gt;
&lt;p&gt;但如果信号量的计数到了 &lt;code&gt;max&lt;/code&gt;，计数会代表什么呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果用计数代表资源&lt;/strong&gt;：每次 Give 都对应一次 Take。计数到了 &lt;code&gt;max&lt;/code&gt;，说明所有资源都已经归还了。这时候再 Give，找不到对应的 Take，是调用方的逻辑错误。错误应该报告，而阻塞等待会把错误藏起来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如果用计数代表事件&lt;/strong&gt;：计数到了 &lt;code&gt;max&lt;/code&gt;，说明待处理的事件已存满，等待方一定会被叫醒。对二值信号量，多出来的这次 Give 没有带来新信息。对计数信号量，会丢掉一次计数，这时候应该把 &lt;code&gt;max&lt;/code&gt; 设大一点，而不是让发信号的一方停下来等。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两种情况下，Give 阻塞等待都换不回任何东西！&lt;/p&gt;
&lt;p&gt;此外，Give 很常在中断里调用，而中断不能阻塞。所以 FreeRTOS 把 Give 的等待时间固定为 0，信号量里也就不需要发送者等待链表。&lt;/p&gt;
&lt;h2 id="take唤醒的优先级"&gt;Take唤醒的优先级
&lt;/h2&gt;&lt;p&gt;多个任务都在等待信号量的时候，等待链表会按照任务的优先级排序（通常是 &lt;code&gt;xTaskCreate&lt;/code&gt; 时设定的优先级），优先级相同就先到先得。&lt;/p&gt;
&lt;p&gt;这个排序是在插入时候实现的。当任务因为 Take 不到而休眠时，内核会按照它当时的优先级将它插入等待链表中合适的位置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优先级高的排在前面；&lt;/li&gt;
&lt;li&gt;优先级相同的，排在已有同级任务的后面，所以同级之间先来先得。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Give 时只叫醒链表最前面的那一个，所以高优先级任务先拿到。&lt;/p&gt;
&lt;p&gt;这条规则不是信号量独有的，队列也是这样，因为信号量的底层就是队列。&lt;/p&gt;
&lt;h2 id="give在中断里调用会发生什么"&gt;Give在中断里调用，会发生什么？
&lt;/h2&gt;&lt;p&gt;Give 可以在中断里调用，这时候在 Take 中阻塞的任务会不会马上被唤醒取决于两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;被叫醒的任务优先级是否高于被中断打断的那个任务&lt;/li&gt;
&lt;li&gt;中断结尾有没有请求切换&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个机制和前面队列说到的 &lt;code&gt;xQueueSendFromISR&lt;/code&gt; 完全一样，因为信号量的底层就是队列。&lt;/p&gt;
&lt;p&gt;中断里要用 &lt;code&gt;xSemaphoreGiveFromISR(sem, &amp;amp;woken)&lt;/code&gt;，不能用普通的 &lt;code&gt;xSemaphoreGive&lt;/code&gt;。
它只做两件事：计数 +1，把等待链表最前面的任务移动到就绪链表（没任务等待就跳过）。
被叫醒的任务这时候只是可以跑了，并没有在跑。中断还没执行完，内核不会在中断里换任务。&lt;/p&gt;
&lt;p&gt;退出中断的时候：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果被叫醒的任务优先级更高，并且结尾调用了 &lt;code&gt;portYIELD_FROM_ISR(woken)&lt;/code&gt;，被叫醒的任务就会马上开跑；&lt;/li&gt;
&lt;li&gt;如果被叫醒的任务优先级更高，但是没有调用 yield，被打断的任务会继续跑、被叫醒的任务会在下一次调度的时候跑；&lt;/li&gt;
&lt;li&gt;如果被叫醒的任务优先级相同或者更低，被打断的任务会继续跑。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;woken&lt;/code&gt; 由 GiveFromISR 填写，只有被叫醒的任务优先级高于被打断任务时才会设为 &lt;code&gt;pdTRUE&lt;/code&gt;，所以标准写法是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;BaseType_t woken &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pdFALSE;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;xSemaphoreGiveFromISR&lt;/span&gt;(sem, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;woken);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;portYIELD_FROM_ISR&lt;/span&gt;(woken);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这样写，需要立刻切换时就会切，不需要时就回到原来的任务。&lt;/p&gt;</description></item></channel></rss>