3. LiteOS-M内核基础:同步与互斥

做嵌入式开发,尤其是跑RTOS的时候,最头疼的问题是什么?我个人觉得,不是中断嵌套有多深,也不是内存碎片怎么处理,而是——资源竞争。

你想想看,两个任务同时去操作一个串口,数据不乱才怪。LiteOS-M提供了几套机制来解决这类问题:信号量、互斥锁、事件标志组。今天我们就一个一个掰开来看。

核心观点:同步与互斥是RTOS的基石。不理解它们,你写的多任务程序就是“伪并发”,迟早要踩坑。

3.1 信号量:二值信号量与计数信号量

信号量,说白了就是一个“资源计数器”。任务想要使用某个资源,就得先“拿下”这个信号量;用完了再“还回去”。

3.1.1 二值信号量

二值信号量的值只有0和1。它通常用来做任务同步,比如一个任务等待某个事件发生,另一个任务去触发这个事件。

我在项目中遇到过这样一个场景:一个传感器采集任务,采集完数据后要通知处理任务去计算。用二值信号量就非常合适——采集任务释放信号量,处理任务等待信号量。

特性 二值信号量 计数信号量
取值范围 0 或 1 0 ~ 最大值(可配置)
典型用途 任务同步、中断与任务同步 资源管理、有限资源池
释放操作 将值置为1 计数值+1
获取操作 将值置为0 计数值-1

代码示例:创建并使用二值信号量

// 创建二值信号量
osSemaphoreId_t my_binary_sem;
my_binary_sem = osSemaphoreNew(1, 0, NULL);  // 最大计数1,初始值0

// 任务A:等待信号量
void task_A(void *arg) {
    while (1) {
        osSemaphoreAcquire(my_binary_sem, osWaitForever);
        // 收到信号,开始处理数据
        process_data();
    }
}

// 任务B:释放信号量
void task_B(void *arg) {
    while (1) {
        collect_data();
        osSemaphoreRelease(my_binary_sem);  // 通知任务A
        osDelay(100);
    }
}

我的习惯:二值信号量初始值设为0,这样任务一开始就会阻塞等待,直到有人释放。如果初始值设为1,那第一次获取就直接成功了,失去了同步的意义。

3.1.2 计数信号量

计数信号量可以管理多个同类资源。比如你有5个DMA通道,多个任务都要用,计数信号量就能控制同时最多5个任务在使用。

我曾经在一个音频处理项目里用过计数信号量来管理硬件编解码器实例。系统里有3个编解码器硬件单元,计数信号量初始值设为3,任务获取到信号量才能使用编解码器,用完后释放。这样就不会出现两个任务同时操作同一个硬件单元的情况。

// 创建计数信号量,管理3个硬件资源
osSemaphoreId_t hw_pool_sem;
hw_pool_sem = osSemaphoreNew(3, 3, NULL);  // 最大计数3,初始值3

void task_use_hw(void *arg) {
    while (1) {
        // 获取一个硬件资源
        osSemaphoreAcquire(hw_pool_sem, osWaitForever);
        // 使用硬件...
        use_hardware_unit();
        // 释放资源
        osSemaphoreRelease(hw_pool_sem);
        osDelay(50);
    }
}

3.2 互斥锁:优先级继承

互斥锁和信号量看起来有点像,但有一个本质区别:互斥锁有所有权概念。谁拿了锁,谁才能释放锁。信号量则没有这个限制,任何任务都可以释放。

但互斥锁真正厉害的地方,是优先级继承机制。嗯,这里要注意,这是解决优先级反转问题的关键。

3.2.1 优先级反转问题

先说说什么是优先级反转。假设有三个任务:高优先级H、中优先级M、低优先级L。L先拿到了互斥锁,然后H来了,想拿锁但拿不到,只能等待。这时候M来了,它不需要锁,所以它抢占了L的CPU时间。结果就是:H在等L,L被M抢占了,H反而被M“间接”阻塞了。

这合理吗?当然不合理。高优先级的任务反而被低优先级的任务“拖死”了。

3.2.2 优先级继承如何解决

LiteOS-M的互斥锁实现了优先级继承。当高优先级任务H被低优先级任务L阻塞时,L会临时“继承”H的优先级,直到L释放锁。这样M就无法抢占L了,H就能尽快拿到锁。

场景 无优先级继承 有优先级继承
L持有锁,H等待 L保持原优先级 L提升到H的优先级
M抢占L M可以抢占L M无法抢占L(L优先级已提升)
H等待时间 不确定,可能很长 可预测,L释放锁后H立即运行
// 创建互斥锁
osMutexId_t my_mutex;
my_mutex = osMutexNew(NULL);

void shared_resource_task(void *arg) {
    while (1) {
        osMutexAcquire(my_mutex, osWaitForever);
        // 访问共享资源
        access_shared_resource();
        osMutexRelease(my_mutex);
        osDelay(10);
    }
}

避坑指南:我曾经在一个项目中,因为互斥锁使用不当导致系统偶尔“卡死”。后来发现是一个低优先级任务在持有锁的情况下被挂起了,导致高优先级任务永远等不到锁。记住:持有互斥锁的任务不要做长时间的阻塞操作,比如等待消息队列、延时等。

3.3 事件标志组

事件标志组,说白了就是一组二进制标志位。每个位代表一个事件,任务可以等待多个事件同时发生,或者等待其中任意一个发生。

我个人觉得,事件标志组是LiteOS-M里最灵活的同步机制。它不像信号量那样只能“计数”,也不像互斥锁那样只能“独占”。你可以用位运算来组合事件,实现复杂的同步逻辑。

3.3.1 事件标志组的核心操作

  • 设置事件位:将指定的一个或多个位置1
  • 清除事件位:将指定的一个或多个位清0
  • 等待事件位:等待指定的一个或多个位被置1,支持“与”模式和“或”模式
// 创建事件标志组
osEventFlagsId_t my_events;
my_events = osEventFlagsNew(NULL);

#define EVENT_BIT_0  (1U << 0)  // 传感器就绪
#define EVENT_BIT_1  (1U << 1)  // 数据到达
#define EVENT_BIT_2  (1U << 2)  // 定时器超时

// 任务1:等待多个事件
void task_wait_events(void *arg) {
    uint32_t flags;
    while (1) {
        // 等待EVENT_BIT_0和EVENT_BIT_1同时发生
        flags = osEventFlagsWait(my_events, 
                                 EVENT_BIT_0 | EVENT_BIT_1,
                                 osFlagsWaitAll,  // 与模式:所有位都置1
                                 osWaitForever);
        if (flags & (EVENT_BIT_0 | EVENT_BIT_1)) {
            // 两个事件都发生了,开始处理
            handle_combined_event();
        }
    }
}

// 任务2:触发事件
void task_trigger_events(void *arg) {
    while (1) {
        // 模拟传感器就绪
        osEventFlagsSet(my_events, EVENT_BIT_0);
        osDelay(10);
        // 模拟数据到达
        osEventFlagsSet(my_events, EVENT_BIT_1);
        osDelay(100);
    }
}

我的经验:事件标志组特别适合做“多条件触发”的场景。比如一个任务要等“按键按下”且“定时器超时”才执行某个动作,用事件标志组就非常直观。如果用信号量,你得搞两个信号量,逻辑反而复杂了。

3.4 三种机制的对比与选型

很多初学者会问:我到底该用哪个?我的建议是:

  • 做同步(一个任务通知另一个任务):用二值信号量
  • 管理资源池(多个同类资源):用计数信号量
  • 保护共享资源(防止多个任务同时访问):用互斥锁
  • 等待多个条件(复杂事件组合):用事件标志组

下面这张图展示了这三种机制在LiteOS-M内核中的关系:

LiteOS-M 同步与互斥机制 信号量 互斥锁 事件标志组 二值信号量 计数信号量 优先级继承 多事件等待 选型建议 • 任务同步 → 二值信号量 • 资源管理 → 计数信号量 • 保护共享资源 → 互斥锁(优先级继承) • 复杂条件等待 → 事件标志组

3.5 实战中的注意事项

最后,分享几个我在实际调试中总结的经验:

  • 信号量不要滥用:能用互斥锁的地方别用信号量,信号量没有优先级继承,容易出优先级反转问题。
  • 互斥锁的持有时间要短:锁住后尽快释放,不要在锁内做延时、等待等阻塞操作。
  • 事件标志组注意清除时机:事件被消费后要及时清除对应位,否则下次等待会误触发。
  • 中断中不要使用互斥锁:互斥锁会引起任务调度,中断中不能做阻塞等待。中断中释放信号量是可以的。

我曾经踩过的坑:在一个多传感器数据融合的项目中,我用了信号量来保护共享数据缓冲区。结果高优先级任务经常被阻塞很长时间,后来才发现是优先级反转。换成互斥锁后,问题立刻解决。所以,保护共享资源,优先用互斥锁,别图省事用信号量。