通过临界区实现 RTOS 任务之间共享资源的保护
概述
上节在共享资源的介绍中我们介绍了共享资源面临的准确性、完整性容易遭到破坏的一些场景。
临界区是为了避免并行访问共享资源导致非期望或者错误行为而保护对应资源的一种机制。
具体来讲,临界区是一种上锁-去锁机制,建立临界区后,就对该段代码进行了上锁,其他任务、中断均无法再次进入该段代码,除非去锁。
典型的用法是:
taskENTER_CRITICAL();
code();
taskEXIT_CRITICAL();
注意,临界区其实只是一个通俗的使用上锁-去锁机制保护一段代码的概念,实现临界区的方法有很多,临界区本身的性质在不同 RTOS 系统中也不同,比如有的系统中临界区是支持嵌套的,有的则不支持嵌套。在大部分 RTOS 系统,通过**关闭全局中断来实现临界区机制。**即上述 API 对应的底层逻辑是:
ESP-IDF 中对临界区的实现是关闭不大于 configMAX_SYSCALL_INTERRUPT_PRIORITY
中断的所有中断(包括 SysTick 中断)。同时,一些 ESP32是双核 CPU 系统,因此不同于传统的 RTOS 系统,ESP-IDF 中的临界区中增加了一个变量来实现这种机制:
taskENTER_CRITICAL(&mux);// 加锁
code();//关键代码
taskEXIT_CRITICAL(&mux);// 去锁
其对应的底层逻辑是:
// 关闭全局中断,去获得 mux 锁
// 关键代码
// 重新启动全局中断,释放 mux 锁
需求及功能解析
临界区为什么能实现共享资源的保护
在 RTOS 中的任务调度与三种任务模型 章节中,我们介绍了任务的调度是通过 SysTick 中断完成的,即每个 SysTick 中断到来时都会触发任务调度器工作,检查是否需要切换任务,即更换 CPU 的使用权。
如前所述,临界区是通过关闭全局中断来实现的,这其中包括 SysTick 中断。即调用进入临界区的 API后,设备的中断停止了,这导致了两个方面的变化:
1)SysTick 中断被禁用,无法触发任务切换,因此临界区内的代码,不会被其他任务打断执行。
2)设备中断被禁用,无法响应新的中断,因此临界区内的代码,不会被其他中断打断执行。
所以,临界区内的代码在每次使用时都是独占的,完全不会被干扰。
临界区的副作用
1)对任务而言,SysTick 中断被禁用,无法触发任务切换,因此同优先级任务、高优先级任务均无法被执行,对任务的实时调度产生影响。
2)对中断而言,设备中断被禁用,无法响应新的中断,因此设备的中断响应实时性遭到破坏。
总结:得于斯者毁于斯,因此临界区在发挥作用时,也有对应得副作用。
临界区内代码的设计原则
1)临界区应尽可能短。如果可能,将尽可能多的处理和/或事件处理推迟到临界区之外。
2)临界段持续的时间越长,挂起的中断延迟的时间就越长,因此对于读操作,最好只是拷贝原时值,对于写操作,最好只是更改原始值的简单操作。
3)典型的临界区应该只访问几个数据结构和/或硬件寄存器。
4)FreeRTOS API不应在临界区内调用。
5)用户不应在临界区内调用任何阻塞(block)或会停止代码执行直到它完成才返回的函数(这类函数学名叫 yielding functions)。
示例解析
上节在共享资源的介绍中我们介绍了共享资源面临的准确性、完整性容易遭到破坏的一些场景。一种简单的示例是一个任务对全局变量执行 10 次 加1 操作,一个任务对全局变量执行 10 次 减1 操作,最终该全局变量的值一定是 0?答案是否定的。因此本节演示使用临界区这种机制后的情况,这种错误会被避免掉。
不添加临界区的情况下,执行的结果如下:
This is esp32 chip with 2 CPU core(s), WiFi/BT/BLE, Minimum free heap size: 295348 bytes
task1 done
task2 done
task3 done, and counter=-11
添加临界区得的情况下,执行的最终结果才正确为0,具体 log 显示如下:
This is esp32 chip with 2 CPU core(s), WiFi/BT/BLE, Minimum free heap size: 295340 bytes
task1 done
task2 done
task3 done, and counter=0
小伙伴们可以在测试程序中进行验证这两种情况。
讨论
1)ESP-IDF 中的临界区对嵌套使用的支持
支持嵌套,去锁的个数应与上锁的个数相同:
taskENTER_CRITICAL(&mux);
code1();
taskENTER_CRITICAL(&mux);
code2();
taskEXIT_CRITICAL(&mux);
taskEXIT_CRITICAL(&mux);
总结
1)临界区是为了避免并行访问共享资源导致非期望或者错误行为而保护对应资源的一种上锁-去锁机制。
2)实现临界区的方法有很多,在大部分 RTOS 系统,通过关闭全局中断来实现临界区机制,ESP32 中也是如此。
3)ESP32 是双核系统,因此对临界区的 API 添加了一个参数 mutex 来保证双核下的临界区工作正常。
4)临界区可以保证临界区内的代码不被中断、任务打断。
5)临界区有副作用,因此临界区内的代码应该尽可能短,并且不能包含任何延时或在阻塞。我们将在下节讲述影响更小的共享资源保护方法。
资源链接
1)Learning-FreeRTOS-with-esp32 系列博客介绍
2)对应示例的 code 链接 (点击直达代码仓库)
3)下一篇: