HRTOS应用示例:信号量机制详解

在实时操作系统中,多个任务可能需要访问同一个共享资源。例如:

  • 公共硬件设备

  • 串口

  • 显示屏

  • 存储设备

  • 共享数据

  • 临界资源

如果多个任务同时访问这些资源,就可能产生资源竞争。

信号量(Semaphore)是 RTOS 中用于任务同步和资源访问控制的重要机制。

本文以 HRTOS_Semaphore.c 示例为基础,通过两个任务竞争同一个信号量,演示 HRTOS 中信号量的初始化、获取、使用和释放过程,并结合裸机开发说明信号量解决的问题,同时分析 AI 编写 HRTOS 应用程序时需要注意的设计原则。


一、实验目的

本示例创建两个任务:

复制代码
TASK_A
优先级 3

TASK_B
优先级 4

两个任务共同使用:

复制代码
#define SEM_KEY 1

也就是说,两个任务竞争同一个信号量。

核心流程为:

复制代码
任务A ──┐
        ├──→ SEM_KEY
任务B ──┘

任意时刻只有成功获取信号量的任务可以进入资源使用区域。

因此,本实验主要验证:

多个任务竞争同一个信号量时,HRTOS 如何保证资源访问的互斥性。


二、为什么需要信号量

假设系统中有一个共享资源:

复制代码
共享资源
    ↑
 ┌──┴──┐
任务A  任务B

如果任务 A 和任务 B 同时访问:

复制代码
任务A → 使用资源
任务B → 同时使用资源

就可能产生冲突。

例如两个任务同时操作同一个硬件设备:

复制代码
任务A:发送数据AAAA
任务B:发送数据BBBB

如果没有任何保护机制,最终可能出现:

复制代码
A B A B A B ...

导致数据混乱。

信号量可以把资源访问变成:

复制代码
任务A
  ↓
获取信号量
  ↓
使用资源
  ↓
释放信号量

任务B
  ↓
等待信号量
  ↓
获取信号量
  ↓
使用资源
  ↓
释放信号量

从而避免多个任务同时进入资源访问区域。


三、本示例的整体设计

本示例包含两个任务。

任务 A

复制代码
优先级:3

周期性尝试获取信号量。

成功后:

复制代码
获取信号量
    ↓
LED_A翻转
    ↓
延时50
    ↓
释放信号量

任务 B

复制代码
优先级:4

同样竞争:

复制代码
SEM_KEY

成功后:

复制代码
获取信号量
    ↓
LED_B翻转
    ↓
延时50
    ↓
释放信号量

因此两个任务形成:

复制代码
TASK_A ──┐
         │
         ▼
      SEM_KEY
         ▲
         │
TASK_B ──┘

四、信号量的初始化

系统入口首先执行:

复制代码
os_sem_init(SEM_KEY, 1)

其中:

复制代码
SEM_KEY = 1

表示信号量的编号。

第二个参数:

复制代码
1

表示信号量初始计数值。

可以理解为:

复制代码
SEM_KEY
初始值 = 1

这意味着系统最开始允许一个任务成功获取该信号量。


五、为什么初始化之后还要调用 os_sem_post()

本示例最后还有:

复制代码
os_sem_post(SEM_KEY);

这一句非常值得注意。

因为示例采用的实际信号量使用流程,需要保证信号量在任务开始竞争时处于可用状态。

完整初始化流程为:

复制代码
os_sem_init()
      ↓
创建任务
      ↓
os_sem_post()
      ↓
任务开始竞争信号量

因此,在实际使用 HRTOS 信号量时,应当严格依据 HRTOS 当前版本的信号量实现和 API 语义来设计初始化流程,而不能仅根据其他 RTOS 的经验猜测。


六、任务如何获取信号量

任务 A:

复制代码
if (os_sem_wait(SEM_KEY) == 2)

任务 B:

复制代码
if (os_sem_wait(SEM_KEY) == 2)

两个任务使用的是同一个:

复制代码
SEM_KEY = 1

因此,它们实际上是在竞争同一个信号量。

当任务成功获取后,才执行:

复制代码
sem_count++;

LED_A = ~LED_A;

或者:

复制代码
sem_count++;

LED_B = ~LED_B;

也就是说:

只有获取信号量成功,任务才能进入资源使用区域。


七、为什么要判断返回值

这里不是简单调用:

复制代码
os_sem_wait(SEM_KEY);

而是:

复制代码
if (os_sem_wait(SEM_KEY) == 2)

说明应用程序需要根据 HRTOS API 的返回结果判断信号量获取是否成功。

只有:

复制代码
返回值 == 2

时,本示例才继续执行资源访问逻辑。

因此 AI 编写 HRTOS 程序时,不能只根据函数名称猜测返回值含义。

例如不能直接假设:

复制代码
1 = 成功

或者:

复制代码
0 = 成功

应该以 HRTOS 当前版本的 API 定义和官方示例为准。


八、获取成功以后发生什么

以任务 A 为例:

复制代码
if (os_sem_wait(SEM_KEY) == 2)
{
    sem_count++;

    LED_A = ~LED_A;

    os_delay(TICK_A);

    os_sem_post(SEM_KEY);
}

可以分成四个阶段:

复制代码
① 获取信号量
       ↓
② 进入资源使用区域
       ↓
③ 使用资源
       ↓
④ 释放信号量

其中:

复制代码
os_delay(TICK_A);

用于模拟任务占用共享资源的过程。

这里并不是说实际项目中获取信号量以后必须延时,而是为了让测试过程更加明显。


九、为什么获取信号量以后不能忘记释放

这是使用信号量最重要的规则之一。

获取:

复制代码
os_sem_wait(SEM_KEY)

成功以后,最终必须执行:

复制代码
os_sem_post(SEM_KEY);

否则可能出现:

复制代码
任务A获取信号量
      ↓
进入资源使用区域
      ↓
没有释放
      ↓
任务B一直无法获取

最终可能导致其他任务长期等待。

因此,可以把信号量的基本使用模式记成:

复制代码
Wait
 ↓
使用资源
 ↓
Post

即:

获取 → 使用 → 释放


十、两个任务为什么使用同一个信号量

代码中:

复制代码
#define SEM_KEY 1

任务 A:

复制代码
os_sem_wait(SEM_KEY)

任务 B:

复制代码
os_sem_wait(SEM_KEY)

它们使用相同的信号量 ID。

因此:

复制代码
        SEM_KEY
        /     \
       /       \
   TASK_A    TASK_B

如果使用不同的信号量:

复制代码
TASK_A → SEM_A

TASK_B → SEM_B

那么两个任务实际上不会通过同一个信号量互斥。

所以:

需要保护同一个共享资源的任务,必须使用同一个同步对象。


十一、为什么两个任务设置不同优先级

本示例:

复制代码
TASK_A = 优先级3
TASK_B = 优先级4

任务 B 优先级更高。

这样设计可以让实验同时体现:

复制代码
任务优先级
+
信号量竞争

但需要注意:

信号量解决的是资源访问问题,优先级解决的是任务调度问题。

二者不是同一个概念。

可以简单理解为:

复制代码
优先级
    ↓
决定谁更应该获得CPU

信号量
    ↓
决定谁可以进入共享资源访问区域

十二、信号量和任务延时不是一回事

代码中同时出现:

复制代码
os_sem_wait()

和:

复制代码
os_delay()

但它们承担完全不同的作用。

os_sem_wait()

用于:

复制代码
等待资源/同步条件

os_delay()

用于:

复制代码
让任务进入延时状态

因此不能把:

复制代码
os_delay(50);

简单理解成信号量的替代品。

二者解决的问题不同。


十三、与裸机开发的区别

裸机程序可能通过变量实现资源保护:

复制代码
if (busy == 0)
{
    busy = 1;

    // 使用资源

    busy = 0;
}

这种方式在简单程序中可以使用,但随着任务数量增加,任务状态和调度关系会越来越复杂。

RTOS 则可以使用信号量:

复制代码
if (os_sem_wait(SEM_KEY) == 2)
{
    // 使用资源

    os_sem_post(SEM_KEY);
}

资源访问关系更加明确。

因此,从裸机开发进入 RTOS 开发后,可以逐渐从:

自己维护各种状态变量

转变为:

使用 RTOS 提供的同步机制管理任务之间的资源竞争。


十四、完整代码

复制代码
#include "hrtos.h"

/*==================== 配置 ====================*/
#define TASK_A       1
#define TASK_B       2

#define PRIO_A       3
#define PRIO_B       4

#define STACK_A      5
#define STACK_B      5

#define SEM_KEY      1

#define TICK_A       50
#define TICK_B       50

/*==================== LED 定义 ====================*/
sbit LED_A = P1^0;   /* 任务A指示灯 */
sbit LED_B = P1^1;   /* 任务B指示灯 */

/*==================== 全局 ====================*/
static u8 sem_count = 0;

/*==================== 任务A ====================*/
static void task_a(void)
{
    LED_A = 1;

    while (1)
    {
        /*
         * 获取信号量
         */
        if (os_sem_wait(SEM_KEY) == 2)
        {
            sem_count++;

            /* 获取成功 → 翻转LED */
            LED_A = ~LED_A;

            /*
             * 模拟占用资源
             */
            os_delay(TICK_A);

            /*
             * 释放信号量
             */
            os_sem_post(SEM_KEY);
        }

        os_delay(10);
    }
}

/*==================== 任务B ====================*/
static void task_b(void)
{
    LED_B = 1;

    while (1)
    {
        /*
         * 获取信号量
         */
        if (os_sem_wait(SEM_KEY) == 2)
        {
            sem_count++;

            /* 获取成功 → 翻转LED */
            LED_B = ~LED_B;

            /*
             * 模拟占用资源
             */
            os_delay(TICK_B);

            /*
             * 释放信号量
             */
            os_sem_post(SEM_KEY);
        }

        os_delay(10);
    }
}

/*==================== 系统入口 ====================*/
void hrtos_main(void)
{
    if (os_sem_init(SEM_KEY, 1) != 1)
    {
        while (1);
    }

    if (os_task_create(task_a,
                       TASK_A,
                       PRIO_A,
                       STACK_A) != 1)
    {
        while (1);
    }

    if (os_task_create(task_b,
                       TASK_B,
                       PRIO_B,
                       STACK_B) != 1)
    {
        while (1);
    }

    os_sem_post(SEM_KEY);
}

十五、实验现象

程序运行以后,可以通过两个 LED 观察任务获取信号量的情况。

复制代码
LED_A
 ↓
TASK_A成功获取信号量

LED_B
 ↓
TASK_B成功获取信号量

两个任务不断竞争同一个:

复制代码
SEM_KEY

成功获取后才执行 LED 翻转和模拟资源占用。

因此可以直观观察两个任务对同一个同步对象的竞争过程。


十六、信号量的核心使用模型

把整个示例简化以后,实际上就是:

复制代码
if (os_sem_wait(SEM_KEY) == 2)
{
    /* 进入受保护区域 */

    // 使用共享资源

    /* 释放信号量 */
    os_sem_post(SEM_KEY);
}

这就是 HRTOS 应用中非常典型的信号量使用模式。

可以记住:

复制代码
获取
 ↓
临界资源
 ↓
释放

而不是:

复制代码
获取
 ↓
一直占用

十七、这个示例对 AI 编写 HRTOS 程序的意义

信号量属于非常容易被 AI "写得像,但实际上不一定对"的功能。

例如 AI 可能熟悉 FreeRTOS、RT-Thread 等系统,然后直接按照其他系统的 API 习惯生成代码。

但不同 RTOS 的:

  • API 名称

  • 返回值

  • 初始化方式

  • 计数规则

  • 等待机制

  • 调度行为

都可能不同。

因此:

不能因为函数名字相似,就认为底层行为完全相同。

例如本示例明确使用:

复制代码
os_sem_wait(SEM_KEY) == 2

判断获取结果。

这就是 HRTOS 自身 API 语义的一部分。


十八、AI 编写 HRTOS 应用程序时的正确方法

如果让 AI 编写 HRTOS 信号量程序,应该明确告诉 AI:

复制代码
1. 使用 HRTOS 官方信号量 API。
2. 使用 os_sem_init() 初始化信号量。
3. 使用 os_sem_wait() 获取信号量。
4. 获取成功后才能访问共享资源。
5. 使用 os_sem_post() 释放信号量。
6. 获取和释放必须形成完整配对。
7. 严格按照 HRTOS 的返回值判断规则编写。
8. 不要直接套用其他 RTOS 的信号量实现。

这样才能减少 AI 根据其他 RTOS 经验"自行脑补"的问题。


十九、总结

HRTOS_Semaphore.c 通过两个任务竞争同一个信号量,完整演示了 HRTOS 中资源互斥的基本思想。

核心流程为:

复制代码
TASK_A ──┐
         │
         ▼
      SEM_KEY
         ▲
         │
TASK_B ──┘
         │
         ▼
     获取成功
         │
         ▼
     使用资源
         │
         ▼
     释放信号量

信号量解决的核心问题是:

多个任务访问共享资源时,如何避免资源竞争。

而优先级解决的是:

多个任务同时就绪时,谁应该优先获得 CPU。

理解这两个概念,是从裸机开发进入 HRTOS 应用开发的重要一步。

对于 AI 辅助开发同样如此。生成 HRTOS 代码时,不能只看 API 名称,更应该理解 HRTOS 自身的 API 语义、任务状态和调度机制,再根据这些规则设计应用程序。

相关推荐
JavaPub-rodert3 分钟前
AI Agent 怎么接上长期记忆?从 Graphiti MCP Server 讲透 MCP、Episode、搜索与记忆工具化
人工智能
大模型任我行9 分钟前
Meta:强化学习权重传输不再卡顿
人工智能·语言模型·自然语言处理·论文笔记
会员果汁33 分钟前
函数调用和变长参数
c语言
sugarzhangnotes1 小时前
【无标题】
开发语言·人工智能·python
盼小辉丶1 小时前
PyTorch强化学习实战(27)——进化策略在强化学习中的应用
人工智能·pytorch·深度学习·强化学习
东离与糖宝1 小时前
Agent长期记忆六大方案对比,彻底解决AI失忆问题
人工智能
小小测试开发1 小时前
Prompt评估:加一句「请一步步思考」,结构化输出的解析失败率从 2% 涨到 17%
人工智能·prompt
xiaohaiAIgeo1 小时前
【2026年】实验室应急预案中通风系统的关键作用
大数据·人工智能·科普知识
想用offer打牌1 小时前
Personal Agent爆火 - 它到底是个什么
人工智能·后端·ai编程
IT_陈寒1 小时前
Vue的v-if和v-for混用居然是个天坑
前端·人工智能·后端