SylixOS 下的锁与信号量
本文基于 SylixOS 操作系统,对系统中的 “锁” 和 “信号量” 做一次梳理。
1. 先区分“锁”和“信号量”
在并发程序里,“锁”和“信号量”经常被混着说,但它们关注的问题并不完全相同。
- 锁更强调对临界区的互斥访问,以及 “谁持有、谁释放” 的所有权语义。
- 信号量更强调资源数量控制,或者线程之间的事件同步。
2. SylixOS 中的四类同步原语
SylixOS 提供了四类常用同步原语:
| 类型 | 典型 API | 核心语义 | 典型场景 |
|---|---|---|---|
| 二进制信号量 | API_SemaphoreB* |
计数值只有 0/1,表示“无信号/有信号” |
线程同步、ISR 通知任务、简单资源可用标志 |
| 计数型信号量 | API_SemaphoreC* |
计数值范围为 0..N |
资源池、生产者-消费者、事件计数 |
| 互斥信号量 | API_SemaphoreM* |
带所有权、优先级继承/天花板、递归控制 | 临界区保护、共享数据结构加锁 |
| 读写信号量 | API_SemaphoreRW* |
允许多读并发、写独占 | 路由表、配置表、读多写少的数据结构 |
这也是阅读 SylixOS 源码时最重要的一点:
- 二进制信号量和计数型信号量本质上是“事件/资源计数机制”。
- 互斥信号量虽然名字里带“信号量”,但在并发语义上属于“锁”,更准确地说是互斥锁。
- 读写信号量虽然名字里带“信号量”,但在并发语义上也属于“锁”,更准确地说是读写锁。
也就是说,SylixOS 在接口命名和源码注释中保留了“互斥信号量”“读写信号量”的说法;但从并发控制的本质来看,这两者都应该放在“锁”的范畴里理解。
如果把二进制信号量直接当成互斥锁来理解,很多行为会显得“怪怪的”;但如果把它看成“0/1 令牌”或“单比特事件”,它的行为就很一致了。
3. 通用选项与等待队列
无论是哪种信号量,SylixOS 都支持两类常见等待策略:
| 选项 | 含义 |
|---|---|
LW_OPTION_WAIT_PRIORITY |
按优先级顺序唤醒等待者 |
LW_OPTION_WAIT_FIFO |
按先进先出顺序唤醒等待者 |
对象属性方面还提供了:
| 选项 | 含义 |
|---|---|
LW_OPTION_OBJECT_GLOBAL |
全局对象 |
LW_OPTION_OBJECT_LOCAL |
本地对象 |
这两个选项控制的是内核对象的作用域和生命周期。
从对象属性的角度看:
LW_OPTION_OBJECT_GLOBAL表示把这个信号量或锁创建成全局对象;LW_OPTION_OBJECT_LOCAL表示把这个信号量或锁创建成局部对象。
两者最直观的区别在于生命周期不同:
- 全局对象通常不会随着创建它的进程退出而自动释放,更适合内核、驱动或系统级模块共享使用;
- 局部对象则更接近普通进程内对象,其生命周期通常跟当前进程上下文相关。
因此,普通应用程序一般使用 LW_OPTION_OBJECT_LOCAL;而对内核程序、驱动、内核模块来说,创建内核对象时通常应使用 LW_OPTION_OBJECT_GLOBAL。
另外,一些高级选项只对互斥量或读写锁有意义:
| 选项 | 适用对象 | 说明 |
|---|---|---|
LW_OPTION_DELETE_SAFE |
SemaphoreM / SemaphoreRW |
启用安全删除相关保护 |
LW_OPTION_SIGNAL_INTER |
多类事件对象 | 等待可被信号中断,返回 EINTR |
LW_OPTION_INHERIT_PRIORITY |
SemaphoreM |
优先级继承 |
LW_OPTION_PRIORITY_CEILING |
SemaphoreM |
优先级天花板 |
LW_OPTION_RECURSIVE |
SemaphoreM / 写侧 SemaphoreRW |
允许递归获取,默认行为 |
LW_OPTION_ERRORCHECK |
SemaphoreM / 写侧 SemaphoreRW |
递归获取时报错 |
LW_OPTION_NORMAL |
SemaphoreM / 写侧 SemaphoreRW |
不做递归保护,一般不推荐 |
LW_OPTION_RW_PREFER_WRITER |
SemaphoreRW |
写优先 |
4. 二进制信号量:最常见的基础同步原语
4.1 基本语义
二进制信号量的创建接口如下:
LW_API LW_OBJECT_HANDLE API_SemaphoreBCreate(CPCHAR pcName,
BOOL bInitValue,
ULONG ulOption,
LW_OBJECT_ID *pulId);
- 参数 pcName 是二进制信号量的名字;
- 参数 bInitValue 是二进制信号量的初始值(FALSE 或 TRUE);
- 参数 ulOption 是二进制信号量的创建选项如下表所示;
- 输出参数 pulId 用于返回二进制信号量的句柄(同返回值),可以为 NULL
| 宏名 | 含义 |
|---|---|
| LW_OPTION_WAIT_PRIORITY | 按优先级顺序等待 |
| LW_OPTION_WAIT_FIFO | 按先入先出顺序等待 |
| LW_OPTION_OBJECT_GLOBAL | 全局对象 |
| LW_OPTION_OBJECT_LOCAL | 本地对象 |
从 SylixOS 的实现可以看到,二进制信号量的核心状态非常简单:
LW_API
LW_OBJECT_HANDLE API_SemaphoreBCreate (CPCHAR pcName,
BOOL bInitValue,
ULONG ulOption,
LW_OBJECT_ID *pulId)
{
.......
pevent->EVENT_ucType = LW_TYPE_EVENT_SEMB; /* 对象类型:二进制信号量 */
pevent->EVENT_ulCounter = (ULONG)bInitValue; /* 初始是否有信号,只能是 0/1 */
pevent->EVENT_ulMaxCounter = (ULONG)LW_TRUE; /* 最大值固定为 1,不可累加 */
pevent->EVENT_ulOption = ulOption; /* 保存等待策略:优先级/FIFO */
pevent->EVENT_pvPtr = LW_NULL; /* 扩展指针,普通 BSem 不关心 */
pevent->EVENT_pvTcbOwn = LW_NULL; /* 不维护 owner,不是互斥量 */
......
}
LW_API
ULONG API_SemaphoreBPend (LW_OBJECT_HANDLE ulId, ULONG ulTimeout)
{
INTREG iregInterLevel;
PLW_CLASS_TCB ptcbCur;
REGISTER UINT16 usIndex;
REGISTER PLW_CLASS_EVENT pevent;
REGISTER UINT8 ucPriorityIndex;
REGISTER PLW_LIST_RING *ppringList;
ULONG ulTimeSave; /* 系统事件记录 */
INT iSchedRet;
ULONG ulEventOption; /* 事件创建选项 */
/* 根据信号量句柄,获取内核事件块下标 */
usIndex = _ObjectGetIndex(ulId);
......
pevent = &_K_eventBuffer[usIndex];
......
/*
* 当 EVENT_ulCounter == 1
* 修改 EVENT_ulCounter = 0 后直接退出。
* 否则进入阻塞等待
*/
if (pevent->EVENT_ulCounter) { /* 事件有效 */
pevent->EVENT_ulCounter = LW_FALSE;
__KERNEL_EXIT_IRQ(iregInterLevel); /* 退出内核 */
return (ERROR_NONE);
}
/* 调用者明确要求不等待,立刻返回,不进入等待队列 */
if (ulTimeout == LW_OPTION_NOT_WAIT) { /* 不等待 */
__KERNEL_EXIT_IRQ(iregInterLevel); /* 退出内核 */
_ErrorHandle(ERROR_THREAD_WAIT_TIMEOUT); /* 超时 */
return (ERROR_THREAD_WAIT_TIMEOUT);
}
......
/* 等待超时时间 */
if (ulTimeout == LW_OPTION_WAIT_INFINITE) {
ptcbCur->TCB_ulDelay = 0ul; /* 无限等待 */
} else {
ptcbCur->TCB_ulDelay = ulTimeout; /* 有限等待,供时钟扫描 */
}
/*
* 否则阻塞等待。
* 根据创建信号量时的等待策略,加入优先级等待表或 FIFO 等待表
*/
if (pevent->EVENT_ulOption & LW_OPTION_WAIT_PRIORITY) { /* 按优先级等待 */
_EVENT_INDEX_Q_PRIORITY(ptcbCur->TCB_ucPriority, ucPriorityIndex);
_EVENT_PRIORITY_Q_PTR(EVENT_SEM_Q, ppringList, ucPriorityIndex);
ptcbCur->TCB_ppringPriorityQueue = ppringList; /* 记录等待队列位置 */
_EventWaitPriority(ptcbCur, pevent, ppringList); /* 加入优先级等待表 */
} else { /* 按 FIFO 等待 */
_EVENT_FIFO_Q_PTR(EVENT_SEM_Q, ppringList); /* 确定 FIFO 队列的位置 */
_EventWaitFifo(ptcbCur, pevent, ppringList); /* 加入 FIFO 等待表 */
}
......
}
从下面的 API_SemaphoreBPost() 函数可以看出,每次 post 最多只会唤醒一个阻塞在该信号量上的任务。
LW_API
ULONG API_SemaphoreBPost (LW_OBJECT_HANDLE ulId)
{
INTREG iregInterLevel;
REGISTER UINT16 usIndex;
REGISTER PLW_CLASS_EVENT pevent;
REGISTER PLW_CLASS_TCB ptcb;
REGISTER PLW_LIST_RING *ppringList; /* 等待队列地址 */
/* 根据信号量句柄,获取内核事件块下标 */
usIndex = _ObjectGetIndex(ulId);
......
pevent = &_K_eventBuffer[usIndex];
......
/* 如果该信号量上有等待者,则尝试去唤醒 */
if (_EventWaitNum(EVENT_SEM_Q, pevent)) {
if (pevent->EVENT_ulOption & LW_OPTION_WAIT_PRIORITY) { /* 优先级等待队列 */
_EVENT_DEL_Q_PRIORITY(EVENT_SEM_Q, ppringList); /* 激活优先级等待线程 */
ptcb = _EventReadyPriorityLowLevel(pevent, LW_NULL, ppringList);
} else {
_EVENT_DEL_Q_FIFO(EVENT_SEM_Q, ppringList); /* 激活FIFO等待线程 */
ptcb = _EventReadyFifoLowLevel(pevent, LW_NULL, ppringList);
}
/* 可以看到,这里只取出了一个等待任务 */
KN_INT_ENABLE(iregInterLevel); /* 使能中断 */
_EventReadyHighLevel(ptcb,
LW_THREAD_STATUS_SEM,
LW_SCHED_ACT_INTERRUPT); /* 处理 TCB */
MONITOR_EVT_LONG2(MONITOR_EVENT_ID_SEMB, MONITOR_EVENT_SEM_POST,
ulId, ptcb->TCB_ulId, LW_NULL);
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
} else { /* 没有线程等待 */
/*
* 只有该信号量上没有等待者时,才会修改 EVENT_ulCounter 计数值
*/
if (pevent->EVENT_ulCounter == LW_FALSE) { /* 检查是否还有空间加 */
pevent->EVENT_ulCounter = (ULONG)LW_TRUE;
__KERNEL_EXIT_IRQ(iregInterLevel); /* 退出内核 */
return (ERROR_NONE);
} else { /* 已经满了 */
__KERNEL_EXIT_IRQ(iregInterLevel); /* 退出内核 */
_ErrorHandle(ERROR_EVENT_FULL);
return (ERROR_EVENT_FULL);
}
}
}
API_SemaphoreBPost() 的实现里隐藏着一个非常关键的语义:
- 如果当前有等待者,
post会把这次信号直接交给某个等待线程,而不是先把计数值改回1。 - 只有在没有等待者时,
post才会真的把EVENT_ulCounter置为1。
如果从 “信号量自己到底保存了什么” 这个角度看,原因会更具体:
EVENT_ulCounter == 1的真实含义是:公共位置上还有一个谁都可以来拿的信号/令牌。EVENT_ulCounter == 0的真实含义是:这个令牌已经不在公共位置上了。
于是,当 A post 时如果发现 B 已经在等待,这次 post 的语义就不是“先把令牌放回公共位置”,而是“把这次令牌直接交给 B”。
B 被唤醒并从 BPend() 的阻塞点继续向后执行时,就已经算消费掉了这次信号。
所以这里不能再把 counter 改回 1。因为一旦改回 1,内核状态就会同时表达两件互相冲突的事:
- 这次
post已经交给了 B; - 公共位置上还额外有一个空闲信号,其他线程也可以再来拿。
这等于把一次 post 变成了两次可消费机会。
更本质的原因是:B 已经拿到了这次信号,因此信号量内部不能再声称自己还额外持有一个未消费信号。
4.2 工程上它既可做同步,也可形成互斥效果
先看“初值为 0,更像同步器”的写法:
sem = API_SemaphoreBCreate("sync", LW_FALSE, option, LW_NULL);
/* 初始没有信号,等待者先阻塞 */
/* task A */
API_SemaphoreBPend(sem, LW_OPTION_WAIT_INFINITE); /* 这里会一直等,直到别人 post */
/* task B / ISR */
API_SemaphoreBPost(sem); /* 发送一次 0/1 事件,唤醒等待者 */
这就是最典型的“任务同步”用法:不是在保护谁的所有权,而是在表达“事件到了没有”。
再看“初值为 1,形成互斥效果”的写法:
sem = API_SemaphoreBCreate("bsem", LW_TRUE, option, LW_NULL);
/* 初始给一个 0/1 令牌 */
API_SemaphoreBPend(sem, LW_OPTION_WAIT_INFINITE); /* 先拿走令牌,再进入临界区 */
/* critical section */
API_SemaphoreBPost(sem); /* 用完后再把令牌放回去 */
这时它在效果上就很像“只有一个名额的门闩”:
- 第一个进入者把唯一令牌拿走;
- 后续进入者因为
counter == 0被阻塞; - 当前执行者
post之后,下一个等待者才有机会继续。
如果你把它这样用,就可以更容易理解“为什么通常要等 B 执行完再 post”:
- 不是因为内核把 B 认成了 owner,要求它必须“解锁”;
- 而是因为在这种用法里,B 手里正拿着那个唯一令牌;
- B 不再需要临界区访问权时,主动
post一次,才等于把这个令牌重新放回去,或者直接转交给下一个等待者; - 如果 B 不
post,那counter就会一直保持0,后面的线程自然会一直等下去。
所以很多资料会写一句经验话:
- 二进制信号量初值为
0,常作同步。 - 二进制信号量初值为
1,常作互斥。
这句话在“工程用法”层面是成立的;但如果把它直接等同于“初值为 1 的二进制信号量就是锁”,就会把概念说偏。
但从源码结构看,它和真正的互斥锁还是有本质差异:
BCreate()里虽然也有EVENT_pvTcbOwn字段,但二进制信号量并不维护 owner。BPend()成功时只是做counter = 0,不会记录“谁拿到了它”。BPost()也不会检查“是不是同一个线程来释放”。- 没有优先级继承、优先级天花板、递归计数这些互斥量语义。
信号量更像是:
- 它表达的是 “有没有一个可消费信号” ,不是 “这把锁归谁持有” 。
5. 计数型信号量:可累加的资源计数器
5.1 基本语义
计数型信号量的创建接口如下:
LW_API LW_OBJECT_HANDLE API_SemaphoreCCreate(CPCHAR pcName,
ULONG ulInitCounter,
ULONG ulMaxCounter,
ULONG ulOption,
LW_OBJECT_ID *pulId);
- 参数 pcName 是计数型信号量的名字;
- 参数 ulInitCounter 是计数型信号量的初始值;
- 参数 ulMaxCounter 是计数型信号量的最大值;
- 参数 ulOption 是计数型信号量的创建选项如表 7.2 所示;
- 输出参数 pulId 返回计数型信号量的 ID(同返回值),可以为 NULL
从创建接口看,计数型信号量与二进制信号量最大的不同,在于它允许显式指定初值和上限。初始化的关键字段可以概括为:
pevent->EVENT_ucType = LW_TYPE_EVENT_SEMC;
pevent->EVENT_ulCounter = ulInitCounter;
pevent->EVENT_ulMaxCounter = ulMaxCounter;
pevent->EVENT_ulOption = ulOption;
因此它的行为更像一个受上限约束的资源计数器。与二进制信号量相比,主要差异在于:
EVENT_ulCounter的取值范围不再是0/1,而是0..ulMaxCounter;pend成功时执行--;post在没有等待者时执行++。
API_SemaphoreCPend() 的关键路径如下:
LW_API
ULONG API_SemaphoreCPend (LW_OBJECT_HANDLE ulId, ULONG ulTimeout)
{
INTREG iregInterLevel;
PLW_CLASS_TCB ptcbCur;
REGISTER UINT16 usIndex;
REGISTER PLW_CLASS_EVENT pevent;
REGISTER UINT8 ucPriorityIndex;
REGISTER PLW_LIST_RING *ppringList;
ULONG ulTimeSave; /* 系统事件记录 */
INT iSchedRet;
ULONG ulEventOption; /* 事件创建选项 */
/* 根据信号量句柄,获取内核事件块下标 */
usIndex = _ObjectGetIndex(ulId);
......
pevent = &_K_eventBuffer[usIndex];
......
/*
* 从这里可以看出,直到计数值减到 0 之前,pend 都不会阻塞的
*/
if (pevent->EVENT_ulCounter) { /* 事件有效 */
pevent->EVENT_ulCounter--;
__KERNEL_EXIT_IRQ(iregInterLevel); /* 退出内核 */
return (ERROR_NONE);
}
/* 调用者明确要求不等待,立刻返回,不进入等待队列 */
if (ulTimeout == LW_OPTION_NOT_WAIT) { /* 不等待 */
__KERNEL_EXIT_IRQ(iregInterLevel); /* 退出内核 */
_ErrorHandle(ERROR_THREAD_WAIT_TIMEOUT); /* 超时 */
return (ERROR_THREAD_WAIT_TIMEOUT);
}
......
/* 等待超时时间 */
if (ulTimeout == LW_OPTION_WAIT_INFINITE) {
ptcbCur->TCB_ulDelay = 0ul; /* 无限等待 */
} else {
ptcbCur->TCB_ulDelay = ulTimeout; /* 有限等待,供时钟扫描 */
}
......
/* 阻塞等待。根据创建信号量时的等待策略,加入优先级等待表或 FIFO 等待表 */
if (pevent->EVENT_ulOption & LW_OPTION_WAIT_PRIORITY) { /* 按优先级等待 */
_EVENT_INDEX_Q_PRIORITY(ptcbCur->TCB_ucPriority, ucPriorityIndex);
_EVENT_PRIORITY_Q_PTR(EVENT_SEM_Q, ppringList, ucPriorityIndex);
ptcbCur->TCB_ppringPriorityQueue = ppringList; /* 记录等待队列位置 */
_EventWaitPriority(ptcbCur, pevent, ppringList); /* 加入优先级等待表 */
} else { /* 按 FIFO 等待 */
_EVENT_FIFO_Q_PTR(EVENT_SEM_Q, ppringList); /* 确定 FIFO 队列的位置 */
_EventWaitFifo(ptcbCur, pevent, ppringList); /* 加入 FIFO 等待表 */
}
......
}
LW_API
ULONG API_SemaphoreCPost (LW_OBJECT_HANDLE ulId)
{
INTREG iregInterLevel;
REGISTER UINT16 usIndex;
REGISTER PLW_CLASS_EVENT pevent;
REGISTER PLW_CLASS_TCB ptcb;
REGISTER PLW_LIST_RING *ppringList; /* 等待队列地址 */
usIndex = _ObjectGetIndex(ulId);
.......
pevent = &_K_eventBuffer[usIndex];
.......
/* 如果该信号量上有等待者,则尝试去唤醒 */
if (_EventWaitNum(EVENT_SEM_Q, pevent)) {
if (pevent->EVENT_ulOption & LW_OPTION_WAIT_PRIORITY) { /* 优先级等待队列 */
_EVENT_DEL_Q_PRIORITY(EVENT_SEM_Q, ppringList); /* 激活优先级等待线程 */
ptcb = _EventReadyPriorityLowLevel(pevent, LW_NULL, ppringList);
} else {
_EVENT_DEL_Q_FIFO(EVENT_SEM_Q, ppringList); /* 激活FIFO等待线程 */
ptcb = _EventReadyFifoLowLevel(pevent, LW_NULL, ppringList);
}
KN_INT_ENABLE(iregInterLevel); /* 使能中断 */
_EventReadyHighLevel(ptcb,
LW_THREAD_STATUS_SEM,
LW_SCHED_ACT_INTERRUPT); /* 处理 TCB */
MONITOR_EVT_LONG2(MONITOR_EVENT_ID_SEMC, MONITOR_EVENT_SEM_POST,
ulId, ptcb->TCB_ulId, LW_NULL);
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
} else { /* 没有线程等待 */
/*
* 与二进制信号相同,只有当前信号量上没有等待者,才能增加 EVENT_ulCounter 计数值
*/
if (pevent->EVENT_ulCounter < pevent->EVENT_ulMaxCounter) { /* 检查是否还有空间加 */
pevent->EVENT_ulCounter++;
__KERNEL_EXIT_IRQ(iregInterLevel); /* 退出内核 */
return (ERROR_NONE);
} else { /* 已经满了 */
__KERNEL_EXIT_IRQ(iregInterLevel); /* 退出内核 */
_ErrorHandle(ERROR_EVENT_FULL);
return (ERROR_EVENT_FULL);
}
}
}
5.2 推荐使用场景
计数型信号量适合表达“最多可并行使用 N 份资源”的问题,例如:
- 设备 ID 池;
- 固定数量的缓冲区;
- 生产者-消费者中的“可消费项个数”;
- 需要保留事件次数的通知机制。
一个典型例子是缓冲区池:
- 创建时把
ulInitCounter设置为缓冲区个数; - 每拿走一个缓冲区,执行一次
CPend(); - 每归还一个缓冲区,执行一次
CPost()。
当计数减到 0 时,新的申请者就会阻塞,直到其他线程归还资源。
从工程用法上看,计数型信号量也有和二进制信号量类似的“引申用法”:
sem_evt = API_SemaphoreCCreate("evt", 0, 128, option, LW_NULL);
/* 初值 0:更像事件计数同步器 */
/* post 一次,就累计一次事件 */
sem_pool = API_SemaphoreCCreate("pool", N, N, option, LW_NULL);
/* 初值 N:更像 N 份资源门闩 */
/* 最多允许 N 个执行者并发进入 */
sem_one = API_SemaphoreCCreate("one", 1, 1, option, LW_NULL);
/* N=1 时效果上很像单资源互斥 */
/* 但依然没有 owner 语义 */
可以把它概括成三句话:
- 初值为
0的计数型信号量,常用来做“事件次数不能丢”的同步。 - 初值为
N的计数型信号量,常用来做N份同类资源的并发准入控制。 - 当
N=1时,它会表现出互斥效果,但概念上仍然是“资源计数为 1 的信号量”,不是互斥锁。
6. 互斥量:真正意义上的“锁”
如果你的需求是“保护临界区”或“保护共享数据结构”,那么在 SylixOS 中更合适的选择通常不是 SemaphoreB,而是 SemaphoreM。
6.1 互斥锁比二进制信号量多了什么
互斥锁的核心特征有四个:
- 有 owner:谁获取了锁,内核会记录下来。
- 只能由 owner 释放:不是持有者就不能
post。 - 支持优先级保护:可选优先级继承或优先级天花板。
- 支持递归/错误检查:可以控制同一线程再次获取同一把锁时的行为。
API_SemaphoreMPend() 在获取成功时,会保存当前持有者:
LW_API
ULONG API_SemaphoreMPend (LW_OBJECT_HANDLE ulId, ULONG ulTimeout)
{
......
/* 这意味着互斥量不是简单地把一个布尔位清零,而是同时建立了 "所有权关系"。 */
if (pevent->EVENT_ulCounter) { /* 事件有效 */
pevent->EVENT_ulCounter = LW_FALSE;
pevent->EVENT_ulMaxCounter = (ULONG)ptcbCur->TCB_ucPriority;
pevent->EVENT_pvTcbOwn = (PVOID)ptcbCur; /* 保存线程信息 */
if (pevent->EVENT_ulOption & LW_OPTION_DELETE_SAFE) { /* 安全模式设定 */
LW_THREAD_SAFE_INKERN(ptcbCur);
}
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
}
/*
* 紧接着,互斥锁还会检查递归获取场景
* 这也是它与二进制信号量最本质的差异之一:互斥锁并不是单纯“等一个 0/1 状态”,而是在管理一把有 owner 的锁。
*/
if (!(pevent->EVENT_ulOption & LW_OPTION_NORMAL)) { /* 需要递归支持或递归检查 */
if (pevent->EVENT_pvTcbOwn == (PVOID)ptcbCur) { /* 是否是自己连续调用 */
if (pevent->EVENT_ulOption & LW_OPTION_ERRORCHECK) {
__KERNEL_EXIT(); /* 退出内核 */
_ErrorHandle(EDEADLK); /* 退出 */
return (EDEADLK);
}
/* 临时计数器++ */
pevent->EVENT_pvPtr = (PVOID)((ULONG)pevent->EVENT_pvPtr + 1);
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
}
}
if (ulTimeout == LW_OPTION_NOT_WAIT) { /* 不等待 */
__KERNEL_EXIT(); /* 退出内核 */
_ErrorHandle(ERROR_THREAD_WAIT_TIMEOUT); /* 超时 */
return (ERROR_THREAD_WAIT_TIMEOUT);
}
/* 使用优先级天花板或优先级继承策略,解决优先级反转问题 */
_EventPrioTryBoost(pevent, ptcbCur); /* 尝试提升所属任务优先级 */
......
}
6.2 释放时的行为:不是“放空”,而是“转交”
API_SemaphoreMPost() 释放互斥量时,也能看出“锁所有权转移”的语义:
LW_API
ULONG API_SemaphoreMPost (LW_OBJECT_HANDLE ulId)
{
PLW_CLASS_TCB ptcbCur;
......
LW_TCB_GET_CUR_SAFE(ptcbCur); /* 当前任务控制块 */
......
if (ptcbCur != (PLW_CLASS_TCB)pevent->EVENT_pvTcbOwn) { /* 是否是拥有者 */
__KERNEL_EXIT(); /* 退出内核 */
_ErrorHandle(ERROR_EVENT_NOT_OWN); /* 没有事件所有权 */
return (ERROR_EVENT_NOT_OWN);
}
if (pevent->EVENT_pvPtr) { /* 检测是否进行了递归调用 */
pevent->EVENT_pvPtr = (PVOID)((ULONG)pevent->EVENT_pvPtr - 1); /* 临时计数器-- */
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
}
iregInterLevel = KN_INT_DISABLE();
/* 如果互斥锁上有等待者,尝试去唤醒。每次最多只能唤醒一个任务 */
if (_EventWaitNum(EVENT_SEM_Q, pevent)) {
if (pevent->EVENT_ulOption & LW_OPTION_WAIT_PRIORITY) { /* 优先级等待队列 */
_EVENT_DEL_Q_PRIORITY(EVENT_SEM_Q, ppringList); /* 激活优先级等待线程 */
ptcb = _EventReadyPriorityLowLevel(pevent, LW_NULL, ppringList);
} else {
_EVENT_DEL_Q_FIFO(EVENT_SEM_Q, ppringList); /* 激活FIFO等待线程 */
ptcb = _EventReadyFifoLowLevel(pevent, LW_NULL, ppringList);
}
KN_INT_ENABLE(iregInterLevel);
/*
* 因为优先级保护的原因,pend 过程中会暂时改变任务的优先级。
* 当任务执行结束、post 释放锁时,尝试返回之前的优先级
*/
_EventPrioTryResume(pevent, ptcbCur); /* 尝试返回之前的优先级 */
/* 前面已经选中一个任务 ptcb 准备唤醒,更新互斥锁的持有者信息 */
pevent->EVENT_ulMaxCounter = (ULONG)ptcb->TCB_ucPriority;
pevent->EVENT_pvTcbOwn = (PVOID)ptcb; /* 保存线程信息 */
_EventReadyHighLevel(ptcb,
LW_THREAD_STATUS_SEM,
LW_SCHED_ACT_INTERRUPT); /* 处理 TCB */
if (pevent->EVENT_ulOption & LW_OPTION_DELETE_SAFE) {
LW_THREAD_SAFE_INKERN(ptcb); /* 将激活任务设置为安全 */
}
/* 这里真正开始调度 */
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
} else { /* 没有线程等待 */
KN_INT_ENABLE(iregInterLevel);
/* 如果互斥锁上没人等待,则恢复互斥锁计数值 EVENT_ulCounter */
if (pevent->EVENT_ulCounter == LW_FALSE) { /* 检查是否还有空间加 */
pevent->EVENT_ulCounter = (ULONG)LW_TRUE;
pevent->EVENT_ulMaxCounter = LW_PRIO_LOWEST; /* 清空保存信息 */
pevent->EVENT_pvTcbOwn = LW_NULL;
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
} else { /* 已经满了 */
__KERNEL_EXIT(); /* 退出内核 */
_ErrorHandle(ERROR_EVENT_FULL);
return (ERROR_EVENT_FULL);
}
}
}
6.3 递归与错误检查
SylixOS 的互斥量默认支持递归获取:
- 同一线程重复
MPend(),不会立即阻塞; - 递归次数通过内部计数保存;
- 只有执行对应次数的
MPost()后,锁才真正释放。
如果配置了 LW_OPTION_ERRORCHECK,同一线程递归获取会返回错误而不是继续成功。
如果配置了 LW_OPTION_NORMAL,则不做递归保护。这个选项通常不推荐,因为它更接近“由调用者自己保证不会递归加锁”。
6.4 优先级继承与天花板
互斥量最重要的工程价值之一,是能够处理优先级反转问题。
SylixOS 提供了两种互斥量优先级策略:
LW_OPTION_INHERIT_PRIORITY:优先级继承;LW_OPTION_PRIORITY_CEILING:优先级天花板。
这也是为什么在内核、驱动、VMM、资源管理等关键路径中,代码大量使用 API_SemaphoreMPend() / API_SemaphoreMPost(),而不是 SemaphoreB。
6.5 推荐使用场景
下列需求优先考虑互斥量:
- 保护链表、哈希表、全局状态、设备上下文;
- 需要明确“谁持有锁”;
- 需要防范优先级反转;
- 需要递归锁或错误检查;
- 需要把“同步原语”准确建模为“锁”。
7. 读写锁:为“读多写少”而生
7.1 基本语义
概念上看,这里更准确的说法其实是“读写锁”。只是 SylixOS 的接口命名仍然使用 SemaphoreRW,因此文中会同时保留这组 API 名称。
读写锁的目标是:
- 允许多个读者并发访问;
- 写者必须独占访问;
- 写锁持有期间不允许读者和其他写者进入。
SylixOS 提供读写接口如下:
LW_API ULONG API_SemaphoreRWPendR(LW_OBJECT_HANDLE ulId,
ULONG ulTimeout);
/* 读写信号量等待读 */
LW_API ULONG API_SemaphoreRWPendW(LW_OBJECT_HANDLE ulId,
ULONG ulTimeout);
/* 读写信号量等待写 */
LW_API ULONG API_SemaphoreRWPost(LW_OBJECT_HANDLE ulId);
/* 读写信号量释放 */
其中:
RWPendR()获取读锁;RWPendW()获取写锁;RWPost()释放读锁或写锁。
LW_API
ULONG API_SemaphoreRWPendR (LW_OBJECT_HANDLE ulId, ULONG ulTimeout)
{
......
/*
* 读写锁上没有“写”请求时,读接口不会阻塞,只会修改读者计数
* 同时也可以看出:只要有写者在等待,新的读者就不再继续累加,而是转入阻塞
*/
if ((pevent->EVENT_iStatus == EVENT_RW_STATUS_R) &&
(_EventWaitNum(EVENT_RW_Q_W, pevent) == 0)) { /* 当前为读状态并且没有写请求 */
pevent->EVENT_ulCounter++; /* 操作数++ */
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
}
......
if (ulTimeout == LW_OPTION_WAIT_INFINITE) { /* 是否是无穷等待 */
ptcbCur->TCB_ulDelay = 0ul;
} else {
ptcbCur->TCB_ulDelay = ulTimeout; /* 设置超时时间 */
}
__KERNEL_TIME_GET_IGNIRQ(ulTimeSave, ULONG); /* 记录系统时间 */
/* 如果有写事件,则需要阻塞等待 */
if (pevent->EVENT_ulOption & LW_OPTION_WAIT_PRIORITY) { /* 按优先级等待 */
_EVENT_INDEX_Q_PRIORITY(ptcbCur->TCB_ucPriority, ucPriorityIndex);
_EVENT_PRIORITY_Q_PTR(EVENT_RW_Q_R, ppringList, ucPriorityIndex);
ptcbCur->TCB_ppringPriorityQueue = ppringList; /* 记录等待队列位置 */
_EventWaitPriority(ptcbCur, pevent, ppringList); /* 加入优先级等待表 */
} else { /* 按 FIFO 等待 */
_EVENT_FIFO_Q_PTR(EVENT_RW_Q_R, ppringList); /* 确定 FIFO 队列的位置 */
_EventWaitFifo(ptcbCur, pevent, ppringList); /* 加入 FIFO 等待表 */
}
......
}
LW_API
ULONG API_SemaphoreRWPendW (LW_OBJECT_HANDLE ulId, ULONG ulTimeout)
{
......
/* 只有当前没有读者/写者占用时,写操作才能立即成功 */
if (pevent->EVENT_ulCounter == 0) { /* 当前没有任何操作 */
pevent->EVENT_ulCounter++;
pevent->EVENT_iStatus = EVENT_RW_STATUS_W;
pevent->EVENT_pvTcbOwn = (PVOID)ptcbCur; /* 保存线程信息 */
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
}
if (pevent->EVENT_pvTcbOwn == (PVOID)ptcbCur) { /* 写递归 */
pevent->EVENT_pvPtr = (PVOID)((ULONG)pevent->EVENT_pvPtr + 1); /* 临时计数器++ */
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
}
......
/* 加入等待队列 */
if (pevent->EVENT_ulOption & LW_OPTION_WAIT_PRIORITY) { /* 按优先级等待 */
_EVENT_INDEX_Q_PRIORITY(ptcbCur->TCB_ucPriority, ucPriorityIndex);
_EVENT_PRIORITY_Q_PTR(EVENT_RW_Q_W, ppringList, ucPriorityIndex);
ptcbCur->TCB_ppringPriorityQueue = ppringList; /* 记录等待队列位置 */
_EventWaitPriority(ptcbCur, pevent, ppringList); /* 加入优先级等待表 */
} else { /* 按 FIFO 等待 */
_EVENT_FIFO_Q_PTR(EVENT_RW_Q_W, ppringList); /* 确定 FIFO 队列的位置 */
_EventWaitFifo(ptcbCur, pevent, ppringList); /* 加入 FIFO 等待表 */
}
......
}
LW_API
ULONG API_SemaphoreRWPost (LW_OBJECT_HANDLE ulId)
{
......
if (pevent->EVENT_ulCounter == 0) { /* 没有加锁 */
__KERNEL_EXIT(); /* 退出内核 */
_ErrorHandle(ERROR_EVENT_NOT_OWN); /* 没有事件所有权 */
return (ERROR_EVENT_NOT_OWN);
}
if (pevent->EVENT_iStatus == EVENT_RW_STATUS_R) { /* 当前为读操作 */
if (pevent->EVENT_ulCounter > 1) { /* 还有其他读操作正在执行 */
pevent->EVENT_ulCounter--;
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
} else { /* 不存在其他操作者 */
if (!(pevent->EVENT_ulOption & LW_OPTION_RW_PREFER_WRITER)) {
if (_EventWaitNum(EVENT_RW_Q_R, pevent)) {
bIgnWrite = LW_TRUE; /* 忽略激活写者, 优先激活读者 */
}
}
/* 如果已经不存在 “读” 者,则尝试唤醒 “写” 者 */
goto __release_pend;
}
} else { /* 写操作 */
if (pevent->EVENT_pvTcbOwn != (PVOID)ptcbCur) {
__KERNEL_EXIT(); /* 退出内核 */
_ErrorHandle(ERROR_EVENT_NOT_OWN); /* 没有事件所有权 */
return (ERROR_EVENT_NOT_OWN);
}
if (pevent->EVENT_pvPtr) { /* 检测是否进行了连续调用 */
pevent->EVENT_pvPtr = (PVOID)((ULONG)pevent->EVENT_pvPtr - 1);
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
}
__release_pend:
iregInterLevel = KN_INT_DISABLE(); /* 关闭中断 */
if (_EventWaitNum(EVENT_RW_Q_W, pevent) && !bIgnWrite) { /* 存在等待写的任务 */
/* 尝试唤醒 “写” 者 */
if (pevent->EVENT_ulOption & LW_OPTION_WAIT_PRIORITY) {
_EVENT_DEL_Q_PRIORITY(EVENT_RW_Q_W, ppringList); /* 激活优先级等待线程 */
ptcb = _EventReadyPriorityLowLevel(pevent, LW_NULL, ppringList);
} else {
_EVENT_DEL_Q_FIFO(EVENT_RW_Q_W, ppringList); /* 激活FIFO等待线程 */
ptcb = _EventReadyFifoLowLevel(pevent, LW_NULL, ppringList);
}
KN_INT_ENABLE(iregInterLevel);
_EventReadyHighLevel(ptcb,
LW_THREAD_STATUS_SEM,
LW_SCHED_ACT_INTERRUPT); /* 处理 TCB */
pevent->EVENT_pvTcbOwn = (PVOID)ptcb;
pevent->EVENT_iStatus = EVENT_RW_STATUS_W;
/* 真正产生调度的地方 */
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
} else if (_EventWaitNum(EVENT_RW_Q_R, pevent)) { /* 存在等待读的任务 */
pevent->EVENT_ulCounter--;
/*
* 唤醒 “读” 者。注意,这里是激活所有读任务,而不是激活某个读任务。
* 因为 “读” 者与 “读” 者之间可以并行,不互斥
*/
while (_EventWaitNum(EVENT_RW_Q_R, pevent)) { /* 激活全部读任务 */
if (pevent->EVENT_ulOption & LW_OPTION_WAIT_PRIORITY) {
_EVENT_DEL_Q_PRIORITY(EVENT_RW_Q_R, ppringList); /* 激活优先级等待线程 */
ptcb = _EventReadyPriorityLowLevel(pevent, LW_NULL, ppringList);
} else {
_EVENT_DEL_Q_FIFO(EVENT_RW_Q_R, ppringList); /* 激活FIFO等待线程 */
ptcb = _EventReadyFifoLowLevel(pevent, LW_NULL, ppringList);
}
pevent->EVENT_ulCounter++; /* 增加使用者计数 */
KN_INT_ENABLE(iregInterLevel); /* 打开中断 */
_EventReadyHighLevel(ptcb,
LW_THREAD_STATUS_SEM,
LW_SCHED_ACT_OTHER); /* 处理 TCB */
iregInterLevel = KN_INT_DISABLE(); /* 关闭中断 */
}
KN_INT_ENABLE(iregInterLevel);
pevent->EVENT_pvTcbOwn = LW_NULL;
pevent->EVENT_iStatus = EVENT_RW_STATUS_R; /* 恢复为读状态 */
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
} else {
KN_INT_ENABLE(iregInterLevel);
pevent->EVENT_ulCounter--;
pevent->EVENT_pvTcbOwn = LW_NULL;
pevent->EVENT_iStatus = EVENT_RW_STATUS_R; /* 恢复为读状态 */
__KERNEL_EXIT(); /* 退出内核 */
return (ERROR_NONE);
}
}
}
7.2 推荐使用场景
读写锁适合下面这类数据结构:
- 路由表;
- 配置表;
- 缓存元数据;
- 读远多于写的共享索引。
如果写操作很频繁,或者临界区很短,读写锁未必一定比互斥量更划算;这时直接使用 SemaphoreM 往往更简单。
8. 进阶接口与补充说明
除了最常用的 Create/Pend/Post 外,SylixOS 还提供了一些很实用的补充接口。
8.1 TryPend
BTryPend() 和 CTryPend() 的意义不是“名字更短”,而是:
- 它们是专门设计的无阻塞尝试接口;
- 可用于中断上下文;
- 与
Pend(..., LW_OPTION_NOT_WAIT)在调用约束上并不完全相同。
8.2 Status / StatusEx
这些接口适合做调试、观测和运行期诊断,例如:
- 当前计数值是多少;
- 当前有多少线程阻塞;
- 互斥量当前 owner 是谁;
- 读写锁当前是读模式还是写模式。
其中,互斥量和读写锁的扩展状态接口对排查死锁和优先级反转尤其有帮助。
8.3 Release
API_SemaphoreBRelease() / API_SemaphoreCRelease() 属于批量释放接口。
以二进制信号量为例,它的核心结构可以概括为:
for (; ulReleaseCounter > 0; ulReleaseCounter--) {
if (_EventWaitNum(EVENT_SEM_Q, pevent)) {
/* 优先唤醒等待者 */
} else {
pevent->EVENT_ulCounter = (ULONG)LW_TRUE;
}
}
它们的语义不是“强行把计数直接加到某个值”,而是重复执行“交付一个信号/资源名额”的过程:
- 有等待者时,优先唤醒等待者;
- 没有等待者时,才真正回写计数值。
8.4 Flush
Flush 的作用是唤醒当前所有等待者,更像一次“广播式解除阻塞”。
它并不是常规的资源释放操作,因此使用时需要非常谨慎:
- 对“资源保护”语义来说,
Flush往往不是合适工具; - 对“事件同步”语义来说,它更接近一次特殊控制操作。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)