信号量资源链表机制
信号量资源链表机制
一、概述
os_semaphore_create 在创建信号量后,会将其注册到一个全局资源链表中。本文详细解释这个链表的结构、自旋锁保护机制,以及相关的宏展开过程。
二、两个关键变量
定义在 kernel/source/os_sem.c 第 44-45 行:
static os_list_node_t gs_os_semaphore_resource_list_head = OS_LIST_INIT(gs_os_semaphore_resource_list_head);
static OS_DEFINE_SPINLOCK(gs_os_semaphore_resource_list_lock);| 变量 | 类型 | 作用 |
|---|---|---|
gs_os_semaphore_resource_list_head | os_list_node_t | 全局信号量资源链表的头节点 |
gs_os_semaphore_resource_list_lock | os_spinlock_t | 保护链表的自旋锁 |
三、gs_os_semaphore_resource_list_lock 自旋锁详解
3.1 定义与宏展开
static OS_DEFINE_SPINLOCK(gs_os_semaphore_resource_list_lock);OS_DEFINE_SPINLOCK 宏定义在 kernel/include/os_spinlock.h 第 83 行:
#define OS_DEFINE_SPINLOCK(var) os_spinlock_t var = {SPIN_LOCK_INIT SPINLOCK_DEBUG_INIT SPINLOCK_PADDING_INIT}编译器预处理时,将宏参数 var 替换为实际传入的 gs_os_semaphore_resource_list_lock,展开后等价于:
static os_spinlock_t gs_os_semaphore_resource_list_lock = { SPIN_LOCK_INIT SPINLOCK_DEBUG_INIT SPINLOCK_PADDING_INIT };逐项解释:
static— 文件作用域,仅在os_sem.c内可见os_spinlock_t— 自旋锁类型,定义在kernel/include/os_spinlock.hgs_os_semaphore_resource_list_lock— 变量名,gs_= 全局静态(Global Static),resource_list= 资源链表,lock= 锁
3.2 os_spinlock_t 结构体
定义在 os_spinlock.h 第 41-56 行:
struct os_spinlock
{
#ifdef OS_USING_SMP
arch_spinlock_t arch_lock; // SMP 架构下的底层自旋锁
#endif
#ifdef OS_USING_SPINLOCK_CHECK
int32_t owner_cpu; // 持有锁的 CPU 编号(调试用)
uint32_t object_inited; // 初始化标志(调试用)
void *owner; // 持有锁的线程指针(调试用)
#endif
#if !defined(OS_USING_SMP) && !defined(OS_USING_SPINLOCK_CHECK)
uint8_t padding; // 非 SMP 且非调试模式下的填充字段
#endif
};结构体有 3 种成员组合,取决于编译配置:
| 编译配置 | 结构体实际成员 |
|---|---|
OS_USING_SMP=y | arch_spinlock_t arch_lock |
OS_USING_SPINLOCK_CHECK=y(SMP 关闭) | int32_t owner_cpu、uint32_t object_inited、void *owner |
| SMP 关闭 且 DEBUG 关闭 | uint8_t padding(仅 1 字节,占位用) |
3.3 三个初始化宏
定义在 os_spinlock.h 第 59-81 行:
// 1. 仅 SMP 模式下初始化 arch_lock
#ifdef OS_USING_SMP
#define SPIN_LOCK_INIT .arch_lock = ARCH_SPINLOCK_INIT_VALUE(SPINLOCK_ARCH_UNLOCKED),
#else
#define SPIN_LOCK_INIT // 非 SMP 时为空
#endif
// 2. 仅调试模式下初始化调试字段
#ifdef OS_USING_SPINLOCK_CHECK
#define SPINLOCK_DEBUG_INIT .owner_cpu = -1, .object_inited = OS_SPINLOCK_INIT_FLAG, .owner = OS_NULL,
#else
#define SPINLOCK_DEBUG_INIT // 非调试时为空
#endif
// 3. 既非 SMP 也非调试模式时,初始化 padding
#if !defined(OS_USING_SMP) && !defined(OS_USING_SPINLOCK_CHECK)
#define SPINLOCK_PADDING_INIT .padding = 0,
#else
#define SPINLOCK_PADDING_INIT // 其他情况为空
#endif3.4 STM32F103ZET6 上的实际展开
该项目是单核 MCU(Cortex-M3),os_config.h 中未启用 OS_USING_SMP 和 OS_USING_SPINLOCK_CHECK:
SPIN_LOCK_INIT → (空)
SPINLOCK_DEBUG_INIT → (空)
SPINLOCK_PADDING_INIT → .padding = 0,最终展开结果:
static os_spinlock_t gs_os_semaphore_resource_list_lock = { .padding = 0 };3.5 为什么单核不需要真正的自旋锁?
多核(SMP)系统:
CPU 0 ──┐
CPU 1 ──┼── 需要真正的自旋锁(硬件原子操作)
CPU 2 ──┤ 一个 CPU 自旋等待时,其他 CPU 可以继续执行
CPU 3 ──┘ 必须用内存屏障确保可见性
单核系统(STM32F103ZET6):
CPU 0 ──── 只有一个核,不存在竞争
spin_lock 的实现就是关中断
spin_unlock 的实现就是开中断
不需要"自旋等待"
padding = 0 只是占位,不参与实际逻辑在单核系统上,整个 gs_os_semaphore_resource_list_lock 实际上是一个几乎无开销的占位变量,真正的互斥保护由关中断完成。
3.6 os_spin_lock 详解
os_spin_lock 定义在 kernel/source/os_spinlock.c 第 148-167 行:
void os_spin_lock(os_spinlock_t *lock)
{
OS_ASSERT((lock != OS_NULL));
os_schedule_lock();
#ifdef OS_USING_SPINLOCK_CHECK
_k_spin_lock_dbg(lock);
#endif
#ifdef OS_USING_SMP
arch_spin_lock(&lock->arch_lock);
#endif
#ifdef OS_USING_SPINLOCK_CHECK
_k_spin_lock_set_owner(lock);
#endif
return;
}对于 STM32F103ZET6(单核、无调试),OS_USING_SMP 和 OS_USING_SPINLOCK_CHECK 均未定义,实际只执行:
void os_spin_lock(os_spinlock_t *lock)
{
OS_ASSERT((lock != OS_NULL));
os_schedule_lock(); // ← 唯一的有效操作
return;
}os_schedule_lock() 的实现
os_schedule_lock() 定义在 kernel/source/os_sched.c 第 894-909 行:
void os_schedule_lock(void)
{
OS_KERNEL_INIT(); // 声明局部变量 irq_save
OS_KERNEL_ENTER(); // 关中断 + 保存中断状态
g_os_sched_lock_cnt++; // 调度锁计数器 +1
OS_KERNEL_EXIT(); // 恢复中断状态
return;
}宏展开过程:
OS_KERNEL_INIT() → os_ubase_t irq_save; // 声明变量
OS_KERNEL_ENTER() → irq_save = os_irq_lock(); // 关中断,保存 PRIMASK
OS_KERNEL_EXIT() → os_irq_unlock(irq_save); // 恢复 PRIMASKos_irq_lock() 与 os_irq_unlock() 详解
这对函数定义在 arch/arm/armv7m/arch_interrupt.c,是 ARM Cortex-M3 上控制中断开关的底层接口。
os_irq_lock()(第 121-133 行)— 关中断并保存状态:
os_ubase_t os_irq_lock(void)
{
os_ubase_t primask;
__asm__ __volatile__(
"MRS %0, PRIMASK\n" // ① 读取 PRIMASK 寄存器到 primask 变量
"CPSID I" // ② 设置 PRIMASK.I = 1,屏蔽所有可屏蔽中断
: "=r"(primask)
:
: "memory");
return primask; // 返回保存的中断状态
}os_irq_unlock()(第 135-144 行)— 恢复中断状态:
void os_irq_unlock(os_ubase_t irq_save)
{
__asm__ __volatile__(
"MSR PRIMASK, %0" // 将 irq_save 写回 PRIMASK 寄存器
:
: "r"(irq_save)
: "memory");
return;
}PRIMASK 寄存器说明:
PRIMASK 是 ARM Cortex-M3 的特殊功能寄存器,只有 1 位有效:
| PRIMASK.I 值 | 含义 |
|---|---|
0 | 所有可屏蔽中断正常响应 |
1 | 所有可屏蔽中断被屏蔽(NMI 和 HardFault 除外) |
工作流程:
os_irq_lock() os_irq_unlock(irq_save)
┌─────────────────────┐ ┌─────────────────────┐
│ 读取 PRIMASK → irq_save │ │
│ (假设 PRIMASK = 0) │ │ 将 irq_save 写回 │
│ │ │ PRIMASK │
│ CPSID I │ │ PRIMASK = 0 │
│ (PRIMASK ← 1) │ │ (中断恢复开启) │
│ │ │ │
│ 返回 irq_save = 0 │ └─────────────────────┘
└─────────────────────┘为什么不是简单的"开/关中断"?
如果 os_irq_unlock 无条件执行 CPSID I 清零(开中断),会破坏调用者原本的中断状态。例如在中断服务程序(ISR)中调用 os_spin_lock,此时中断已经是关闭的,os_spin_unlock 后应该仍然关闭。
通过 保存-恢复 机制,os_irq_lock 返回的状态值 (irq_save) 精确记录了调用前的中断状态,os_irq_unlock 只是将其还原,确保不会意外改变中断状态。
调度锁计数器的作用
g_os_sched_lock_cnt 定义在 kernel/source/os_sched.c 第 76-87 行,有 SMP 和非 SMP 两个版本:
// os_sched.c 第 66-89 行
#ifdef OS_USING_SMP
static int16_t gs_os_sched_lock_cnt[OS_SMP_MAX_CPUS] = {0}; // SMP:每个 CPU 一个计数器
// ...
#else
// ...
int16_t g_os_sched_lock_cnt = 0; // 非 SMP:全局单个计数器
#endif对于 STM32F103ZET6(非 SMP),使用第 86 行的全局变量:
int16_t g_os_sched_lock_cnt = 0; // 初始值为 0,调度器正常工作这个计数器控制是否允许任务调度(上下文切换)。内核在需要切换任务时会检查这个计数器(OS_KERNEL_EXIT_SCHED 宏):
// kernel/source/os_kernel_internal.h 第 298-318 行
#define OS_KERNEL_EXIT_SCHED()
do {
if ((OS_NULL == g_os_current_task) || (0 != g_os_sched_lock_cnt))
{
OS_KERNEL_EXIT(); // 计数器 > 0,不切换任务,只恢复中断
}
else
{
g_os_next_task = g_os_high_task; // 找到最高优先级任务
if (g_os_current_task != g_os_next_task)
{
os_task_switch(); // 执行上下文切换
}
OS_KERNEL_EXIT();
}
} while (0)规则:g_os_sched_lock_cnt > 0 时,即使有更高优先级的任务就绪,也不会发生任务切换。
完整调用链总结
os_spin_lock(&gs_os_semaphore_resource_list_lock)
│
├─ OS_ASSERT(lock != OS_NULL) // 断言:锁指针非空
│
└─ os_schedule_lock() // 锁定调度器
│
├─ irq_save = os_irq_lock() // ① 保存 PRIMASK,关中断(CPSID I)
├─ g_os_sched_lock_cnt++ // ② 调度锁计数器 +1
└─ os_irq_unlock(irq_save) // ③ 恢复 PRIMASK(中断状态回滚)举例:os_spin_lock 加锁过程
假设系统中有 3 个任务:Task A(正在运行)、Task B、Task C,当前 g_os_sched_lock_cnt = 0。
场景:Task A 调用 os_semaphore_create,触发 os_spin_lock 保护资源链表。
加锁前状态:
g_os_sched_lock_cnt = 0 ← 调度器正常工作
当前运行:Task A执行 os_spin_lock() 后:
g_os_sched_lock_cnt = 1 ← 调度器被锁定
当前运行:Task A
此时如果 Task B 就绪(例如被中断唤醒),调度器不会切换到 Task B为什么需要这一步?
考虑这个场景:
Task A Task B(更高优先级)
│ │
├─ os_spin_lock() │
│ g_os_sched_lock_cnt = 1 │
│ ├─ 被中断唤醒,变为就绪态
│ │ (但调度器被锁定,无法切换)
├─ 遍历资源链表 │
│ (链表处于中间状态) │
│ │
├─ os_spin_unlock() │
│ g_os_sched_lock_cnt = 0 │
│ → 触发调度,切换到 Task B │ ← 此时链表已完整,安全切换
│ │如果没有调度锁,Task A 在链表操作的中间状态被抢占,Task B 可能看到不一致的链表数据,导致内核崩溃。
3.7 os_spin_unlock 详解
对应的 os_spin_unlock 定义在 kernel/source/os_spinlock.c 第 170-189 行:
void os_spin_unlock(os_spinlock_t *lock)
{
OS_ASSERT((lock != OS_NULL));
#ifdef OS_USING_SPINLOCK_CHECK
_k_spin_lock_clr_owner(lock);
#endif
#ifdef OS_USING_SMP
arch_spin_unlock(&lock->arch_lock);
#endif
os_schedule_unlock();
return;
}对于 STM32F103ZET6,实际只执行 os_schedule_unlock(),即:
void os_spin_unlock(os_spinlock_t *lock)
{
os_schedule_unlock(); // 调度锁计数器 -1,如果为 0 则允许调度
return;
}os_schedule_unlock() 的实现
os_schedule_unlock() 定义在 kernel/source/os_sched.c 第 920-956 行:
void os_schedule_unlock(void)
{
int16_t sched_lock_cnt;
OS_KERNEL_INIT();
OS_KERNEL_ENTER();
sched_lock_cnt = --g_os_sched_lock_cnt; // 计数器 -1
if (sched_lock_cnt == 0)
{
OS_KERNEL_EXIT_SCHED(); // 计数器归零,触发调度检查(见下方详解)
}
else if (sched_lock_cnt > 0)
{
OS_KERNEL_EXIT(); // 计数器仍 > 0,只恢复中断
}
else
{
OS_ASSERT_EX(OS_FALSE, "Task use schedule lock correctly");
// 计数器 < 0,说明解锁次数多于加锁次数,内核报错
}
return;
}关键逻辑:g_os_sched_lock_cnt 减到 0 时,调用 OS_KERNEL_EXIT_SCHED() 检查是否有更高优先级的任务需要运行。如果减到负数,说明加锁/解锁不匹配,内核会触发断言。
OS_KERNEL_EXIT_SCHED() 详解
OS_KERNEL_EXIT_SCHED() 定义在 kernel/source/os_kernel_internal.h 第 298-318 行,展开后等价于:
// 宏展开(os_kernel_internal.h 第 298-318 行)
#define OS_KERNEL_EXIT_SCHED()
do {
if ((OS_NULL == g_os_current_task) || (0 != g_os_sched_lock_cnt))
{
OS_KERNEL_EXIT(); // 条件 A:仅恢复中断
}
else
{
g_os_next_task = g_os_high_task; // ① 取最高优先级就绪任务
if (g_os_current_task != g_os_next_task) // ② 判断是否需要切换
{
OS_KERNEL_LOCK_REF_DEC(); // ③ 临时解锁调度锁
os_task_switch(); // ④ 执行上下文切换
OS_KERNEL_LOCK_REF_INC(); // ⑤ 恢复调度锁
}
OS_KERNEL_EXIT(); // ⑥ 恢复中断
}
} while (0)逐步解释:
条件 A(不触发调度):
g_os_current_task == NULL:系统尚未启动任务调度器g_os_sched_lock_cnt != 0:调度锁计数器还没减到 0(例如嵌套加锁:加锁 2 次,解锁 1 次,计数器 = 1)
条件 B(触发调度检查):
- 计数器已减到 0,调度器恢复工作
| 步骤 | 代码 | 作用 |
|---|---|---|
| ① | g_os_next_task = g_os_high_task | 从就绪队列中取出最高优先级任务 |
| ② | if (g_os_current_task != g_os_next_task) | 如果当前任务就是最高优先级,不需要切换 |
| ③ | OS_KERNEL_LOCK_REF_DEC() | 内核锁引用计数 -1 |
| ④ | os_task_switch() | 执行实际的上下文切换 |
| ⑤ | OS_KERNEL_LOCK_REF_INC() | 内核锁引用计数 +1(恢复) |
| ⑥ | OS_KERNEL_EXIT() | 恢复中断状态(PRIMASK) |
步骤 ③④⑤⑥ 详解:
这四个步骤在 g_os_current_task != g_os_next_task(需要切换任务)时依次执行:
③ OS_KERNEL_LOCK_REF_DEC()(os_kernel_internal.h 第 267-271 行):
#define OS_KERNEL_LOCK_REF_DEC()
do {
g_os_kernel_lock_cnt--; // 内核锁引用计数 -1
} while (0)g_os_kernel_lock_cnt 是内核锁引用计数器(os_kernel_internal.h 第 248 行声明,os_sched.c 中定义),用于跟踪调度锁的嵌套深度。此处 -1 是为上下文切换做准备,防止新任务看到错误的锁状态。
④ os_task_switch()(arch/arm/armv7/arch_task.c 第 77-88 行):
void os_task_switch(void)
{
if (g_os_int_nest_cnt == 0)
{
// 任务上下文:直接切换
os_hw_context_switch_from_task();
}
else
{
// 中断上下文:延迟切换
g_os_task_switch_interrupt_flag = 1;
}
}- 任务上下文(
g_os_int_nest_cnt == 0):调用os_hw_context_switch_from_task(),通过 PendSV 异常保存当前任务的 R4-R11、PSP 寄存器到栈中,然后恢复目标任务的寄存器,CPU 开始执行目标任务 - 中断上下文(
g_os_int_nest_cnt > 0):仅设置g_os_task_switch_interrupt_flag = 1,等中断返回后再执行切换(避免在 ISR 中直接操作栈指针导致的问题)
⑤ OS_KERNEL_LOCK_REF_INC()(os_kernel_internal.h 第 273-277 行):
#define OS_KERNEL_LOCK_REF_INC()
do {
g_os_kernel_lock_cnt++; // 内核锁引用计数 +1(恢复原值)
} while (0)与 ③ 配对,恢复内核锁引用计数。
⑥ OS_KERNEL_EXIT()(os_kernel_internal.h 第 286-290 行):
#define OS_KERNEL_EXIT()
do {
os_irq_unlock(irq_save); // 将之前保存的 PRIMASK 值写回寄存器
} while (0)调用 os_irq_unlock() 将 PRIMASK 恢复到 os_spin_lock 调用前的状态(通常是 0,即中断开启)。
举例:os_spin_unlock 解锁过程
延续上面的例子,Task A 完成链表操作后调用 os_spin_unlock:
解锁前状态:
g_os_sched_lock_cnt = 1 ← 调度器仍被锁定
当前运行:Task A
Task B:就绪态(等待调度)执行 os_spin_unlock() 后:
g_os_sched_lock_cnt = 0 ← 计数器归零,调度器解锁
OS_KERNEL_EXIT_SCHED() 触发调度检查
发现 Task B 优先级更高 → 切换到 Task B
当前运行:Task B ← 上下文切换完成完整时序图:
时间线 ──────────────────────────────────────────────────────→
Task A: 运行 → os_spin_lock → 操作链表 → os_spin_unlock → 等待
↓(被切换)
Task B: 等待 → 等待 → 等待 → 等待 → 等待 → 就绪 → 运行 → 运行
↑
调度器解锁
检测到 Task B 更高优先级
执行上下文切换调度锁 vs 自旋锁的区别
在单核系统中:
- 关中断(
CPSID I):防止当前任务被中断打断,保护极短的临界区(如g_os_sched_lock_cnt++本身) - 调度锁(
g_os_sched_lock_cnt):防止任务切换,保护较长的临界区(如链表遍历和修改)
两者配合使用:关中断保证 g_os_sched_lock_cnt++ 的原子性,调度锁保证整个链表操作不被其他任务打断。
四、gs_os_semaphore_resource_list_head 资源链表详解
4.1 类型说明
os_list_node_t 是双向链表节点,定义在 kernel/include/os_list.h 第 41-46 行:
struct os_list_node {
struct os_list_node *next; // 指向下一个节点
struct os_list_node *prev; // 指向上一个节点
};4.2 OS_LIST_INIT 宏展开
#define OS_LIST_INIT(name) { &(name), &(name) }代入后展开为:
static os_list_node_t gs_os_semaphore_resource_list_head = {
.next = &gs_os_semaphore_resource_list_head,
.prev = &gs_os_semaphore_resource_list_head
};4.3 为什么 next 和 prev 都指向自己?
这是空链表的标准初始化方式:
gs_os_semaphore_resource_list_head
┌──────────┐
│ next ───┼──→ 指向自己 ─┐
│ prev ───┼──→ 指向自己 ─┤
└──────────┘ │
↑ │
└────────────────────┘
含义:链表为空,没有挂载任何信号量节点4.4 什么时候创建的?
它是编译时创建的,不是运行时。static 全局变量由编译器在 .data 段中分配内存,初始值 { &自己, &自己 } 作为初始数据写入固件。内核启动后直接可用,不需要任何函数来"创建"它。
4.5 链表变化过程
所有通过 os_semaphore_create 创建的信号量都会被挂载到这个链表上(通过 os_list_add_tail)。
假设先后创建了 sem_1 和 sem_2 两个信号量,链表的变化过程如下:
初始状态(空链表):head.next 和 head.prev 都指向自己,表示链表为空。
head
↗ ↖
┗━━━━┛ (next 和 prev 都指向自己,链表为空)注册 sem_1 后(调用 os_list_add_tail):将 sem_1 的 resource_node 插入到 head 之后。
head ←→ sem_1
↗ ↖
┗━━━━━━━━━┛ (head ↔ sem_1,双向链接)注册 sem_2 后(再次调用 os_list_add_tail):将 sem_2 的 resource_node 插入到 sem_1 之后。
head ←→ sem_1 ←→ sem_2
↑ ↑
└──────────────────┘ (首尾相连,形成环形链表)每个节点内部结构:
head (gs_os_semaphore_resource_list_head)
├── next → sem_1
└── prev → sem_2
sem_1 (os_semaphore_t 的 resource_node 成员)
├── next → sem_2
└── prev → head
sem_2 (os_semaphore_t 的 resource_node 成员)
├── next → head
└── prev → sem_1os_list_add_tail 的作用
os_list_add_tail 将新节点插入到链表尾部(即 head.prev 之后),并更新相关指针使双向链接保持正确。
五、os_semaphore_dummy_t 占位结构体
os_semaphore_dummy_t 是一个占位结构体,定义在 kernel/include/os_dummy.h。它的成员名都带 _dummy 后缀,用于用户代码中静态分配内存。内核内部通过 OS_TYPE_CONVERT 类型转换将其视为 os_semaphore_t 使用,两者内存布局完全一致。
典型用法:
// 1. 静态定义控制块
OS_SEMAPHORE_DEFINE(my_sem);
// 2. 传入创建函数
os_semaphore_id id = os_semaphore_create(&my_sem, "my_sem", 1, 1);六、总结
| 组件 | 类型 | 作用 |
|---|---|---|
gs_os_semaphore_resource_list_head | os_list_node_t | 全局资源链表头节点,所有信号量挂载于此 |
gs_os_semaphore_resource_list_lock | os_spinlock_t | 保护链表的自旋锁,确保并发安全 |
OS_LIST_INIT | 宏 | 初始化空链表(next/prev 指向自己) |
OS_DEFINE_SPINLOCK | 宏 | 定义并初始化自旋锁 |
os_semaphore_dummy_t | 结构体 | 用户侧占位类型,与 os_semaphore_t 内存布局兼容 |