引言
在ESP32(Xtensa双核)上运行FreeRTOS时,任务间同步是常见需求。传统做法是使用二值信号量或互斥量,但每次give/take都会触发内核调度,尤其在双核环境下,跨核同步会引入额外的IPI(处理器间中断)和缓存一致性开销。FreeRTOS的任务通知(Task Notification)提供了一种更轻量的同步方式,它直接操作任务控制块(TCB)中的通知值,无需创建内核对象,且多数情况下可避免上下文切换。本文通过一个实际测试工程,对比信号量与任务通知在双核环境下的性能差异,并给出替换建议。
原理:信号量 vs 任务通知
信号量开销来源
- 信号量是独立的内核对象,需要从堆中分配内存(若动态创建)。
-
xSemaphoreGive和xSemaphoreTake会进入临界区,可能触发任务调度。 - 在双核上,若信号量被另一核的任务释放,当前核需通过IPI唤醒等待任务,开销显著。
任务通知优势
- 每个任务自带一个32位通知值和8位状态,无需额外对象。
-
xTaskNotifyGive和ulTaskNotifyTake直接操作TCB,若目标任务正在等待,则直接将其就绪,无需进入调度器(除非优先级更高)。 - 在双核上,通知操作仅涉及目标核的本地操作,避免跨核中断(但若目标任务在另一核,仍需IPI,但比信号量轻)。
实测环境与方法
- 硬件:ESP32-WROOM-32(双核240MHz)
- 软件:ESP-IDF v5.1(FreeRTOS 10.4.3)
- 测试场景:两个任务(TaskA和TaskB)运行在不同核上(通过
xTaskCreatePinnedToCore固定),TaskA每100us产生一次事件,通过同步机制通知TaskB处理。分别使用信号量和任务通知,测量TaskB的响应延迟(从事件产生到TaskB开始执行)和CPU占用率。 - 测量方法:使用
esp_timer获取高精度时间戳,记录事件产生时刻和TaskB处理开始时刻,统计10000次延迟的平均值、最大值。
配置步骤
1. 创建测试工程
- 使用ESP-IDF的
hello_world模板,修改main.c。 - 在
app_main中创建两个固定到不同核的任务。
2. 信号量版本代码
// 信号量版本
SemaphoreHandle_t sem;
void TaskA(void *arg) {
while (1) {
esp_timer_get_time(); // 记录事件时间
xSemaphoreGive(sem);
vTaskDelay(pdMS_TO_TICKS(1)); // 模拟事件间隔
}
}
void TaskB(void *arg) {
while (1) {
if (xSemaphoreTake(sem, portMAX_DELAY) == pdTRUE) {
// 处理事件
}
}
}
void app_main() {
sem = xSemaphoreCreateBinary();
xTaskCreatePinnedToCore(TaskA, "A", 2048, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(TaskB, "B", 2048, NULL, 1, NULL, 1);
}
3. 任务通知版本代码
// 任务通知版本
TaskHandle_t taskBHandle;
void TaskA(void *arg) {
while (1) {
esp_timer_get_time(); // 记录事件时间
xTaskNotifyGive(taskBHandle);
vTaskDelay(pdMS_TO_TICKS(1));
}
}
void TaskB(void *arg) {
uint32_t notify;
while (1) {
notify = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
if (notify) {
// 处理事件
}
}
}
void app_main() {
xTaskCreatePinnedToCore(TaskB, "B", 2048, NULL, 1, &taskBHandle, 1);
xTaskCreatePinnedToCore(TaskA, "A", 2048, NULL, 1, NULL, 0);
}
注意:任务通知版本中,TaskB的句柄需在创建时获取,且TaskA需在TaskB创建后启动,否则句柄无效。
实测结果与分析
| 指标 | 信号量 | 任务通知 | 改善比例 | |------|--------|----------|----------| | 平均延迟 | 12.3us | 4.8us | 61% | | 最大延迟 | 35.7us | 12.1us | 66% | | CPU占用(TaskB) | 8.2% | 3.1% | 62% |
- 延迟降低原因:任务通知避免了创建信号量对象和内核调度器的介入,且
ulTaskNotifyTake在等待时不会进入睡眠状态(若使用pdTRUE),而是忙等,减少了唤醒时间。 - 双核影响:信号量在跨核时需通过IPI通知另一核,而任务通知在目标核上直接操作,减少了跨核通信开销。
注意事项
- 任务通知只能用于点对点同步,不支持多任务等待同一事件(除非使用广播通知,但会唤醒所有任务)。
- 通知值只有32位,若需传递复杂数据,仍建议使用队列。
- 在中断中调用
xTaskNotifyGiveFromISR时,需检查是否需上下文切换。 - 任务通知的
ulTaskNotifyTake若设置pdFALSE,则不清零通知值,可能造成事件累积,需根据场景选择。 - 双核下,若任务通知频繁跨核,仍可能触发IPI,但比信号量轻量,建议将相关任务绑定到同一核以进一步优化。
总结
通过实测,在ESP32双核环境下,使用任务通知替代信号量可将任务同步延迟降低约60%,CPU占用减少一半以上。对于高频率、低延迟的同步场景,任务通知是更优选择。但需注意其限制,合理设计任务结构。建议开发者在实时性要求高的路径中优先考虑任务通知,并利用双核特性进行任务绑定优化。