デッドロックのデバッグ

スレッドがコードまたはその他のリソースへの排他アクセスを必要とする場合は、ロック要求します。 可能であれば、Windows はスレッドにこのロックを与えることによって応答します。 この時点で、システム内の他の何もロックされたコードにアクセスできません。 これは常に発生し、適切に記述されたマルチスレッド アプリケーションの通常の部分です。 特定のコード セグメントは一度に 1 つのロックしか持つことができませんが、複数のコード セグメントがそれぞれ独自のロックを持つことができます。

2 つ以上のスレッドが、互換性のないシーケンスで 2 つ以上のリソースに対してロックを要求すると、デッドロック が発生します。 たとえば、Thread One がリソース A のロックを取得し、リソース B へのアクセスを要求したとします。一方、スレッド 2 はリソース B のロックを取得し、リソース A へのアクセスを要求します。どちらのスレッドも、他のスレッドのロックが放棄されるまで続行できないため、どちらのスレッドも続行できません。

ユーザー モードのデッドロックは、通常は 1 つのアプリケーションの複数のスレッドが、同じリソースへの互いのアクセスをブロックした場合に発生します。 ただし、複数のアプリケーションの複数のスレッドは、グローバル イベントやセマフォなどのグローバル/共有リソースへの相互のアクセスをブロックすることもできます。

カーネル モードのデッドロックは、(同じプロセスまたは異なるプロセスから) 複数のスレッドが互いの同じカーネル リソースへのアクセスをブロックした場合に発生します。

デッドロックのデバッグに使用されるプロシージャは、デッドロックがユーザー モードで発生するかカーネル モードで発生するかによって異なります。

ユーザー モードデッドロックのデバッグ

ユーザー モードでデッドロックが発生した場合は、次の手順を使用してデバッグします。

  1. !ntsdexts.locks 拡張機能を発行します。 ユーザー モードでは、デバッガー プロンプト で 「!locks 」と入力 するだけで済みます。ntsdexts プレフィックスが想定されます。

  2. この拡張機能には、現在のプロセスに関連付けられているすべての重要なセクションと、所有しているスレッドの ID と各クリティカル セクションのロック数が表示されます。 クリティカル セクションのロックカウントが 0 の場合、ロックされません。 ~(スレッドステータス)コマンドを使用して、他の重要なセクションを所有するスレッドに関する情報を表示します。

  3. これらの各スレッドの kb (Display Stack Backtrace) コマンドを使用して、他の重要なセクションで待機しているかどうかを判断します。

  4. これらの kb コマンドの出力を使用すると、デッドロックを見つけることができます。2 つのスレッドはそれぞれ、もう一方のスレッドによって保持されているロックを待機しています。 まれに、循環パターンで 2 つ以上のスレッドがロックを保持することによってデッドロックが発生する可能性がありますが、ほとんどのデッドロックには 2 つのスレッドのみが含まれます。

この手順の図を次に示します。 まず、 !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 番目のクリティカル セクションのロックカウントは 2 であるため、デッドロックの原因として考えられます。 所有スレッドには、0xA3のスレッド ID があります。

このスレッドを見つけるには、~ (スレッドの状態) コマンドを使用してすべてのスレッドを 一覧表示し、この 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

この表示では、最初の項目はデバッガーの内部スレッド番号です。 2 番目の Id 項目 (フィールド) には、小数点で区切られた 2 つの 16 進数が含まれています。 小数点の前の数値はプロセス 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 拡張機能によって一覧表示された 3 番目の重要なセクションでした。

つまり、2 番目のクリティカル セクションを所有するスレッド 4 は、3 番目のクリティカル セクションを待機しています。 次に、3 番目のクリティカル セクションにも注意を向けます。このセクションもロックされています。 所有スレッドには、スレッド 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 拡張機能によって生成された一覧の 2 番目の重要なセクションでした。

これがデッドロックです。 2 番目のクリティカル セクションを所有するスレッド 4 は、3 番目のクリティカル セクションを待機しています。 2 番目のクリティカル セクションを所有するスレッド 4 は、3 番目のクリティカル セクションを待機しています。

このデッドロックの性質を確認したら、通常のデバッグ手法を使用してスレッド 4 と 6 を分析できます。

カーネル モードデッドロックのデバッグ

カーネル モードでのデッドロックのデバッグに役立つデバッガー拡張機能がいくつかあります。

  • !kdexts.locks 拡張機能は、カーネル リソースに保持されているすべてのロックと、これらのロックを保持しているスレッドに関する情報を表示します。 ユーザー モードでは、デバッガー プロンプト で 「!locks 」と入力 するだけで済みます。dexts プレフィックスが想定されます。

  • !qlocks エクステンションは、キューに入っているすべてのスピンロックの状態を表示します。

  • !wdfkd.wdfspinlock 拡張機能は、カーネル モード ドライバー フレームワーク (KMDF) スピン ロック オブジェクトに関する情報を表示します。

  • !deadlock 拡張機能は、ドライバー検証ツールと組み合わせて使用され、デッドロックの原因となる可能性のあるコード内のロックの一貫性のない使用を検出します。

カーネル モードでデッドロックが発生した場合は、!kdexts.locks 拡張機能を使用して、スレッドによって現在取得されているすべてのロックを一覧表示します。

通常は、実行中のスレッドが必要とするリソースの排他ロックを保持している非実行スレッドを見つけることで、Microsoft Windows 2000 のデッドロックを特定できます。 ほとんどのロックは共有されます。