共用方式為


偵錯死結

當執行緒需要獨佔存取程式碼或其他資源時,它會要求 鎖定。 如果可以,Windows 會藉由將此鎖定提供給執行緒來回應。 此時,系統中的任何其他專案都無法存取鎖定的程式碼。 這一切都會發生,而且是任何寫入良好多執行緒應用程式的一般部分。 雖然特定程式碼片段一次只能有一個鎖定,但多個程式碼區段可以各自有自己的鎖定。

當兩個或多個執行緒在兩個或多個資源上要求鎖定不相容時,就會發生 死結 。 例如,假設 Thread One 已取得資源 A 的鎖定,然後要求存取資源 B。同時,執行緒二已取得資源 B 的鎖定,然後要求存取資源 A。兩個執行緒都無法繼續,直到另一個執行緒的鎖定被放棄為止,因此,兩個執行緒都無法繼續。

使用者模式死結會在多個執行緒通常是單一應用程式的執行緒封鎖彼此對相同資源的存取時發生。 不過,多個應用程式的多個執行緒也可以封鎖彼此對全域/共用資源的存取,例如全域事件或旗號。

當多個執行緒 (來自相同進程或不同進程的多個執行緒,) 封鎖彼此對相同核心資源的存取時,就會發生核心模式死結。

用來偵錯死結的程式取決於死結髮生在使用者模式或核心模式中。

偵錯 User-Mode 死結

在使用者模式中發生死結時,請使用下列程式進行偵錯:

  1. 發出 !ntsdexts.locks 延伸模組。 在使用者模式中,您只要在偵錯工具提示字元中輸入 !locks ;假設 ntsdexts 前置詞

  2. 此延伸模組會顯示與目前進程相關聯的所有重要區段,以及擁有線程的識別碼,以及每個重要區段的鎖定計數。 如果關鍵區段的鎖定計數為零,則不會鎖定。 使用 ~ (執行緒狀態) 命令,查看擁有其他重要區段之執行緒的相關資訊。

  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,因此是死結的可能原因。 擁有線程的執行緒識別碼為 0xA3。

您可以使用 ~ (執行緒狀態) 命令列出所有線程,並尋找具有此識別碼的執行緒,以尋找此執行緒:

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 欄位) 包含兩個十六進位數位,並以小數點分隔。 小數點之前的數位是進程識別碼;小數點之後的數位是執行緒識別碼。 在此範例中,您會看到執行緒識別碼0xA3對應至執行緒數位 4。

然後使用 kb (Display 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 正在等候第三個重要區段。 現在請留意第三個重要區段,這也會鎖定。 擁有線程具有線程識別碼0xA9。 返回您先前看到的命令輸出 ~ ,請注意,具有此識別碼的執行緒為執行緒號碼 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延伸模組會顯示 Kernel-Mode Driver Framework (KMDF) 微調鎖定物件的相關資訊。

  • !deadlock擴充功能會與驅動程式驗證器搭配使用,以偵測程式碼中可能會造成死結的鎖定不一致使用。

在核心模式中發生死結時,請使用 !kdexts.locks 擴充功能來列出執行緒目前取得的所有鎖定。

您通常可以找出死結,方法是尋找一個執行中線程,該執行緒在執行執行緒所需的資源上保留獨佔鎖定。 大部分的鎖定都是共用的。