<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Queue on Klin's Notebook 🍉</title><link>https://klinlike.github.io/tags/queue/</link><description>Recent content in Queue on Klin's Notebook 🍉</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><copyright>Klin</copyright><lastBuildDate>Thu, 17 Sep 2026 19:40:08 +0800</lastBuildDate><atom:link href="https://klinlike.github.io/tags/queue/index.xml" rel="self" type="application/rss+xml"/><item><title>RTOS多任务传数据：全局变量、环形buffer和队列</title><link>https://klinlike.github.io/posts/2026-09-17-rtos-task-ipc/</link><pubDate>Thu, 17 Sep 2026 19:40:08 +0800</pubDate><guid>https://klinlike.github.io/posts/2026-09-17-rtos-task-ipc/</guid><description>&lt;p&gt;多任务间传输数据常用的做法有三种：全局变量、环形buffer、队列。&lt;/p&gt;
&lt;h2 id="全局变量"&gt;全局变量
&lt;/h2&gt;&lt;p&gt;全局变量实现起来最方便，但是有硬伤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只能保存1份数据&lt;/li&gt;
&lt;li&gt;没有阻塞唤醒，消费者只能轮询，浪费CPU&lt;/li&gt;
&lt;li&gt;没有互斥，所以可能会读到没写完的半成品数据。因此只适合存简单数据，不适合比如一组相关的数据保存在结构体或者数组中。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="环形buffer"&gt;环形buffer
&lt;/h2&gt;&lt;p&gt;环形buffer本质就是一个数组+两个下标（&lt;code&gt;r&lt;/code&gt;+&lt;code&gt;w&lt;/code&gt;），在逻辑上把数组首尾接成环。
环形buffer是提供给生产者-消费者模式的两个任务用的。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;r==w&lt;/code&gt;，环形buffer就是空的。因此环形buffer数组的格子不能用完，因为用完也是 &lt;code&gt;r==w&lt;/code&gt; 了，也就是用1个格子换来了空和满不歧义。&lt;/p&gt;
&lt;p&gt;环形buffer不要加 &lt;code&gt;count&lt;/code&gt;，因为一旦加上这个变量，消费者和生产者两个任务就有可能同时写这个变量，而 &lt;code&gt;count++&lt;/code&gt; 在CPU上通常不是原子的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从内存读到寄存器&lt;/li&gt;
&lt;li&gt;寄存器 +1&lt;/li&gt;
&lt;li&gt;写回内存&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同样地，环形buffer也没有阻塞唤醒，需要任务自己去判断，可能会造成忙等。&lt;/p&gt;
&lt;h2 id="队列"&gt;队列
&lt;/h2&gt;&lt;p&gt;可以把队列理解为加了互斥保护和阻塞唤醒的环形buffer。&lt;/p&gt;
&lt;p&gt;有了互斥保护，环形buffer中不敢用的 &lt;code&gt;count&lt;/code&gt; 在队列中能用了。
也因为有了互斥保护，队列是可以多读多写的，不像环形buffer那样只支持生产者-消费者两个任务一读一写。&lt;/p&gt;
&lt;p&gt;队列内部其实有三样东西：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;环形buffer，用来保存数据&lt;/li&gt;
&lt;li&gt;send list，想写队列但是队列满了，在这里阻塞等待的任务&lt;/li&gt;
&lt;li&gt;receiver list，想读队列但队列空了，在这里阻塞等待的任务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;写队列成功后，会看 receiver list 里有没有任务，有就把第一个移动回就绪链表。
同样地，读队列成功后，会看 send list 中有没有任务，有就把第一个移动回就绪链表。
就是用这种方式，队列实现了阻塞唤醒。&lt;/p&gt;
&lt;p&gt;读到空队列的时候，任务会被同时挂到两个地方：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;挂进队列的 receiver list&lt;/li&gt;
&lt;li&gt;挂进系统的 delay list&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以一个阻塞中的读队列任务，会同时出现在两条链表里。&lt;/p&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;h2 id="队列如果在如果超时的一瞬间刚好被写入会怎样"&gt;队列如果在如果超时的一瞬间刚好被写入，会怎样？
&lt;/h2&gt;&lt;p&gt;这种情况是不会发生的，因为超时检查和操作队列都会先进入内核的临界区，会关中断、锁调度，保证了别的改队列的代码要等待当前这段代码执行完成。
这种临界区不是高级语言的那种 mutex 临界区，而是通过关中断实现的。一旦关中断，所有的代码都无法抢占运行了。也因为这样，关中断内执行的操作必须要短时间完成。&lt;/p&gt;
&lt;p&gt;更关键的是：读队列不是“被叫醒就立刻带着某种结果返回”，而是醒了再看一眼队列里到底有没有东西：
如果有东西，就当作是成功了，把数据拿走然后返回。
如果是空的，就可以确认是超时醒来的，会返回错误码。&lt;/p&gt;</description></item></channel></rss>