关注

信号量为什么“不占CPU“

这是一个非常深刻的问题!理解这个问题,就能真正掌握RTOS的核心设计思想。让我从多个角度解释信号量为什么"不占CPU"

一、本质:信号量不是"东西",而是"规则"

信号量在内存中只是一个整数变量(如0或1),它本身并不运行,也不消耗CPU时间。真正影响CPU的是等待信号量的任务

// 信号量本质上就是一个整数
typedef struct {
    int count;           // 信号量值
    List_t waitingList;  // 等待该信号量的任务列表
} Semaphore_t;

二、关键:等待机制让任务"主动让出CPU"

当任务执行 xSemaphoreTake() 时:

void flashLED(void *pvParam) {
    while (1) {
        // 这里发生了什么?
        if (xSemaphoreTake(xSemaLED, portMAX_DELAY) == pdTRUE) {
            // 执行业务逻辑
        }
    }
}

内部执行流程:

// 简化的信号量Take实现
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait) {
    // 1. 进入临界区(关中断)
    taskENTER_CRITICAL();
    
    // 2. 检查信号量值
    if (xSemaphore->count > 0) {
        // 有信号量,直接拿走
        xSemaphore->count--;
        taskEXIT_CRITICAL();
        return pdTRUE;  // 立即返回,继续执行
    }
    
    // 3. 没有信号量,需要等待
    if (xTicksToWait == 0) {
        // 不等待,直接返回失败
        taskEXIT_CRITICAL();
        return pdFALSE;
    }
    
    // 4. 【关键】将当前任务从就绪列表移到等待列表
    vListRemove(&(pxCurrentTCB->xStateListItem));  // 从就绪列表移除
    vListInsertEnd(&(xSemaphore->waitingList),     // 插入等待列表
                   &(pxCurrentTCB->xStateListItem));
    
    // 5. 设置任务的阻塞超时时间
    pxCurrentTCB->xTicksToWait = xTicksToWait;
    
    taskEXIT_CRITICAL();
    
    // 6. 【核心】触发任务切换,让出CPU
    taskYIELD();  // 这行代码让当前任务停止运行!
    
    // 7. 当任务再次被唤醒时,从这里继续执行
    // 此时信号量已经获得,直接返回成功
    return pdTRUE;
}

三、核心机制:任务状态切换

RTOS中的任务状态机:

                    ┌──────────────┐
                    │   运行态     │
                    │ (Running)    │
                    └──────┬───────┘
                           │
            xSemaphoreTake │ xSemaphoreGive
            (无信号量)     │ (中断或其他任务)
                           ↓
                    ┌──────────────┐
                    │   阻塞态     │◄──────┐
                    │ (Blocked)    │       │
                    └──────┬───────┘       │
                           │               │
              超时或信号到来│               │
                           ↓               │
                    ┌──────────────┐       │
                    │   就绪态     │───────┘
                    │ (Ready)      │   调度器选择
                    └──────────────┘   最高优先级任务运行

具体过程:

// 场景1:信号量可用(count=1)
任务执行 xSemaphoreTake() → count变为0 → 任务继续运行 → CPU被占用

// 场景2:信号量不可用(count=0)
任务执行 xSemaphoreTake() → 任务从"运行态""阻塞态" → 触发任务切换 → 
当前任务暂停 → CPU切换到其他就绪任务 → 当前任务不再占用CPU

四、直观类比:银行柜台

想象一个银行柜台(CPU)和等待办理业务的人(任务):

场景1:没有信号量(轮询)

// 糟糕的设计:任务不断检查
void flashLED() {
    while(1) {
        if (buttonPressed) {  // 不断轮询检查
            toggleLED();
        }
        // CPU一直在空转,浪费电!
    }
}

就像一个人站在柜台前,每隔1秒就问"按钮按了吗?",即使没人按,他也一直站着(占用CPU)。

场景2:使用信号量(阻塞)

// 好的设计:等待信号
void flashLED() {
    while(1) {
        xSemaphoreTake(buttonSemaphore, portMAX_DELAY);  // 等待
        toggleLED();  // 有人按才执行
    }
}

就像取号系统:

  • 没有人按按钮时,任务去休息区坐着(阻塞态)
  • CPU可以去服务其他任务(或者进入低功耗模式)
  • 按钮按下时,叫号(发出信号),任务才去柜台(运行态)

五、实际运行示例

假设系统中有三个任务:

// 任务1:LED控制(优先级2)
void taskLED(void *pvParam) {
    while(1) {
        xSemaphoreTake(sem, portMAX_DELAY);  // 等待信号
        toggleLED();
    }
}

// 任务2:数据显示(优先级1)
void taskDisplay(void *pvParam) {
    while(1) {
        updateDisplay();
        vTaskDelay(100);  // 每秒更新10次
    }
}

// 任务3:串口通信(优先级1)
void taskSerial(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?

// ❌ 轮询方式:占CPU
void flashLED() {
    while(1) {
        if (digitalRead(22) == LOW) {  // 不断检查
            toggleLED();
        }
        // 即使没有按键,也在空转
        // CPU 100% 被占用
    }
}

// ✅ 信号量方式:不占CPU
void flashLED() {
    while(1) {
        xSemaphoreTake(sem, portMAX_DELAY);  // 阻塞等待
        toggleLED();  // 只有信号来时执行
    }
}

区别

  • 轮询:任务始终在"运行态",CPU被占用
  • 信号量:任务在"阻塞态",不参与CPU调度

七、深入内核:任务控制块(TCB)

每个任务都有一个TCB,记录了任务的状态:

typedef struct tskTaskControlBlock {
    volatile StackType_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->xItemValue = xTicksToWait;

此时,调度器在寻找下一个运行任务时:

  • 不会扫描等待列表中的任务
  • 这些任务完全不被考虑
  • CPU时间全部给其他任务

八、实验验证

你可以用以下代码验证信号量是否真的不占CPU:

#include <Arduino.h>
#include <esp32-hal.h>

SemaphoreHandle_t sem;
volatile uint32_t idleCounter = 0;

// 空闲任务钩子(统计空闲时间)
void vApplicationIdleHook(void) {
    idleCounter++;  // 空闲任务每运行一次,计数器+1
}

void taskWait(void *pvParam) {
    while(1) {
        // 等待信号量,永远阻塞
        xSemaphoreTake(sem, portMAX_DELAY);
        Serial.println("Received signal!");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void setup() {
    Serial.begin(115200);
    sem = xSemaphoreCreateBinary();
    
    // 创建等待任务
    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在空闲任务
}

void loop() {}

运行结果会显示:空闲计数器在10秒内增加了数百万次,证明CPU没有空转在等待任务上。

九、总结

信号量不占CPU的本质原因

  1. 信号量只是数据:在内存中只是一个整数变量
  2. 等待是任务主动让出xSemaphoreTake() 会触发任务切换
  3. 状态管理:等待的任务从"运行态"→"阻塞态",不再参与调度
  4. 事件驱动:只有信号量被Give时,才会唤醒任务

类比

  • 信号量 = 门铃按钮(不耗电)
  • 等待任务 = 在沙发上等门铃的人(不消耗注意力)
  • CPU = 可以做其他事的人(比如看电视、看书)

这就是RTOS能够高效管理CPU资源的核心秘密!

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/leiming6/article/details/159612264

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--