Partilhar via


Debugando um impasse

Quando um thread precisa de acesso exclusivo ao código ou a algum outro recurso, ele solicita um bloqueio. Se puder, o Windows responde 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 multithreaded bem escrito. Embora um segmento de código específico só possa ter um bloqueio de cada vez, vários segmentos de código podem ter cada um o seu próprio bloqueio.

Um impasse ocorre quando dois ou mais threads solicitam bloqueios em dois ou mais recursos numa sequência que não é compatível. Por exemplo, suponha que o Thread One adquiriu um bloqueio no Recurso A e, em seguida, solicita 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 prosseguir até que o bloqueio do outro thread seja abandonado e, portanto, nenhum thread pode continuar.

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

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

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

Depurando um impasse de User-Mode

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

  1. Emitir a extensão !ntsdexts.locks. No modo de usuário, você pode simplesmente 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 o ID do thread proprietário e a contagem de bloqueio para cada seção crítica. Se uma seção crítica tiver uma contagem de bloqueio de zero, ela não será bloqueada. Use o comando ~ (Thread Status) para ver informações sobre os threads que possuem as outras seções críticas.

  3. Use o comando kb (Display Stack Backtrace) para cada um destes threads para determinar se estão aguardando por outras seções críticas.

  4. Utilizando a saída desses comandos kb, pode encontrar o deadlock: dois encadeamentos, cada um aguardando por um bloqueio mantido pelo outro encadeamento. Em casos raros, um deadlock pode ser causado por mais de dois threads segurando travas em um padrão circular, mas a maioria dos deadlocks envolve apenas dois threads.

Aqui está uma ilustração deste procedimento. Você começa 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 bloqueio de 2 e é, portanto, uma possível causa de um impasse. O thread proprietário tem um ID de thread de 0xA3.

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

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

Em seguida, utilize o comando kb (Display Stack Backtrace) para exibir a pilha correspondente ao número 4 da thread:

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 ele não só tem um bloqueio, mas também está aguardando o código que está bloqueado por outra coisa. Podemos descobrir qual seção crítica estamos esperando olhando para o primeiro parâmetro da chamada para WaitForCriticalSection. Este é o primeiro endereço sob Args to Child: "24e750". Portanto, este tópico está aguardando na 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 segmento 4, que possui a segunda seção crítica, está em espera pela 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 ID de thread 0xA9. Voltando à saída do ~ comando que você viu anteriormente, observe que o thread com essa ID é o thread número 6. Exiba o retrocesso da 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

Este tópico também está esperando que uma seção crítica seja liberada. Neste caso, ele está esperando na seção crítica em 0x68629100. Esta foi a segunda seção crítica na lista gerada anteriormente pela extensão !locks .

Este é o impasse. O segmento 4, que possui a segunda seção crítica, está aguardando a terceira seção crítica. O segmento 6, que possui a terceira seção crítica, está à espera da segunda seção crítica.

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

Depurando um deadlock de Kernel-Mode

Existem 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 nos recursos do kernel e os threads que contêm esses bloqueios. (No modo kernel, você pode simplesmente 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 spin-lock do Kernel-Mode Driver Framework (KMDF).

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

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

Normalmente, você pode identificar o deadlock localizando um thread não executor que contém um bloqueio exclusivo em um recurso que é exigido por um thread em execução. A maioria das fechaduras são compartilhadas.