교착 상태 디버깅

스레드가 코드 또는 일부 다른 리소스에 대한 단독 액세스가 필요한 경우 잠금을 요청합니다. 가능하면 Windows는 스레드에 이 잠금을 제공하여 응답합니다. 이 시점에서 시스템의 다른 어떤 것도 잠긴 코드에 액세스할 수 없습니다. 이 작업은 항상 발생하며 잘 작성된 다중 스레드 애플리케이션의 정상적인 부분입니다. 특정 코드 세그먼트는 한 번에 하나의 잠금만 가질 수 있지만 여러 코드 세그먼트는 각각 고유한 잠금을 가질 수 있습니다.

두 개 이상의 스레드가 호환되지 않는 시퀀스에서 둘 이상의 리소스에 대한 잠금을 요청한 경우 교착 상태가 발생합니다. instance 경우 Thread One이 리소스 A에 대한 잠금을 획득한 다음 Resource B에 대한 액세스를 요청한다고 가정합니다. 한편 스레드 2는 리소스 B에 대한 잠금을 획득한 다음 리소스 A에 대한 액세스를 요청합니다. 두 스레드 모두 다른 스레드의 잠금이 포기될 때까지 진행할 수 없으므로 두 스레드 모두 진행할 수 없습니다.

사용자 모드 교착 상태는 일반적으로 단일 애플리케이션의 여러 스레드가 동일한 리소스에 대한 서로의 액세스를 차단한 경우에 발생합니다. 그러나 여러 애플리케이션의 여러 스레드는 전역 이벤트 또는 세마포와 같은 전역/공유 리소스에 대한 서로의 액세스를 차단할 수도 있습니다.

커널 모드 교착 상태는 여러 스레드(동일한 프로세스 또는 고유 프로세스)가 서로 동일한 커널 리소스에 대한 액세스를 차단한 경우에 발생합니다.

교착 상태를 디버그하는 데 사용되는 절차는 교착 상태가 사용자 모드에서 발생하는지 또는 커널 모드에서 발생하는지에 따라 달라집니다.

User-Mode 교착 상태 디버깅

사용자 모드에서 교착 상태가 발생하면 다음 절차를 사용하여 디버그합니다.

  1. !ntsdexts.locks 확장을 실행합니다. 사용자 모드에서는 디버거 프롬프트에서 !locks 만 입력할 수 있습니다. ntsdexts 접두사를 가정합니다.

  2. 이 확장은 현재 프로세스와 연결된 모든 중요한 섹션과 소유 스레드의 ID 및 각 중요 섹션에 대한 잠금 수를 표시합니다. 중요한 섹션의 잠금 수가 0이면 잠기지 않습니다. ~ (스레드 상태) 명령을 사용하여 다른 중요한 섹션을 소유한 스레드에 대한 정보를 확인합니다.

  3. 이러한 각 스레드에 대해 kb(Display Stack Backtrace) 명령을 사용하여 다른 중요한 섹션에서 대기하고 있는지 여부를 확인합니다.

  4. 이러한 kb 명령의 출력을 사용하여 교착 상태를 찾을 수 있습니다. 두 스레드는 각각 다른 스레드가 보유한 잠금에서 대기하고 있습니다. 드문 경우지만 교착 상태는 두 개 이상의 스레드가 순환 패턴으로 잠금을 유지하여 발생할 수 있지만 대부분의 교착 상태에는 두 개의 스레드만 포함됩니다.

이 절차의 예는 다음과 같습니다. !ntdexts.locks 확장으로 시작합니다.

0:006>  !locks 
CritSec ftpsvc2!g_csServiceEntryLock+0 at 6833dd68
LockCount          0
RecursionCount     1
OwningThread       a7
EntryCount         0
ContentionCount    0
*** Locked

CritSec isatq!AtqActiveContextList+a8 at 68629100
LockCount          2
RecursionCount     1
OwningThread       a3
EntryCount         2
ContentionCount    2
*** Locked

CritSec +24e750 at 24e750
LockCount          6
RecursionCount     1
OwningThread       a9
EntryCount         6
ContentionCount    6
*** Locked

표시된 첫 번째 중요 섹션에는 잠금이 없으므로 무시할 수 있습니다.

표시되는 두 번째 중요 섹션의 잠금 수는 2이므로 교착 상태의 원인일 수 있습니다. 소유 스레드의 스레드 ID는 0xA3.

~ (스레드 상태) 명령을 사용하여 모든 스레드를 나열하고 이 ID가 있는 스레드를 찾아 이 스레드를 찾을 수 있습니다.

0:006>  ~
   0  Id: 1364.1330 Suspend: 1 Teb: 7ffdf000 Unfrozen
   1  Id: 1364.17e0 Suspend: 1 Teb: 7ffde000 Unfrozen
   2  Id: 1364.135c Suspend: 1 Teb: 7ffdd000 Unfrozen
   3  Id: 1364.1790 Suspend: 1 Teb: 7ffdc000 Unfrozen
   4  Id: 1364.a3 Suspend: 1 Teb: 7ffdb000 Unfrozen
   5  Id: 1364.1278 Suspend: 1 Teb: 7ffda000 Unfrozen
.  6  Id: 1364.a9 Suspend: 1 Teb: 7ffd9000 Unfrozen
   7  Id: 1364.111c Suspend: 1 Teb: 7ffd8000 Unfrozen
   8  Id: 1364.1588 Suspend: 1 Teb: 7ffd7000 Unfrozen

이 디스플레이에서 첫 번째 항목은 디버거의 내부 스레드 번호입니다. 두 번째 항목( Id 필드)에는 소수점으로 구분된 두 개의 16진수가 포함됩니다. 소수점 앞의 숫자는 프로세스 ID입니다. 소수점 뒤의 숫자는 스레드 ID입니다. 이 예제에서는 스레드 ID 0xA3 스레드 번호 4에 해당합니다.

그런 다음 kb(Stack Backtrace 표시) 명령을 사용하여 스레드 번호 4에 해당하는 스택을 표시합니다.

0:006>  ~4 kb
  4  id: 97.a3   Suspend: 0 Teb 7ffd9000 Unfrozen
ChildEBP RetAddr  Args to Child
014cfe64 77f6cc7b 00000460 00000000 00000000 ntdll!NtWaitForSingleObject+0xb
014cfed8 77f67456 0024e750 6833adb8 0024e750 ntdll!RtlpWaitForCriticalSection+0xaa 
014cfee0 6833adb8 0024e750 80000000 01f21cb8 ntdll!RtlEnterCriticalSection+0x46
014cfef4 6833ad8f 01f21cb8 000a41f0 014cff20 ftpsvc2!DereferenceUserDataAndKill+0x24
014cff04 6833324a 01f21cb8 00000000 00000079 ftpsvc2!ProcessUserAsyncIoCompletion+0x2a
014cff20 68627260 01f21e0c 00000000 00000079 ftpsvc2!ProcessAtqCompletion+0x32
014cff40 686249a5 000a41f0 00000001 686290e8 isatq!I_TimeOutContext+0x87
014cff5c 68621ea7 00000000 00000001 0000001e isatq!AtqProcessTimeoutOfRequests_33+0x4f
014cff70 68621e66 68629148 000ad1b8 686230c0 isatq!I_AtqTimeOutWorker+0x30
014cff7c 686230c0 00000000 00000001 000c000a isatq!I_AtqTimeoutCompletion+0x38
014cffb8 77f04f2c 00000000 00000001 000c000a isatq!SchedulerThread_297+0x2f
00000001 000003e6 00000000 00000001 000c000a kernel32!BaseThreadStart+0x51

이 스레드는 WaitForCriticalSection 함수를 호출합니다. 즉, 잠금이 있을 뿐만 아니라 다른 항목에 의해 잠긴 코드를 기다리고 있습니다. WaitForCriticalSection 호출의 첫 번째 매개 변수를 확인하여 대기 중인 중요한 섹션을 확인할 수 있습니다. Args to Child: "24e750"의 첫 번째 주소입니다. 따라서 이 스레드는 주소 0x24E750 중요한 섹션에서 대기하고 있습니다. 이 섹션은 이전에 사용한 !locks 확장에 나열된 세 번째 중요 섹션입니다.

즉, 두 번째 중요 섹션을 소유하는 스레드 4는 세 번째 중요 섹션을 기다리고 있습니다. 이제 잠긴 세 번째 중요 섹션으로 주의를 돌립니다. 소유 스레드에는 스레드 ID 0xA9 있습니다. 이전에 본 명령의 ~ 출력으로 돌아가면 이 ID가 있는 스레드는 스레드 번호 6입니다. 이 스레드에 대한 스택 백트레이스 표시:

0:006>  ~6 kb 
ChildEBP RetAddr  Args to Child
0155fe38 77f6cc7b 00000414 00000000 00000000 ntdll!NtWaitForSingleObject+0xb
0155feac 77f67456 68629100 6862142e 68629100 ntdll!RtlpWaitForCriticalSection+0xaa 
0155feb4 6862142e 68629100 0009f238 686222e1 ntdll!RtlEnterCriticalSection+0x46
0155fec0 686222e1 0009f25c 00000001 0009f238 isatq!ATQ_CONTEXT_LISTHEAD__RemoveFromList
0155fed0 68621412 0009f238 686213d1 0009f238 isatq!ATQ_CONTEXT__CleanupAndRelease+0x30
0155fed8 686213d1 0009f238 00000001 01f26bcc isatq!AtqpReuseOrFreeContext+0x3f
0155fee8 683331f7 0009f238 00000001 01f26bf0 isatq!AtqFreeContext+0x36
0155fefc 6833984b ffffffff 00000000 00000000 ftpsvc2!ASYNC_IO_CONNECTION__SetNewSocket
0155ff18 6833adcd 77f05154 01f26a58 00000000 ftpsvc2!USER_DATA__Cleanup+0x47
0155ff28 6833ad8f 01f26a58 000a3410 0155ff54 ftpsvc2!DereferenceUserDataAndKill+0x39
0155ff38 6833324a 01f26a58 00000000 00000040 ftpsvc2!ProcessUserAsyncIoCompletion+0x2a
0155ff54 686211eb 01f26bac 00000000 00000040 ftpsvc2!ProcessAtqCompletion+0x32
0155ff88 68622676 000a3464 00000000 000a3414 isatq!AtqpProcessContext+0xa7
0155ffb8 77f04f2c abcdef01 ffffffff 000ad1b0 isatq!AtqPoolThread+0x32
0155ffec 00000000 68622644 abcdef01 00000000 kernel32!BaseThreadStart+0x51

이 스레드도 중요한 섹션이 해제되기를 기다리고 있습니다. 이 경우 0x68629100 중요한 섹션을 기다리고 있습니다. 이 섹션은 이전에 !locks 확장에 의해 생성된 목록의 두 번째 중요 섹션입니다.

교착 상태입니다. 두 번째 중요 섹션을 소유하는 스레드 4는 세 번째 중요 섹션을 기다리고 있습니다. 세 번째 중요 섹션을 소유하는 스레드 6은 두 번째 중요 섹션을 기다리고 있습니다.

이 교착 상태의 특성을 확인한 후에는 일반적인 디버깅 기술을 사용하여 스레드 4 및 6을 분석할 수 있습니다.

Kernel-Mode 교착 상태 디버깅

커널 모드에서 교착 상태를 디버깅하는 데 유용한 몇 가지 디버거 확장이 있습니다.

  • !kdexts.locks 확장은 커널 리소스에 보관된 모든 잠금 및 이러한 잠금을 보유하는 스레드에 대한 정보를 표시합니다. 커널 모드에서는 디버거 프롬프트에서 !locks 만 입력할 수 있습니다. kdexts 접두사는 가정합니다.

  • !qlocks 확장은 대기된 모든 스핀 잠금의 상태를 표시합니다.

  • !wdfkd.wdfspinlock 확장은 KMDF(Kernel-Mode Driver Framework) 스핀 잠금 개체에 대한 정보를 표시합니다.

  • !deadlock 확장은 드라이버 검증 도구와 함께 사용하여 교착 상태를 일으킬 가능성이 있는 코드에서 잠금의 일관되지 않은 사용을 검색합니다.

커널 모드에서 교착 상태가 발생하면 !kdexts.locks 확장을 사용하여 스레드에서 현재 획득한 모든 잠금을 나열합니다.

일반적으로 실행 중인 스레드에 필요한 리소스에 대한 배타적 잠금을 보유하는 실행되지 않는 스레드 하나를 찾아 교착 상태를 정확히 파악할 수 있습니다. 대부분의 잠금은 공유됩니다.