當線程需要程式代碼或其他資源的獨佔存取權時,它會要求 鎖定。 如果可以,Windows 會藉由將此鎖定提供給線程來回應。 此時,系統中沒有任何其他專案可以存取鎖定的程序代碼。 這會一直發生,而且是任何撰寫良好多線程應用程式的一般部分。 雖然特定程式代碼區段一次只能有一個鎖,但多個程式代碼區段可以各自有自己的鎖。
死結 發生於兩個或多個線程要求鎖定兩個或多個資源時,順序不相容。 例如,假設 Thread One 已取得資源 A 的鎖定,然後要求存取資源 B。同時,線程二已取得資源 B 的鎖定,然後要求存取資源 A。兩個線程都無法繼續,直到其他線程的鎖定被放棄為止,因此,兩個線程都無法繼續。
當多個線程通常是單一應用程式的線程封鎖彼此對相同資源的存取時,就會發生使用者模式死結。 不過,多個應用程式的多個線程也可以封鎖彼此存取全域/共用資源的存取權,例如全域事件或號誌。
當多個線程(來自相同進程或不同進程)封鎖彼此對相同核心資源的存取時,就會發生內核模式死結。
用來偵錯死結的程式取決於死結發生在使用者模式或內核模式中。
偵錯 User-Mode 死結
當使用者模式發生死結時,請使用下列程式進行偵錯:
執行 !ntsdexts.locks 擴充功能。 在使用者模式中,您可以直接在偵錯工具的提示符號輸入 !locks;系統會假設使用 ntsdexts 作為前置詞。
此延伸模組會顯示與目前進程相關聯的所有重要區段,以及擁有線程的標識碼,以及每個重要區段的鎖定計數。 如果關鍵區段的鎖定計數為零,則不會鎖定。 使用 ~ (線程狀態) 命令來查看擁有其他重要區段之線程的相關信息。
針對每個線程使用 kb (Display Stack Backtrace) 命令來判斷它們是否正在等候其他重要區段。
您可以使用這些 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(顯示堆疊回溯) 命令來顯示對應於線程編號 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 擴充功能來列出所有線程目前取得的鎖定。
您通常可以找出死結的方法是,找到一個持有執行緒所需資源獨佔鎖的非執行緒。 大部分的鎖是共用的。