p41 ~ p44(约 54 分钟)
信号量的本质
信号量的本质是一个队列,只是不涉及数据的真正传输,只做「数据个数」的加减统计。名字拆开看:信号是提醒作用,量是计数作用。
用「三辆车想进城」来讲:进城要先到售票中心买票,票就是信号量,票的张数就是「量」。
Take(取票) 想进城先 Take。有票就计数减 1,拿票进城; 没票可以在售票处等(阻塞,把自己挂进等待链表)
Give(放票) 领导打电话来放票,计数累加,并唤醒等待者; 每放一张票只唤醒一个等待者。 Give 永不阻塞——放到上限后直接返回失败,不等待。结构上和常规队列对比着看:
常规队列 环形缓冲区 + 读位置 + 写位置 + 计数值 + 发送者链表 + 接收者链表
信号量 把队列的结构挪过来复用,但没有环形缓冲区、没有读写位置、 没有发送者链表,多了一个 max value(最大计数值), 对应队列的「深度」操作上也能一一对应:
写队列 send 拷贝数据 + count 累加 + 唤醒等数据的人Give 不拷贝数据 + count 累加 + 唤醒等票的人
读队列 receive 拷贝数据 + count 减减 + 唤醒等写的人Take 有票就 count 减减,不唤醒任何人(买票的顾客不会去叫醒领导)把票数限定为 1,同一时间就只能有一辆车进城——这就是互斥。
控制车辆运行
在第 17 个程序(遥控器 1/2/3 控制三辆车)基础上改出第 18 个程序:去掉按键读取和判断,一上来就显示汽车、隐藏原位置、调坐标重新显示,然后 vTaskDelay(50ms),到达最右边时任务退出(自杀)。三辆车没有任何约束,几乎同时从左冲到右。
第 19 个程序加一个「票站」信号量,初始值 = 2,于是只有两辆车能拿到票跑完全程。两种信号量:
/* 计数型:可以指定最大值和初始值 */SemaphoreHandle_t ticket = xSemaphoreCreateCounting(max, init);
/* 二值型:不需要参数,取值只有 0/1,创建时初始值就是 0 */SemaphoreHandle_t sem = xSemaphoreCreateBinary();[待确认] 计数信号量的最大值,字幕里说得自相矛盾(「写三也好……不要写三」),只能确定必须大于等于初始值 2。
汽车任务显示完自己的形状之后,先 xSemaphoreTake(ticket, xTicksToWait) 才能移动;超时值可以传 tick 数,也可以传 portMAX_DELAY(永远等待)。到达最右边时 xSemaphoreGive 释放,把票还给票站并唤醒下一辆。
第 20 个程序把初始值改成 1,同一时间就只有一辆车能跑,跑完释放再轮到下一辆。
唤醒顺序有个规则:等待链表里高优先级任务可以「后来先到」插到队首;同优先级按调用 Take 的时刻先后排;释放时取链表第一个运行。
二值信号量要手动 Give 一次置 1:直接创建出来值是 0,三辆车全都拿不到;得故意 Give 一次把值设成 1,再 Give 就无效了(最大值是 1,后面几次不起作用)。这时候一车跑到终点释放、二车跑、三车跑,依次进行。
优先级反转
计数型和二值信号量都可能出现优先级反转:正常是高优先级先跑,反转之后低优先级先跑、高优先级反而跑不了。
成因分三步(信号量初始值 = 1):
① 最低优先级的任务先被创建、先运行,Take 拿到信号量② 中优先级任务随后创建,优先级更高,会长时间运行③ 最高优先级任务最后创建,也想 Take 但失败(票已经被低优先级拿走了),阻塞等待结果是 3 号(最高)被 2 号(中)阻塞,这就是优先级反转。关键在中优先级任务:它不拿信号量,却一直占着 CPU,把持票的低优先级任务挤下去;低优先级不释放,高优先级就永远等。
在第 20 个程序基础上改出第 21 个程序,创建三个任务、优先级依次提高:
任务 1(低) 一进来 Take 信号量 → 移动汽车 → 到最右 Give 释放
任务 2(中) 复制任务 1,但不 Take 也不 Give; 先 vTaskDelay 让它稍后再跑,然后把移动里的 vTaskDelay 换成忙等 (一直查询时间、不进入阻塞状态),全程不放弃 CPU
任务 3(高) 最后启动,延时 2 秒后再 Take;拿到就跑到最右并 Give现象:任务 1 先跑,任务 2 一跑,任务 1 和任务 3 都动不了;任务 1 跑到终点自杀之后任务 2 才能跑,任务 1 释放信号量之后任务 3 才能跑——任务 3 反而最后运行。再改一下:让任务 2 一直不自杀、不放弃 CPU,任务 1 和任务 3 就都被卡死(任务 3 虽然有最高优先级,但信号量被任务 1 占着,任务 1 跑不了也就不会释放)。
生活里可以这么理解:学校机房有台超算,学生按指纹先用上了;主任带人来参观嫌吵,让先别用,堵着不走;校长有十万火急的事想用超算,却被学生占了先机,只能干等——学生被主任卡住迟迟不归还,校长越来越火。
互斥量:领导临时提拔你
用互斥量(MUTEX)解决优先级反转。第 22 个程序,在第 21 个基础上改。先用常规信号量的时候,最高优先级的第三辆车被中间那辆车卡住跑不了;把相关代码换成互斥量之后,第三辆车能启动,第一辆车到终点后第三辆车就可以启动。
机制可以这样讲。校长要等学生用完超算,但学生被参观的主任卡住了。于是校长在等待的时候临时「提拔」学生,把学生的级别提升到跟自己一样,学生就能压过主任,立刻继续用超算;学生用完、归还超算使用权之后,再乖乖恢复自己原来的级别——但它已经把校长唤醒了,校长马上用上超算,主任拦不住校长。
时间轴:
① 学生获得超算② 主任带人参观③ 校长想用,但超算被占,临时把学生提拔到校长同级,学生立刻续用④ 学生用完,恢复自己的级别,同时唤醒校长⑤ 校长用完⑥ 才轮到主任继续参观这就是优先级继承:学生继承了校长的优先级,释放互斥量之后恢复成原来的优先级。
互斥量是信号量的一种变种,额外实现了优先级继承和优先级恢复。创建函数是 xSemaphoreCreateMutex(),没有参数,返回 SemaphoreHandle_t。跟进源码可以看到,创建时数值先是 0,初始化时被 +1 提升为 1,也就是初始值为 1。
换互斥量的过程中程序一度卡死。原因是任务 2 正在刷新 LCD,最高优先级的任务 3 抢占进来之后忙等一个标志变量变成 0,而任务 2 还没把它清零、又跑不了,于是任务 3 死循环。这暴露了「用全局变量保护临界资源」的隐患。
顺手就把 LCD 和 MPU6050 共用的 I2C 总线改成用互斥量保护:在 lcd.c 里创建 LCD_I2C_Mutex,实现成一对函数 LCD_GetI2C() / LCD_PutI2C()(一开始叫 wait/release,后来改成 get/put 更对称)。访问 LCD、MPU6050 读写 I2C 之前先 Get,用完 Put,Get 的时候一直等待(阻塞当前任务)。同时补上 HAL 头文件,修掉 lcd.c 里漏写的分号。
[待确认] 互斥量变量名的字幕识别不清。
最终现象:任务 1 先跑,被任务 2 抢占,之后任务 1 和任务 2 交叉运行(看起来像同时跑,其实是切换很快)——因为任务 1 虽然被提升了优先级,但过程中调用了 vTaskDelay 进入阻塞,任务 2 才有机会跑,时间一到任务 1 又抢回来。任务 1 到终点释放,唤醒任务 3;任务 3 跑完自杀;任务 2 继续跑。
坑
- 二值信号量创建出来是 0,要手动 Give 一次才能用,再 Give 无效。
- 计数信号量的最大值必须大于等于初始值。
- 优先级反转的元凶是中优先级任务,它不碰信号量却一直占 CPU。
- 互斥量有优先级继承,信号量没有;要保护临界资源就别拿信号量凑合。
- 用全局变量保护临界资源迟早出事,I2C 这种共享总线应该用互斥量。
请输入编辑凭据,只有站点所有者可以修改文章。