Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

Win32 临界区实现原理浅析

Posted on 2004-07-08 10:09  Flier Lu  阅读(6494)  评论(0编辑  收藏  举报
Win32 临界区实现原理浅析

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

   

 Visio 2003 版本的流程图

     接着的_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