4.1 同步原语


4.1 同步原语

本节摘要:同步原语控制「谁在什么条件下可以继续跑」。本节讲清临界区的三级保护手段、信号量与互斥量的本质区别、计数信号量的两类用法与事件组的多条件等待,并给出一套按场景选型的矩阵。

想象这样一个瞬间:采样任务刚把一个浮点值写到缓冲区的前一半,处理任务被唤醒,把前后两半拼在一起读走——它读到的是半个新值半个旧值。这类竞态不挑场合,只挑概率,于是总在实验室不出现、在客户现场高发。本节是第四章的第一节,目标是把「让两个执行流按约定顺序行动」的全部原语摊开讲透,下一节再谈数据本身的搬运。

临界区与三级保护

被多个执行流访问、且至少有一方在写的内存区域叫共享资源,访问它的代码段叫临界区。保护临界区有三级手段,强度与代价递增。

第一级是关中断,最硬也最霸道:处理器不再响应任何中断,截止期管理的根基被暂时拔掉,因此持有关中断的时间必须短到微秒级。适合保护「几行寄存器读写」这种极短操作。第二级是暂停调度:中断照常响应,但调度器不再切换任务,中断服务程序里若访问共享数据仍会竞态。适合纯任务间的短临界区。第三级是互斥量:粒度最细、持锁时间可以较长,还附带优先级继承,是任务间共享资源的默认选择:

/* 三级保护的典型用法 */ void vSensorWrite(float value) { taskENTER_CRITICAL(); /* 一级:短寄存器序列可用 */ REG_SAMPLE_CFG |= CFG_MASK; REG_SAMPLE_CFG &= ~CFG_MASK; taskEXIT_CRITICAL(); xSemaphoreTake(xCfgMutex, portMAX_DELAY); /* 三级:长临界区 */ g_calibration.offset = value * g_calibration.gain; g_calibration.ready = pdTRUE; xSemaphoreGive(xCfgMutex); }

纪律只有一条:临界区里不要调用可能阻塞的函数。抱着互斥量再等队列,等于把三把锁缠成一团,4.3 节的死锁正是从这里长出来的。

信号量与互斥量:长得像,不是一回事

这是本章最容易混淆、面试与事故中出现率都最高的一对。二值信号量只有「有、无」两种状态,语义是事件是否发生:中断给一个、任务等一个,谁「给」的都行,没有归属者。互斥量的语义是资源的独占权:谁加的锁必须由谁解,内核为此记录持有者。三个由此产生的硬性差别:

其一,互斥量支持优先级继承——低优先级任务持锁时,内核临时把它提升到等锁最高任务的优先级,堵住 4.3 节要讲的优先级反转;信号量没有持有者概念,无从继承。其二,互斥量禁止在中断里使用(持有者必须是任务),而信号量的给出天然属于中断。其三,互斥量支持递归持有——同一任务可重复加锁、等量解锁,适合函数多层调用都要持锁的代码结构。

结论可以背下来:保护共享资源用互斥量,同步事件通知用信号量。把二值信号量当锁用,系统多数时候正常,直到低优先级任务偶然拿着「锁」被中等优先级任务插队——反转现场就此成型。

计数信号量的两个身份

把二值扩展成计数,得到两种经典用法。当资源池计数器用:池里有五个缓冲区,信号量初值设五,取一块减一、还一块加一,取到零即排队等待——资源的数量约束变成了内核管理的等待队列。当事件积压计数器用:中断每来一帧数据给一次,任务一次取走处理,计数不会因任务暂时忙不过来而丢失事件个数(丢的是细节,保的是次数)。

/* 中断侧:事件到达即计数 */ void USART_IRQHandler(void) { BaseType_t xHigher = pdFALSE; uint8_t byte = REG_USART_RX; xSemaphoreGiveFromISR(xRxCountSem, &xHigher); portYIELD_FROM_ISR(xHigher); /* 需要时在出口切换 */ } /* 任务侧:逐个取出处理,天然串行化 */ void vRxTask(void *arg) { for (;;) { xSemaphoreTake(xRxCountSem, portMAX_DELAY); vHandleOneByte(); } }

注意中断侧的两个规矩:只能调用带 FromISR 后缀的版本;给出后用端口宏在出口统一请求切换——这就是 2.1 节说的「先记账、后切换」。

事件组:多个条件同时成立才放行

信号量表达「一件事发生了」,事件组表达「若干件事的与、或组合」。把每个事件编码成一位,任务可以等待「位一与位三同时置起」或「位二或位五任一置起」,典型如设备启动流程:网络就绪、传感器自检通过、配置加载完成,三者齐备才允许进入运行态。相比「三个信号量轮流等」,事件组把多条件等待变成一次原子判断,避免了「等齐的过程中条件又变化」的缝隙。

图:同步原语选型矩阵

图:同步原语选型矩阵

本节要点回顾

  • 临界区三级保护按强度与代价递增:关中断、暂停调度、互斥量;
  • 临界区内禁止阻塞调用,这是预防死锁的第一条纪律;
  • 信号量传事件、互斥量管独占,持有者概念决定了两者的全部差异;
  • 优先级继承只存在于互斥量,把信号量当锁用等于裸奔进 4.3 节的陷阱;
  • 计数信号量一物两用:资源池计数与事件积压计数;
  • 多条件同时满足的等待交给事件组,别用多个信号量手工拼接。

常见问题

问:临界区到底多短算短? 给一个可操作的标尺:关中断临界区以「几十条指令、微秒级」为限,因为它直接抬高全系统中断延迟的下限;互斥量持锁时间以「毫秒级」为上限,超过就应重新审视设计——持锁过久是反转与死锁风险的放大器。

问:递归互斥量什么时候用? 函数调用链多层都要持同一把锁、又不想把锁一路传递时。它是便利性与复杂度的交易:递归计数掩盖了「到底谁持有」的细节,滥用会让锁的边界模糊。优先重构调用结构,实在拆不开再用递归。

问:事件组与全局标志位变量有什么区别? 标志位加轮询是事件组的穷人版:它没有原子性(读改写之间条件可能变化)、没有等待队列(全靠轮询)。事件组把「条件齐备即唤醒」做成原子操作,语义与效率都不可同日而语。

问:同一把锁能既在任务又在中断里用吗? 不能。中断没有阻塞等待的能力,需要互斥的中断共享数据只有关中断一类手段。设计时把「中断写、任务读」的数据改成队列传递,绕开这道墙——这正是 4.2 节管线的又一价值。

一个综合小练习

拿一个真实场景串起本节:多路传感器 DMA 半块完成中断要通知处理任务、处理任务要与显示任务共享一帧结果、启动流程要求传感器自检与配置加载都完成后才允许运行。正确组合是:DMA 中断用计数信号量或直接队列投递(FromISR 版本);处理与显示任务间用互斥量保护帧并开继承;启动门槛用事件组按位等待。三种原语各就各位——选型的意义不是记住名字,是让每个约束都由最合适的机制承担。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U