按照嵌入式系统教材的通用分层,一个嵌入式系统自下而上由硬件层、中间层与应用软件层构成。硬件层提供处理器、存储器与外设接口,中间层提供板级支持与设备驱动,应用软件层则承载真正的业务逻辑。当业务逻辑复杂到一定程度,把整个软件写成一个大循环或者单一顺序流程就不再可行,实时操作系统于是引入多任务模型:把功能拆解成若干个任务,每个任务拥有独立的栈空间与控制流,由内核调度器决定哪个任务在什么时刻占用处理器。任务在软考与嵌入式教材中又常被称为线程,它是处理器资源分配与调度的基本单位,多个任务在同一处理器上并发执行,微观上轮流占有处理器,宏观上同时推进。
多个任务并发运行,立刻产生两类刚性需求。第一类是数据交换:采集任务从传感器读到的原始数据必须交给处理任务计算,处理任务算出的结果必须交给显示任务刷新界面,三段流水线之间数据要流动。第二类是行为协调:显示任务必须等处理任务把数据写完整之后才能读,处理任务必须等采集任务采集完毕才能开始计算,任务之间在时序上存在先后依赖。任务间通信就是操作系统提供的、让并发任务之间传递数据与协调时序的全部机制的总称。教材与考试认可的经典机制包括:共享内存、消息传递、信号量、互斥量、事件标志组、管道与套接字。其中共享内存与消息传递解决通信问题,信号量与互斥量解决同步与互斥问题,事件标志组解决多事件组合等待问题。
2023年下半年系统架构设计师综合知识真题第1题直接考察了这个概念。题目原文是:嵌入式操作系统中,任务间通信的方法中,哪一种是最直接、最明显的通信方法,也是访问共享的数据结构;哪一种作为更高级的通信方式,能够在同一处理器的各个任务间传递任意长度的信息。选项依次是共享内存、Socket、消息传递与信号量,参考答案分别是共享内存与消息传递。这道题的意义在于它揭示了命题人对该考点的组织逻辑:通信机制要放在两个维度上比较,一个是直接性,一个是灵活性。共享内存不经过任何中间环节,直接读写数据结构,因此最直接、最明显;消息传递把数据打包后交给内核转发,能够承载任意长度的信息,因此更高级。理解这两个维度,后续所有机制辨析就有了统一的坐标系。
通信与同步的分野值得单独强调。通信回答的问题是数据到哪里去,同步回答的问题是谁先走、谁等待。共享内存偏向通信,信号量偏向同步,消息队列两者兼备,互斥量专注互斥。很多考生把信号量误当成通信手段,认为信号量"传递了信息",这是概念模糊的根源。信号量的本质是一个计数器,它的变化只能表达数量的增减,不能承载具体的数据内容。把"传递数据"与"协调时序"两件事分开,这个考点的半壁江山就稳了。
第一条铁律是数据必须流动。嵌入式系统是数据加工流水线,采集、处理、显示、存储各个环节分属不同任务,任何两个环节之间都要有数据通道。第二条铁律是资源必须互斥。打印机、串口、共享缓冲区这类资源一次只能被一个任务使用,没有互斥保护必然产生数据错乱,工业控制与医疗设备中这类错误的代价不可接受。第三条铁律是时序必须确定。实时系统的正确性不仅取决于计算结果,还取决于结果产生的时间,任务必须在截止期之前完成,通信机制的延迟必须可预测、有上界。这三条铁律决定了任务间通信是嵌入式操作系统内核的标配组件,而不是可有可无的附加功能。
共享内存的本质是让多个任务直接映射同一块物理内存区域。这块区域可以是全局变量、静态数组、结构体,也可以是内核专门划出的共享内存段。任务甲把数据写入这块区域,任务乙直接从这块区域读出,中间不经过任何内核转发环节。数据从生产者到达消费者,路径就是一次总线读写,没有任何复制发生。这就是"最直接、最明显"的技术含义:数据通路最短,中间环节为零。
理解共享内存的速度优势,需要对照消息传递的复制开销。消息传递机制中,发送方要把数据从自己的缓冲区复制到内核维护的队列,接收方再从队列复制到自己的缓冲区,至少两次数据搬运。如果消息体较大,或者交换频率很高,复制的处理器开销与内存总线占用就相当可观。共享内存把这两次复制全部省掉,因此在吞吐量上具有绝对优势,是大量数据、高频交换场景的首选。命题人考察"最直接"时,背后的技术依据正是这个零复制特性。
共享内存的快是有代价的,代价就是互斥责任完全落在开发者肩上。多个任务并发读写同一内存单元时,读写操作不是原子的。以典型的先读后写竞态为例:计数变量的当前值是10,任务甲读出10,还未来得及写回,调度器切换到任务乙,任务乙也读出10,各自加1后分别写回,最终变量的值是11而不是12,两次操作只产生一次效果。这种结果取决于任务执行时序、无法预测的现象,就是竞争条件。消息传递为什么没有这个问题?因为内核在复制过程中完成了串行化,同一时刻只有一个任务在操作队列。共享内存没有任何中间人替你排队,快与不安全是一枚硬币的两面。
还有一个容易被忽视的细节:编译器优化会加剧竞态。现代编译器会在寄存器中缓存内存变量,循环中反复读取的变量可能被优化成只读一次。嵌入式开发中用volatile关键字修饰共享变量,正是告诉编译器该变量可能被其他任务或中断服务程序异步修改,禁止对其做寄存器缓存优化。共享内存的正确使用,从来都是硬件机制与语言机制配合的结果。
第一种武器是关中断。在单核处理器上,中断是打断任务正常执行的唯一外力。进入临界区前关闭中断,退出后恢复中断,可以保证临界区内的代码不被打断。但关中断直接破坏系统的实时响应能力,中断延迟随之上升,因此只适用于几个指令周期的极短临界区,例如修改一个指针、翻转一个标志位。第二种武器是原子操作。处理器提供测试并置位、比较并交换这类指令,在一个总线周期内完成读改写全过程,天然不可分割。原子操作适合保护单个变量的更新,例如计数器加一、标志位置位,是最高效的同步原语。第三种武器是互斥锁,也就是互斥量。长临界区的标准解法,任务获取不到锁就挂起等待,让出处理器,不做空转浪费。三种武器的选择原则是:极短临界区用关中断,单变量用原子操作,长临界区用互斥锁。
生产者消费者模式是共享内存最经典的应用形态。生产任务往环形缓冲区写入数据,消费任务从中读出,写指针只被生产任务修改,读指针只被消费任务修改,指针本身不需要额外的互斥保护,缓冲区满时生产任务等待,缓冲区空时消费任务等待。这个模型同时用到了共享内存承载数据、计数信号量记录空槽与满槽数量、互斥保护临界区,是任务间通信知识点的集大成者,也是真题命题的高发场景。环形缓冲区的满与空判断是经典易错点:当读指针与写指针相等时,缓冲区可能全空也可能全满,工程上要么牺牲一个槽位区分两种状态,要么引入计数器变量,命题人常在此处设置陷阱。
消息传递的模型与共享内存截然不同。发送任务把一段数据打包成消息,交给操作系统;操作系统把消息放进内核维护的队列;接收任务在合适的时机从队列中取出。消息经过内核中转,发送方与接收方彻底解耦:发送时接收方可以不在运行,接收时发送方可以已经结束,中间由队列缓冲。这种解耦是共享内存做不到的,共享内存的双方必须在时间上会合。
"更高级"体现在三个方面。第一,同步语义内建。消息队列的发送与接收操作各自可以选择阻塞与非阻塞,阻塞接收天然实现等待数据到达的同步效果,不需要再额外配置信号量,一个机制同时完成通信与同步两件事。第二,承载任意长度的信息。消息体的大小由开发者定义,内核只负责整体搬运,不关心消息内容,这与信号量只能表达一个整数值形成鲜明对比。第三,结构化管理。消息通常带有优先级属性,高优先级消息可以插队提前投递,紧急事件优先处理,这是共享内存无法提供的服务。
以经典轻量级内核的实现为例。μC/OS-II区分了消息邮箱与消息队列两种结构:邮箱只容纳一个消息指针,发送操作是覆盖式投递,后发覆盖先发;消息队列是消息指针的数组,按先进先出或者按优先级排队。FreeRTOS用队列控制块描述一个队列,记录队列长度、消息大小、头指针与尾指针,发送时把数据按字节复制进队列的存储区,接收时再复制出来。可以看出,内核复制是所有消息队列实现绕不开的一步。这正是它比共享内存慢的根源,也是它换来安全性的代价:复制完成之后,发送方的缓冲区如何修改都不会影响已经投递的消息,消息的完整性由内核保证。
队列满时的行为策略是命题人常考的细节。阻塞发送在队列满时让任务挂起,直到有空位;非阻塞发送立即返回失败;部分内核还支持丢弃策略,或者把队首最旧的消息挤出。中断
本篇完!