1. 信号量基础概念:什么是信号量、为什么需要信号量、信号量的本质

各位同学,咱们今天聊聊信号量。说实话,我刚接触FreeRTOS那会儿,对信号量也是一头雾水。总觉得这东西不就是个计数器吗?有啥好讲的?后来在项目里栽了跟头,才明白——嗯,信号量远没你想的那么简单。

1.1 什么是信号量

信号量,说白了就是一个资源管理工具。它用来控制多个任务对共享资源的访问。你可以把它想象成一个停车场门口的计数器:

  • 停车场有10个车位,计数器初始值是10
  • 每进来一辆车,计数器减1
  • 每出去一辆车,计数器加1
  • 计数器为0时,新来的车就得排队等着

在FreeRTOS里,信号量就是这个计数器。它记录着当前可用的资源数量。任务想要使用资源,就得先"获取"信号量;用完了再"释放"回去。

核心要点:信号量不是数据,而是一种同步机制。它不传递数据,只传递"资源可用"这个状态。

1.2 为什么需要信号量

你想想看,在裸机编程里,我们经常用全局变量来做标志位。比如:

uint8_t data_ready = 0;

// 中断中
void ISR(void) {
    data_ready = 1;
}

// 主循环中
void main_loop(void) {
    if (data_ready) {
        data_ready = 0;
        process_data();
    }
}

这种做法在裸机里没问题。但到了RTOS里,就出大问题了。为什么?

  • 任务调度不可控:你不知道哪个任务先执行,哪个后执行
  • 资源竞争:两个任务同时修改同一个变量,数据就乱了
  • 优先级反转:高优先级任务可能被低优先级任务阻塞

我在项目中遇到过这样一个坑:两个任务共享一个串口发送数据。没有用信号量保护,结果两个任务同时调用printf,打印出来的字符串混在一起,完全没法看。从那以后,我养成了一个习惯——只要涉及共享资源,先上信号量再说

我的建议:信号量不是万能的,但没有信号量是万万不能的。在FreeRTOS里,信号量是解决资源竞争最基础、最常用的手段。

1.3 信号量的本质

信号量的本质是什么?我把它拆成两个部分来看:

1.3.1 计数值

每个信号量内部都有一个计数值。这个值表示当前可用的资源数量。

信号量类型 计数值范围 典型用途
二值信号量 0 或 1 互斥访问、事件通知
计数型信号量 0 ~ 最大值 资源池管理、生产者-消费者

计数值的变化规则很简单:

  • xSemaphoreTake():计数值减1。如果为0,任务阻塞
  • xSemaphoreGive():计数值加1。如果有任务阻塞,唤醒一个

1.3.2 任务阻塞列表

这才是信号量真正的"灵魂"。每个信号量内部都维护着一个任务阻塞列表。当计数值为0时,想要获取信号量的任务就会被挂到这个列表上。

我打个比方:你去银行办业务,柜台没人(计数值>0),你直接办。柜台有人(计数值=0),你就得排队(进入阻塞列表)。柜台办完一个(释放信号量),叫下一个人(唤醒阻塞列表中的任务)。

注意:任务在阻塞列表里不是"死等",而是挂起状态。CPU会去执行其他任务。等信号量可用时,FreeRTOS的调度器会把这个任务重新加入就绪队列。

这里有个细节很多人会忽略:阻塞列表是有优先级的。高优先级的任务会排在前面,优先获得信号量。这就是FreeRTOS的"优先级继承"机制在信号量上的体现。

1.4 信号量的两种类型

FreeRTOS提供了两种信号量,我简单说一下区别:

1.4.1 二值信号量

计数值只有0和1。常用于:

  • 任务与中断之间的同步
  • 任务与任务之间的事件通知
  • 简单的互斥访问
// 创建二值信号量
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();

// 中断中释放
void ISR(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 任务中获取
void Task(void *pvParameters) {
    while(1) {
        if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
            // 处理事件
        }
    }
}

1.4.2 计数型信号量

计数值可以大于1。常用于:

  • 管理多个相同资源(比如多个缓冲区)
  • 生产者-消费者模式
  • 限流控制
// 创建计数型信号量,初始有5个资源
SemaphoreHandle_t xSemaphore = xSemaphoreCreateCounting(5, 5);

// 生产者
void ProducerTask(void *pvParameters) {
    while(1) {
        // 生产一个资源
        xSemaphoreGive(xSemaphore);
        vTaskDelay(100);
    }
}

// 消费者
void ConsumerTask(void *pvParameters) {
    while(1) {
        if(xSemaphoreTake(xSemaphore, pdMS_TO_TICKS(1000)) == pdTRUE) {
            // 消费一个资源
        }
    }
}

避坑指南:我曾经在项目里把二值信号量和计数型信号量搞混了。二值信号量创建后初始值是0,需要先Give一次才能用。而计数型信号量可以指定初始值。这个细节让我调试了整整一个下午。

1.5 信号量的内部实现(简版)

FreeRTOS的信号量底层其实就是一个队列。队列长度是1(二值)或N(计数型),队列项大小是0。为什么是0?因为信号量不传递数据,只传递"有货"这个状态。

我画个简图帮你理解:

信号量结构体(简化版):
{
    int32_t             uxCurrentCount;    // 当前计数值
    int32_t             uxInitialCount;    // 初始计数值
    UBaseType_t         uxMaxCount;        // 最大计数值(仅计数型)
    List_t              xTasksWaitingToTake;  // 阻塞任务列表
}

当你调用xSemaphoreTake时,FreeRTOS会:

  1. 关中断(保护临界区)
  2. 检查uxCurrentCount是否大于0
  3. 如果大于0,减1,开中断,返回成功
  4. 如果等于0,把当前任务加入xTasksWaitingToTake列表
  5. 触发任务调度,切换到其他任务
  6. 当信号量被释放时,从列表中取出最高优先级的任务
  7. 恢复该任务的执行

记住:信号量的所有操作都是在临界区内完成的。这就是为什么信号量是线程安全的——它保证了计数值的修改不会被中断或其他任务打断。

1.6 小结

信号量这东西,说难不难,说简单也不简单。我总结三个要点:

  • 信号量是计数器:记录可用资源数量
  • 信号量是同步工具:协调任务之间的执行顺序
  • 信号量是阻塞机制:资源不够时,任务会挂起等待

下一章,我会带你深入信号量的创建、获取和释放操作,以及那些让我掉过坑的细节。咱们到时候见。