FreeRTOS---队列与信号量详解

目录

一、消息队列

二、二值信号量

三、计数信号量

四、优先级翻转与互斥信号量

五、队列集

六、总结

 FreeRTOS 提供了丰富的任务间同步和通信机制,包括队列、二值信号量、计数信号量、互斥信号量,以及队列集等。以下内容将对这些功能逐一介绍,并提供示例帮助理解。

一、消息队列

消息队列(Queue)用于任务之间传递数据或事件。队列可以存储多个固定大小的消息,并以 FIFO(先进先出)的方式处理。

比喻
想象一个“共享邮箱”。

每个任务就像一个人,他们通过这个邮箱交换信息。邮箱容量有限,当邮箱满了,发送者必须等到空间空出来才能放新的信件;当邮箱空了,接收者需要等待新信件到达。

使用场景:

用于任务之间或中断与任务之间的数据传递。

适合传递小量数据(如状态值、传感器数据等)。

实际场景
任务 A 需要向任务 B 发送一个传感器数据,比如温度值,就可以用消息队列。

优点:

线程安全:消息队列内部使用互斥机制,避免并发访问问题。

支持多生产者、多消费者:可以让多个任务同时发送或接收消息。

FIFO机制:消息按照发送顺序处理,避免乱序。

内存消耗较大:需要分配足够的队列缓冲区。

性能受限:频繁的拷贝操作可能增加处理时间。

适合传递小数据:不适合传递大块数据。

相关 API

创建队列

QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);

uxQueueLength:队列可容纳的消息数量。

uxItemSize:每条消息的大小(字节)。

发送消息

BaseType_t xQueueSend(QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait);

从中断中发送消息

BaseType_t xQueueSendFromISR(QueueHandle_t xQueue, const void * pvItemToQueue, BaseType_t * pxHigherPriorityTaskWoken);

接收消息

BaseType_t xQueueReceive(QueueHandle_t xQueue, void * pvBuffer, TickType_t xTicksToWait);

示例

QueueHandle_t xQueue;

void SenderTask(void *pvParameters) {
    int data = 0;
    while (1) {
        data++;
        xQueueSend(xQueue, &data, portMAX_DELAY);  // 向队列发送数据
        printf("Sent: %d\n", data);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void ReceiverTask(void *pvParameters) {
    int receivedData;
    while (1) {
        if (xQueueReceive(xQueue, &receivedData, portMAX_DELAY)) {  // 从队列接收数据
            printf("Received: %d\n", receivedData);
        }
    }
}

int main(void) {
    xQueue = xQueueCreate(10, sizeof(int));  // 创建队列,长度为10,存储int类型
    xTaskCreate(SenderTask, "Sender", 128, NULL, 1, NULL);
    xTaskCreate(ReceiverTask, "Receiver", 128, NULL, 1, NULL);
    vTaskStartScheduler();
    return 0;
}

二、二值信号量

二值信号量(Binary Semaphore)只有两种状态:可用和不可用。它用于任务同步或事件通知。

比喻
想象一个“电梯通行灯”。

当灯是绿灯时,表示电梯可以使用(信号量被释放)。当灯是红灯时,表示电梯被占用(信号量被获取)。只有一个人能通过该通道(最多有一个任务持有信号量)。

使用场景:

用于简单的任务同步,例如中断通知任务完成某个操作。

用于控制对某个资源的独占访问。

实际场景
某任务等待中断事件触发,中断服务函数通过释放二值信号量通知任务。

优点:
  1. 轻量级:占用内存少,操作简单。
  2. 事件通知:适用于中断触发的事件处理。
  3. 二选一:只有两种状态(已释放/已获取),逻辑清晰。
缺点:
  1. 功能单一:不能记录多个事件发生的情况。
  2. 没有优先级继承:当任务竞争资源时,可能发生优先级翻转问题。

相关 API

创建信号量

SemaphoreHandle_t xSemaphoreCreateBinary(void);
释放信号量

BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
从中断中释放信号量

BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t * pxHigherPriorityTaskWoken);
获取信号量

BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);

示例

SemaphoreHandle_t xBinarySemaphore;

void ISR_Handler(void) {
    xSemaphoreGiveFromISR(xBinarySemaphore, NULL);  // 在中断中释放信号量
}

void Task(void *pvParameters) {
    while (1) {
        if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY)) {  // 等待信号量
            printf("Semaphore taken, performing task\n");
        }
    }
}

int main(void) {
    xBinarySemaphore = xSemaphoreCreateBinary();
    xTaskCreate(Task, "Task", 128, NULL, 1, NULL);
    vTaskStartScheduler();
    return 0;
}

三、计数信号量

计数信号量(Counting Semaphore)维护一个计数值,用于控制对共享资源的访问或记录多个事件。

比喻
想象一个“停车场计数器”。

停车场有多个车位,每次一辆车进入,就减少一个车位(获取信号量)。每次有一辆车离开,就增加一个车位(释放信号量)。车位数量不能超过停车场的总容量(信号量的最大计数值)。

使用场景:

用于管理多个相同资源的访问,例如限制对多通道资源的并发访问。

用于计数事件发生的次数。

实际场景
一个任务控制对共享资源(如硬件模块)的访问,资源数量有限(如多通道 ADC 的多个输入通道)。

优点:
  1. 灵活性高:可以计数多个事件或管理多资源。
  2. 并发性支持:支持多任务对多个资源的访问。
  3. 适合事件计数:能够记录事件未被处理的次数。
缺点:
  1. 复杂性增加:需要仔细设计初始计数值和最大计数值。
  2. 不支持优先级继承:与二值信号量一样,可能发生优先级翻转问题。
相关 API

创建计数信号量

SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);


示例

SemaphoreHandle_t xCountingSemaphore;

void ProducerTask(void *pvParameters) {
    while (1) {
        xSemaphoreGive(xCountingSemaphore);  // 增加计数
        printf("Produced an item\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void ConsumerTask(void *pvParameters) {
    while (1) {
        if (xSemaphoreTake(xCountingSemaphore, portMAX_DELAY)) {  // 消费资源
            printf("Consumed an item\n");
        }
    }
}

int main(void) {
    xCountingSemaphore = xSemaphoreCreateCounting(5, 0);  // 最大计数为5,初始为0
    xTaskCreate(ProducerTask, "Producer", 128, NULL, 1, NULL);
    xTaskCreate(ConsumerTask, "Consumer", 128, NULL, 1, NULL);
    vTaskStartScheduler();
    return 0;
}

四、优先级翻转与互斥信号量

优先级翻转

当高优先级任务等待低优先级任务释放资源时,中间优先级任务可能阻塞高优先级任务的运行,导致优先级翻转问题。

优先级翻转(Priority Inversion)是实时系统中一种常见的问题。当高优先级任务(Task H)等待低优先级任务(Task L)释放某个共享资源时,如果有一个中等优先级任务(Task M)占用CPU时间片,并且没有参与资源争夺,Task M 会阻塞 Task H 的运行。结果就是,高优先级任务的执行被间接推迟,从而违背了实时系统优先级的初衷。

优先级翻转的发生条件

优先级翻转一般在以下条件下发生:

  1. 共享资源:多个任务需要访问同一个共享资源(如全局变量、硬件设备)。
  2. 任务优先级不同:系统中存在不同优先级的任务。
  3. 不正确的资源管理机制:没有使用合适的同步工具(如互斥信号量)来保护资源

比喻
想象一个“超市收银队伍”。

高优先级任务就像 VIP 顾客,他们应该先结账。低优先级任务是普通顾客,按顺序结账。假如收银员(资源)正在服务低优先级顾客,而中优先级顾客不断插队,高优先级顾客就会被无限阻塞(优先级翻转)。

实际场景
任务 A(高优先级)需要使用某资源,而任务 B(低优先级)持有该资源,这时任务 C(中优先级)阻塞了任务 B 的运行,导致 A 被阻塞。

优点:
  1. 暴露潜在问题:帮助设计者意识到系统中可能存在的任务竞争问题。
  2. 通过优先级继承机制解决:互斥信号量可以解决优先级翻转问题。
缺点:
  1. 高优先级任务可能被长时间阻塞:对实时系统性能有负面影响。
  2. 调试难度较大:发生优先级翻转的场景可能隐藏较深。
互斥信号量简介

互斥信号量(Mutex)是一种特殊的二值信号量,附带优先级继承机制,用于解决优先级翻转问题。

比喻
想象一个“餐馆单人卫生间”。

卫生间一次只能容纳一个人(信号量被获取)。如果一个 VIP 需要使用卫生间,而普通顾客占用了卫生间,VIP 的优先级会暂时提升,直到他完成后优先级恢复正常(优先级继承)。

使用场景:

用于保护共享资源(如全局变量)的访问,防止多个任务同时修改资源。

可用于防止优先级翻转问题。

实际场景
保护共享资源(如全局变量)免受任务并发访问,并防止优先级翻转。

优点:
  1. 支持优先级继承:防止优先级翻转,确保系统高效运行。
  2. 保护共享资源:提供线程安全的资源访问。
  3. 灵活性高:可以应用于任务之间和任务-中断之间的同步。
缺点:
  1. 性能开销:优先级继承机制增加了处理开销。
  2. 死锁风险:不正确的使用可能导致死锁问题。

相关 API

创建互斥信号量

SemaphoreHandle_t xSemaphoreCreateMutex(void);
释放互斥信号量

BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
获取互斥信号量

 BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);

示例

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>

/* 互斥信号量句柄 */
SemaphoreHandle_t xMutex;

/* 共享资源(模拟打印) */
void SharedResource(const char *message) {
    for (int i = 0; i < 3; i++) {
        printf("%s: %d\n", message, i);
        vTaskDelay(pdMS_TO_TICKS(100)); // 模拟操作耗时
    }
}

/* 任务1 */
void Task1(void *pvParameters) {
    while (1) {
        /* 获取互斥信号量 */
        if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            printf("Task1 accessing shared resource...\n");
            SharedResource("Task1");
            printf("Task1 releasing shared resource...\n");
            /* 释放互斥信号量 */
            xSemaphoreGive(xMutex);
        }
        vTaskDelay(pdMS_TO_TICKS(500)); // 模拟任务间隔
    }
}

/* 任务2 */
void Task2(void *pvParameters) {
    while (1) {
        /* 获取互斥信号量 */
        if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            printf("Task2 accessing shared resource...\n");
            SharedResource("Task2");
            printf("Task2 releasing shared resource...\n");
            /* 释放互斥信号量 */
            xSemaphoreGive(xMutex);
        }
        vTaskDelay(pdMS_TO_TICKS(700)); // 模拟任务间隔
    }
}

int main(void) {
    /* 创建互斥信号量 */
    xMutex = xSemaphoreCreateMutex();

    /* 检查互斥信号量是否创建成功 */
    if (xMutex == NULL) {
        printf("Failed to create mutex!\n");
        return -1;
    }

    /* 创建任务 */
    xTaskCreate(Task1, "Task1", 128, NULL, 2, NULL);
    xTaskCreate(Task2, "Task2", 128, NULL, 1, NULL);

    /* 启动调度器 */
    vTaskStartScheduler();

    /* 如果程序运行到这里,说明调度器启动失败 */
    for (;;);
    return 0;
}

五、队列集

 队列集(Queue Set)允许多个队列或信号量共享一个等待点,用于处理多个事件来源的场景。

比喻
想象一个“快递收发中心”。

多个快递员(队列或信号量)会把包裹送到收发中心。收发中心的工作人员从中选择最早到达的包裹进行处理。不用每次检查所有快递员,而是集中处理。

使用场景:

用于监控多个消息队列或信号量,处理最早到达的事件。

一个任务需要从多个数据源中选择事件。

实际场景
一个任务需要处理来自多个队列或信号量的事件。

优点:
  1. 集中管理:可以通过一个 API 监控多个队列或信号量。
  2. 提高效率:不需要轮询多个队列,节省 CPU 时间。
  3. 支持多种同步机制:既支持消息队列,也支持信号量。
缺点:
  1. 内存消耗较大:需要额外的内存来存储队列集信息。
  2. 复杂性高:使用和调试队列集比单一队列或信号量复杂。

相关 API

创建队列集

QueueSetHandle_t xQueueCreateSet(UBaseType_t uxEventQueueLength);
将队列添加到队列集

BaseType_t xQueueAddToSet(QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet);
从队列集中获取事件

QueueSetMemberHandle_t xQueueSelectFromSet(QueueSetHandle_t xQueueSet, TickType_t xTicksToWait);


示例

QueueSetHandle_t xQueueSet;
QueueHandle_t xQueue1, xQueue2;

void Task(void *pvParameters) {
    QueueSetMemberHandle_t xActivatedMember;
    int receivedData;

    for (;;) {
        xActivatedMember = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);  // 等待事件
        if (xActivatedMember == xQueue1) {
            xQueueReceive(xQueue1, &receivedData, 0);
            printf("Received from Queue1: %d\n", receivedData);
        } else if (xActivatedMember == xQueue2) {
            xQueueReceive(xQueue2, &receivedData, 0);
            printf("Received from Queue2: %d\n", receivedData);
        }
    }
}

int main(void) {
    xQueue1 = xQueueCreate(5, sizeof(int));
    xQueue2 = xQueueCreate(5, sizeof(int));
    xQueueSet = xQueueCreateSet(10);

    xQueueAddToSet(xQueue1, xQueueSet);
    xQueueAddToSet(xQueue2, xQueueSet);

    xTaskCreate(Task, "Task", 128, NULL, 1, NULL);
    vTaskStartScheduler();
    return 0;
}

六、总结

总结

消息队列:任务间数据传递。

二值信号量:事件同步。

计数信号量:资源计数。

互斥信号量:解决优先级翻转。

队列集:管理多个事件来源。

这些功能可以灵活组合,满足多种实时应用需求。

形象化比喻一览表

总结对比表:通过这些对比可以更好地选择合适的机制,解决具体应用中的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值