Debuggen eines Deadlocks

Wenn ein Thread exklusiven Zugriff auf Code oder eine andere Ressource benötigt, fordert er eine Sperre an. Wenn dies möglich ist, antwortet Windows, indem es dem Thread diese Sperre gibt. An diesem Punkt kann nichts anderes im System auf den gesperrten Code zugreifen. Dies geschieht ständig und ist ein normaler Bestandteil jeder gut geschriebenen Multithreadanwendung. Obwohl ein bestimmtes Codesegment jeweils nur eine Sperre aufweisen kann, können mehrere Codesegmente jeweils über eine eigene Sperre verfügen.

Ein Deadlock tritt auf, wenn mindestens zwei Threads Sperren für zwei oder mehr Ressourcen in einer inkompatiblen Sequenz angefordert haben. Angenommen, Thread 1 hat für instance eine Sperre für Ressource A erworben und fordert dann Zugriff auf Ressource B an. In der Zwischenzeit hat Thread 2 eine Sperre für Ressource B erhalten und fordert dann Zugriff auf Ressource A an. Keiner der Threads kann fortfahren, bis die Sperre des anderen Threads aufgehoben ist, und daher kann keiner der Threads fortfahren.

Deadlocks im Benutzermodus treten auf, wenn mehrere Threads, in der Regel einer einzelnen Anwendung, den Zugriff des anderen auf dieselbe Ressource blockiert haben. Mehrere Threads mehrerer Anwendungen können jedoch auch den Zugriff auf eine globale/freigegebene Ressource blockieren, z. B. ein globales Ereignis oder Semaphor.

Kernelmodus-Deadlocks entstehen, wenn mehrere Threads (aus demselben Prozess oder aus unterschiedlichen Prozessen) den Zugriff auf dieselbe Kernelressource blockiert haben.

Die zum Debuggen eines Deadlocks verwendete Prozedur hängt davon ab, ob der Deadlock im Benutzermodus oder im Kernelmodus auftritt.

Debuggen eines User-Mode Deadlocks

Wenn ein Deadlock im Benutzermodus auftritt, führen Sie das folgende Verfahren aus, um ihn zu debuggen:

  1. Stellen Sie die Erweiterung !ntsdexts.locks aus. Im Benutzermodus können Sie einfach !locks an der Debuggeraufforderung eingeben. das Präfix ntsdexts wird angenommen.

  2. Diese Erweiterung zeigt alle kritischen Abschnitte an, die dem aktuellen Prozess zugeordnet sind, zusammen mit der ID für den besitzenden Thread und der Sperranzahl für jeden kritischen Abschnitt. Wenn ein kritischer Abschnitt die Sperranzahl von 0 aufweist, ist er nicht gesperrt. Verwenden Sie den Befehl ~ (Threadstatus), um Informationen zu den Threads anzuzeigen, die die anderen wichtigen Abschnitte besitzen.

  3. Verwenden Sie den Befehl kb (Stack Backtrace anzeigen) für jeden dieser Threads, um zu bestimmen, ob sie auf andere wichtige Abschnitte warten.

  4. Mithilfe der Ausgabe dieser kb-Befehle können Sie den Deadlock finden: zwei Threads, die jeweils auf eine Sperre warten, die vom anderen Thread gehalten wird. In seltenen Fällen kann ein Deadlock durch mehr als zwei Threads verursacht werden, die Sperren in einem kreisförmigen Muster halten, aber die meisten Deadlocks umfassen nur zwei Threads.

Hier sehen Sie eine Abbildung dieses Verfahrens. Sie beginnen mit der Erweiterung !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

Der erste kritische Abschnitt, der angezeigt wird, weist keine Sperren auf und kann daher ignoriert werden.

Der zweite angezeigte kritische Abschnitt weist eine Sperranzahl von 2 auf und ist daher eine mögliche Ursache für einen Deadlock. Der besitzende Thread verfügt über eine Thread-ID von 0xA3.

Sie können diesen Thread finden, indem Sie alle Threads mit dem Befehl ~ (Threadstatus) auflisten und nach dem Thread mit der folgenden ID suchen:

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

In dieser Anzeige ist das erste Element die interne Threadnummer des Debuggers. Das zweite Element (das Id Feld) enthält zwei hexadezimale Zahlen, die durch einen Dezimalpunkt getrennt sind. Die Zahl vor dem Dezimalpunkt ist die Prozess-ID. die Zahl nach dem Dezimalpunkt ist die Thread-ID. In diesem Beispiel sehen Sie, dass die Thread-ID 0xA3 der Threadnummer 4 entspricht.

Anschließend verwenden Sie den Befehl kb (Display Stack Backtrace), um den Stapel anzuzeigen, der der Threadnummer 4 entspricht:

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

Beachten Sie, dass dieser Thread über einen Aufruf der WaitForCriticalSection-Funktion verfügt, was bedeutet, dass er nicht nur über eine Sperre verfügt, er wartet auch auf Code, der durch etwas anderes gesperrt ist. Wir können herausfinden, auf welchen kritischen Abschnitt wir warten, indem wir den ersten Parameter des Aufrufs von WaitForCriticalSection betrachten. Dies ist die erste Adresse unter Args to Child: "24e750". Dieser Thread wartet also auf den kritischen Abschnitt an adresse 0x24E750. Dies war der dritte kritische Abschnitt, der von der !locks-Erweiterung aufgeführt wurde, die Sie zuvor verwendet haben.

Mit anderen Worten, Thread 4, der den zweiten kritischen Abschnitt besitzt, wartet auf den dritten kritischen Abschnitt. Wenden Sie sich nun dem dritten kritischen Abschnitt zu, der ebenfalls gesperrt ist. Der besitzende Thread verfügt über 0xA9 thread-ID. Wenn Sie zur Ausgabe des Befehls zurückkehren, den ~ Sie zuvor gesehen haben, beachten Sie, dass der Thread mit dieser ID Threadnummer 6 ist. Anzeigen des Stapelrückverfolgungs für diesen Thread:

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

Auch dieser Thread wartet auf die Freigabe eines kritischen Abschnitts. In diesem Fall wartet sie auf den kritischen Abschnitt bei 0x68629100. Dies war der zweite kritische Abschnitt in der Liste, die zuvor von der Erweiterung !locks generiert wurde.

Dies ist der Deadlock. Thread 4, der den zweiten kritischen Abschnitt besitzt, wartet auf den dritten kritischen Abschnitt. Thread 6, der den dritten kritischen Abschnitt besitzt, wartet auf den zweiten kritischen Abschnitt.

Nachdem Sie die Art dieses Deadlocks bestätigt haben, können Sie die üblichen Debugtechniken verwenden, um die Threads 4 und 6 zu analysieren.

Debuggen eines Kernel-Mode Deadlocks

Es gibt mehrere Debuggererweiterungen, die zum Debuggen von Deadlocks im Kernelmodus nützlich sind:

  • Die Erweiterung !kdexts.locks zeigt Informationen zu allen Sperren für Kernelressourcen und zu den Threads an, die diese Sperren enthalten. (Im Kernelmodus können Sie einfach !locks an der Debuggeraufforderung eingeben. Das Präfix kdexts wird angenommen.)

  • Die Erweiterung !qlocks zeigt den Zustand aller Spinsperren in der Warteschlange an.

  • Die Erweiterung !wdfkd.wdfspinlock zeigt Informationen zu einem Kernel-Mode Driver Framework (KMDF) Spin-Lock-Objekt an.

  • Die Erweiterung !deadlock wird in Verbindung mit Driver Verifier verwendet, um inkonsistente Verwendung von Sperren im Code zu erkennen, die das Potenzial haben, Deadlocks zu verursachen.

Wenn im Kernelmodus ein Deadlock auftritt, verwenden Sie die Erweiterung !kdexts.locks , um alle derzeit von Threads erworbenen Sperren aufzulisten.

Sie können den Deadlock normalerweise lokalisieren, indem Sie einen nicht ausgeführten Thread finden, der eine exklusive Sperre für eine Ressource enthält, die von einem ausführenden Thread benötigt wird. Die meisten Sperren werden freigegeben.