Поделиться через


Отладка взаимоблокировки

Если потоку требуется монопольный доступ к коду или другому ресурсу, он запрашивает блокировку. Если это возможно, Windows отвечает, давая эту блокировку потоку. На этом этапе ничего другого в системе не может получить доступ к заблокированном коду. Это происходит все время и является нормальной частью любого хорошо написанного многопоточного приложения. Хотя определенный сегмент кода может содержать только одну блокировку за раз, несколько сегментов кода могут иметь собственную блокировку.

Взаимоблокировка возникает, когда два или более потоков запрашивают блокировки на двух или более ресурсах в несовместимой последовательности. Например, предположим, что Поток Один захватил блокировку ресурса 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 отображает информацию об объекте блокировки вращения (spin-lock) структуры драйвера Kernel-Mode Framework (KMDF).

  • Расширение !взаимная блокировка используется вместе со средством проверки драйверов для обнаружения несогласованного использования блокировок в вашем коде, что может привести к взаимным блокировкам.

Если взаимоблокировка возникает в режиме ядра, используйте расширение !kdexts.locks для перечисления всех блокировок, которые в данный момент удерживаются потоками.

Обычно можно определить взаимоблокировку, найдя один неисполняющий поток, удерживающий эксклюзивную блокировку ресурса, необходимого выполняемому потоку. Большинство замков общие.