




已阅读5页,还剩10页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
Windows 下的临界区中的代码死锁解开Windows下的临界区中的代码死锁2011-06-08 10:03解开Windows下的临界区中的代码死锁发布日期:1/13/2005|更新日期:1/13/2005原文:Pietrek和Russ Osterlund本文假定您熟悉Win32、C+和多线程处理。下载本文的代码:CriticalSections.exe(415KB)摘要临界区是一种防止多个线程同时执行一个特定代码节的机制,这一主题并没有引起太多关注,因而人们未能对其深刻理解。在需要跟踪代码中的多线程处理的性能时,对Windows中临界区的深刻理解非常有用。本文深入研究临界区的原理,以揭示在查找死锁和确认性能问题过程中的有用信息。它还包含一个便利的实用工具程序,可以显示所有临界区及其当前状态。在我们许多年的编程实践中,对于Win32临界区没有受到非常多的under the hood关注而感到非常奇怪。当然,您可能了解有关临界区初始化与使用的基础知识,但您是否曾经花费时间来深入研究WINNT.H中所定义的CRITICAL_SECTION结构呢?在这一结构中有一些非常有意义的好东西被长期忽略。我们将对此进行补充,并向您介绍一些很有意义的技巧,这些技巧对于跟踪那些难以察觉的多线程处理错误非常有用。更重要的是,使用我们的MyCriticalSections实用工具,可以明白如何对CRITICAL_SECTION进行微小地扩展,以提供非常有用的特性,这些特性可用于调试和性能调整(要下载完整代码,参见本文顶部的链接)。老实说,作者们经常忽略CRITICAL_SECTION结构的部分原因在于它在以下两个主要Win32代码库中的实现有很大不同:MicrosoftWindows95和Windows NT。人们知道这两种代码库都已经发展出大量后续版本(其最新版本分别为Windows Me和Windows XP),但没有必要在此处将其一一列出。关键在于Windows XP现在已经发展得非常完善,开发商可能很快就会停止对Windows 95系列操作系统的支持。我们在本文中就是这么做的。诚然,当今最受关注的是Microsoft.NET Framework,但是良好的旧式Win32编程不会很快消失。如果您拥有采用了临界区的现有Win32代码,您会发现我们的工具以及对临界区的说明都非常有用。但是请注意,我们只讨论Windows NT及其后续版本,而没有涉及与.NET相关的任何内容,这一点非常重要。临界区:简述如果您非常熟悉临界区,并可以不假思索地进行应用,那就可以略过本节。否则,请向下阅读,以对这些内容进行快速回顾。如果您不熟悉这些基础内容,则本节之后的内容就没有太大意义。临界区是一种轻量级机制,在某一时间内只允许一个线程执行某个给定代码段。通常在修改全局数据(如集合类)时会使用临界区。事件、多用户终端执行程序和信号量也用于多线程同步,但临界区与它们不同,它并不总是执行向内核模式的控制转换,这一转换成本昂贵。稍后将会看到,要获得一个未占用临界区,事实上只需要对内存做出很少的修改,其速度非常快。只有在尝试获得已占用临界区时,它才会跳至内核模式。这一轻量级特性的缺点在于临界区只能用于对同一进程内的线程进行同步。临界区由WINNT.H中所定义的RTL_CRITICAL_SECTION结构表示。因为您的C+代码通常声明一个CRITICAL_SECTION类型的变量,所以您可能对此并不了解。研究WINBASE.H后您会发现:typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;我们将在短时间内揭示RTL_CRITICAL_SECTION结构的实质。此时,重要问题在于CRITICAL_SECTION(也称作RTL_CRITICAL_SECTION)只是一个拥有易访问字段的结构,这些字段可以由KERNEL32 API操作。在将临界区传递给InitializeCriticalSection时(或者更准确地说,是在传递其地址时),临界区即开始存在。初始化之后,代码即将临界区传递给EnterCriticalSection和LeaveCriticalSection API。一个线程自EnterCriticalSection中返回后,所有其他调用EnterCriticalSection的线程都将被阻止,直到第一个线程调用LeaveCriticalSection为止。最后,当不再需要该临界区时,一种良好的编码习惯是将其传递给DeleteCriticalSection。在临界区未被使用的理想情况中,对EnterCriticalSection的调用非常快速,因为它只是读取和修改用户模式内存中的内存位置。否则(在后文将会遇到一种例外情况),阻止于临界区的线程有效地完成这一工作,而不需要消耗额外的CPU周期。所阻止的线程以内核模式等待,在该临界区的所有者将其释放之前,不能对这些线程进行调度。如果有多个线程被阻止于一个临界区中,当另一线程释放该临界区时,只有一个线程获得该临界区。深入研究:RTL_CRITICAL_SECTION结构即使您已经在日常工作中使用过临界区,您也非常可能并没有真正了解超出文档之外的内容。事实上存在着很多非常容易掌握的内容。例如,人们很少知道一个进程的临界区是保存于一个链表中,并且可以对其进行枚举。实际上,WINDBG支持!locks命令,这一命令可以列出目标进程中的所有临界区。我们稍后将要谈到的实用工具也应用了临界区这一鲜为人知的特征。为了真正理解这一实用工具如何工作,有必要真正掌握临界区的内部结构。记着这一点,现在开始研究RTL_CRITICAL_SECTION结构。为方便起见,将此结构列出如下:struct RTL_CRITICAL_SECTIONPRTL_CRITICAL_SECTION_DEBUG DebugInfo;LONG LockCount;LONG RecursionCount;HANDLE OwningThread;HANDLE LockSemaphore;ULONG_PTR SpinCount;以下各段对每个字段进行说明。DebugInfo此字段包含一个指针,指向系统分配的伴随结构,该结构的类型为RTL_CRITICAL_SECTION_DEBUG。这一结构中包含更多极有价值的信息,也定义于WINNT.H中。我们稍后将对其进行更深入地研究。LockCount这是临界区中最重要的一个字段。它被初始化为数值-1;此数值等于或大于0时,表示此临界区被占用。当其不等于-1时,OwningThread字段(此字段被错误地定义于WINNT.H中-应当是DWORD而不是HANDLE)包含了拥有此临界区的线程ID。此字段与(RecursionCount-1)数值之间的差值表示有多少个其他线程在等待获得该临界区。RecursionCount此字段包含所有者线程已经获得该临界区的次数。如果该数值为零,下一个尝试获取该临界区的线程将会成功。OwningThread此字段包含当前占用此临界区的线程的线程标识符。此线程ID与GetCurrentThreadId之类的API所返回的ID相同。LockSemaphore此字段的命名不恰当,它实际上是一个自复位事件,而不是一个信号。它是一个内核对象句柄,用于通知操作系统:该临界区现在空闲。操作系统在一个线程第一次尝试获得该临界区,但被另一个已经拥有该临界区的线程所阻止时,自动创建这样一个句柄。应当调用DeleteCriticalSection(它将发出一个调用该事件的CloseHandle调用,并在必要时释放该调试结构),否则将会发生资源泄漏。SpinCount仅用于多处理器系统。MSDN文档对此字段进行如下说明:在多处理器系统中,如果该临界区不可用,调用线程将在对与该临界区相关的信号执行等待操作之前,旋转dwSpinCount次。如果该临界区在旋转操作期间变为可用,该调用线程就避免了等待操作。旋转计数可以在多处理器计算机上提供更佳性能,其原因在于在一个循环中旋转通常要快于进入内核模式等待状态。此字段默认值为零,但可以用InitializeCriticalSectionAndSpinCount API将其设置为一个不同值。RTL_CRITICAL_SECTION_DEBUG结构前面我们注意到,在RTL_CRITICAL_SECTION结构内,DebugInfo字段指向一个RTL_CRITICAL_SECTION_DEBUG结构,该结构给出如下:struct _RTL_CRITICAL_SECTION_DEBUGWORD Type;WORD CreatorBackTraceIndex;RTL_CRITICAL_SECTION*CriticalSection;LIST_ENTRY ProcessLocksList;DWORD EntryCount;DWORD ContentionCount;DWORD Spare2;这一结构由InitializeCriticalSection分配和初始化。它既可以由NTDLL内的预分配数组分配,也可以由进程堆分配。RTL_CRITICAL_SECTION的这一伴随结构包含一组匹配字段,具有迥然不同的角色:有两个难以理解,随后两个提供了理解这一临界区链结构的关键,两个是重复设置的,最后两个未使用。下面是对RTL_CRITICAL_SECTION字段的说明。Type此字段未使用,被初始化为数值0。CreatorBackTraceIndex此字段仅用于诊断情形中。在注册表项HKLMSoftwareMicrosoftWindows NTCurrentVersionImage File Execution OptionsYourProgram之下是keyfield、GlobalFlag和StackTraceDatabaseSizeInMb值。注意,只有在运行稍后说明的Gflags命令时才会显示这些值。这些注册表值的设置正确时,CreatorBackTraceIndex字段将由堆栈跟踪中所用的一个索引值填充。在MSDN中搜索GFlags文档中的短语create user mode stack trace database和enlarging the user-mode stack trace database,可以找到有关这一内容的更多信息。CriticalSection指向与此结构相关的RTL_CRITICAL_SECTION。图1说明该基础结构以及RTL_CRITICAL_SECTION、RTL_CRITICAL_SECTION_DEBUG和事件链中其他参与者之间的关系。图1临界区处理流程ProcessLocksList LIST_ENTRY是用于表示双向链表中节点的标准Windows数据结构。RTL_CRITICAL_SECTION_DEBUG包含了链表的一部分,允许向前和向后遍历该临界区。本文后面给出的实用工具说明如何使用Flink(前向链接)和Blink(后向链接)字段在链表中的成员之间移动。任何从事过设备驱动程序或者研究过Windows内核的人都会非常熟悉这一数据结构。EntryCount/ContentionCount这些字段在相同的时间、出于相同的原因被递增。这是那些因为不能马上获得临界区而进入等待状态的线程的数目。与LockCount和RecursionCount字段不同,这些字段永远都不会递减。Spares这两个字段未使用,甚至未被初始化(尽管在删除临界区结构时将这些字段进行了清零)。后面将会说明,可以用这些未被使用的字段来保存有用的诊断值。即使RTL_CRITICAL_SECTION_DEBUG中包含多个字段,它也是常规临界区结构的必要成分。事实上,如果系统恰巧不能由进程堆中获得这一结构的存储区,InitializeCriticalSection将返回为STATUS_NO_MEMORY的LastError结果,然后返回处于不完整状态的临界区结构。临界区状态当程序执行、进入与离开临界区时,RTL_CRITICAL_SECTION和RTL_CRITICAL_SECTION_DEBUG结构中的字段会根据临界区所处的状态变化。这些字段由临界区API中的簿记代码更新,在后面将会看到这一点。如果程序为多线程,并且其线程访问是由临界区保护的公用资源,则这些状态就更有意义。但是,不管代码的线程使用情况如何,有两种状态都会出现。第一种情况,如果LockCount字段有一个不等于-1的数值,此临界区被占用,OwningThread字段包含拥有该临界区的线程的线程标识符。在多线程程序中,LockCount与RecursionCount联合表明当前有多少线程被阻止于该临界区。第二种情况,如果RecursionCount是一个大于1的数值,其告知您所有者线程已经重新获得该临界区多少次(也许不必要),该临界区既可以通过调用EnterCriticalSection、也可以通过调用TryEnterCriticalSection获得。大于1的任何数值都表示代码的效率可能较低或者可能在以后发生错误。例如,访问公共资源的任何C+类方法可能会不必要地重新进入该临界区。注意,在大多数时间里,LockCount与RecursionCount字段中分别包含其初始值-1和0,这一点非常重要。事实上,对于单线程程序,不能仅通过检查这些字段来判断是否曾获得过临界区。但是,多线程程序留下了一些标记,可以用来判断是否有两个或多个线程试图同时拥有同一临界区。您可以找到的标记之一是即使在该临界区未被占用时LockSemaphore字段中仍包含一个非零值。这表示:在某一时间,此临界区阻止了一个或多个线程-事件句柄用于通知该临界区已被释放,等待该临界区的线程之一现在可以获得该临界区并继续执行。因为OS在临界区阻止另一个线程时自动分配事件句柄,所以如果您在不再需要临界区时忘记将其删除,LockSemaphore字段可能会导致程序中发生资源泄漏。在多线程程序中可能遇到的另一状态是EntryCount和ContentionCount字段包含一个大于零的数值。这两个字段保存有临界区对一个线程进行阻止的次数。在每次发生这一事件时,这两个字段被递增,但在临界区存在期间不会被递减。这些字段可用于间接确定程序的执行路径和特性。例如,EntryCount非常高时则意味着该临界区经历着大量争用,可能会成为代码执行过程中的一个潜在瓶颈。在研究一个死锁程序时,还会发现一种似乎无法进行逻辑解释的状态。一个使用非常频繁的临界区的LockCount字段中包含一个大于-1的数值,也就是说它被线程所拥有,但是OwningThread字段为零(这样就无法找出是哪个线程导致问题)。测试程序是多线程的,在单处理器计算机和多处理器计算机中都会出现这种情况。尽管LockCount和其他值在每次运行中都不同,但此程序总是死锁于同一临界区。我们非常希望知道是否有任何其他开发人员也遇到了导致这一状态的API调用序列。构建一个更好的捕鼠器在我们学习临界区的工作方式时,非常偶然地得到一些重要发现,利用这些发现可以得到一个非常好的实用工具。第一个发现是ProcessLocksList LIST_ENTRY字段的出现,这使我们想到进程的临界区可能是可枚举的。另一个重大发现是我们知道了如何找出临界区列表的头。还有一个重要发现是可以在没有任何损失的情况下写RTL_CRITICAL_SECTION的Spare字段(至少在我们的所有测试中如此)。我们还发现可以很容易地重写系统的一些临界区例程,而不需要对源文件进行任何修改。最初,我们由一个简单的程序开始,其检查一个进程中的所有临界区,并列出其当前状态,以查看是否拥有这些临界区。如果拥有,则找出由哪个线程拥有,以及该临界区阻止了多少个线程?这种做法对于OS的狂热者们比较适合,但对于只是希望有助于理解其程序的典型的程序员就不是非常有用了。即使是在最简单的控制台模式Hello World程序中也存在许多临界区。其中大部分是由USER32或GDI32之类的系统DLL创建,而这些DLL很少会导致死锁或性能问题。我们希望有一种方法能滤除这些临界区,而只留下代码中所关心的那些临界区。RTL_CRITICAL_SECTION_DEBUG结构中的Spare字段可以很好地完成这一工作。可以使用其中的一个或两个来指示:这些临界区是来自用户编写的代码,而不是来自OS。于是,下一个逻辑问题就变为如何确定哪些临界区是来自您编写的代码。有些读者可能还记得Matt Pietrek 2001年1月的Under The Hood专栏中的LIBCTINY.LIB。LIBCTINY所采用的一个技巧是一个LIB文件,它重写了关键Visual C+运行时例程的标准实现。将LIBCTINY.LIB文件置于链接器行的其他LIB之前,链接器将使用这一实现,而不是使用Microsoft所提供的导入库中的同名后续版本。为对临界区应用类似技巧,我们创建InitializeCriticalSection的一个替代版本及其相关导入库。将此LIB文件置于KERNEL32.LIB之前,链接器将链接我们的版本,而不是KERNEL32中的版本。对InitializeCriticalSection的实现显示在图2中。此代码在概念上非常简单。它首先调用KERNEL32.DLL中的实际InitializeCriticalSection。接下来,它获得调用InitializeCriticalSection的代码地址,并将其贴至RTL_CRITICAL_SECTION_DEBUG结构的备用字段之一。我们的代码如何确定调用代码的地址呢?x86 CALL指令将返回地址置于堆栈中。CriticalSectionHelper代码知道该返回地址位于堆栈帧中一个已知的固定位置。实际结果是:与CriticalSectionHelper.lib正确链接的任何EXE或DLL都将导入我们的DLL(CriticalSectionHelper.DLL),并占用应用了备用字段的临界区。这样就使事情简单了许多。现在我们的实用工具可以简单地遍历进程中的所有临界区,并且只显示具有正确填充的备用字段的临界区信息。那么需要为这一实用工具付出什么代价呢?请稍等,还有更多的内容!因为您的所有临界区现在都包含对其进行初始化时的地址,实用工具可以通过提供其初始化地址来识别各个临界区。原始代码地址本身没有那么有用。幸运的是,DBGHELP.DLL使代码地址向源文件、行号和函数名称的转换变得非常容易。即使一个临界区中没有您在其中的签名,也可以将其地址提交给DBGHELP.DLL。如果将其声明为一个全局变量,并且如果符号可用,则您就可以在原始源代码中确定临界区的名称。顺便说明一下,如果通过设置_NT_SYMBOL_PATH环境变量,并设置DbgHelp以使用其Symbol Server下载功能,从而使DbgHelp发挥其效用,则会得到非常好的结果。MyCriticalSections实用工具我们将所有这些思想结合起来,提出了MyCriticalSections程序。MyCriticalSections是一个命令行程序,在不使用参数运行该程序时可以看到一些选项:Syntax:MyCriticalSections PIDoptionsOptions:/a=all critical sections/e=show only entered critical sections/v=verbose唯一需要的参数是Program ID或PID(十进制形式)。可以用多种方法获得PID,但最简单的方法可能就是通过Task Manager。在没有其他选项时,MyCriticalSections列出了来自代码模块的所有临界区状态,您已经将CriticalSectionHelper.DLL链接至这些代码模块。如果有可用于这一(些)模块的符号,代码将尝试提供该临界区的名称,以及对其进行初始化的位置。要查看MyCriticalSections是如何起作用的,请运行Demo.EXE程序,该程序包含在下载文件中。Demo.EXE只是初始化两个临界区,并由一对线程进入这两个临界区。图3显示运行MyCriticalSections 2040的结果(其中2040为Demo.EXE的PID)。在该图中,列出了两个临界区。在本例中,它们被命名为csMain和yetAnotherCriticalSection。每个Address:行显示了CRITICAL_SECTION的地址及其名称。Initialized in行包含了在其中初始化CRITICAL_SECTION的函数名。代码的Initialized at行显示了源文件和初始化函数中的行号。对于csMain临界区,您将看到锁定数为0、递归数为1,表示一个已经被一线程获得的临界区,并且没有其他线程在等待该临界区。因为从来没有线程被阻止于该临界区,所以Entry Count字段为0。现在来看yetAnotherCriticalSection,会发现其递归数为3。快速浏览Demo代码可以看出:主线程调用EnterCriticalSection三次,所以事情的发生与预期一致。但是,还有一个第二线程试图获得该临界区,并且已经被阻止。同样,LockCount字段也为3。此输出显示有一个等待线程。MyCriticalSections拥有一些选项,使其对于更为勇敢的探索者非常有用。/v开关显示每个临界区的更多信息。旋转数与锁定信号字段尤为重要。您经常会看到NTDLL和其他DLL拥有一些旋转数非零的临界区。如果一个线程在获得临界区的过程中曾被锁定,则锁定信号字段为非零值。/v开关还显示了RTL_CRITICAL_SECTION_DEBUG结构中备用字段的内容。/a开关显示进程中的所有临界区,即使其中没有CriticalSectionHelper.DLL签名也会显示。如果使用/a,则请做好有大量输出的准备。真正的黑客希望同时使用/a和/v,以显示进程中全部内容的最多细节。使用/a的一个小小的好处是会看到NTDLL中的LdrpLoaderLock临界区。此临界区在DllMain调用和其他一些重要时间内被占用。LdrpLoaderLock是许多不太明显、表面上难以解释的死锁的形成原因之一。(为使MyCriticalSection能够正确标记LdrpLoaderLock实例,需要用于NTDLL的PDB文件可供使用。)/e开关使程序仅显示当前被占用的临界区。未使用/a开关时,只显示代码中被占用的临界区(如备用字段中的签名所指示)。采用/a开关时,将显示进程中的全部被占用临界区,而不考虑其来源。那么,希望什么时候运行MyCriticalSections呢?一个很明确的时间是在程序被死锁时。检查被占用的临界区,以查看是否有什么使您惊讶的事情。即使被死锁的程序正运行于调试器的控制之下,也可以使用MyCriticalSections。另一种使用MyCriticalSections的时机是在对有大量多线程的程序进行性能调整时。在阻塞于调试器中的一个使用频繁、非重入函数时,运行MyCriticalSections,查看在该时刻占用了哪些临界区。如果有很多线程都执行相同任务,就非常容易导致一种情形:一个线程的大部分时间被消耗在等待获得一个使用频繁的临界区上。如果有多个使用频繁的临界区,这造成的后果就像花园的浇水软管打了结一样。解决一个争用问题只是将问题转移到下一个容易造成阻塞的临界区。一个查看哪些临界区最容易导致争用的好方法是在接近程序结尾处设置一个断点。在遇到断点时,运行MyCriticalSections并查找具有最大Entry Count值的临界区。正是这些临界区导致了大多数阻塞和线程转换。尽管MyCriticalSections运行于Windows 2000及更新版本,但您仍需要一个比较新的DbgHelp.DLL版本-5.1版或更新版本。Windows XP中提供这一版本。也可以由其他使用DbgHelp的工具中获得该版本。例如,Debugging Tools For Windows下载中通常拥有最新的DbgHelp.DLL。深入研究重要的临界区例程此最后一节是为那些希望理解临界区实现内幕的勇敢读者提供的。对NTDLL进行仔细研究后可以为这些例程及其支持子例程创建伪码(见下载中的NTDLL(CriticalSections).cpp)。以下KERNEL32 API组成临界区的公共接口:InitializeCriticalSection InitializeCriticalSectionAndSpinCount DeleteCriticalSection TryEnterCriticalSection EnterCriticalSection LeaveCriticalSection前两个API只是分别围绕NTDLL API RtlInitializeCriticalSection和RtlInitializeCriticalSectionAndSpinCount的瘦包装。所有剩余例程都被提交给NTDLL中的函数。另外,对RtlInitializeCriticalSection的调用是另一个围绕RtlInitializeCriticalSectionAndSpinCount调用的瘦包装,其旋转数的值为0。使用临界区的时候实际上是在幕后使用以下NTDLL API:RtlInitializeCriticalSectionAndSpinCount RtlEnterCriticalSection RtlTryEnterCriticalSection RtlLeaveCriticalSection RtlDeleteCriticalSection在这一讨论中,我们采用Kernel32名称,因为大多数Win32程序员对它们更为熟悉。InitializeCriticalSectionAndSpinCount对临界区的初始化非常简单。RTL_CRITICAL_SECTION结构中的字段被赋予其起始值。与此类似,分配RTL_CRITICAL_SECTION_DEBUG结构并对其进行初始化,将RtlLogStackBackTraces调用中的返回值赋予CreatorBackTraceIndex,并建立到前面临界区的链接。顺便说一声,CreatorBackTraceIndex一般接收到的值为0。但是,如果有Gflags和Umdh实用工具,可以输入以下命令:Gflags/i MyProgram.exe+ust Gflags/i MyProgram.exe/tracedb 24这些命令使得MyProgram的Image File Execution Options下添加了注册表项。在下一次执行MyProgram时会看到此字段接收到一个非0数值。有关更多信息,参阅知识库文章Q 268343Umdhtools.exe:How to Use Umdh.exe to Find Memory Leaks。临界区初始化中另一个需要注意的问题是:前64个RTL_CRITICAL_SECTION_DEBUG结构不是由进程堆中分配,而是来自位于NTDLL内的.data节的一个数组。在完成临界区的使用之后,对DeleteCriticalSection(其命名不当,因为它只删除RTL_CRITICAL_SECTION_ DEBUG)的调用遍历一个同样可理解的路径。如果由于线程在尝试获得临界区时被阻止而创建了一个事件,将通过调用ZwClose来销毁该事件。接下来,在通过RtlCriticalSectionLock获得保护之后(NTDLL以一个临界区保护它自己的内部临界区列表-您猜对了),将调试信息从链中清除,对该临界区链表进行更新,以反映对该信息的清除操作。该内存由空值填充,并且如果其存储区是由进程堆中获得,则调用RtlFreeHeap将使得其内存被释放。最后,以零填充RTL_CRITICAL_SECTION。有两个API要获得受临界区保护的资源-TryEnterCriticalSection和EnterCriticalSection。如果一个线程需要进入一个临界区,但在等待被阻止资源变为可用的同时,可执行有用的工作,那么TryEnterCriticalSection正是您需要的API。此例程测试此临界区是否可用;如果该临界区被占用,该代码将返回值FALSE,为该线程提供继续执行另一任务的机会。否则,其作用只是相当于EnterCriticalSection。如果该线程在继续进行之前确实需要拥有该资源,则使用EnterCriticalSection。此时,取消用于多处理器计算机的SpinCount测试。这一例程与TryEnterCriticalSection类似,无论该临界区是空闲的或已经被该线程所拥有,都调整对该临界区的簿记。注意,最重要的LockCount递增是由x86lock前缀完成的,这一点非常重要。这确保了在某一时间内只有一个CPU可以修改该LockCount字段。(事实上,Win32 InterlockedIncrement API只是一个具有相同锁定前缀的ADD指令。)如果调用线程无法立即获得该临界区,则调用RtlpWaitForCriticalSection将该线程置于等待状态。在多处理器系统中,EnterCriticalSection旋转SpinCount所指定的次数,并在每次循环访问中测试该临界区的可用性。如果此临界区在循环期间变为空闲,该线程获得该临界区,并继续执行。RtlpWaitForCriticalSection可能是这里所给的所有过程中最为复杂、最为重要的一个。这并不值得大惊小怪,因为如果存在一个死锁并涉及临界区,则利用调试器进入该进程就可能显示出RtlpWaitForCriticalSection内ZwWaitForSingleObject调用中的至少一个线程。如伪码中所显示,在RtlpWaitForCriticalSection中有一点簿记工作,如递增EntryCount和ContentionCount字段。但更重要的是:发出对LockSemaphore的等待,以及对等待结果的处理。默认情况是将一个空指针作为第三个参数传递给ZwWaitForSingleObject调用,请求该等待永远不要超时。如果允许超时,将生成调试消息字符串,并再次开始等待。如果不能从等待中成功返回,就会产生中止该进程的错误。最后,在从ZwWaitForSingleObject调用中成功返回时,则执行从RtlpWaitForCriticalSection返回,该线程现在拥有该临界区。RtlpWaitForCriticalSection必须认识到的一个临界条件是该进程正在被关闭,并且正在等待加载程序锁定(LdrpLoaderLock)临界区。RtlpWaitForCriticalSection一定不能允许该线程被阻止,但是必须跳过该等待,并允许继续进行关闭操作。LeaveCriticalSection不像EnterCriticalSection那样复杂。如果在递减RecursionCount之后,结果不为0(意味着该线程仍然拥有该临界区),则该例程将以ERROR_SUCCESS状态返回。这就是为什么需要用适当数目的Leave调用来平衡Enter调用。如果该计数为0,则OwningThread字段被清零,LockCount被递减。如果还有其他线程在等待,例如LockCount大于或等于0,则调用RtlpUnWaitCriticalSection。此帮助器例程创建LockSemaphore(如果其尚未存在),并发出该信号提醒操作系统:该线程已经释放该临界区。作为通知的一部分,等待线程之一退出等待状态,为运行做好准备。最后要说明的一点是,MyCriticalSections程序如何确定临界区链的起始呢?如果有权访问NTDLL的正确调试符号,则对该列表的查找和遍历非常简单。首先,定位符号RtlCriticalSectionList,清空其内容(它指向第一个RTL_CRITICAL_SECTION_DEBUG结构),并开始遍历。但是,并不是所有的系统都有调试符号,RtlCriticalSectionLi
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- emft考试试题及答案
- 建筑电工试题及答案
- 休克抢救面试题及答案
- 外科上学期考试题及答案
- 廉洁为民面试题及答案
- 产后饮食考试题及答案
- 小猪障碍测试题及答案
- 危运装卸员试题及答案
- 中药敷药试题及答案
- 2025年广西大学文学院招聘考试笔试试题(含答案)
- 《大学》原文(古本)
- 员工综合素质与能力考核表
- 五年制诊断学水肿血尿等
- GB/T 9480-2001农林拖拉机和机械、草坪和园艺动力机械使用说明书编写规则
- GB/T 6569-2006精细陶瓷弯曲强度试验方法
- 2023年员工标准劳动合同范本版7篇
- 中国邮票目录大全(2015版)
- 倪海厦针灸讲义
- 施工放样测量记录表
- 音乐美学课件
- 《基因组学》课件第3章 基因组作图-2015
评论
0/150
提交评论