
记得小时候看《天龙八部》,少林寺被围攻时,方丈问虚竹外面有多少人。虚竹说“好多人”,方丈却故作高深地答“错,只有两个人,名和利”。这教科书式的装逼,我本可以给90分。

可惜后来情节急转直下:刀架脖子上了,方丈才想起问真实人数,虚竹却牢记“教诲”仍说“两个人”,气得方丈推门一看,立马被打脸。

每次想到这,我都忍俊不禁。如今轮到我来说“计算机科学只有两个问题——时间和空间”时,也不免担心有巴掌等着。但事实并非故弄玄虚,接下来的文章,我们会用一系列实例来探讨嵌入式乃至计算机科学中,如何处理这对核心矛盾。
今天,就从流(Stream) 与块(Block) 的使用哲学开始。
串行的“流”和随机的“块”
虽然不是Linux首创,但“流文件”和“块文件”的概念让流(Stream)与块(Block)的处理思想深入人心。本质上,它们代表了数据处理中两种截然不同的策略:“以时间换空间” 和 “以空间换时间”。
1、以空间换时间的块处理
块处理最显著的特征,是将待处理数据保存在一段连续的存储器中,允许我们随机地以任意顺序和方式进行访问和处理。简单说,就是牺牲存储空间来换取数据访问的便利性,降低访问和处理的时间成本。因此,块处理是典型的用存储器空间换取处理时间的策略。
举例来说,要处理一段文字中的几个单词,我们可以先将整段文字连续存入RAM,再用基于块的字符串函数操作。由于是随机访问,处理效率主要由算法决定,访问的顺序和次数几乎没有限制。
这里需要特别强调两点:
块处理中,字符串处理的效率瓶颈由算法本身决定,数据访问成本固定且最低。块处理的优势在于,以任意顺序访问任意数据时,成本最低。它常被理解为批量处理,是效率的代名词。
流处理中,字符串处理的效率瓶颈,往往受限于 “从串行流中获取下一个数据的时间成本”。有时,处理数据本身的时间甚至小到可以忽略。
常见情况是:算法处理好当前字符后,“等”了很久,下一个字符还没就绪。或者,处理一个字符的时间,远少于从流中读取下一个字符的耗时。实际应用中,这两种情况常同时出现——比如任务从串口缓冲区读取字符进行帧解析时,队列操作和等待字符接收的时间,往往比单个字符的处理时间长好几个数量级。

小结一下:块(Block)处理就是消耗存储器空间存储目标数据,让处理算法能以任意有利的方式(随机)访问,从而提升整体吞吐量(节省时间)的数据处理方式。这是一个以存储器空间换取访问时间的策略。
块的表现形式,是一段可随机访问的存储器空间。“可随机访问”是其本质,并不要求数据在物理上连续(例如,可用哈希算法关联离散的数据块)。
2、以时间换空间的流处理
流处理最显著的特点是数据在时间轴上的单向性,也就是串行性。处理方一次只能接收或处理一个数据单元。最大的限制是访问顺序——必须按发送方提供的顺序进行。正是这种限制,让流的处理方常感别扭。
因此,为了访问方便,常会将部分流先缓冲成小块,再以块方式处理,例如串行通信中的数据帧(Frame)。
如果不是因为“贫穷”(如带宽、存储器大小限制),谁愿意限制自己的访问自由呢?

如果“贫穷”是宿命,不妨换个角度:通过对访问顺序加以限制,我们可以节省大量资源(如带宽、缓存)。因此,流处理的优势显而易见——无需大块存储空间暂存数据,这在资源紧张(尤其是SRAM小的MCU)中非常流行。

代价是:受限于访问顺序,流处理算法往往更复杂,某些功能甚至无法单靠流处理实现(需局部结合块处理);同时,数据存取造成的等待时间浪费也很可观。可以说,流处理的本质是“零库存”手工作坊,一切必须按部就班。提高流处理效率的关键,是把数据传输的等待时间以流水线方式利用起来。
再小结一下:流(Stream)处理是以消耗更多处理器时间,并增加访问顺序限制为代价,来节省存储器空间的一种处理方式。这是一个以时间换空间的策略。

流块互转
流和块各有优劣,在嵌入式系统中,能否结合它们的特点,根据场景只凸显优势而弱化缺点呢?

在典型的嵌入式数据流中,每个处理环节(Process)对时间和空间的偏好不同。请注意:
- 数据流可能涉及多个子系统(甚至远端服务器)。
- 有的Process对性能(时间)敏感且RAM宽裕,应利用块处理“以空间换时间”,将RAM优势转化为性能优势。
- 有的Process对空间敏感(常源于成本)、对性能要求不高(如低波特率UART、红外遥控),此时采用流处理换取成本优势几乎是不二之选。
那么,当相邻Process偏好不同时,如何协调?
场景一:生产者用“流”,消费者用“块”
- 消费者提供一个队列Q,并将用于保存数据的内存块MEM提供给Q作为缓冲区,初始化为空队列。
- 永久封堵Q的出队接口。
- 将Q的入队接口提供给生产者。
- 当队列满时,将MEM取出,交给消费者处理。
- 如果MEM是堆分配的,则尝试从堆获取新MEM,重复步骤1;消费者处理完MEM后,应在适当时机将其释放回堆。
场景二:生产者用“块”,消费者用“流”
- 生产者提供一个队列Q,并将保存数据的内存块MEM提供给Q作为缓冲区,初始化为满队列。
- 永久封堵Q的入队接口。
- 将Q的出队接口提供给消费者。
- 当队列为空时:1)若MEM是堆分配的,则将其释放回堆,等待生产者提供新数据块,回到步骤1;2)若MEM是静态分配的唯一块,则此时应将其归还给生产者用于下一次数据生产。
通过以上描述,我们发现队列(Queue) 在流块转换中扮演了关键桥梁。这里不仅引入了“将队列初始化为空”,还引入了“将队列初始化为满”的概念。
简单来说:封住出口(初始化为空)的队列,可将流转为块;封住入口(初始化为满)的队列,可将块还原为流。队列是一个典型的“时空转换”机构。

题外话:队列不是万能药
记得小学时那道“游泳池一边进水一边放水”的题吗?
- 当进水速度 > 出水速度,水位必涨,池深只决定多久会溢出。
- 当进水速度 < 出水速度,水位必降,根本存不住水。
队列本质上就是个游泳池。如果入队速度恒定大于出队速度,队列深度只决定它多久会爆满。但如果入队速度平均小于等于出队速度,那么当入队速度瞬时超过出队速度时,队列的深度直接决定了它能缓冲这种“瞬时高峰”多久。
很多人把队列当成万能药,觉得数据传输“可能”速度不一致,就用一个,却不管这差异是瞬时的还是恒定的。仿佛用了队列就能高枕无忧,把脑袋埋进沙子里,通信问题就不存在了:“我用队列了啊?为什么还有问题?”——这种想法实在要不得。
在复杂的系统设计中,理解队列作为时空转换器的本质与适用边界,远比盲目使用更重要。希望本文的探讨,能帮助你在云栈社区的交流与实践中,更精准地运用这一工具。