调试死锁

当线程需要对代码或其他资源进行独占访问时,它会请求 锁定。 如果可以,Windows 会通过将此锁分配给线程来做出响应。 此时,系统中的任何其他内容都无法访问锁定的代码。 这种情况一直发生,并且是任何编写良好的多线程应用程序的正常部分。 尽管特定代码段一次只能有一个锁,但多个代码段可以各自有自己的锁。

当两个或多个线程以不兼容的顺序请求对两个或多个资源的锁定时,会出现 死锁 。 例如,假设线程 1 已获取资源 A 上的锁,然后请求访问资源 B。同时,线程二已获取资源 B 上的锁,然后请求访问资源 A。在放弃其他线程的锁之前,两个线程都无法继续,因此,两个线程都无法继续。

当多个线程(通常是单个应用程序)阻止彼此访问同一资源时,会出现用户模式死锁。 但是,多个应用程序的多个线程也可能阻止彼此访问全局/共享资源,例如全局事件或信号量。

当多个线程从同一进程或不同进程 (,) 阻止彼此访问同一内核资源时,就会发生内核模式死锁。

用于调试死锁的过程取决于死锁是在用户模式还是在内核模式下发生。

调试 User-Mode 死锁

在用户模式下发生死锁时,请使用以下过程对其进行调试:

  1. 发出 !ntsdexts.locks 扩展。 在用户模式下,只需在调试器提示符处键入 !locks ;假定为 ntsdexts 前缀。

  2. 此扩展显示与当前进程关联的所有关键部分,以及拥有线程的 ID 和每个关键节的锁计数。 如果关键节的锁计数为零,则它不会锁定。 使用 ~ (线程状态) 命令查看有关拥有其他关键部分的线程的信息。

  3. 为每个线程使用 kb (显示堆栈回溯) 命令来确定它们是否正在等待其他关键部分。

  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 的第二项) 包含两个用小数点分隔的十六进制数。 小数点前的数字是进程 ID;小数点后面的数字是线程 ID。 在此示例中,可以看到线程 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 正在等待第三个关键节。 现在,请将注意力转向第三个关键部分,该部分也已锁定。 拥有线程具有线程 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 扩展显示有关 Kernel-Mode Driver Framework (KMDF) spin-lock 对象的信息。

  • !死锁扩展与驱动程序验证程序结合使用,以检测代码中可能导致死锁的不一致使用锁。

在内核模式下发生死锁时,请使用 !kdexts.locks 扩展列出线程当前获取的所有锁。

通常,可以通过查找一个未执行的线程来查明死锁,该线程持有执行线程所需的资源上的独占锁。 大多数锁都是共享的。