信号量为什么“不占CPU“

发布时间:2026/5/19 12:57:05

信号量为什么“不占CPU“ 这是一个非常深刻的问题理解这个问题就能真正掌握RTOS的核心设计思想。让我从多个角度解释信号量为什么不占CPU。一、本质信号量不是东西而是规则信号量在内存中只是一个整数变量如0或1它本身并不运行也不消耗CPU时间。真正影响CPU的是等待信号量的任务。// 信号量本质上就是一个整数typedefstruct{intcount;// 信号量值List_t waitingList;// 等待该信号量的任务列表}Semaphore_t;二、关键等待机制让任务主动让出CPU当任务执行xSemaphoreTake()时voidflashLED(void*pvParam){while(1){// 这里发生了什么if(xSemaphoreTake(xSemaLED,portMAX_DELAY)pdTRUE){// 执行业务逻辑}}}内部执行流程// 简化的信号量Take实现BaseType_txSemaphoreTake(SemaphoreHandle_t xSemaphore,TickType_t xTicksToWait){// 1. 进入临界区关中断taskENTER_CRITICAL();// 2. 检查信号量值if(xSemaphore-count0){// 有信号量直接拿走xSemaphore-count--;taskEXIT_CRITICAL();returnpdTRUE;// 立即返回继续执行}// 3. 没有信号量需要等待if(xTicksToWait0){// 不等待直接返回失败taskEXIT_CRITICAL();returnpdFALSE;}// 4. 【关键】将当前任务从就绪列表移到等待列表vListRemove((pxCurrentTCB-xStateListItem));// 从就绪列表移除vListInsertEnd((xSemaphore-waitingList),// 插入等待列表(pxCurrentTCB-xStateListItem));// 5. 设置任务的阻塞超时时间pxCurrentTCB-xTicksToWaitxTicksToWait;taskEXIT_CRITICAL();// 6. 【核心】触发任务切换让出CPUtaskYIELD();// 这行代码让当前任务停止运行// 7. 当任务再次被唤醒时从这里继续执行// 此时信号量已经获得直接返回成功returnpdTRUE;}三、核心机制任务状态切换RTOS中的任务状态机┌──────────────┐ │ 运行态 │ │ (Running) │ └──────┬───────┘ │ xSemaphoreTake │ xSemaphoreGive (无信号量) │ (中断或其他任务) ↓ ┌──────────────┐ │ 阻塞态 │◄──────┐ │ (Blocked) │ │ └──────┬───────┘ │ │ │ 超时或信号到来│ │ ↓ │ ┌──────────────┐ │ │ 就绪态 │───────┘ │ (Ready) │ 调度器选择 └──────────────┘ 最高优先级任务运行具体过程// 场景1信号量可用count1任务执行xSemaphoreTake()→ count变为0→ 任务继续运行 → CPU被占用// 场景2信号量不可用count0任务执行xSemaphoreTake()→ 任务从运行态→阻塞态→ 触发任务切换 → 当前任务暂停 → CPU切换到其他就绪任务 → 当前任务不再占用CPU四、直观类比银行柜台想象一个银行柜台CPU和等待办理业务的人任务场景1没有信号量轮询// 糟糕的设计任务不断检查voidflashLED(){while(1){if(buttonPressed){// 不断轮询检查toggleLED();}// CPU一直在空转浪费电}}就像一个人站在柜台前每隔1秒就问按钮按了吗即使没人按他也一直站着占用CPU。场景2使用信号量阻塞// 好的设计等待信号voidflashLED(){while(1){xSemaphoreTake(buttonSemaphore,portMAX_DELAY);// 等待toggleLED();// 有人按才执行}}就像取号系统没有人按按钮时任务去休息区坐着阻塞态CPU可以去服务其他任务或者进入低功耗模式按钮按下时叫号发出信号任务才去柜台运行态五、实际运行示例假设系统中有三个任务// 任务1LED控制优先级2voidtaskLED(void*pvParam){while(1){xSemaphoreTake(sem,portMAX_DELAY);// 等待信号toggleLED();}}// 任务2数据显示优先级1voidtaskDisplay(void*pvParam){while(1){updateDisplay();vTaskDelay(100);// 每秒更新10次}}// 任务3串口通信优先级1voidtaskSerial(void*pvParam){while(1){processSerialData();vTaskDelay(50);// 每秒更新20次}}CPU占用情况没有信号量时按钮未按 ┌─────────────────────────────────────────────────────────┐ │ taskDisplay │ taskSerial │ taskDisplay │ taskSerial │ ... │ │ (运行) │ (运行) │ (运行) │ (运行) │ │ └─────────────────────────────────────────────────────────┘ CPU 100% 忙碌 使用信号量时按钮未按 ┌─────────────────────────────────────────────────────────┐ │ taskDisplay │ taskSerial │ taskDisplay │ taskSerial │ ... │ │ (运行) │ (运行) │ (运行) │ (运行) │ │ └─────────────────────────────────────────────────────────┘ taskLED处于阻塞态不占用CPU其他任务正常执行 按钮按下后 ┌─────────────────────────────────────────────────────────┐ │ taskLED │ taskDisplay │ taskSerial │ taskLED │ ... │ │ (运行) │ (运行) │ (运行) │ (运行) │ │ └─────────────────────────────────────────────────────────┘ 信号量唤醒taskLED立即抢占CPU因为优先级更高六、为什么轮询会占CPU// ❌ 轮询方式占CPUvoidflashLED(){while(1){if(digitalRead(22)LOW){// 不断检查toggleLED();}// 即使没有按键也在空转// CPU 100% 被占用}}// ✅ 信号量方式不占CPUvoidflashLED(){while(1){xSemaphoreTake(sem,portMAX_DELAY);// 阻塞等待toggleLED();// 只有信号来时执行}}区别轮询任务始终在运行态CPU被占用信号量任务在阻塞态不参与CPU调度七、深入内核任务控制块TCB每个任务都有一个TCB记录了任务的状态typedefstructtskTaskControlBlock{volatileStackType_t*pxTopOfStack;// 堆栈指针ListItem_t xStateListItem;// 状态列表项ListItem_t xEventListItem;// 事件列表项UBaseType_t uxPriority;// 优先级TickType_t xTicksToWait;// 等待超时时间// ... 其他字段}TCB_t;当任务等待信号量时// 1. 从就绪列表移除vListRemove((pxCurrentTCB-xStateListItem));// 2. 插入等待列表信号量的等待队列vListInsertEnd((xSemaphore-waitingList),(pxCurrentTCB-xEventListItem));// 3. 标记任务为阻塞态pxCurrentTCB-xEventListItem-xItemValuexTicksToWait;此时调度器在寻找下一个运行任务时不会扫描等待列表中的任务这些任务完全不被考虑CPU时间全部给其他任务八、实验验证你可以用以下代码验证信号量是否真的不占CPU#includeArduino.h#includeesp32-hal.hSemaphoreHandle_t sem;volatileuint32_tidleCounter0;// 空闲任务钩子统计空闲时间voidvApplicationIdleHook(void){idleCounter;// 空闲任务每运行一次计数器1}voidtaskWait(void*pvParam){while(1){// 等待信号量永远阻塞xSemaphoreTake(sem,portMAX_DELAY);Serial.println(Received signal!);vTaskDelay(pdMS_TO_TICKS(1000));}}voidsetup(){Serial.begin(115200);semxSemaphoreCreateBinary();// 创建等待任务xTaskCreate(taskWait,Wait,2048,NULL,1,NULL);// 10秒后给出信号量vTaskDelay(pdMS_TO_TICKS(10000));xSemaphoreGive(sem);// 再等10秒看空闲计数器vTaskDelay(pdMS_TO_TICKS(10000));Serial.print(Idle counter: );Serial.println(idleCounter);// 会看到很大的数值证明CPU在空闲任务}voidloop(){}运行结果会显示空闲计数器在10秒内增加了数百万次证明CPU没有空转在等待任务上。九、总结信号量不占CPU的本质原因信号量只是数据在内存中只是一个整数变量等待是任务主动让出xSemaphoreTake()会触发任务切换状态管理等待的任务从运行态→阻塞态不再参与调度事件驱动只有信号量被Give时才会唤醒任务类比信号量 门铃按钮不耗电等待任务 在沙发上等门铃的人不消耗注意力CPU 可以做其他事的人比如看电视、看书这就是RTOS能够高效管理CPU资源的核心秘密

相关新闻