Partilhar via


Depurando um deadlock

Quando um thread precisa de acesso exclusivo ao código ou a algum outro recurso, ele solicita um bloqueio. Se puder, o Windows responderá dando esse bloqueio ao thread. Neste ponto, nada mais no sistema pode acessar o código bloqueado. Isso acontece o tempo todo e é uma parte normal de qualquer aplicativo multithread bem escrito. Embora um segmento de código específico possa ter apenas um bloqueio por vez, vários segmentos de código podem ter seu próprio bloqueio.

Um deadlock surge quando dois ou mais threads solicitaram bloqueios em dois ou mais recursos, em uma sequência incompatível. Por exemplo, suponha que o Thread One tenha adquirido um bloqueio no Recurso A e, em seguida, solicite acesso ao Recurso B. Enquanto isso, o Thread Two adquiriu um bloqueio no Recurso B e, em seguida, solicita acesso ao Recurso A. Nenhum thread pode continuar até que o bloqueio do outro thread seja renunciado e, portanto, nenhum thread pode continuar.

Os deadlocks no modo de usuário surgem quando vários threads, geralmente de um único aplicativo, bloquearam o acesso um do outro ao mesmo recurso. No entanto, vários threads de vários aplicativos também podem bloquear o acesso um do outro a um recurso global/compartilhado, como um evento global ou semáforo.

Os deadlocks no modo kernel surgem quando vários threads (do mesmo processo ou de processos distintos) bloquearam o acesso uns dos outros ao mesmo recurso de kernel.

O procedimento usado para depurar um deadlock depende se o deadlock ocorre no modo de usuário ou no modo kernel.

Depurando um deadlock de User-Mode

Quando ocorrer um deadlock no modo de usuário, use o seguinte procedimento para depurá-lo:

  1. Emita a extensão !ntsdexts.locks . No modo de usuário, basta digitar !locks no prompt do depurador; o prefixo ntsdexts é assumido.

  2. Essa extensão exibe todas as seções críticas associadas ao processo atual, juntamente com a ID do thread proprietário e a contagem de bloqueios para cada seção crítica. Se uma seção crítica tiver uma contagem de bloqueios igual a zero, ela não será bloqueada. Use o comando ~ (Status do Thread) para ver informações sobre os threads que possuem as outras seções críticas.

  3. Use o comando kb (Exibir Backtrace de Pilha) para cada um desses threads para determinar se eles estão aguardando outras seções críticas.

  4. Usando a saída desses comandos kb , você pode encontrar o deadlock: dois threads que estão cada um aguardando um bloqueio mantido pelo outro thread. Em casos raros, um deadlock pode ser causado por mais de dois threads que mantêm bloqueios em um padrão circular, mas a maioria dos deadlocks envolve apenas dois threads.

Aqui está uma ilustração deste procedimento. Comece com a extensão !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

A primeira seção crítica exibida não tem bloqueios e, portanto, pode ser ignorada.

A segunda seção crítica exibida tem uma contagem de bloqueios de 2 e, portanto, é uma possível causa de um deadlock. O thread proprietário tem uma ID de thread de 0xA3.

Você pode encontrar esse thread listando todos os threads com o comando ~ (Status do Thread) e procurando o thread com esta 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

Nessa exibição, o primeiro item é o número de thread interno do depurador. O segundo item (o Id campo) contém dois números hexadecimais separados por um ponto decimal. O número antes do ponto decimal é a ID do processo; o número após o ponto decimal é a ID do thread. Neste exemplo, você verá que a ID do thread 0xA3 corresponde ao thread número 4.

Em seguida, use o comando kb (Exibir Backtrace de Pilha) para exibir a pilha que corresponde ao número de thread 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

Observe que esse thread tem uma chamada para a função WaitForCriticalSection , o que significa que, além de ter um bloqueio, ele está aguardando o código que está bloqueado por outra coisa. Podemos descobrir em qual seção crítica estamos esperando examinando o primeiro parâmetro da chamada para WaitForCriticalSection. Este é o primeiro endereço em Args para Filho: "24e750". Portanto, esse thread está aguardando a seção crítica no endereço 0x24E750. Esta foi a terceira seção crítica listada pela extensão !locks que você usou anteriormente.

Em outras palavras, o thread 4, que possui a segunda seção crítica, está aguardando a terceira seção crítica. Agora, volte sua atenção para a terceira seção crítica, que também está bloqueada. O thread proprietário tem 0xA9 de ID de thread. Retornando à saída do ~ comando que você viu anteriormente, observe que o thread com essa ID é o thread número 6. Exiba o backtrace de pilha para este 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

Esse thread também está aguardando a liberação de uma seção crítica. Nesse caso, ele está aguardando a seção crítica em 0x68629100. Esta foi a segunda seção crítica na lista gerada anteriormente pela extensão !locks .

Este é o deadlock. O Thread 4, que possui a segunda seção crítica, está aguardando a terceira seção crítica. O Thread 6, que possui a terceira seção crítica, está aguardando a segunda seção crítica.

Depois de confirmar a natureza desse deadlock, você pode usar as técnicas de depuração usuais para analisar os threads 4 e 6.

Depurando um deadlock de Kernel-Mode

Há várias extensões de depurador que são úteis para depurar deadlocks no modo kernel:

  • A extensão !kdexts.locks exibe informações sobre todos os bloqueios mantidos em recursos de kernel e os threads que contêm esses bloqueios. (No modo kernel, você pode apenas digitar !locks no prompt do depurador; o prefixo kdexts é assumido.)

  • A extensão !qlocks exibe o estado de todos os bloqueios de rotação enfileirados.

  • A extensão !wdfkd.wdfspinlock exibe informações sobre um objeto de bloqueio de rotação KMDF (Kernel-Mode Driver Framework).

  • A extensão !deadlock é usada em conjunto com o Verificador de Driver para detectar o uso inconsistente de bloqueios em seu código que têm o potencial de causar deadlocks.

Quando ocorrer um deadlock no modo kernel, use a extensão !kdexts.locks para listar todos os bloqueios adquiridos atualmente por threads.

Normalmente, você pode identificar o deadlock encontrando um thread não em execução que contém um bloqueio exclusivo em um recurso exigido por um thread em execução. A maioria dos bloqueios são compartilhados.