别用信号量了——FreeRTOS任务通知到底有多香
// 就这个函数让我debug了三个小时 void vTaskA(void *pvParameters) { uint32_t ulNotifiedValue; for(;;) { ulNotifiedValue ulTaskNotifyTake(pdTRUE, portMAX_DELAY); if(ulNotifiedValue 5) { process_batch(ulNotifiedValue); } } }上周做一个小项目两个task之间传数据我第一反应就是上信号量。毕竟从学RTOS第一天开始老师就告诉你信号量是用来同步的嘛。结果写完后发现一个问题——高频场景下信号量带来的上下文切换开销比我预想的大得多。然后翻了一下FreeRTOS的手册发现有个叫Task Notification的东西从V8.2.0就有了。说实话我之前一直以为这玩意儿就是个阉割版信号量根本没当回事。直到这次被逼到墙角仔细看了一遍才发现——这东西才是亲儿子。对比一下就知道了。如果用二进制信号量做同步你得先creat一个Semaphore然后give/take。光create这一步就要走一遍队列机制消息传递路径大概是task→queue→另一个task。中间怎么也得走好几个函数调用。换成Task Notification呢// Task A - 发送方 xTaskNotifyGive(xTaskBHandle); // Task B - 接收方 ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100));就这两行。没有额外的queue创建没有多余的IPC对象。每个TCB里本来就预留了一个32位的notification value和state完全零额外开销。调用xTaskNotifyGive本质上就是在目标task的TCB里写一个值然后检查一下优先级——如果对方的优先级比自己高就立刻切换。我之前做的一个项目每秒要处理几千次事件同步用信号量写出来功能没问题但是用示波器抓了一下任务切换的频率明显偏高。换了Task Notification之后单看任务切换次数至少降了三分之一。不过这东西有个坑。它的工作方式跟信号量有个本质区别——信号量是独立的同步对象而Task Notification是绑定在task上的。意思就是你没办法用一个Notification去同步多个task。Notification是一对一的一个特定的task只能被一个sender通知。如果你想做一对多、多对一、多对多这种复杂的同步拓扑Notification就不够用了得老老实实上队列或者事件组。还有一个容易翻车的地方——Notification value溢出。// 坑人的场景 void vSender(void *pvParameters) { for(;;) { // 连续调用give多次 xTaskNotifyGive(xReceiverHandle); xTaskNotifyGive(xReceiverHandle); xTaskNotifyGive(xReceiverHandle); vTaskDelay(pdMS_TO_TICKS(1)); } } void vReceiver(void *pvParameters) { for(;;) { // 这里take一次以为只少了一个count uint32_t count ulTaskNotifyTake(pdFALSE, portMAX_DELAY); // pdFALSE表示不管累积了多少只取一次 // 实际丢了多少个通知你根本不知道 } }注意ulTaskNotifyTake的第二个参数。pdTRUE是一次性清空拿到累积的所有通知次数pdFALSE是只取1剩下的不清空。很多人以为pdFALSE会按信号量的方式一个一个消费实际上它只是拿1个不清空计数——如果你发得快收得慢notification value会一直往上堆直到0xFFFFFFFF然后溢出归零。别问我怎么知道的。所以如果你用Notification来做counting semaphore的替代品一定要想清楚你的场景- 生产者快于消费者 → 用pdTRUE批量消费- 一对一精确同步 → Notification是最佳选择- 需要多个task同时等待同一个事件 → 还是用队列或事件组吧另外Notification还有一个骚操作——直接传32位数据。// 发送时还能带个值过去 xTaskNotify(xTaskHandle, 0x12345678, eSetValueWithOverwrite); // 接收端 uint32_t value; xTaskNotifyWait(0, ULONG_MAX, value, portMAX_DELAY); // value 0x12345678这就不是信号量能干的事了。等于说Notification把同步和传一个整数这两个事合并了。有些简单的状态机通知场景一个Notification搞定省掉一个队列的开销。之前看到有人问有了Notification是不是就不用学信号量了我觉得不是。这俩是不同维度的工具。Notification胜在轻量和速度适合高频、一对一的场景信号量适合需要独立同步对象、多对多拓扑、以及需要明确的release-acquire语义的地方。FreeRTOS文档里自己都说了Notification的主要限制就是each task can only have one notification state。最后放一个我常用的写法生产者和消费者之间传一个简单的阈值状态void vSensorTask(void *pvParameters) { uint32_t reading; for(;;) { reading read_sensor(); if(reading THRESHOLD_HIGH) { // 带数据通知——告诉处理任务具体读数 xTaskNotify(vProcessTask, reading, eSetValueWithOverwrite); } else if(reading THRESHOLD_LOW) { xTaskNotify(vProcessTask, 0, eSetValueWithOverwrite); } vTaskDelay(pdMS_TO_TICKS(50)); } } void vProcessTask(void *pvParameters) { uint32_t value; BaseType_t ret; for(;;) { ret xTaskNotifyWait(0x00, 0xFFFFFFFF, value, pdMS_TO_TICKS(100)); if(ret pdPASS) { if(value 0) { enter_low_power_mode(); } else { handle_high_reading(value); } } } }说实话以前总觉得FreeRTOS这么小一个内核功能肯定不够用。但用久了才发现它那些看起来不起眼的小特性很多才是真正干活的东西。Notification就是典型——API就几个函数文档也就几页但用好了能把代码写得又轻又干净。