信号量
多个线程在读写某个共享数据(全局变量等)时必须通过某种方法实现共享数据的互斥访问或者同步访问(例如线程 B 等待线程 A 的结果以继续运行)。其中,信号量是一种最常见的方法。
实际上,信号量是一种约定机制:在共享资源的互斥访问中,它约定当一个线程获得信号量(Wait)后,其他线程不可以再次获得该信号量直到信号量被释放(Give);在同步机制中,它约定等待信号量(Take)的线程(或者说等待信号更确切)在收到信号量之前应该处于阻塞状态,直到其他线程发送该信号量(Post)。
通常情况下,对信号量只能实施三种操作:创建一个信号量(Create)、等待信号量(Wait)或者挂起(Pend)、给信号量(Give)或者发送(Post)。操作系统应该包含一个等待信号量线程队列(用于存放等待信号量的线程),当信号量可以被获得时,操作系统依据某种策略从队列中选择一个可以获得信号量的线程以继续运行。
SylixOS 信号量包括四种类型:二进制信号量、计数型信号量、互斥信号量(简称互斥量)和读写信号量。
二进制信号量的取值限定于 FALSE 和 TRUE;而计数型信号量的最小取值为 0,最大取值在创建计数型信号量时决定。
二进制信号量主要应用在以下场合:
- 有允许线程访问的一个资源,使用二进制信号量作为互斥手段,初始值为 TRUE。
- 线程或中断通知另一个线程某件事件发生,初始值为 FALSE。
计数型信号量主要应用在以下场合:
- 有允许线程访问的 n 个资源,使用计数型信号量作为资源剩余计数,初始值为 n。
- 线程或中断通知另一个线程某种事件发生,使用计数型信号量作为事件计数,初始值为 0。
二进制信号量
正如之前所述,信号量使用前必须要创建。SylixOS 提供下面函数来创建一个二进制信号量。
#include <SylixOS.h>
LW_HANDLE Lw_SemaphoreB_Create(CPCHAR pcName,
BOOL bInitValue,
ULONG ulOption,
LW_OBJECT_ID *pulId);
函数 Lw_SemaphoreB_Create 原型分析:
- 此函数返回二进制信号量的句柄,失败时为 NULL 并设置错误号。
- 参数 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_OPTION_WAIT_PRIORITY)和 FIFO(LW_OPTION_WAIT_FIFO)。优先级方式则根据线程的优先级从队列中取出符合条件的线程运行;FIFO 方式则根据先入先出的原则从队列中取出符合条件的线程运行。
需要注意的是,LW_OPTION_WAIT_PRIORITY 和 LW_OPTION_WAIT_FIFO 只能二选一,同样 LW_OPTION_OBJECT_GLOBAL 和 LW_OPTION_OBJECT_LOCAL 也只能二选一。
参数 bInitValue 的不同决定了二进制信号量的用途不同,当 bInitValue 的值为 TRUE 时可以用于共享资源的互斥访问,如下图所示。
当 bInitValue 的值为 FALSE 时可以用于多线程间的同步,如下图所示。
不再需要的二进制信号量可以调用以下函数将其删除,SylixOS 将回收其占用的内核资源(试图使用被删除的二进制信号量将出现未知的错误)。
#include <SylixOS.h>
ULONG Lw_SemaphoreB_Delete(LW_HANDLE *pulId);
函数 Lw_SemaphoreB_Delete 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 pulId 是二进制信号量的句柄。
一个线程如果需要等待一个二进制信号量,可以调用 Lw_SemaphoreB_Wait 函数。需要注意的是,中断服务程序不能调用 Lw_SemaphoreB_Wait 函数来等待一个二进制信号量,因为该函数在二进制信号量值为 FALSE 时会阻塞当前执行的任务,而中断服务程序用来处理最紧急的事情,因此是不允许被阻塞的,否则其他线程将得不到调度的机会。
#include <SylixOS.h>
ULONG Lw_SemaphoreB_Wait(LW_HANDLE ulId,
ULONG ulTimeout);
ULONG Lw_SemaphoreB_TryWait(LW_HANDLE ulId);
以上函数原型分析:
- 函数成功返回 0,失败返回错误号。
- 参数 ulId 是二进制信号量的句柄。
- 参数 ulTimeout 是等待的超时时间,单位为时钟节拍 Tick。
参数 ulTimeout 除了可以使用数字外还可以使用如下表所示的宏。
宏名 | 含义 |
---|---|
LW_OPTION_NOT_WAIT | 不等待立即退出 |
LW_OPTION_WAIT_INFINITE | 永远等待 |
LW_OPTION_WAIT_A_TICK | 等待一个时钟嘀嗒 |
LW_OPTION_WAIT_A_SECOND | 等待一秒 |
SylixOS 为二进制信号量等待提供了一种超时机制,当等待的时间超时时立即返回并设置 errno 为 ERROR_THREAD_WAIT_TIMEOUT。
Lw_SemaphoreB_TryWait 是一种无阻塞的信号量等待函数,该函数与 Lw_SemaphoreB_Wait 的区别在于,如果二进制信号量创建的初始值为 FALSE,Lw_SemaphoreB_TryWait 会立即退出并返回,而 Lw_SemaphoreB_Wait 则会阻塞直至被唤醒。
中断服务程序可以使用 Lw_SemaphoreB_TryWait 函数尝试等待二进制信号量,因为 Lw_SemaphoreB_TryWait 函数在二进制信号量的值为 FALSE 时会立即返回,不会阻塞当前线程。
释放一个二进制信号量可以调用 Lw_SemaphoreB_Post、Lw_SemaphoreB_Post2 或者 Lw_SemaphoreB_Release 函数。
Lw_SemaphoreB_Post2 函数返回时可以通过参数 pulId 返回被激活的线程句柄,如果参数 pulId 被置为 NULL 时行为与 Lw_SemaphoreB_Post 相同。
#include <SylixOS.h>
ULONG Lw_SemaphoreB_Post(LW_HANDLE ulId);
函数 Lw_SemaphoreB_Post 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 ulId 是二进制信号量的句柄。
#include <SylixOS.h>
ULONG Lw_SemaphoreB_Post2(LW_HANDLE ulId, LW_HANDLE *pulId);
- 函数 Lw_SemaphoreB_Post2 原型分析:。
- 此函数成功返回 0,失败返回错误号。
- 参数 ulId 是二进制信号量的句柄。
- 参数 pulId 返回被激活的线程 ID。
#include <SylixOS.h>
ULONG Lw_SemaphoreB_Release(LW_HANDLE ulId,
ULONG ulReleaseCounter,
BOOL *pbPreviousValue);
函数 Lw_SemaphoreB_Release 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 ulId 是二进制信号量的句柄。
- 参数 ulReleaseCounter 是释放二进制信号量的次数。
- 输出参数 pbPreviousValue 用于接收原来的二进制信号量状态,可以为 NULL。
Lw_SemaphoreB_Release 函数是一个高级 API,当有多个线程等待同一个信号量时,调用该函数可以一次性将它们释放(POSIX 线程屏障调用该函数来一次释放多个等待的线程)。
下图显示二进制信号量的基本操作函数在线程与线程之间、中断与线程之间的操作过程。
调用 Lw_ SemaphoreB_Clear 函数将清除二进制信号量,这将使二进制信号量的初始值置为 FALSE。
#include <SylixOS.h>
ULONG Lw_SemaphoreB_Clear(LW_HANDLE ulId);
函数 Lw_SemaphoreB_Clear 原型分析:
- 此函数返回错误号。
- 参数 ulId 是二进制信号量的句柄。
调用 Lw_ SemaphoreB_Flush 函数将释放等待在指定信号量上的所有线程。
#include <SylixOS.h>
ULONG Lw_SemaphoreB_Flush(LW_HANDLE ulId,
ULONG *pulThreadUnblockNum);
函数 Lw_SemaphoreB_Flush 原型分析:
- 此函数返回错误号。
- 参数 ulId 是二进制信号量的句柄。
- 输出参数 pulThreadUnblockNum 用于接收被解除阻塞的线程数,可以为 NULL。
Lw_SemaphoreB_Status 函数返回一个有效信号量的状态信息。
#include <SylixOS.h>
ULONG Lw_SemaphoreB_Status(LW_HANDLE ulId,
BOOL *pbValue,
ULONG *pulOption,
ULONG *pulThreadBlockNum);
函数 Lw_SemaphoreB_Status 原型分析:
- 此函数返回错误号。
- 参数 ulId 是二进制信号量的句柄。
- 输出参数 pbValue 用于接收二进制信号量当前的值(FALSE 或 TRUE)。
- 输出参数 pulOption 用于接收二进制信号量的创建选项。
- 输出参数 pulThreadBlockNum 用于接收当前阻塞在该二进制信号量的线程数。
调用 Lw_SemaphoreB_GetName 函数可以获得指定信号量的名字。
#include <SylixOS.h>
ULONG Lw_SemaphoreB_GetName(LW_HANDLE ulId, PCHAR pcName)
函数 Lw_SemaphoreB_GetName 原型分析:
- 此函数返回错误号。
- 参数 ulId 是二进制信号量的句柄。
- 输出参数 pcName 是二进制信号量的名字,pcName 应该指向一个大小为 LW_CFG_OBJECT_NAME_SIZE 的字符数组。
#include <SylixOS.h>
ULONG Lw_SemaphoreB_WaitEx(LW_HANDLE ulId,
ULONG ulTimeout,
PVOID *ppvMsgPtr);
函数 Lw_SemaphoreB_WaitEx 原型分析:
- 此函数返回错误号。
- 参数 ulId 是二进制信号量的句柄。
- 参数 ulTimeout 是等待的超时时间,单位为时钟嘀嗒 Tick。
- 输出参数 ppvMsgPtr(一个空类型指针的指针)用于接收 Lw_SemaphoreB_PostEx 函数传递的消息。
#include <SylixOS.h>
ULONG Lw_SemaphoreB_PostEx(LW_HANDLE ulId,
PVOID pvMsgPtr);
函数 Lw_SemaphoreB_PostEx 原型分析:
- 此函数返回错误号。
- 参数 ulId 是二进制信号量的句柄。
- 参数 pvMsgPtr 是消息指针(一个空类型的指针,可以指向任意类型的数据),该消息将被传递到 Lw_SemaphoreB_WaitEx 函数的输出参数 ppvMsgPtr。
Lw_SemaphoreB_WaitEx 和 Lw_SemaphoreB_PostEx 函数增加了消息传递功能,通过参数 pvMsgPtr 可以在信号量中传递额外消息,例如下面程序片段展示了该过程:
threadA ()
{
PVOID ppvMsgPtr;
Lw_SemaphoreB_WaitEx(ulId, &ppvMsgPtr);
}
threadB ()
{
Lw_SemaphoreB_PostEx(ulId, "msg");
}
注意:
threadA 和 threadB 分别是两个不同的线程,上面过程实现了一个简单的线程间同步的同时创建额外消息。
由于 Lw_SemaphoreB_WaitEx 和 Lw_SemaphoreB_PostEx 函数组合已经起到传统 RTOS 的邮箱的作用,所以 SylixOS 没有提供邮箱的 API。
下面程序展示了 SylixOS 二进制信号量的使用,程序创建两个线程和一个 SylixOS 二进制信号量,两个线程分别都对变量 _G_iCount 进行自增操作和打印,使用 SylixOS 二进制信号量作为访问变量 _G_iCount 的互斥手段。
#include <SylixOS.h>
static LW_HANDLE _G_hLock;
static INT _G_iCount = 0;
static PVOID tTestA (PVOID pvArg)
{
INT iError;
while (1) {
iError = Lw_SemaphoreB_Wait(_G_hLock, LW_OPTION_WAIT_INFINITE);
if (iError != ERROR_NONE) {
break;
}
_G_iCount++;
printf("tTestA(): count = %d\n", _G_iCount);
Lw_SemaphoreB_Post(_G_hLock);
Lw_Time_MSleep(500);
}
return (LW_NULL);
}
static PVOID tTestB (PVOID pvArg)
{
INT iError;
while (1) {
iError = Lw_SemaphoreB_Wait(_G_hLock, LW_OPTION_WAIT_INFINITE);
if (iError != ERROR_NONE) {
break;
}
_G_iCount++;
printf("tTestB(): count = %d\n", _G_iCount);
Lw_SemaphoreB_Post(_G_hLock);
Lw_Time_MSleep(500);
}
return (LW_NULL);
}
int main (int argc, char *argv[])
{
LW_HANDLE hThreadAId;
LW_HANDLE hThreadBId;
_G_hLock = Lw_SemaphoreB_Create("count_lock",
LW_TRUE,
LW_OPTION_WAIT_FIFO |
LW_OPTION_OBJECT_LOCAL,
LW_NULL);
if (_G_hLock == LW_OBJECT_HANDLE_INVALID) {
printf("semaphore create failed.\n");
return (-1);
}
hThreadAId = Lw_Thread_Create("t_testa", tTestA, LW_NULL, LW_NULL);
if (hThreadAId == LW_OBJECT_HANDLE_INVALID) {
printf("t_testa create failed.\n");
return (-1);
}
hThreadBId = Lw_Thread_Create("t_testb", tTestB, LW_NULL, LW_NULL);
if (hThreadBId == LW_OBJECT_HANDLE_INVALID) {
printf("t_testb create failed.\n");
return (-1);
}
Lw_Thread_Join(hThreadAId, LW_NULL);
Lw_Thread_Join(hThreadBId, LW_NULL);
Lw_SemaphoreB_Delete(&_G_hLock);
return (0);
}
在 SylixOS Shell 下运行程序:
# ./Binary_Semaphore
tTestA(): count = 1
tTestB(): count = 2
tTestA(): count = 3
tTestB(): count = 4
tTestA(): count = 5
tTestB(): count = 6
......
# ts
NAME TID PID PRI STAT LOCK SAFE DELAY PAGEFAILS FPU CPU
---------------- ------- ----- --- ---- ---- ---- ---------- --------- --- ---
Binary_Semaphore 4010084 41 200 JOIN 0 0 1 USE 0
t_testa 4010085 41 200 SLP 0 404 0 0
t_testb 4010086 41 200 SLP 0 404 0 0
计数型信号量
如前所述,计数型信号量通常用于多个线程共享使用某资源,例如,用信号量管理某设备 ID 池(ID pool),假设该 ID 池可以同时申请 15 个设备 ID,这种情况我们使用计数型信号量对 ID 池进行互斥访问,每申请一个设备 ID 计数型信号量进行减 1 操作,当减为 0 时,再次申请 ID 的线程将被阻塞,如果设备 ID 被释放计数型信号量进行加 1 操作,此时新的线程可以再次申请,如下图显示了该过程。
一个 SylixOS 计数型信号量可以调用 Lw_SemaphoreC_Create 函数进行创建,如果创建成功将返回一个计数型信号量的句柄。
#include <SylixOS.h>
LW_HANDLE Lw_SemaphoreC_Create(CPCHAR pcName,
ULONG ulInitCounter,
ULONG ulMaxCounter,
ULONG ulOption,
LW_OBJECT_ID *pulId);
函数 Lw_SemaphoreC_Create 原型分析:
- 此函数成功返回计数型信号量的句柄,失败时返回 NULL 并设置错误号。
- 参数 pcName 是计数型信号量的名字。
- 参数 ulInitCounter 是计数型信号量的初始值。
- 参数 ulMaxCounter 是计数型信号量的最大值。
- 参数 ulOption 是计数型信号量的创建选项如下表所示。
- 输出参数 pulId 返回计数型信号量的 ID(同返回值),可以为 NULL。
计数型信号量的取值范围为 0 <= 计数值(ulInitCounter)< ulMaxCounter。特殊地,如果 ulInitCounter 的值为 0,则可以应用于多线程间的同步。
一个不再使用的计数型信号量,可以调用以下函数将其删除。删除后的信号量系统自动回收其占用的系统资源(试图使用被删除的计数型信号量将出现未知的错误)。
宏名 | 含义 |
---|---|
LW_OPTION_WAIT_PRIORITY | 按优先级顺序等待 |
LW_OPTION_WAIT_FIFO | 按先入先出顺序等待 |
LW_OPTION_OBJECT_GLOBAL | 全局对象 |
LW_OPTION_OBJECT_LOCAL | 本地对象 |
#include <SylixOS.h>
ULONG Lw_SemaphoreC_Delete(LW_HANDLE *pulId);
- 此函数成功返回 0,失败返回错误号。
- 参数 pulId 是计数型信号量的句柄。
线程如果需要等待一个计数型信号量,可以调用 Lw_SemaphoreC_Wait 函数,需要注意的是,中断服务程序不能调用 Lw_SemaphoreC_Wait 函数等待一个计数型信号量,因为 Lw_SemaphoreC_Wait 函数在计数型信号量值为 0(线程同步功能)时会阻塞当前线程。
#include <SylixOS.h>
ULONG Lw_SemaphoreC_Wait(LW_HANDLE ulId,
ULONG ulTimeout);
ULONG Lw_SemaphoreC_TryWait(LW_HANDLE ulId);
以上两个函数原型分析:
- 函数成功返回 0,失败返回错误号。
- 参数 ulId 是计数型信号量的句柄。
- 参数 ulTimeout 是等待的超时时间,单位为时钟嘀嗒 Tick。
Lw_SemaphoreC_TryWait 和 Lw_SemaphoreC_Wait 的区别在于,如果计数型信号量当前的值为 0,Lw_SemaphoreC_TryWait 会立即退出,并返回 ERROR_THREAD_WAIT_TIMEOUT,而 Lw_SemaphoreC_Wait 则会阻塞直到被唤醒。
中断服务程序可以使用 Lw_SemaphoreC_TryWait 函数尝试等待计数型信号量,Lw_SemaphoreC_TryWait 函数在计数型信号量的值为 0 时会立即返回,不会阻塞当前线程。
释放一个计数型信号量可以调用 Lw_SemaphoreC_Post 函数。
#include <SylixOS.h>
ULONG Lw_SemaphoreC_Post(LW_HANDLE ulId);
函数 Lw_SemaphoreC_Post 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 ulId 是计数型信号量的句柄。
一次释放多个计数型信号量可以调用 Lw_SemaphoreC_Release 函数。
#include <SylixOS.h>
ULONG Lw_SemaphoreC_Release(LW_HANDLE ulId,
ULONG ulReleaseCounter,
ULONG *pulPreviousCounter);
函数 Lw_SemaphoreC_Release 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 ulId 是计数型信号量的句柄。
- 参数 ulReleaseCounter 是释放计数型信号量的次数。
- 输出参数 pulPreviousCounter 用于接收原来的信号量计数,可以为 NULL。
Lw_SemaphoreC_Release 是一个高级 API,POSIX 读写锁调用该函数来同时释放多个读写线程。
下图显示计数型信号量的基本操作函数在线程与线程之间、中断与线程之间的操作过程。
调用 Lw_SemaphoreC_Clear 函数将清除计数型信号量,这将使计数型信号量的初始值置为 0。
#include <SylixOS.h>
ULONG Lw_SemaphoreC_Clear(LW_HANDLE ulId);
函数 Lw_SemaphoreC_Clear 原型分析:
- 此函数成功时返回 ERROR_NONE,失败时返回错误号。
- 参数 ulId 是计数型信号量的句柄。
调用 Lw_ SemaphoreC_Flush 函数将释放等待在指定计数型信号量上的所有线程。
#include <SylixOS.h>
ULONG Lw_SemaphoreC_Flush(LW_HANDLE ulId,
ULONG *pulThreadUnblockNum);
函数 Lw_SemaphoreC_Flush 原型分析:
- 此函数成功时返回 ERROR_NONE,失败时返回错误号。
- 参数 ulId 是计数型信号量的句柄。
- 输出参数 pulThreadUnblockNum 用于接收被解除阻塞的线程数,可以为 NULL。
以下两个函数可以获得指定计数型信号量的状态信息。
#include <SylixOS.h>
ULONG Lw_SemaphoreC_Status(LW_HANDLE ulId,
ULONG *pulCounter,
ULONG *pulOption,
ULONG *pulThreadBlockNum);
ULONG Lw_SemaphoreC_StatusEx(LW_HANDLE ulId,
ULONG *pulCounter,
ULONG *pulOption,
ULONG *pulThreadBlockNum,
ULONG *pulMaxCounter);
以上两个函数原型分析:
- 以上两个函数均返回错误号。
- 参数 ulId 是计数型信号量的句柄。
- 输出参数 pulCounter 用于接收计数型信号量当前的值。
- 输出参数 pulOption 用于接收计数型信号量的创建选项。
- 输出参数 pulThreadBlockNum 用于接收当前阻塞在该计数型信号量的线程数。
- 输出参数 pulMaxCounter 用于接收该计数型信号量的最大计数值。
Lw_SemaphoreC_GetName 函数可以获得指定计数型信号量的名字。
#include <SylixOS.h>
ULONG Lw_SemaphoreC_GetName(LW_HANDLE ulId, PCHAR pcName);
函数 Lw_SemaphoreC_GetName 原型分析:
- 此函数返回错误号。
- 参数 ulId 是计数型信号量的句柄。
- 输出参数 pcName 是计数型信号量的名字,pcName 应该指向一个大小为 LW_CFG_OBJECT_NAME_SIZE 的字符数组。
下面程序展示了 SylixOS 计数型信号量的使用,程序创建两个线程和一个 SylixOS 计数型信号量;计数型信号量作为资源剩余计数,资源的初始数目为 5,最大数目为 100;线程 A 是资源的消费者,线程 B 是资源的生产者。
#include <SylixOS.h>
static LW_HANDLE _G_hResCntSema;
static PVOID tTestA (PVOID pvArg)
{
INT iError;
while (1) {
iError = Lw_SemaphoreC_Wait(_G_hResCntSema, LW_OPTION_WAIT_INFINITE);
if (iError != ERROR_NONE) {
break;
}
printf("tTestA(): get a resource\n");
}
return (LW_NULL);
}
static PVOID tTestB (PVOID pvArg)
{
while (1) {
Lw_Time_SSleep(1);
Lw_SemaphoreC_Post(_G_hResCntSema);
}
return (LW_NULL);
}
int main (int argc, char *argv[])
{
LW_HANDLE hThreadAId;
LW_HANDLE hThreadBId;
_G_hResCntSema = Lw_SemaphoreC_Create("res_sema",
5,
100,
LW_OPTION_WAIT_FIFO |
LW_OPTION_OBJECT_LOCAL,
LW_NULL);
if (_G_hResCntSema == LW_OBJECT_HANDLE_INVALID) {
printf("semaphore create failed.\n");
return (-1);
}
hThreadAId = Lw_Thread_Create("t_testa", tTestA, LW_NULL, LW_NULL);
if (hThreadAId == LW_OBJECT_HANDLE_INVALID) {
printf("t_testa create failed.\n");
return (-1);
}
hThreadBId = Lw_Thread_Create("t_testb", tTestB, LW_NULL, LW_NULL);
if (hThreadBId == LW_OBJECT_HANDLE_INVALID) {
printf("t_testb create failed.\n");
return (-1);
}
Lw_Thread_Join(hThreadAId, LW_NULL);
Lw_Thread_Join(hThreadBId, LW_NULL);
Lw_SemaphoreC_Delete(&_G_hResCntSema);
return (0);
}
在 SylixOS Shell 下运行程序:
# ./Counting_Semaphore
tTestA(): get a resource
tTestA(): get a resource
tTestA(): get a resource
tTestA(): get a resource
tTestA(): get a resource
tTestA(): get a resource
......
# ts
NAME TID PID PRI STAT LOCK SAFE DELAY PAGEFAILS FPU CPU
---------------- ------- ----- --- ---- ---- ---- ---------- --------- --- ---
Counting_Semaphore 4010081 40 200 JOIN 0 0 1 USE 0
t_testa 4010082 40 200 SEM 0 0 0 USE 0
t_testb 4010083 40 200 SLP 0 200 0 0
互斥信号量
在介绍二进制信号量时,曾讨论到如果二进制信号量创建时设置参数 bInitValue 为 TRUE,则可以用于互斥访问共享资源。实际上,SylixOS 的二进制信号量实现的互斥性是将一个变量初始化标记为 1,等待信号量(Wait)时将该变量减 1(此时等于 0),如果另一个线程再次等待该信号量将阻塞,直到该信号量被释放(变量加 1),这样就实现了共享资源的互斥访问。
如果系统中只有两个线程,上面的过程是没有问题的。但是一旦有多个线程介入,上面过程将出现以下问题:
一个高优先级的线程可能也要访问同一个共享资源(这是完全有可能的),此时只能阻塞等待,但是可能会有另一个中等优先级的线程将占有信号量的线程抢占。这个过程导致了高优先级线程很长时间得不到运行(这是 SylixOS 不允许出现的情况)。
以上过程出现的问题就是经典的优先级反转问题,我们将在“优先级反转”继续讨论优先级反转问题。
互斥信号量用于共享资源需要互斥访问的场合,可以理解为初始值为 TRUE 的带优先级天花板和优先级继承机制(意在解决优先级反转问题)的二进制信号量,只有拥有互斥信号量的线程才有权释放互斥信号量。
注意:
因为互斥信号量需要记录拥有者线程和调整优先级,而中断的优先级不允许修改,因此中断服务程序不能对互斥信号量进行操作。
下面的伪代码片段展示了互斥信号量的使用过程。
定义全局共享资源 (_G_shared)
void *thread (void *)
{
等互斥信号量 (Wait)
操作共享资源 (_G_shared)
释放互斥信号量 (Post)
线程退出 (Exit)
}
void main_func (void)
{
定义互斥信号量句柄 (semM)
创建互斥信号量 (Create)
创建线程 (thread)
线程JOIN (join)
删除互斥信号量 (Delete)
}
一个 SylixOS 互斥信号量必须要调用 Lw_SemaphoreM_Create 函数创建之后才能使用,如果创建成功,该函数将返回一个互斥信号量的句柄。
#include <SylixOS.h>
LW_HANDLE Lw_SemaphoreM_Create(CPCHAR pcName,
UINT8 ucCeilingPriority,
ULONG ulOption,
LW_OBJECT_ID *pulId);
函数 Lw_SemaphoreM_Create 原型分析:
- 此函数成功返回互斥信号量的句柄,失败返回 NULL 并设置错误号。
- 参数 pcName 是互斥信号量的名字。
- 参数 ucCeilingPriority 在使用优先级天花板算法时有效,此参数为天花板优先级。
- 参数 ulOption 是互斥信号量的创建选项。
- 输出参数 pulId 返回互斥信号量的句柄(同返回值),可以为 NULL。
创建选项包含了二进制信号的创建选项,此外还可以使用如下表所示的互斥信号量特有的创建选项。
宏名 | 含义 |
---|---|
LW_OPTION_INHERIT_PRIORITY | 优先级继承算法 |
LW_OPTION_PRIORITY_CEILING | 优先级天花板算法 |
LW_OPTION_NORMAL | 递归加锁时不检查 (不推荐) |
LW_OPTION_ERRORCHECK | 递归加锁时报错 |
LW_OPTION_RECURSIVE | 支持递归加锁 |
注意:
LW_OPTION_INHERIT_PRIORITY 和 LW_OPTION_PRIORITY_CEILING 只能二选一,同样 LW_OPTION_NORMAL 和 LW_OPTION_ERRORCHECK 及 LW_OPTION_RECURSIVE 只能三选一。
一个不再使用的互斥信号量,可以调用以下函数将其删除。删除后的信号量系统自动回收其占用的系统资源(试图使用被删除的互斥信号量将出现未知的错误)。
#include <SylixOS.h>
ULONG Lw_SemaphoreM_Delete(LW_HANDLE *pulId);
函数 Lw_SemaphoreM_Delete 原型分析:
- 此函数返回错误号。
- 参数 pulId 是互斥信号量的句柄。
线程如果需要等待一个互斥信号量,可以调用 Lw_SemaphoreM_Wait 函数。释放一个互斥信号量使用 Lw_SemaphoreM_Post 函数。
#include <SylixOS.h>
ULONG Lw_SemaphoreM_Wait(LW_HANDLE ulId,
ULONG ulTimeout);
函数 Lw_SemaphoreM_Wait 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 ulId 是互斥信号量的句柄。
- 参数 ulTimeout 是等待的超时时间,单位为时钟嘀嗒 Tick。
#include <SylixOS.h>
ULONG Lw_SemaphoreM_Post(LW_HANDLE ulId);
函数 Lw_SemaphoreM_Post 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 ulId 是互斥信号量的句柄。
需要注意的是,只有互斥信号量的拥有者才能释放该互斥信号量。
下图显示了互斥信号量的基本操作函数在线程中的操作过程。
下面函数可以获得互斥信号量的状态信息。
#include <SylixOS.h>
ULONG Lw_SemaphoreM_Status(LW_HANDLE ulId,
BOOL *pbValue,
ULONG *pulOption,
ULONG *pulThreadBlockNum);
ULONG Lw_SemaphoreM_StatusEx(LW_HANDLE ulId,
BOOL *pbValue,
ULONG *pulOption,
ULONG *pulThreadBlockNum,
LW_HANDLE *pulOwnerId);
以上两个函数原型分析:
- 函数成功返回 0,失败返回错误号。
- 参数 ulId 是互斥信号量的句柄。
- 输出参数 pbValue 用于接收互斥信号量当前的状态。
- 输出参数 pulOption 用于接收互斥信号量的创建选项。
- 输出参数 pulThreadBlockNum 用于接收当前阻塞在该互斥信号量的线程数。
- 输出参数 pulOwnerId 用于接收当前拥有该互斥信号量的线程的句柄。
如果想获得一个互斥信号量的名字,可以调用以下函数。
#include <SylixOS.h>
ULONG Lw_SemaphoreM_GetName(LW_HANDLE ulId,
PCHAR pcName);
函数 Lw_SemaphoreM_GetName 原型分析:
- 此函数成功返回 0,失败返回错误号。
- 参数 ulId 是互斥信号量的句柄。
- 输出参数 pcName 是互斥信号量的名字,pcName 应该指向一个大小为 LW_CFG_OBJECT_NAME_SIZE 的字符数组。
下面程序展示了 SylixOS 互斥信号量的使用,程序创建两个不同优先级的线程和一个 SylixOS 互斥信号量,两个线程分别都对变量 _G_iCount 进行自增操作和打印,使用 SylixOS 互斥信号量作为访问变量 _G_iCount 的互斥手段,其中互斥信号量使用优先级继承算法。
#include <SylixOS.h>
static LW_HANDLE _G_hLock;
static INT _G_iCount = 0;
static PVOID tTestA (PVOID pvArg)
{
INT iError;
while (1) {
iError = Lw_SemaphoreM_Wait(_G_hLock, LW_OPTION_WAIT_INFINITE);
if (iError != ERROR_NONE) {
break;
}
_G_iCount++;
printf("tTestA(): count = %d\n", _G_iCount);
Lw_SemaphoreM_Post(_G_hLock);
Lw_Time_MSleep(500);
}
return (LW_NULL);
}
static PVOID tTestB (PVOID pvArg)
{
INT iError;
while (1) {
iError = Lw_SemaphoreM_Wait(_G_hLock, LW_OPTION_WAIT_INFINITE);
if (iError != ERROR_NONE) {
break;
}
_G_iCount++;
printf("tTestB(): count = %d\n", _G_iCount);
Lw_SemaphoreM_Post(_G_hLock);
Lw_Time_MSleep(500);
}
return (LW_NULL);
}
int main (int argc, char *argv[])
{
LW_CLASS_THREADATTR threadattr;
LW_HANDLE hThreadAId;
LW_HANDLE hThreadBId;
_G_hLock = Lw_SemaphoreM_Create("count_lock",
LW_PRIO_HIGH,
LW_OPTION_WAIT_FIFO |
LW_OPTION_OBJECT_LOCAL|
LW_OPTION_INHERIT_PRIORITY |
LW_OPTION_ERRORCHECK,
LW_NULL);
if (_G_hLock == LW_OBJECT_HANDLE_INVALID) {
printf("mutex create failed.\n");
return (-1);
}
Lw_ThreadAttr_Build(&threadattr,
4 * LW_CFG_KB_SIZE,
LW_PRIO_NORMAL - 1,
LW_OPTION_THREAD_STK_CHK,
LW_NULL);
hThreadAId = Lw_Thread_Create("t_testa", tTestA, &threadattr, LW_NULL);
if (hThreadAId == LW_OBJECT_HANDLE_INVALID) {
printf("t_testa create failed.\n");
return (-1);
}
Lw_ThreadAttr_Build(&threadattr,
4 * LW_CFG_KB_SIZE,
LW_PRIO_NORMAL,
LW_OPTION_THREAD_STK_CHK,
LW_NULL);
hThreadBId = Lw_Thread_Create("t_testb", tTestB, &threadattr, LW_NULL);
if (hThreadBId == LW_OBJECT_HANDLE_INVALID) {
printf("t_testb create failed.\n");
return (-1);
}
Lw_Thread_Join(hThreadAId, LW_NULL);
Lw_Thread_Join(hThreadBId, LW_NULL);
Lw_SemaphoreM_Delete(&_G_hLock);
return (0);
}
在 SylixOS Shell 下运行程序:
# ./Mutex_Semaphore
tTestA(): count = 1
tTestB(): count = 2
tTestA(): count = 3
tTestB(): count = 4
tTestA(): count = 5
tTestB(): count = 6
......
# ts
NAME TID PID PRI STAT LOCK SAFE DELAY PAGEFAILS FPU CPU
---------------- ------- ----- --- ---- ---- ---- ---------- --------- --- ---
Mutex_Semaphore 401007e 39 200 JOIN 0 0 1 USE 0
t_testa 401007f 39 199 SLP 0 284 0 0
t_testb 4010080 39 200 SLP 0 284 0 0
读写信号量
正如 POSIX 读写锁 中介绍的情形,当出现多个读者,单个写者的情况时,单纯地使用互斥信号量将极大地减弱多线程操作系统的处理性能。为了满足这种高并发的处理速度问题,SylixOS 引入读写信号量,它的应用场景类似于 POSIX 读写锁。
SylixOS 读写信号量满足写优先的原则,也就是说,如果已经存在写信号量,则不能再申请读信号量,直到写信号量被释放。但是当已经存在读信号量时,可以再次请求读信号量。这种机制满足读的高并发性。
一个 SylixOS 读写信号量必须要调用 Lw_SemaphoreRW_Create 函数创建之后才能使用,如果创建成功,该函数将返回一个读写信号量的句柄。
#include <SylixOS.h>
LW_HANDLE Lw_SemaphoreRW_Create(CPCHAR pcName,
ULONG ulOption,
LW_OBJECT_ID *pulId);
函数 Lw_SemaphoreRW_Create 原型分析:
- 此函数成功返回读写信号量的句柄,失败返回 NULL 并设置错误号。
- 参数 pcName 是读写信号量的名字。
- 参数 ulOption 是读写信号量的创建选项。
- 输出参数 pulId 返回读写信号量的句柄(同返回值),可以为 NULL。
一个不再使用的读写信号量,可以调用以下函数将其删除。删除后的信号量系统自动回收其占用的系统资源(试图使用被删除的读写信号量将出现未知的错误)。
#include <SylixOS.h>
ULONG Lw_SemaphoreRW_Delete(LW_HANDLE *pulId);
函数 Lw_SemaphoreRW_Delete 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 pulId 是读写信号量的句柄。
线程如果需要等待一个读信号量,可以调用 Lw_SemaphoreRW_RWait 函数,如果需要等待一个写信号量,可以调用 Lw_SemaphoreRW_WWait 函数。释放一个读写信号量使用 Lw_SemaphoreRW_Post 函数。
#include <SylixOS.h>
ULONG Lw_SemaphoreRW_RWait(LW_HANDLE ulId,
ULONG ulTimeout);
函数 Lw_SemaphoreRW_RWait 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 ulId 是读信号量的句柄。
- 参数 ulTimeout 是等待的超时时间,单位为时钟嘀嗒 Tick。
#include <SylixOS.h>
ULONG Lw_SemaphoreRW_WWait(LW_HANDLE ulId,
ULONG ulTimeout);
函数 Lw_SemaphoreRW_WWait 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 ulId 是写信号量的句柄。
- 参数 ulTimeout 是等待的超时时间,单位为时钟嘀嗒 Tick。
#include <SylixOS.h>
ULONG Lw_SemaphoreRW_Post(LW_HANDLE ulId);
函数 Lw_SemaphoreRW_Post 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 ulId 是读写信号量的句柄。
需要注意的是,只有读写信号量的拥有者才能释放该读写信号量。
调用以下函数可以获得读写信号量的详细信息:
#include <SylixOS.h>
ULONG Lw_SemaphoreRW_Status(LW_OBJECT_HANDLE ulId,
ULONG *pulRWCount,
ULONG *pulRPend,
ULONG *pulWPend,
ULONG *pulOption,
LW_OBJECT_HANDLE *pulOwnerId);
函数 Lw_SemaphoreRW_Status 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误号。
- 参数 ulId 是读写信号量的句柄。
- 参数 pulRWCount 返回当前有多少线程正在并发操作读写信号量,此参数可以为 NULL。
- 参数 pulRPend 返回当前读操作阻塞的数量,此参数可以为 NULL。
- 参数 pulWPend 返回当前写操作阻塞的数量,此参数可以为 NULL。
- 参数 pulOption 返回当前读写信号量的选项信息,此参数可以为 NULL。
- 参数 pulOwnerId 返回当前写信号量的拥有者 ID,此参数可以为 NULL。