信号量
信号量
📚 本节导读
学习时长: 约 25 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: 任务管理基础、C 语言
信号量(Semaphore)是操作系统中最经典的同步原语之一。OneOS 提供了计数信号量,用于资源管理和任务同步。它是理解更复杂 IPC 机制的基础。
🎯 学习目标
- 理解信号量的核心概念:计数器 + 等待队列
- 掌握 P 操作(wait)和 V 操作(post)的含义
- 能够使用二值信号量实现任务同步
- 能够使用计数信号量实现资源管理
- 理解 PRIO 和 FIFO 唤醒策略的区别
一、信号量概念
1.1 什么是信号量
信号量是一个非负整数计数器,配合一个任务等待队列。它提供两个原子操作:
┌───────────────────────────────────────────────────────────┐
│ 信号量模型 │
│ │
│ ┌──────────┐ P 操作 (wait) ┌──────────┐ │
│ │ 任务 A │ ───────────────────→ │ │ │
│ └──────────┘ 计数器 - 1 │ 信号量 │ │
│ 如果计数 < 0 │ ┌────┐ │ │
│ 任务阻塞等待 │ │计数│ │ │
│ │ └────┘ │ │
│ ┌──────────┐ V 操作 (post) │ ┌────┐ │ │
│ │ 任务 B │ ───────────────────→ │ │队列│ │ │
│ └──────────┘ 计数器 + 1 │ └────┘ │ │
│ 唤醒等待任务 │ │ │
│ └──────────┘ │
└───────────────────────────────────────────────────────────┘- P 操作(wait):尝试获取信号量。如果计数 > 0,计数减 1 并立即返回;如果计数 = 0,任务进入等待队列阻塞。
- V 操作(post):释放信号量。计数加 1,如果有任务正在等待,唤醒其中一个。
1.2 信号量的两种用途
信号量根据"最大值"的不同,分为二值信号量和计数信号量两种,用途完全不同。理解二者的区别是正确使用信号量的关键。
| 类型 | 初始值 | 最大值 | 核心用途 |
|---|---|---|---|
| 二值信号量 | 0 或 1 | 1 | 任务同步、"就绪/事件"通知(只有"有/无"两种状态) |
| 计数信号量 | N | N(>1) | 管理 N 个相同资源(缓冲区槽位、外设通道、可用资源计数) |
1.2.1 二值信号量(Binary Semaphore,最大值 = 1)
本质:一个"是/否"开关,计数只能是 0 或 1。它不关心"有多少资源",只关心"事件是否发生 / 任务是否就绪"。
核心用途:任务间同步(事件通知),尤其常见于中断与任务之间的同步:
中断处理程序 任务
───────────── ────────
外设事件发生
│
▼
os_semaphore_post(sem) ──► 计数 0→1 ──► os_semaphore_wait(sem) 返回,
(中断中) 唤醒任务 任务继续执行(处理事件)典型场景:
| 场景 | 说明 |
|---|---|
| 按键/中断通知任务 | ISR 里 post,任务 wait,实现"中断发生后任务再处理" |
| 生产者-消费者同步 | 生产者产出一个数据 post,消费者 wait 后取走处理 |
| 任务"就绪"通知 | 一个任务完成后 post,通知另一个等待的任务继续 |
二值信号量 vs 事件标志/互斥锁的区别:
- 二值信号量只有"触发/未触发"两种状态,适合单个事件通知。
- 若要表示多个事件的组合,用事件(4.3.3);若要互斥保护共享资源,用互斥锁(4.3.2)。
/* 中断-任务同步示例 */
os_semaphore_id sem;
void uart_rx_irq_handler(void)
{
/* 中断上半部:收到数据,通知任务处理 */
os_semaphore_post(sem); /* 中断中可以 post */
}
void uart_rx_task(void *arg)
{
while (1)
{
os_semaphore_wait(sem, OS_WAIT_FOREVER); /* 等待数据到达 */
handle_rx_data(); /* 处理接收到的数据 */
}
}1.2.2 计数信号量(Counting Semaphore,最大值 = N > 1)
本质:一个"计数器",统计当前有多少个可用的同类型资源。计数从 value(初始可用数)开始,P 取走资源(减 1)、V 归还资源(加 1)。
核心用途:管理 N 个相同资源,控制"最多 N 个任务同时访问":
初始:计数 = N(N 个资源都空闲)
任务 A 取资源 ──► 计数 N-1 任务 B 取资源 ──► 计数 N-2
任务 C 取资源 ──► 计数 N-3 ...
任务 D 取资源 ──► 计数 0(资源用尽,D 阻塞等待)
任务 A 归还资源 ──► 计数 1 → 唤醒 D,D 拿到资源继续典型场景:
| 场景 | 说明 |
|---|---|
| 缓冲区槽位管理 | 环形缓冲区有 N 个槽位,用计数信号量限制最多 N 个生产者 |
| 外设通道/连接数限制 | 最多允许 N 个任务同时使用某个外设 |
| 任务池/线程池 | 限制并发执行的任务数为 N |
/* 管理 3 个可用的网络连接 */
os_semaphore_id conn_sem;
void init(void)
{
conn_sem = os_semaphore_create(OS_NULL, "conn", 3, 3); /* 初始 3 个可用 */
}
void task_use_conn(void *arg)
{
os_semaphore_wait(conn_sem, OS_WAIT_FOREVER); /* 获取一个连接(计数 -1)*/
use_network_connection(); /* 使用连接 */
os_semaphore_post(conn_sem); /* 归还连接(计数 +1)*/
}关键点:计数信号量的 max_value 就是资源的总数量 N。当计数为 0 时,再 wait 的任务会阻塞,直到某个任务 post 归还资源。这样就保证了"并发访问资源的任务数不会超过 N"。
小结:二值信号量关注"事件是否发生"(是/否),计数信号量关注"还有多少资源可用"(0~N)。前者用于同步通知,后者用于资源配额限制。
1.3 信号量最大值
OneOS 信号量的最大值定义在 os_sem.h 中:
#define OS_SEM_MAX_VALUE OS_UINT32_MAX /* 最大值为 uint32_t 的最大值 */二、API 详解
以下 API 定义在 kernel/include/os_sem.h 中,使用时需包含该头文件:
#include <os_sem.h>注意:使用信号量需要在
oneos_config.h中启用OS_USING_SEMAPHORE宏。
2.1 静态定义
#define OS_SEMAPHORE_DEFINE(name) os_semaphore_dummy_t name在编译时定义一个信号量控制块,用于静态创建。name 为变量名。
2.2 创建信号量
os_semaphore_id os_semaphore_create(os_semaphore_dummy_t *semaphore_cb,
const char *name,
uint32_t value,
uint32_t max_value);| 参数 | 说明 |
|---|---|
semaphore_cb | 信号量控制块指针。静态创建时传入 OS_SEMAPHORE_DEFINE 定义的变量地址;动态创建时传 OS_NULL |
name | 信号量名称,用于调试和诊断 |
value | 信号量初始计数值 |
max_value | 信号量最大计数值 |
| 返回值 | 成功返回信号量 ID,失败返回 OS_NULL |
完整源码(kernel/source/os_sem.c,与项目一致):
os_semaphore_id os_semaphore_create(os_semaphore_dummy_t *semaphore_cb,
const char *name,
uint32_t value,
uint32_t max_value)
{
uint8_t alloc_type;
const os_semaphore_t *iter_sem;
os_semaphore_t *sem;
os_bool_t flag;
const os_list_node_t *pos;
OS_ASSERT(max_value >= value);
OS_ASSERT(max_value > 0U);
#ifndef OS_USING_HEAP
OS_ASSERT(OS_NULL != semaphore_cb);
#endif
flag = OS_TRUE;
if (OS_NULL != semaphore_cb)
{
sem = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_cb);
alloc_type = OS_ALLOC_TYPE_STATIC;
os_spin_lock(&gs_os_semaphore_resource_list_lock);
os_list_for_each(pos, &gs_os_semaphore_resource_list_head)
{
iter_sem = os_list_entry(pos, os_semaphore_t, resource_node);
if (iter_sem == sem)
{
sem = OS_NULL;
os_spin_unlock(&gs_os_semaphore_resource_list_lock);
OS_KERN_LOG(KERN_ERROR, SEMAPHORE_TAG, "%s", (OS_KERNEL_ERR_INFO));
flag = OS_FALSE;
break;
}
}
}
#ifdef OS_USING_HEAP
else
{
alloc_type = OS_KOBJ_ALLOC_TYPE_DYNAMIC;
sem = (os_semaphore_t *)OS_KERNEL_MALLOC(sizeof(os_semaphore_t));
if (OS_NULL == sem)
{
OS_KERN_LOG(KERN_ERROR, SEMAPHORE_TAG, "Semaphore memory malloc fail");
flag = OS_FALSE;
}
}
#endif
if (OS_TRUE == flag)
{
_k_semaphore_init(sem, value, max_value, name, alloc_type);
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == (alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
os_spin_lock(&gs_os_semaphore_resource_list_lock);
}
os_list_add_tail(&gs_os_semaphore_resource_list_head, &sem->resource_node);
os_spin_unlock(&gs_os_semaphore_resource_list_lock);
}
return OS_TYPE_CONVERT(os_semaphore_id, sem);
}分步详解:
第一步:参数校验
OS_ASSERT(max_value >= value);
OS_ASSERT(max_value > 0U);
#ifndef OS_USING_HEAP
OS_ASSERT(OS_NULL != semaphore_cb);
#endif详细解释:
| 断言 | 作用 |
|---|---|
OS_ASSERT(max_value >= value) | 最大计数值必须 ≥ 初始计数值 |
OS_ASSERT(max_value > 0U) | 最大计数值必须 > 0 |
OS_ASSERT(OS_NULL != semaphore_cb) | 未启用堆时必须传入静态控制块(无法动态分配) |
第二步:静态创建(处理静态控制块)
if (OS_NULL != semaphore_cb)
{
sem = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_cb);
alloc_type = OS_ALLOC_TYPE_STATIC;
os_spin_lock(&gs_os_semaphore_resource_list_lock);
os_list_for_each(pos, &gs_os_semaphore_resource_list_head)
{
iter_sem = os_list_entry(pos, os_semaphore_t, resource_node);
if (iter_sem == sem)
{
sem = OS_NULL;
os_spin_unlock(&gs_os_semaphore_resource_list_lock);
OS_KERN_LOG(KERN_ERROR, SEMAPHORE_TAG, "%s", (OS_KERNEL_ERR_INFO));
flag = OS_FALSE;
break;
}
}
}详细解释:
- 用户传入非空
semaphore_cb时为静态创建,直接用用户提供的内存作为控制块,标记OS_ALLOC_TYPE_STATIC。 - 加自旋锁遍历全局信号量资源链表,检查该静态控制块地址是否已被占用(防重复创建)。若已存在,置
sem = NULL、flag = OS_FALSE,创建失败。
第三步:动态创建(分配内存)
else
{
alloc_type = OS_KOBJ_ALLOC_TYPE_DYNAMIC;
sem = (os_semaphore_t *)OS_KERNEL_MALLOC(sizeof(os_semaphore_t));
if (OS_NULL == sem)
{
OS_KERN_LOG(KERN_ERROR, SEMAPHORE_TAG, "Semaphore memory malloc fail");
flag = OS_FALSE;
}
}详细解释:
- 用户传入
OS_NULL时为动态创建(需启用OS_USING_HEAP),从堆分配控制块,标记OS_KOBJ_ALLOC_TYPE_DYNAMIC。 - 若堆内存不足(
sem == NULL),打印错误日志并置flag = OS_FALSE。
第四步:初始化并加入资源链表
if (OS_TRUE == flag)
{
_k_semaphore_init(sem, value, max_value, name, alloc_type);
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == (alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
os_spin_lock(&gs_os_semaphore_resource_list_lock);
}
os_list_add_tail(&gs_os_semaphore_resource_list_head, &sem->resource_node);
os_spin_unlock(&gs_os_semaphore_resource_list_lock);
}
return OS_TYPE_CONVERT(os_semaphore_id, sem);详细解释:
| 代码 | 作用 |
|---|---|
_k_semaphore_init(sem, value, max_value, name, alloc_type) | 初始化信号量控制块各字段(计数值、最大值、名称等) |
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == (alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC)) | 动态创建时需加自旋锁(静态创建时在第二步已加过锁) |
os_spin_lock(...) | 动态创建时加自旋锁,保护资源链表 |
os_list_add_tail(&gs_os_semaphore_resource_list_head, &sem->resource_node) | 把信号量登记到全局资源链表,供系统统一管理 |
os_spin_unlock(&gs_os_semaphore_resource_list_lock) | 释放自旋锁 |
return OS_TYPE_CONVERT(os_semaphore_id, sem) | 返回转换后的信号量 ID;失败时 sem 为 NULL,返回 OS_NULL |
2.3 销毁信号量
os_err_t os_semaphore_destroy(os_semaphore_id semaphore_id);| 参数 | 说明 |
|---|---|
semaphore_id | 要销毁的信号量 ID |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 销毁成功 |
销毁信号量,释放其占用的内核资源。如果有任务正在等待该信号量,它们将被唤醒并收到错误码。
完整源码(kernel/source/os_sem.c,与项目一致):
os_err_t os_semaphore_destroy(os_semaphore_id semaphore_id)
{
os_bool_t need_sched;
os_semaphore_t *semaphore;
OS_KERNEL_INIT();
semaphore = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id);
OS_ASSERT(OS_FALSE == os_is_irq_active());
OS_ASSERT(OS_NULL != semaphore);
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited);
#ifndef OS_USING_HEAP
OS_ASSERT(OS_ALLOC_TYPE_STATIC == semaphore->object_alloc_type);
#endif
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&semaphore->lock, &irq_save);
#else
OS_KERNEL_ENTER();
#endif
semaphore->object_inited = OS_KOBJ_DEINITED;
need_sched = k_cancel_all_blocked_task(&semaphore->task_list_head);
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_unlock_irq(&semaphore->lock, irq_save);
if (OS_TRUE == need_sched)
{
OS_KERNEL_ENTER();
OS_KERNEL_EXIT_SCHED();
}
#else
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
#endif
os_spin_lock(&gs_os_semaphore_resource_list_lock);
os_list_del(&semaphore->resource_node);
os_spin_unlock(&gs_os_semaphore_resource_list_lock);
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == semaphore->object_alloc_type)
{
#ifdef OS_USING_HEAP
OS_KERNEL_FREE(semaphore);
semaphore = OS_NULL;
#endif
}
return OS_SUCCESS;
}分步详解:
第一步:准备与参数校验
OS_KERNEL_INIT();
semaphore = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id);
OS_ASSERT(OS_FALSE == os_is_irq_active());
OS_ASSERT(OS_NULL != semaphore);
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited);
#ifndef OS_USING_HEAP
OS_ASSERT(OS_ALLOC_TYPE_STATIC == semaphore->object_alloc_type);
#endif详细解释:
| 断言/语句 | 作用 |
|---|---|
OS_KERNEL_INIT() | 确保内核已初始化 |
OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id) | 把信号量 ID 转换为内部指针 |
OS_ASSERT(OS_FALSE == os_is_irq_active()) | 不能在中断上下文中销毁(销毁会释放内存、唤醒任务) |
OS_ASSERT(OS_NULL != semaphore) | 信号量指针非空 |
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited) | 信号量必须已初始化(防止重复销毁) |
OS_ASSERT(OS_ALLOC_TYPE_STATIC == semaphore->object_alloc_type) | 未启用堆时信号量必须是静态分配(无法释放动态内存) |
第二步:标记去初始化并唤醒所有等待任务
semaphore->object_inited = OS_KOBJ_DEINITED;
need_sched = k_cancel_all_blocked_task(&semaphore->task_list_head);详细解释:
semaphore->object_inited = OS_KOBJ_DEINITED:先标记为"去初始化",防止销毁过程中其他任务再对它操作。k_cancel_all_blocked_task():取消该信号量等待队列上的所有任务(把它们从等待队列移回就绪队列,并设置错误码)。- 返回值
need_sched:如果有任务被唤醒(从阻塞变为就绪),则需要触发调度。
第三步:按需触发调度
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}详细解释:
- 若有任务被唤醒(
need_sched == OS_TRUE),退出临界区并触发调度,让被唤醒的高优先级任务有机会运行。 - 否则仅退出临界区。
第四步:从资源链表移除并释放内存
os_spin_lock(&gs_os_semaphore_resource_list_lock);
os_list_del(&semaphore->resource_node);
os_spin_unlock(&gs_os_semaphore_resource_list_lock);
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == semaphore->object_alloc_type)
{
#ifdef OS_USING_HEAP
OS_KERNEL_FREE(semaphore);
semaphore = OS_NULL;
#endif
}
return OS_SUCCESS;详细解释:
| 代码 | 作用 |
|---|---|
os_list_del(&semaphore->resource_node) | 从全局信号量资源链表移除该信号量 |
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == semaphore->object_alloc_type) | 判断信号量是否为动态分配 |
OS_KERNEL_FREE(semaphore) | 动态分配的信号量释放堆内存 |
return OS_SUCCESS | 返回成功 |
关键点:
- 只释放动态分配的内存:静态创建(用户提供控制块)的信号量不释放内存,动态创建的才
os_free。这与创建时的alloc_type标记一一对应。 - 销毁后,该信号量 ID 失效,不能再用于
os_semaphore_wait()/post()。
2.4 P 操作(等待)
os_err_t os_semaphore_wait(os_semaphore_id semaphore_id, os_tick_t timeout);| 参数 | 说明 |
|---|---|
semaphore_id | 信号量 ID |
timeout | 超时时间(tick 数)。OS_NO_WAIT(0)表示不等待;OS_WAIT_FOREVER 表示永久等待 |
| 返回值 | OS_EOK 成功;OS_ETIMEOUT 超时;OS_ERROR 错误 |
完整源码(kernel/source/os_sem.c,与项目一致):
os_err_t os_semaphore_wait(os_semaphore_id semaphore_id, os_tick_t timeout)
{
os_err_t ret;
os_semaphore_t *semaphore;
OS_KERNEL_INIT();
semaphore = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id);
OS_ASSERT(OS_NULL != semaphore);
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited);
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_active()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_disabled()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_schedule_locked()));
OS_ASSERT((timeout < (OS_TICK_MAX / 2)) || (OS_WAIT_FOREVER == timeout));
ret = OS_SUCCESS;
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&semaphore->lock, &irq_save);
#else
OS_KERNEL_ENTER();
#endif
if (semaphore->count > 0)
{
semaphore->count--;
OS_SEM_HOOK_CALL(sem, wait, success, semaphore_id);
}
else
{
if (OS_NO_WAIT == timeout)
{
OS_SEM_HOOK_CALL(sem, wait, fail, semaphore_id);
ret = OS_BUSY;
}
else
{
OS_ASSERT((OS_NULL != _k_task_self()));
OS_SEM_HOOK_CALL(sem, wait, block, semaphore_id);
if (OS_SEM_WAKE_TYPE_PRIO == semaphore->wake_type)
{
ret = k_block_task(&semaphore->lock, irq_save, &semaphore->task_list_head, timeout, OS_TRUE);
}
else
{
ret = k_block_task(&semaphore->lock, irq_save, &semaphore->task_list_head, timeout, OS_FALSE);
}
if (OS_SUCCESS == ret)
{
OS_SEM_HOOK_CALL(sem, wait, success, semaphore_id);
}
else
{
OS_SEM_HOOK_CALL(sem, wait, timeout, semaphore_id);
}
return ret;
}
}
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_unlock_irq(&semaphore->lock, irq_save);
#else
OS_KERNEL_EXIT();
#endif
return ret;
}分步详解:
第一步:准备与参数校验
OS_KERNEL_INIT();
semaphore = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id);
OS_ASSERT(OS_NULL != semaphore);
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited);
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_active()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_disabled()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_schedule_locked()));
OS_ASSERT((timeout < (OS_TICK_MAX / 2)) || (OS_WAIT_FOREVER == timeout));详细解释:
| 断言 | 作用 |
|---|---|
OS_ASSERT(OS_NULL != semaphore) | 信号量指针非空 |
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited) | 信号量必须已初始化 |
后三条 timeout 断言 | 只有当需要真正等待(timeout != OS_NO_WAIT)时才限制:不能在中断上下文、中断禁用、调度锁定时调用(因为等待会阻塞并切换上下文) |
OS_ASSERT((timeout < OS_TICK_MAX / 2) || (OS_WAIT_FOREVER == timeout)) | 超时值合法(OS_WAIT_FOREVER 例外,永久等待) |
第二步:计数 > 0 —— 直接获取成功
if (semaphore->count > 0)
{
semaphore->count--;
OS_SEM_HOOK_CALL(sem, wait, success, semaphore_id);
}详细解释:若还有可用信号(count > 0),计数减 1 直接返回成功,无需等待。这是"有资源直接拿"的情况。
第三步:计数 = 0 —— 等待或返回忙
else
{
if (OS_NO_WAIT == timeout)
{
OS_SEM_HOOK_CALL(sem, wait, fail, semaphore_id);
ret = OS_BUSY;
}
else
{ ... 阻塞等待 ... }
}详细解释:
timeout == OS_NO_WAIT:信号量已空且不等待,立即返回OS_BUSY(忙,获取失败)。timeout != OS_NO_WAIT:进入阻塞等待(见第四步)。
第四步:阻塞等待(按唤醒策略)
if (OS_SEM_WAKE_TYPE_PRIO == semaphore->wake_type)
{
ret = k_block_task(&semaphore->lock, irq_save, &semaphore->task_list_head, timeout, OS_TRUE);
}
else
{
ret = k_block_task(&semaphore->lock, irq_save, &semaphore->task_list_head, timeout, OS_FALSE);
}详细解释:
k_block_task():把当前任务挂到信号量的等待队列,并让出 CPU(阻塞)。当前任务会一直等到:信号量被 post 唤醒,或超时。- 最后一个参数决定唤醒方式:
OS_TRUE= 按优先级唤醒(PRIO)、OS_FALSE= 按 FIFO 唤醒。 - 返回
OS_SUCCESS(被唤醒拿到信号量)或超时错误码。
2.5 V 操作(释放)
os_err_t os_semaphore_post(os_semaphore_id semaphore_id);释放信号量。可以在中断上下文中调用。
完整源码(kernel/source/os_sem.c,与项目一致):
os_err_t os_semaphore_post(os_semaphore_id semaphore_id)
{
os_err_t ret;
os_task_t *task;
os_semaphore_t *semaphore;
OS_KERNEL_INIT();
semaphore = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id);
OS_ASSERT(OS_NULL != semaphore);
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited);
ret = OS_SUCCESS;
do
{
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&semaphore->lock, &irq_save);
k_kernel_spin_lock();
#else
OS_KERNEL_ENTER();
#endif
if (os_list_empty(&semaphore->task_list_head))
{
if (semaphore->count < semaphore->max_count)
{
semaphore->count++;
OS_SEM_HOOK_CALL(sem, post, success, semaphore_id);
}
else
{
OS_SEM_HOOK_CALL(sem, post, full, semaphore_id);
ret = OS_FULL;
}
}
else
{
task = os_list_first_entry(&semaphore->task_list_head, os_task_t, task_node);
OS_SEM_HOOK_CALL(sem, post, wake, semaphore_id);
if (k_unblock_task(task) == OS_TRUE)
{
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
k_kernel_spin_unlock();
os_spin_unlock_irq(&semaphore->lock, irq_save);
OS_KERNEL_ENTER();
OS_KERNEL_EXIT_SCHED();
#else
OS_KERNEL_EXIT_SCHED();
#endif
break;
}
}
} while (1);
}分步详解:
第一步:准备与参数校验
OS_KERNEL_INIT();
semaphore = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id);
OS_ASSERT(OS_NULL != semaphore);
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited);详细解释:转换指针并断言(非空 + 已初始化)。注意 os_semaphore_post() 没有 timeout 相关的阻塞断言——因为它不阻塞,可以在中断上下文中安全调用(这正是中断-任务同步的关键)。
第二步:没有等待任务 —— 计数加 1
if (os_list_empty(&semaphore->task_list_head))
{
if (semaphore->count < semaphore->max_count)
{
semaphore->count++;
OS_SEM_HOOK_CALL(sem, post, success, semaphore_id);
}
else
{
OS_SEM_HOOK_CALL(sem, post, full, semaphore_id);
ret = OS_FULL;
}
}详细解释:
- 没有任务在等待(等待队列为空)时,说明没有任务被信号量阻塞,此时把计数加 1(相当于"存下一个信号",供以后的 wait 使用)。
if (semaphore->count < semaphore->max_count):判断计数是否已达到最大值。- 未满(
count < max_count):semaphore->count++计数加 1,post 成功。 - 已满(
count == max_count):计数不能再加(否则超过上限),置ret = OS_FULL(信号量已满)。
- 未满(
OS_SEM_HOOK_CALL(...):IPC 钩子调用,用于观测/追踪(详见下方专项说明),对信号量功能本身无影响。
关于 OS_SEM_HOOK_CALL(IPC 钩子)的说明:
OS_SEM_HOOK_CALL(sem, post, success, semaphore_id) 是一个钩子宏,它允许用户在信号量操作发生时插入自定义回调(例如统计信号量的使用次数、追踪 wait/post 事件)。
| 部分 | 含义 |
|---|---|
sem | 域(domain):指明是信号量域的钩子 |
post | 操作(hook point):指明是 post 操作的钩子 |
success / full / wake | 状态(status):本次操作的结果(成功 / 已满 / 唤醒等待者) |
semaphore_id | 传入钩子函数的参数(信号量 ID) |
它默认什么都不做:OS_SEM_HOOK_CALL 在未启用 OS_USING_IPC_HOOK 时被定义为空操作(#define OS_SEM_HOOK_CALL(...),见 os_sem_trace.h),所以不会产生任何额外开销,也不影响信号量的正常功能。
只有满足以下两个条件时它才真正执行:
- 在 Kconfig 中启用
OS_USING_IPC_HOOK。 - 用户通过 IPC 钩子注册 API(如
os_sem_hook_xxx_add())注册了对应状态的回调函数。
启用后,宏会检查 gs_os_hook_sem_post_success 等钩子函数指针是否非空,非空则调用:
if (gs_os_hook_sem_post_success) /* 已注册 post-success 钩子 */
{
gs_os_hook_sem_post_success(semaphore_id); /* 调用用户回调 */
}小结:
OS_SEM_HOOK_CALL是可选的可观测性扩展(IPC 钩子)。默认关闭、零开销,只在需要追踪信号量行为时才启用。理解它时把它当作一条"空语句"即可,不影响上面分析的计数逻辑。
第三步:有等待任务 —— 唤醒一个
else
{
task = os_list_first_entry(&semaphore->task_list_head, os_task_t, task_node);
OS_SEM_HOOK_CALL(sem, post, wake, semaphore_id);
if (k_unblock_task(task) == OS_TRUE)
{
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
k_kernel_spin_unlock();
os_spin_unlock_irq(&semaphore->lock, irq_save);
OS_KERNEL_ENTER();
OS_KERNEL_EXIT_SCHED();
#else
OS_KERNEL_EXIT_SCHED();
#endif
break;
}
}详细解释:
| 代码 | 作用 |
|---|---|
task = os_list_first_entry(&semaphore->task_list_head, ...) | 取出等待队列头部的任务(PRIO 模式为最高优先级,FIFO 模式为最先进来的) |
OS_SEM_HOOK_CALL(sem, post, wake, semaphore_id) | IPC 钩子调用(状态为 wake,即"唤醒等待者"),默认空操作,见上文说明 |
if (k_unblock_task(task) == OS_TRUE) | 唤醒该任务(把它从阻塞队列移回就绪队列);返回 OS_TRUE 表示唤醒成功且需要调度 |
OS_KERNEL_EXIT_SCHED() | 退出临界区并触发调度,让被唤醒的高优先级任务有机会运行 |
break | 跳出 do { ... } while(1) 循环,post 结束 |
关键点:
- 有等待任务时不增加计数,而是直接把信号"转交"给等待队列头部的任务(
k_unblock_task唤醒它)。因为信号已经"交给"了等待者,计数无需再累加。 - SMP 分支中先
k_kernel_spin_unlock()+os_spin_unlock_irq()释放两把锁,再进入临界区触发调度(因为已退出锁保护,需要重新进临界区才能安全触发调度)。 break跳出do-while(1)循环:一旦唤醒成功,post 立即结束。如果唤醒失败(OS_FALSE),则回到循环头重新检查(正常情况下不会出现)。
关键点 —— P/V 操作的对称性:
| 操作 | 无等待者 | 有等待者 |
|---|---|---|
| P(wait) | 计数 -1,直接获取 | 阻塞,进入等待队列 |
| V(post) | 计数 +1,存下信号 | 唤醒等待队列头部任务,不计数 |
这就是信号量的核心:计数代表"当前有多少可用信号",P 取走、V 存放;一旦有任务等待,V 就直接把信号转交给等待任务。
2.6 查询函数
os_err_t os_semaphore_get_value(os_semaphore_id semaphore_id, uint32_t *count);
os_err_t os_semaphore_get_max_value(os_semaphore_id semaphore_id, uint32_t *max_count);
os_bool_t os_semaphore_is_exist(os_semaphore_id semaphore_id);
const char *os_semaphore_get_name(os_semaphore_id semaphore_id);| 函数 | 说明 |
|---|---|
os_semaphore_get_value | 获取当前信号量计数值 |
os_semaphore_get_max_value | 获取信号量最大计数值 |
os_semaphore_is_exist | 检查信号量是否存在 |
os_semaphore_get_name | 获取信号量名称 |
os_semaphore_get_value() 完整源码(kernel/source/os_sem.c,与项目一致):
os_err_t os_semaphore_get_value(os_semaphore_id semaphore_id, uint32_t *count)
{
os_semaphore_t *semaphore;
semaphore = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id);
OS_ASSERT(OS_NULL != semaphore);
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited);
OS_ASSERT(OS_NULL != count);
*count = semaphore->count;
return OS_SUCCESS;
}分步详解:
| 代码 | 作用 |
|---|---|
OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id) | 把信号量 ID 转换为内部指针 |
OS_ASSERT(OS_NULL != semaphore) | 信号量指针非空 |
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited) | 信号量必须已初始化 |
OS_ASSERT(OS_NULL != count) | 传出参数非空(防止写入非法地址) |
*count = semaphore->count | 核心:把信号量计数值写入传出参数 |
return OS_SUCCESS | 返回成功 |
关键点:count 是传出参数(out 参数),通过指针把结果"写回"给调用者。查询计数值常用于判断信号量当前是否有可用资源(count > 0 表示可 wait 而不阻塞)。
使用示例:
uint32_t cnt;
os_semaphore_get_value(sem_id, &cnt);
if (cnt > 0)
{
/* 信号量有可用资源,wait 不会阻塞 */
}
else
{
/* 信号量为空,wait 会阻塞 */
}os_semaphore_get_max_value() —— 获取最大计数值
完整源码(kernel/source/os_sem.c,与项目一致):
os_err_t os_semaphore_get_max_value(os_semaphore_id semaphore_id, uint32_t *max_count)
{
os_semaphore_t *semaphore;
semaphore = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id);
OS_ASSERT(OS_NULL != semaphore);
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited);
OS_ASSERT(OS_NULL != max_count);
*max_count = semaphore->max_count;
return OS_SUCCESS;
}分步详解:
| 代码 | 作用 |
|---|---|
OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id) | 把信号量 ID 转换为内部指针 |
OS_ASSERT(OS_NULL != semaphore) | 信号量指针非空 |
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited) | 信号量必须已初始化 |
OS_ASSERT(OS_NULL != max_count) | 传出参数非空 |
*max_count = semaphore->max_count | 核心:把信号量最大计数值写入传出参数 |
return OS_SUCCESS | 返回成功 |
关键点:与 os_semaphore_get_value() 结构完全一致,只是返回 max_count(最大计数值)而非 count(当前计数值)。查询最大值常用于判断信号量的资源配额上限。
使用示例:
uint32_t max;
os_semaphore_get_max_value(sem_id, &max);
os_kprintf("Semaphore max value: %u\r\n", max);os_semaphore_is_exist() —— 检查信号量是否存在
完整源码(kernel/source/os_sem.c,与项目一致):
os_bool_t os_semaphore_is_exist(os_semaphore_id semaphore_id)
{
os_semaphore_t *iter_sem;
os_bool_t valid_sem;
os_semaphore_t *semaphore;
semaphore = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id);
OS_ASSERT(OS_NULL != semaphore);
valid_sem = OS_FALSE;
os_spin_lock(&gs_os_semaphore_resource_list_lock);
os_list_for_each_entry(iter_sem, &gs_os_semaphore_resource_list_head, os_semaphore_t, resource_node)
{
if (iter_sem == semaphore)
{
valid_sem = OS_TRUE;
break;
}
}
os_spin_unlock(&gs_os_semaphore_resource_list_lock);
return valid_sem;
}分步详解:
| 代码 | 作用 |
|---|---|
OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id) | 把信号量 ID 转换为内部指针 |
OS_ASSERT(OS_NULL != semaphore) | 信号量指针非空 |
valid_sem = OS_FALSE | 初始默认"不存在" |
os_spin_lock(...) | 加自旋锁,保护全局资源链表 |
os_list_for_each_entry(...) | 遍历全局信号量资源链表 |
if (iter_sem == semaphore) | 判断链表中是否存在与该 ID 相同的信号量 |
valid_sem = OS_TRUE | 存在则置 OS_TRUE |
os_spin_unlock(...) | 释放自旋锁 |
return valid_sem | 返回是否存在 |
关键点:
- 与 4.2 任务创建中的防重复检查类似,通过遍历全局资源链表判断信号量是否有效存在(未销毁)。
- 常用于在使用信号量前校验其有效性,避免访问已销毁的信号量。
使用示例:
if (os_semaphore_is_exist(sem_id) == OS_TRUE)
{
/* 信号量有效,可以使用 */
os_semaphore_post(sem_id);
}os_semaphore_get_name() —— 获取信号量名称
完整源码(kernel/source/os_sem.c,与项目一致):
const char *os_semaphore_get_name(os_semaphore_id semaphore_id)
{
os_semaphore_t *semaphore;
semaphore = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id);
OS_ASSERT(OS_NULL != semaphore);
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited);
return semaphore->name;
}分步详解:
| 代码 | 作用 |
|---|---|
OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id) | 把信号量 ID 转换为内部指针 |
OS_ASSERT(OS_NULL != semaphore) | 信号量指针非空 |
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited) | 信号量必须已初始化 |
return semaphore->name | 核心:返回信号量名称字符串指针 |
关键点:
- 返回值是
const char *(名称字符串指针),由创建时传入的name保存。 - 返回的是指针而非副本,调用者只能读取,不应修改。
使用示例:
os_kprintf("Semaphore name: %s\r\n", os_semaphore_get_name(sem_id));2.7 唤醒策略
os_err_t os_semaphore_set_wake_type(os_semaphore_id semaphore_id, uint8_t wake_type);唤醒类型:
| 唤醒类型 | 宏 | 含义 |
|---|---|---|
| 优先级唤醒 | OS_SEM_WAKE_TYPE_PRIO (0x55) | 唤醒等待队列中优先级最高的任务 |
| FIFO 唤醒 | OS_SEM_WAKE_TYPE_FIFO (0xAA) | 唤醒等待队列中等待时间最长(先来)的任务 |
完整源码(kernel/source/os_sem.c,与项目一致):
os_err_t os_semaphore_set_wake_type(os_semaphore_id semaphore_id, uint8_t wake_type)
{
os_err_t ret;
os_semaphore_t *semaphore;
OS_KERNEL_INIT();
semaphore = OS_TYPE_CONVERT(os_semaphore_t *, semaphore_id);
OS_ASSERT(OS_NULL != semaphore);
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited);
OS_ASSERT((OS_SEM_WAKE_TYPE_PRIO == wake_type) || (OS_SEM_WAKE_TYPE_FIFO == wake_type));
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
os_spin_lock_irq(&semaphore->lock, &irq_save);
k_kernel_spin_lock();
#else
OS_KERNEL_ENTER();
#endif
if (os_list_empty(&semaphore->task_list_head))
{
semaphore->wake_type = wake_type;
ret = OS_SUCCESS;
}
else
{
ret = OS_BUSY;
}
#if defined(OS_USING_SMP) && !defined(OS_IPC_MONOLOCK_MODE)
k_kernel_spin_unlock();
os_spin_unlock_irq(&semaphore->lock, irq_save);
#else
OS_KERNEL_EXIT();
#endif
return ret;
}分步详解:
第一步:准备与参数校验
OS_ASSERT(OS_NULL != semaphore);
OS_ASSERT(OS_KOBJ_INITED == semaphore->object_inited);
OS_ASSERT((OS_SEM_WAKE_TYPE_PRIO == wake_type) || (OS_SEM_WAKE_TYPE_FIFO == wake_type));详细解释:
- 前两条断言:信号量非空且已初始化。
- 第三条断言:
wake_type必须是 PRIO 或 FIFO 之一(防止传入非法值)。
第二步:仅在没有等待任务时可修改
if (os_list_empty(&semaphore->task_list_head))
{
semaphore->wake_type = wake_type;
ret = OS_SUCCESS;
}
else
{
ret = OS_BUSY;
}详细解释:
- 只有等待队列为空时才能修改唤醒策略(
semaphore->wake_type = wake_type),返回OS_SUCCESS。 - 若有任务正在等待(等待队列非空),返回
OS_BUSY——因为队列中已有任务按旧策略排序,中途改策略会导致排序错乱。
关键点 —— 何时设置唤醒策略:
应在创建信号量之后、有任务开始等待之前设置。一旦有任务进入等待队列,就无法再修改:
sem_id = os_semaphore_create(OS_NULL, "sem", 0, 1);
os_semaphore_set_wake_type(sem_id, OS_SEM_WAKE_TYPE_FIFO); /* 在无等待者时设置 */PRIO vs FIFO 对比:
| 维度 | PRIO(优先级) | FIFO(先进先出) |
|---|---|---|
| 唤醒顺序 | 按任务优先级,高优先级先唤醒 | 按等待时间,先来的先唤醒 |
| 适用场景 | 高优先级任务应优先获得资源 | 任务地位平等,讲究公平、按序处理 |
| 默认值 | 是(OS_SEM_WAKE_TYPE_PRIO) | 否(需显式设置) |
| 典型用途 | 实时系统中高优先级任务优先 | 数据处理、公平调度 |
📝 本节小结
核心要点回顾
本节围绕信号量机制展开,核心要点如下:
1. 信号量的本质
- 一个非负整数计数器 + 一个任务等待队列,提供 P(wait)和 V(post)两个原子操作。
- 计数代表"当前有多少可用信号":P 取走、V 存放;有等待者时 V 直接把信号转交给等待任务。
2. 两种用途
| 类型 | 初始值/最大值 | 用途 |
|---|---|---|
| 二值信号量 | 0 / 1 | 任务同步、中断-任务通知 |
| 计数信号量 | N / N | 管理 N 个相同资源 |
3. P/V 操作核心逻辑
| 操作 | 无等待者 | 有等待者 |
|---|---|---|
| P(wait) | 计数 -1,直接获取 | 阻塞进等待队列 |
| V(post) | 计数 +1,存下信号 | 唤醒等待队列头部任务 |
4. 关键特性
os_semaphore_post()可以在中断中调用(不阻塞),是中断-任务同步的最常用方式。os_semaphore_wait()阻塞时不能在中断/关中断/调度锁定中调用。- 唤醒策略:PRIO(按优先级)/ FIFO(按等待时间),通过
os_semaphore_set_wake_type()设置。 - 创建方式:静态(
OS_SEMAPHORE_DEFINE)/ 动态(传OS_NULL,需OS_USING_HEAP)。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
在中断中调用 os_semaphore_wait() | 阻塞 API,禁止在中断中调用;中断中只能用 os_semaphore_post() |
| 用信号量保护共享资源(多个写者) | 信号量适合同步/计数,互斥保护应用互斥锁 |
忘记处理 os_semaphore_wait() 超时返回值 | 超时会返回非成功错误码,需判断 OS_EOK 再访问共享数据 |
| 二值信号量与互斥锁混淆 | 信号量无"所有权/优先级继承",互斥锁有,临界区保护用互斥锁 |
| post 到已满信号量 | 返回 OS_FULL,需处理 |
动态创建但未启用 OS_USING_HEAP | 动态创建失败,应改用 OS_SEMAPHORE_DEFINE 静态定义 |
下一步
信号量是同步的基础。接下来请学习下一小节:
- 4.3.2 互斥锁:了解互斥锁与信号量的区别,以及优先级继承机制