更多的锁介绍可以先看看这篇文章:多线程锁详解之【序章】
正文:
如果你有事件对象(Event)的作为基础,那么信号量理解起来将会十分容易。而本文是是建立在认为读者已经理解事件对象和 WaitSingleObject 函数原理的提前下写的,如果读者对这两者不太理解的话,可以先看看我锁详解的相关文章。
信号量,又名信号灯,是用来限定进入代码区域的次数。大家知道,如果事件对象带有信号自动重置的标记,那么即时事件对象处于有信号的状态下, WaitForSingleObject 函数也只能通过一次,想再次通过 WaitForSingleObject 就必须再次调用 SetEvent 函数。如果事件对象附带的是手动重置信号标记,那么在事件对象有信号的状态下, WaitForSingleObject 函数则可以无限次通过,除非调用 ResetEvent 函数进行阻止。
而信号灯则处于只能通过一次或无限次的特性之间。它有限定的进入次数,如果你限定次数为 1 ,那么它表现出来的特征就跟信号自动重置的事件对象一样,如果你限定的次数为无限大(假设支持),那么它表现出来的特征就跟信号手动重置的事件对象一样。
代码:
从代码的角度出发,信号灯对象又应该如何封装装呢?是不是【事件对象 + 限定进入次数】两个变量就可以了呢?从伪代码的概念,确实如此,让我们来看看真正的代码是如何描述这个对象的:
typedef struct _KSEMAPHORE {
DISPATCHER_HEADER Header;
LONG Limit; //限定进入的次数
} KSEMAPHORE, *PKSEMAPHORE, *RESTRICTED_POINTER PRKSEMAPHORE;
让我们再来回顾一下事件对象的描述代码:
typedef struct _KEVENT {
DISPATCHER_HEADER Header;
} KEVENT, *PKEVENT, *RESTRICTED_POINTER PRKEVENT;
嗯,看到了吗,信号量相当于在事件对象上面增加了一个限定进入的次数,而在 DISPATCHER_HEADER 结构体中,有一个 SignalState 变量。在事件对象里面,它是作为布尔值使用的,表示当前对象是否有信号。而在信号量对象中,它是作为一个整数使用,用来作为信号量的闲置位计数。信号量的闲置位计数【SignalState 】等于 0 时(不管何种对象,SignalState 等于 0 均表示为无信号状态),表示没有了多余的位置了,这个时候调用 WaitForSingleObject 会陷入等待。直到 SignalState 大于 0 时,将会唤醒一个或多个WaitForSingleObject 等待。而且 SignalState 还有一个限制,就是它不能够大于 Limit。
既然事件对象我们已经理解了,Limit 变量的作用也清楚了,那我们来写一下伪代码:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpAttributes, // 安全属性, 可以设置NULL
LONG lInitialCount, // 初始值, [0, lMaximumCount]
LONG lMaximumCount, // 最大值, 这也就是在同一时间内能够锁住semaphore之线程的最多个数
LPCTSTR lpName // 名称, 其他线程或进程可以根据名称引用该信号量; NULL则产生无名称信号量
)
{
KSEMAPHORE* pSem = ke_malloc(sizeof(KSEMAPHORE));//内核层分配内存
pSem->Header.Type = KSEMAPHORE_TYPE;//数据类型为信号量对象
pSem->Header.SignalState = lInitialCount;
pSem->Limit = lMaximumCount;
return KeObjectToHandle(pSem);//内核对象转换成用户对象
}
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount, // 现值的增额, 该值不可以是负值或0
LPLONG lpPreviousCount // 藉此传回semaphore原来的现值
)
{
LONG nOldValue = 0;
KSEMAPHORE* pSem = HandleToKeObject(hSemaphore);//把句柄转换内核对象
nOldValue = pSem->Header.SignalState; //记录当前值
Semaphore->Header.SignalState += lReleaseCount;
//WaitListHead 是等待队列
if (nOldValue == 0 && Semaphore->Header.WaitListHead != NULL)
{
//当原来的 SignalState 没有空闲位置,就是没有任何信号了
//并且此时等待队列不为空,那么需要唤醒一部分线程,
//唤醒的数量,在等待队列的节点数量和 SignalState 之间取最小值
//即 int nCount = min(WaitListHead.Count, SignalState);
WakeSomeThread(Semaphore);//现在有信号了,我们唤醒一部分线程
}
*lpPreviousCount = nOldValue;
return TRUE;
}
DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
)
{
KSEMAPHORE* pSem = HandleToKeObject(hSemaphore);//把句柄转换内核对象
if (pSem->Header.SignalState == 0)
{
wait(pSem);//等待被唤醒
//WaitForSingleObject 函数被 ReleaseSemaphore 函数唤醒前
//SignalState 的值肯定被增加过了,所以SignalState必然大于 0
}
//assert(pSem->Header.SignalState > 0);//断言SignalState大于 0
pSem->Header.SignalState--;//唤醒后信号量递减
return 0;
}
嗯嗯,从伪代码看来,确实不算太难,那我们再看看 ReactOS 的实现代码。其实对于大部分的内核对象函数处理,微软都是使用三板斧套路:应用层调用 Nt 层,Nt 层调用 ke 层,而 Nt 层一般都是负责环境构建,比如申请内存,数据格式转换,对象初始化这些动作,逻辑都是很简单的,真正的处理逻辑往往都在 Ke 层。那我们先从两个 Nt 函数看起:
NTSTATUS
NTAPI
NtCreateSemaphore(OUT PHANDLE SemaphoreHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN LONG InitialCount,
IN LONG MaximumCount)
{
PKSEMAPHORE Semaphore;
HANDLE hSemaphore;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
NTSTATUS Status;
PAGED_CODE();
/* Check if we were called from user-mode */
if (PreviousMode != KernelMode)
{
/* Enter SEH Block */
_SEH2_TRY
{
/* Check handle pointer */
ProbeForWriteHandle(SemaphoreHandle);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
/* Return the exception code */
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
/* Make sure the counts make sense */
if ((MaximumCount <= 0) ||
(InitialCount < 0) ||
(InitialCount > MaximumCount))
{
DPRINT("Invalid Count Data!\n");
return STATUS_INVALID_PARAMETER;
}
/* Create the Semaphore Object */
Status = ObCreateObject(PreviousMode,
ExSemaphoreObjectType,
ObjectAttributes,
PreviousMode,
NULL,
sizeof(KSEMAPHORE),
0,
0,
(PVOID*)&Semaphore);
/* Check for Success */
if (NT_SUCCESS(Status))
{
/* Initialize it */
KeInitializeSemaphore(Semaphore,
InitialCount,
MaximumCount);
/* Insert it into the Object Tree */
Status = ObInsertObject((PVOID)Semaphore,
NULL,
DesiredAccess,
0,
NULL,
&hSemaphore);
/* Check for success */
if (NT_SUCCESS(Status))
{
/* Enter SEH Block for return */
_SEH2_TRY
{
/* Return the handle */
*SemaphoreHandle = hSemaphore;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
/* Get the exception code */
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
}
}
/* Return Status */
return Status;
}
NTSTATUS
NTAPI
NtReleaseSemaphore(IN HANDLE SemaphoreHandle,
IN LONG ReleaseCount,
OUT PLONG PreviousCount OPTIONAL)
{
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
PKSEMAPHORE Semaphore;
NTSTATUS Status;
PAGED_CODE();
/* Check if we were called from user-mode */
if ((PreviousCount) && (PreviousMode != KernelMode))
{
/* Entry SEH Block */
_SEH2_TRY
{
/* Make sure the state pointer is valid */
ProbeForWriteLong(PreviousCount);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
/* Return the exception code */
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
/* Make sure count makes sense */
if (ReleaseCount <= 0)
{
DPRINT("Invalid Release Count\n");
return STATUS_INVALID_PARAMETER;
}
/* Get the Object */
Status = ObReferenceObjectByHandle(SemaphoreHandle,
SEMAPHORE_MODIFY_STATE,
ExSemaphoreObjectType,
PreviousMode,
(PVOID*)&Semaphore,
NULL);
/* Check for success */
if (NT_SUCCESS(Status))
{
/* Enter SEH Block */
_SEH2_TRY
{
/* Release the semaphore */
LONG PrevCount = KeReleaseSemaphore(Semaphore,
IO_NO_INCREMENT,
ReleaseCount,
FALSE);
/* Return the old count if requested */
if (PreviousCount) *PreviousCount = PrevCount;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
/* Get the exception code */
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
/* Dereference the Semaphore */
ObDereferenceObject(Semaphore);
}
/* Return Status */
return Status;
}
信号量的 Nt 层处理,和事件对象的操作本质无二,NtCreateSemaphore 负责申请对象(其实就是申请内存,并初始化其中一部分值),然后把申请出来的对象放到句柄表中(ObInsertObject 函数),形成 key-value 的对象关系,最后把 key (句柄)返回用户层。而 NtReleaseSemaphore 就是利用用户层提供的 key (句柄)找到对应的对象(其实就是结构体指针),并调用 Ke 层的逻辑处理,从 key 找到 value 这个动作是由 ObReferenceObjectByHandle 函数完成的,由于内核对象可能存在于多进程之中,也就意味着会存在多个线程对内核对象进行访问,所以 ObReferenceObjectByHandle 找到对象后,还会对它的引用计数加 1 (参考智能指针原理),而在 Ke 层处理完毕后,还会调用一次 ObDereferenceObject 函数。ObDereferenceObject 函数负责将对象的引用计数减 1 ,减 1 后若引用计数为 0,则删除这个对象。
对于信号量的 Nt 层函数,除了 Ke 函数,还有一个 KeInitializeSemaphore 函数值得一看:
VOID
NTAPI
KeInitializeSemaphore(IN PKSEMAPHORE Semaphore,
IN LONG Count,
IN LONG Limit)
{
/* Simply Initialize the Header */
Semaphore->Header.Type = SemaphoreObject;
Semaphore->Header.Size = sizeof(KSEMAPHORE) / sizeof(ULONG);
Semaphore->Header.SignalState = Count;
InitializeListHead(&(Semaphore->Header.WaitListHead));//插入等待队列
/* Set the Limit */
Semaphore->Limit = Limit;
}
其实也是没什么特别的,列出代码是希望大家能够更好地理解信号灯主要使用了那些成员变量。再来看看 Ke 函数的实现:
/*
* @implemented
*/
LONG
NTAPI
KeReleaseSemaphore(IN PKSEMAPHORE Semaphore,
IN KPRIORITY Increment,
IN LONG Adjustment,
IN BOOLEAN Wait)
{
LONG InitialState, State;
KIRQL OldIrql;
PKTHREAD CurrentThread;
ASSERT_SEMAPHORE(Semaphore);
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
/* Lock the Dispatcher Database */
OldIrql = KiAcquireDispatcherLock();//加锁
/* Save the Old State and get new one */
//下面是简单的加减计算,理解清楚 SignalState 和 Limit 的关系
//就很容易看明白
InitialState = Semaphore->Header.SignalState;
State = InitialState + Adjustment;
/* Check if the Limit was exceeded */
if ((Semaphore->Limit < State) || (InitialState > State))
{
/* Raise an error if it was exceeded */
//State 也就是 SignalState
//按道理 SignalState 必然在 0 和 Semaphore->Limit 之间
//如果不是则抛出异常
KiReleaseDispatcherLock(OldIrql);
ExRaiseStatus(STATUS_SEMAPHORE_LIMIT_EXCEEDED);
}
/* Now set the new state */
Semaphore->Header.SignalState = State;
/* Check if we should wake it */
if (!(InitialState) && !(IsListEmpty(&Semaphore->Header.WaitListHead)))
{
/* Wake the Semaphore */
//当原来的 SignalState 没有空闲位置(为 0),就是没有任何信号了
//并且此时等待队列不为空,那么需要唤醒一部分线程,
//唤醒的数量,在等待队列的节点数量和 SignalState 之间取最小值
//即 int nCount = min(WaitListHead.Count, SignalState);
KiWaitTest(&Semaphore->Header, Increment); //唤醒部分线程
}
/* Check if the caller wants to wait after this release */
//是否处于循环调用/连续调用环境,如果是,则外部调用的另一个函数会帮我们释放锁
if (Wait == FALSE)
{
/* Release the Lock */
KiReleaseDispatcherLock(OldIrql);
}
else
{
/* Set a wait */
CurrentThread = KeGetCurrentThread();
//已经调用了 KeAcquireDispatcherDatabaseLock 锁定,
//或者说没有调用 KiReleaseDispatcherLock
//那就以为着下一个访问 Thread 的对象不需要调用 KeAcquireDispatcherDatabaseLock 了
CurrentThread->WaitNext = TRUE;
CurrentThread->WaitIrql = OldIrql;
}
/* Return the previous state */
return InitialState; //返回原来的状态
}
额,逻辑确实挺简单的,该说的注释中都说了,如果你对事件对象足够了解,相信当你看到 KSEMAPHORE 这个结构体的时候就知道它要干嘛,着手点无非就是对于 SignalState 和 Limit 的处理。关于 WaitForSingleObject 函数的伪代码,上面也给出了,真正的 WaitForSingleObject 函数对信号量的 SignalState 递减,是在 KiSatisfyNonMutantWait 这个宏实现的,更详细的WaitForSingleObject 资料大家去看看我的 WaitForSingleObject 文章, 这里我们只说说 KiSatisfyNonMutantWait 宏的实现:
//
// Satisfies the wait of any nonmutant dispatcher object
//
#define KiSatisfyNonMutantWait(Object) \
{ \
if (((Object)->Header.Type & TIMER_OR_EVENT_TYPE) == \
EventSynchronizationObject) \
{ \
/* Synchronization Timers and Events just get un-signaled */ \
(Object)->Header.SignalState = 0; \
} \
else if ((Object)->Header.Type == SemaphoreObject) \
{ \
/* These ones can have multiple states, so we only decrease it */ \
(Object)->Header.SignalState--; \
} \
}
KiSatisfyNonMutantWait 这个宏的意思就是,如果是信号自动重置(事件对象特征),那么 SignalState 等于,如果是 SemaphoreObject 对象,则 SignalState 递减。因为 WaitForSingleObject 能够处理的对象很多,所以它要根据类型判断对 SignalState 变量作出相应的处理。
讨论:
理解完上面的源码大家应该就能够明白,所谓信号量就是在事件对象上加了一个 Limit 作为限制处理,所以信号量是能够替代事件对象的(虽然windows上面没人这样做)。有些同学疑惑,不对啊,如果是手动重置信号,事件对象是可以无限进入的,而信号量有 Limit 变量作为限制,理论上跟事件对象特征不符合。
理论上确实跟事件对象特征不符合,但实际上呢?假设我把 Limit 设置为最大值,那么它就支持上亿次重入。大家想想上亿次重入是什么概念,如果是单线程上亿次重入,那么必然栈溢出,如果是多线程,那起码得有上百万或者过亿条线程去调用这个信号量才能达到 Limit 限制,这个时候系统资源根本撑不起来。而事件变量虽然支持无限次重入,但事实上我们根本不会这样做。所以说理论上特征不符合,实际上是没任何问题的。
而比较有意思的,linux 系统中没有类似于事件对象的实现,但是提供信号量给大家使用,所以大家知道怎么装换了吧。
结语:
其实信号量的内部实现处理,还是比较简单的。不过在实际项目中的它应用比较少,反正我是没用过这个东西,偶尔遇到一些限制访问数量的场景,我最终也是用临界区或事件对象来代替。大家知道有哪些是真正适合信号量使用的场景吗?