68.4 Windows NT: Critical section

临界区在任何操作系统多线程环境中都是非常重要的,它保证一个线程在某一时刻访问一些数据的时候,阻塞其它正要访问这些数据的线程。

下面是Windows NT操作系统的CRITICAL_SECTION声明:

Listing 68.14: (Windows Research Kernel v1.2) public/sdk/inc/nturtl.h

  1. typedef struct _RTL_CRITICAL_SECTION {
  2. PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
  3. //
  4. // The following three fields control entering and exiting the critical
  5. // section for the resource
  6. //
  7. LONG LockCount;
  8. LONG RecursionCount;
  9. HANDLE OwningThread; // from the thread's ClientId->UniqueThread
  10. HANDLE LockSemaphore;
  11. ULONG_PTR SpinCount; // force size on 64-bit systems when packed
  12. } RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

下面展示了EnterCriticalSection()函数的运行过程:

Listing 68.15: Windows 2008/ntdll.dll/x86 (begin)

  1. _RtlEnterCriticalSection@4
  2. var_C = dword ptr -0Ch
  3. var_8 = dword ptr -8
  4. var_4 = dword ptr -4
  5. arg_0 = dword ptr 8
  6. mov edi, edi
  7. push ebp
  8. mov ebp, esp
  9. sub esp, 0Ch
  10. push esi
  11. push edi
  12. mov edi, [ebp+arg_0]
  13. lea esi, [edi+4] ; LockCount
  14. mov eax, esi
  15. lock btr dword ptr [eax], 0
  16. jnb wait ; jump if CF=0
  17. loc_7DE922DD:
  18. mov eax, large fs:18h
  19. mov ecx, [eax+24h]
  20. mov [edi+0Ch], ecx
  21. mov dword ptr [edi+8], 1
  22. pop edi
  23. xor eax, eax
  24. pop esi
  25. mov esp, ebp
  26. pop ebp
  27. retn 4
  28. ... skipped

在这段代码中最重要的指令是BTR(带LOCK前缀):把目的操作数中由源操作数所指定位的值送往标志位CF,并将目的操作数中的该位置0。这是一个原子操作,会阻塞掉其它同时想要访问这段内存的CPU(参看BTR指令的LOCK前缀)。如果LockCount是1,则重置并返回:我们现在正处于临界区。如果不是,则表示其它线程正在占用,将进入等待状态。

使用WaitForSingleObject()进入等待状态。

下面展示了LeaveCriticalSection()函数的运行过程:

Listing 68.16: Windows 2008/ntdll.dll/x86 (begin)

  1. _RtlLeaveCriticalSection@4 proc near
  2. arg_0 = dword ptr 8
  3. mov edi, edi
  4. push ebp
  5. mov ebp, esp
  6. push esi
  7. mov esi, [ebp+arg_0]
  8. add dword ptr [esi+8], 0FFFFFFFFh ;RecursionCount
  9. jnz short loc_7DE922B2
  10. push ebx
  11. push edi
  12. lea edi, [esi+4] ; LockCount
  13. mov dword ptr [esi+0Ch], 0
  14. mov ebx, 1
  15. mov eax, edi
  16. lock xadd [eax], ebx
  17. inc ebx
  18. cmp ebx, 0FFFFFFFFh
  19. jnz loc_7DEA8EB7
  20. loc_7DE922B0:
  21. pop edi
  22. pop ebx
  23. loc_7DE922B2:
  24. xor eax, eax
  25. pop esi
  26. pop ebp
  27. retn 4
  28. ... skipped

XADD指令功能是:交换并相加。这种情况下,LockCount加1并把结果保存到EBX寄存器,同时把1赋值给LockCount。这个操作是原子的,因为它使用了LOCK前缀,这意味着系统会阻塞其它CPU或CPU核心同时访问这块内存。

LOCK前缀是非常重要的:如果两个线程,每个都工作在不同的CPU或CPU核心,它们都能够进入critical section并修改内存数据,这种行为将导致不确定的后果。