# 引言 在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. 信号量版本代码 ```c // 信号量版本 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. 任务通知版本代码 ```c // 任务通知版本 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占用减少一半以上。对于高频率、低延迟的同步场景,任务通知是更优选择。但需注意其限制,合理设计任务结构。建议开发者在实时性要求高的路径中优先考虑任务通知,并利用双核特性进行任务绑定优化。