Debug di un deadlock
Quando un thread richiede l'accesso esclusivo al codice o ad altre risorse, richiede un blocco. Se può, Windows risponde dando questo blocco al thread. A questo punto, niente altro nel sistema può accedere al codice bloccato. Ciò avviene tutto il tempo ed è una normale parte di qualsiasi applicazione multithreaded ben scritta. Anche se un particolare segmento di codice può avere un solo blocco alla volta, più segmenti di codice possono avere un proprio blocco.
Un deadlock si verifica quando due o più thread hanno richiesto blocchi su due o più risorse, in una sequenza incompatibile. Si supponga, ad esempio, che Thread One abbia acquisito un blocco sulla risorsa A e quindi richieda l'accesso alla risorsa B. Nel frattempo, Thread Two ha acquisito un blocco sulla risorsa B e quindi richiede l'accesso alla risorsa A. Nessuno dei due thread può procedere fino a quando il blocco dell'altro thread non viene ricompiuto e, pertanto, nessuno dei due thread può procedere.
I deadlock in modalità utente si verificano quando più thread, in genere di una singola applicazione, hanno bloccato l'accesso alla stessa risorsa. Tuttavia, più thread di più applicazioni possono anche bloccare l'accesso a una risorsa globale/condivisa, ad esempio un evento globale o un semaforo.
I deadlock in modalità kernel si verificano quando più thread (dallo stesso processo o da processi distinti) hanno bloccato l'accesso a ogni altra risorsa del kernel.
La procedura usata per eseguire il debug di un deadlock dipende dal fatto che il deadlock si verifichi in modalità utente o in modalità kernel.
Debug di un deadlock User-Mode
Quando si verifica un deadlock in modalità utente, usare la procedura seguente per eseguirne il debug:
Rilasciare l'estensione !ntsdexts.locks . In modalità utente è sufficiente digitare !lock al prompt del debugger; viene assunto il prefisso ntsdexts .
Questa estensione visualizza tutte le sezioni critiche associate al processo corrente, insieme all'ID per il thread proprietario e al conteggio dei blocchi per ogni sezione critica. Se una sezione critica ha un numero di blocchi pari a zero, non è bloccato. Usare il comando ~ (Stato thread) per visualizzare informazioni sui thread proprietari delle altre sezioni critiche.
Usare il comando kb (Display Stack Backtrace) per ognuno di questi thread per determinare se sono in attesa in altre sezioni critiche.
Usando l'output di questi comandi kb , è possibile trovare il deadlock: due thread che sono in attesa di un blocco mantenuto dall'altro thread. In rari casi, un deadlock potrebbe essere causato da più di due thread che contengono blocchi in un modello circolare, ma la maggior parte dei deadlock implica solo due thread.
Ecco un'illustrazione di questa procedura. Si inizia con l'estensione !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
La prima sezione critica visualizzata non ha blocchi e, pertanto, può essere ignorata.
La seconda sezione critica visualizzata ha un conteggio di blocchi pari a 2 ed è quindi una possibile causa di un deadlock. Il thread proprietario ha un ID thread di 0xA3.
È possibile trovare questo thread elencando tutti i thread con il comando ~ (Stato thread) e cercando il thread con questo 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
In questa visualizzazione il primo elemento è il numero di thread interno del debugger. Il secondo elemento (campo Id
) contiene due numeri esadecimali separati da un punto decimale. Il numero prima del punto decimale è l'ID processo; il numero dopo il punto decimale è l'ID del thread. In questo esempio viene visualizzato che l'ID thread 0xA3 corrisponde al numero di thread 4.
Usare quindi il comando kb (Display Stack Backtrace) per visualizzare lo stack corrispondente al numero di 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
Si noti che questo thread ha una chiamata alla funzione WaitForCriticalSection , che significa che non solo ha un blocco, è in attesa di codice bloccato da un altro elemento. È possibile individuare la sezione critica in attesa esaminando il primo parametro della chiamata a WaitForCriticalSection. Questo è il primo indirizzo in Args to Child: "24e750". Quindi questo thread è in attesa della sezione critica all'indirizzo 0x24E750. Questa è la terza sezione critica elencata dall'estensione !locks usata in precedenza.
In altre parole, thread 4, che possiede la seconda sezione critica, è in attesa della terza sezione critica. Ora si rivolge l'attenzione alla terza sezione critica, che è bloccata. Il thread proprietario dispone di ID thread 0xA9. Tornare all'output del ~ comando visualizzato in precedenza, notare che il thread con questo ID è il numero di thread 6. Visualizzare il backtrace dello stack per questo 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
Anche questo thread è in attesa di liberare una sezione critica. In questo caso, è in attesa della sezione critica in 0x68629100. Questa è la seconda sezione critica nell'elenco generato in precedenza dall'estensione !locks .
Questo è il deadlock. Thread 4, che possiede la seconda sezione critica, è in attesa nella terza sezione critica. Thread 6, che possiede la terza sezione critica, è in attesa nella seconda sezione critica.
Dopo aver confermato la natura di questo deadlock, è possibile usare le normali tecniche di debug per analizzare i thread 4 e 6.
Debug di un deadlock Kernel-Mode
Esistono diverse estensioni del debugger utili per il debug dei deadlock in modalità kernel:
L'estensione !kdexts.locks visualizza informazioni su tutti i blocchi mantenuti sulle risorse del kernel e sui thread che contengono questi blocchi. In modalità kernel è sufficiente digitare !locks al prompt del debugger. Il prefisso kdexts viene assunto.
L'estensione !qlocks visualizza lo stato di tutti i blocchi di spin in coda.
L'estensione !wdfkd.wdfspinlock visualizza informazioni su un oggetto spin-lock di Kernel-Mode Driver Framework (KMDF).
L'estensione !deadlock viene usata insieme a Driver Verifier per rilevare l'uso incoerente dei blocchi nel codice che hanno il potenziale per causare deadlock.
Quando si verifica un deadlock in modalità kernel, usare l'estensione !kdexts.locks per elencare tutti i blocchi attualmente acquisiti dai thread.
In genere è possibile individuare il deadlock individuando un thread non in esecuzione che contiene un blocco esclusivo in una risorsa richiesta da un thread in esecuzione. La maggior parte dei blocchi è condivisa.