http://www.blogcn.com/user8/flier_lu/index.html?id=1205525&run=.0748049
去年11月的MSDN杂志曾刊登过一篇文章 Break Free of Code Deadlocks in Critical Sections Under Windows ,Matt Pietrek 和 Russ Osterlund 两位对临界区(Critical Section)的内部实现做了一次简短的介绍,但点到为止,没有继续深入下去,当时给我的感觉就是痒痒的,呵呵,于是用IDA和SoftIce大致分析了一下临界区的实现,大致弄明白了原理后也就没有深究。现在乘着Win2k源码的东风,重新分析一下这块的内容,做个小小的总结吧 :P
临界区(Critical Section)是Win32中提供的一种轻量级的同步机制,与互斥(Mutex)和事件(Event)等内核同步对象相比,临界区是完全在用户态维护的,所以仅能在同一进程内供线程同步使用,但也因此无需在使用时进行用户态和核心态之间的切换,工作效率大大高于其它同步机制。
临界区的使用方法非常简单,使用 InitializeCriticalSection 或 InitializeCriticalSectionAndSpinCount 函数初始化一个 CRITICAL_SECTION 结构;使用 SetCriticalSectionSpinCount 函数设置临界区的Spin计数器;然后使用 EnterCriticalSection 或 TryEnterCriticalSection 获取临界区的所有权;完成需要同步的操作后,使用 LeaveCriticalSection 函数释放临界区;最后使用 DeleteCriticalSection 函数析构临界区结构。
以下是MSDN中提供的一个简单的例子:
以下为引用:
// Global variable
CRITICAL_SECTION CriticalSection;void main()
{
...// Initialize the critical section one time only.
if (!InitializeCriticalSectionAndSpinCount(&CriticalSection, 0x80000400) )
return;
...// Release resources used by the critical section object.
DeleteCriticalSection(&CriticalSection)
}DWORD WINAPI ThreadProc( LPVOID lpParameter )
{
...// Request ownership of the critical section.
EnterCriticalSection(&CriticalSection);// Access the shared resource.
// Release ownership of the critical section.
LeaveCriticalSection(&CriticalSection);...
}
首先看看构造和析构临界区结构的函数。
InitializeCriticalSection 函数(ntosdll esource.c:1210)实际上是调用 InitializeCriticalSectionAndSpinCount 函数(resource.c:1266)完成功能的,只不过传入一个值为0的初始Spin计数器;InitializeCriticalSectionAndSpinCount 函数主要完成两部分工作:初始化 RTL_CRITICAL_SECTION 结构和 RTL_CRITICAL_SECTION_DEBUG 结构。前者是临界区的核心结构,下面将着重讨论;后者是调试用结构,Matt 那篇文章里面分析的很清楚了,我这儿就不罗嗦了 :P
RTL_CRITICAL_SECTION结构在winnt.h中定义如下:
以下为引用:
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;//
// The following three fields control entering and exiting the critical
// section for the resource
//LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread; // from the thread's ClientId->UniqueThread
HANDLE LockSemaphore;
ULONG_PTR SpinCount; // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
InitializeCriticalSectionAndSpinCount 函数中首先对临界区结构进行了初始化
DebugInfo 字段指向初始化临界区时分配的RTL_CRITICAL_SECTION_DEBUG结构;
LockCount 字段是临界区中最重要的字段,初始值为-1,当临界区被获取(Hold)时此字段大于等于0;
RecursionCount 字段保存当前临界区所有者线程的获取缓冲区嵌套层数,初始值为0;
OwningThread 字段保存当前临界区所有者线程的句柄,初始值为0;
LockSemaphore 字段实际上是一个auto-reset的事件句柄,用于唤醒等待获取临界区的阻塞线程,初始值为0;
SpinCount 字段用于在多处理器环境下完成轻量级的CPU见同步,单处理器时没有使用(初始值为0),多处理器时设置为SpinCount参数值(最大为MAX_SPIN_COUNT=0x00ffffff)。此外 RtlSetCriticalSectionSpinCount 函数(resource.c:1374)的代码与这儿设置SpinCount的代码完全一样。
初始化临界区结构后,函数会根据SpinCount参数的一个标志位判断是否需要预先初始化 LockSemaphore 字段,如果需要则使用NtCreateEvent创建一个具有访问权限的同步用事件核心对象,初始状态为没有激发。这一初始化本来是在 EnterCriticalSection 函数中完成的,将之显式提前可以进一步优化 EnterCriticalSection 函数的性能。
值得注意的是,这一特性仅对Win2k有效。MSDN里面说明如下:
以下为引用:
Windows 2000: If the high-order bit is set, the function preallocates the event used by the EnterCriticalSection function. Do not set this bit if you are creating a large number of critical section objects, because it will consume a significant amount of nonpaged pool. This flag is not necessary on Windows XP and later, and it is ignored.
与之对应的 DeleteCriticalSection 函数完成关闭事件句柄和是否调试结构的功能。
临界区真正的核心代码在win2kprivate tosdlli386critsect.asm里面,包括_RtlEnterCriticalSection、_RtlTryEnterCriticalSection和_RtlLeaveCriticalSection三个函数。
_RtlEnterCriticalSection 函数 (critsect.asm:85) 首先检查SpinCount是否为0,如果不为0则处理多处理器架构下的问题[分支1];如为0则使用原子操作给LockCount加一,并判断是否其值为0。如果加一后LockCount大于0,则此临界区已经被获取[分支2];如为0则获取当前线程TEB中的线程句柄,保存在临界区的OwningThread字段中,并将RecursionCount设置为1。调试版本则调用RtlpCriticalSectionIsOwned函数在调试模式下验证此缓冲区是被当前线程获取的,否则在调试模式下激活调试器。最后还会更新TEB的CountOfOwnedCriticalSections计数器,以及临界区调试结构中的EntryCount字段。
如果此临界区已经被获取[分支2],则判断获取临界区的线程句柄是否与当前线程相符。如果是同一线程则直接将RecursionCount和调试结构的EntryCount字段加一;如果不是当前线程,则调用RtlpWaitForCriticalSection函数等待此临界区,并从头开始执行获取临界区的程序。
多CPU情况的分支处理方式类似,只是多了对SpinCount的双重检查处理。下面是我手工归纳的一张 _RtlEnterCriticalSection 函数流程图,可能有错,欢迎指正 :P
接着的_RtlTryEnterCriticalSection(critsect.asm:343)函数是一个轻量级的尝试获取临界区的函数。伪代码如下:
以下为引用:
if(CriticalSection->LockCount == -1)
{
// 临界区可用
CriticalSection->LockCount = 0;
CriticalSection->OwningThread = TEB->ClientID;
CriticalSection->RecursionCount = 1;return TRUE;
}
else
{
if(CriticalSection->OwningThread == TEB->ClientID)
{
// 临界区是当前线程获取
CriticalSection->LockCount++;
CriticalSection->RecursionCount++;return TRUE;
}
else
{
// 临界区已被其它线程获取
return FALSE;
}
}
最后的_RtlLeaveCriticalSection(critsect.asm:271)函数释放已获取的临界区,实现就比较简单了,实际上就是对嵌套计数器和锁定计数器进行操作。伪代码如下:
以下为引用:
if(--CriticalSection->RecursionCount == 0)
{
// 临界区已不再被使用
CriticalSection->OwningThread = 0;if(--CriticalSection->LockCount)
{
// 仍有线程锁定在临界区上
_RtlpUnWaitCriticalSection(CriticalSection)
}
}
else
{
--CriticalSection->LockCount
}
btw: 看源代码、分析流程再加上写文章时间加起来还没有画那个流程图时间长,真是ft死了...-_-b