Les nouvelles API de Windows Vista et Windows Serveur 2008 : Wait Chain Traversal

Auteur : Eric Vernié, responsable technique en charge des relations développeurs


Document de référence

Révision 1.0


Table des matières

IntroductionIntroduction

Wait Chain TraversalWait Chain Traversal

  • Petit rappel sur une étreinte fatale
  • WCT Comment ça marche ?
  • WCT en action

ConclusionConclusion

Introduction

Depuis maintenant 4 articles nous avons exploré ce que Windows Vista et Windows Serveur 2008, proposent en termes d'API, autour de la robustesse et de la fiabilité

Dans ce 5ième article, nous allons aborder un jeu d'API, un peu particulier, "Wait Chain Traversal" qui permet la détection d'étreinte fatale.

Technologies utilisées:

C/C++

MFC

Visual Studio 2008 Beta 2

(disponible en téléchargement ici : https://msdn2.microsoft.com/en-us/vstudio/aa700831.aspx)

Software development Kit Vista  : https://www.microsoft.com/downloads/details.aspx?familyid=FF6467E6-5BBA-4BF5-B562-9199BE864D29&mg_id=10114

Wait Chain Traversal

Wait Chain Traversal, a été conçu pour diagnostiquer des problèmes d'étreintes fatales (le bisou de la mort, plus connu sous son nom Anglais le Deadlock)

Le but visé avec WCT, étant de fournir assez d'informations pour pouvoir régler rapidement et à moindre coûts les bogues liées au processus et aux threads

C'est un jeu d'API qui je le précise fonctionne en mode user, et est essentiellement utilisé par des composants tels que, des extensions à des débogueurs, des composants de rapport d'erreur tel que "Windows Error Reporting", et des services WDI (Windows Diagnostic Infrastructure). Vous ne l'utiliserez sans doute pas tous les jours dans vos développements, mais si vous commencez à développer des applications Multi-threads, voir fortement parallélisées (ce qui ne tardera sans doute pas avec l'avènement des multi-cœurs), il m'a semblé intéressant de vous en parler, car utilisé à bonne escient, ce jeu d'API pourra vous faire gagner du temps dans le débogage.

Petit rappel sur une étreinte fatale

Une étreinte fatale à lieu, lorsque qu'un Thread T1 essai d'acquérir un verrou V2 sur un objet de synchronisation qui est tenu par un Thread T2. Dans le même temps le Thread T2 essai d'acquérir un Verrou V1 sur un objet de synchronisation qui est tenu par le Thread T1.

En d'autres termes, le Thread T1 avant de libérer son verrou V1 veut acquérir le verrou V2. Mais dans le même temps, le Thread T2 avant de libérer son verrou V2 veut acquérir le verrou V1. Les deux threads s'attendent mutuellement. Alors quel thread thread va lâcher prise en premier ? Malheureusement, aucun dans une application qui n'aurait pas prévue le cas. Dans le cas d'une base de données comme SQL Serveur, si une étreinte fatale est détecté, SQL Serveur choisi lui-même une victime, ce qui est pratique. WCT pourrait vous aider à ajouter ce type service dans votre application.

WCT Comment ça marche ?

WCT est basé sur le concept comme son nom l'indique de chainage d'attente (Wait Chain).

Un chainage d'attente est une alternance de threads et d'objets de synchronisations. Plus précisément dans ce chainage, un thread attend (la signalisation) d'un objet de synchronisation qui la suit et dont le propriétaire est le thread suivant dans la chaine.

C'est un peut fumeux comme explication, alors vite un petit schéma.

C'est un peut fumeux comme explication, alors vite un petit schéma.

WCT retrouve le chainage pour des processus externes, en d'autres termes, WCT peut indiquer dans son chainage que par exemple le thread T1 crée dans un processus PA peut être bloqué à cause du Thread T3 crée dans un processus PB.

Dans notre schéma, le thread T1 essai d'acquérir un verrou sur le mutex m1, mais qui est tenu par le Thread T2, WCT en conclu donc que le thread T1 attend le thread T2. En analysant un thread donné, WCT construira alors une liste de nœuds avec les différents acteurs du chainage. Dans ce nœud, nous retrouverons donc des informations sur les threads qui attendent (Wait-for) un objet de synchronisation tenu par (Owned-By) un autre thread.

WCT n'intervenant pas dans le déroulement du thread qu'il analyse; en d'autres termes comme il ne suspend pas le thread, WCT doit traverser à nouveau le chainage, pour vérifier qu'aucuns liens n'auraient été modifiés. Pour ce faire, il compare le nombre de basculement de contextes, et vérifie que le temps d'attente n'a pas changé. Il conclu à une étreinte fatale, si jamais dans le chainage rien n'a changé lors de son second passage que WCT nomme un cycle.

Actuellement (en attendant que le système d'exploitation lui-même puisse fournir des informations sur l'état de tous les objets de synchronisation), la version 1.0 de WCT ne supporte que les Mutex, les Sections critiques, l'API SendMessage, COM et ALPC. Il n'est donc pas en mesure de détecter des problèmes liés à des objets "events" ou sémaphores.

WCT en action

WCT est constitué enfin de compte de très peu de fonctions. 3 en l'occurrence :

OpenThreadWaitChainSession() : qui permet d'ouvrir en mode synchrone ou asynchrone une session WCT.

GetThreadWaitChain(), qui retrouve des informations sur le chainage d'un thread spécifié. C'est la fonction centrale de WCT.

CloseThreadWaitChainSession(), ferme la session WCT.

 

Voici comment nous procédons dans notre démonstration.

  1. Nous créons un processus à l'aide de la fonction CreateProcess, afin de récupérer son numéro d'identification qui nous servira par la suite.
  2. Nous ouvrons une session WCT (OpenThreadWaitChainSession)
  3. WCT analysant les threads, nous avons besoin d'une liste de threads. La fonction CreateToolhelp32Snapshot, avec le paramètre TH32CS_SNAPTHREAD retrouve l'intégralité des threads du système.
  4. Comme nous ne souhaitons n'analyser que les threads du processus crée à l'étape 1, nous parcourons cette liste à l'aide des fonctions (Thread32First, Thread32Next) en vérifiant à l'aide du numéro de processus, que le thread appartient bien au processus à analyser.
  5. Nous vérifions que le thread est toujours encours d'exécution à l'aide de la fonction GetExitCodeThread
  6. Ensuite, pour un thread donné, nous utilisons la fonction GetThreadWaitChain qui permet de retrouver le chainage. Cette fonction est capable alors d'indiquer à l'appelant à l'aide de son paramètre IsCycle si une étreinte fatale est détectée.
  7. Enfin nous affichons sous forme de texte dans une zone de liste le chainage d'appel du thread.

Voici le listing :

-     Création du processus :

STARTUPINFO si;            ZeroMemory( &si, sizeof(si) );
            si.cb = sizeof(si);
            //m_pi est une structure PROCESS_INFORMATION déclarée en globale;
           //qui nous servira plustard dans l'analyse dans la session WCT
            ZeroMemory( &m_pi, sizeof(m_pi) );
            //Démarrage du le processus enfantCreateProcess(cheminProcessus  ,                                L "",
                                    NULL,                                       NULL,                                         FALSE,
                                       0,
                                         NULL,                                       NULL,
                                        &si,                                          &m_pi )     

-     Ouverture d'une session WCT.

HWCT  WctHandle;WctHandle=OpenThreadWaitChainSession(0,NULL);

Ici nous ouvrons session en mode synchrone. Mais il est possible d'ouvrir une session en mode asynchrone en précisant comme premier paramètre WCT_ASYNC_OPEN_FLAG, dans ce cas là, le second paramètre devra être un pointeur de fonction qui aurait la signature suivante :

VOID CALLBACK WaitChainCallback(  HWCT  WctHandle,  DWORD_PTR  Context,  DWORD  CallbackStatus,  LPDWORD  NodeCount,
  PWAITCHAIN_NODE_INFO NodeInfoArray,  LPBOOL IsCycle);

-     Création d'une liste de threads

HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD,0);

-     Parcourir la liste des threads et vérifier que le thread appartient au processus à analyser

WAITCHAIN_NODE_INFO NodeInfoArray[WCT_MAX_NODE_COUNT];DWORD               Count, i;BOOL                IsCycle;Count =
WCT_MAX_NODE_COUNT;THREADENTRY32 thread;thread.dwSize = sizeof(thread);if (Thread32First (snapshot, &thread))  {  do  {
     //Est-ce que le thread appartient au processus à analyser ?     if (thread.th32OwnerProcessID == m_pi.dwProcessId)
       {        wchar_t buffer[1024];      ZeroMemory(buffer,1024);      HANDLE threadHandle =
OpenThread(THREAD_ALL_ACCESS,                                      FALSE,                                      thread.th32ThreadID);
      if (threadHandle)         {         // vérifie que le thread est tjs actif            DWORD exitCode;
            GetExitCodeThread(threadHandle, &exitCode)          if (exitCode == STILL_ACTIVE)             {
                //Analyse du thread               BOOL b=GetThreadWaitChain(WctHandle,
                                          NULL,                                         WCTP_GETINFO_ALL_FLAGS,                                          thread.th32ThreadID,
                                          &Count,
                                          NodeInfoArray,                                          &IsCycle);
              //WCT a detecté un deadlock                  if (IsCycle){                   wcscat_s(buffer,L"Deadlock !!! ->");
                 }                  //Code omis pour plus de clarté      }while (Thread32Next(snapshot, &thread)); //Continuer le parcours}

GetThreadWaitChain, retourne les informations du chainage dans la structureNodeInfoArrayqui nous servira à identifier les types d'objets (Thread, Mutex, Section Critique etc..) dans le chainage que nous utiliserons pour l'affichage des informations. Pour parcourir le chainage du thread, la variable Count, nous indique le nombre d'objets dans la chaîne.

  for (i = 0; i < Count; i++) {   switch(NodeInfoArray[i].ObjectType)    {        case WctThreadType:            //Code omis
pour plus de clarté                 NodeInfoArray[i].ObjectStatus == WctStatusBlocked) ? L" bloqué " : L"relaché "))
;           break;        case WctMutexType:            //Code omis pour plus de clarté
                             wcscat_s(buffer,NodeInfoArray[i].LockObject.ObjectName);             break;        case
WctCriticalSectionType :               //Code omis pour plus de clarté                            break;       case
WctSendMessageType:               //Code omis pour plus de clarté                             break;       case
WctAlpcType:               //Code omis pour plus de clarté                            break;        case WctComType:
               //Code omis pour plus de clarté                            break;         case
WctThreadWaitType:               //Code omis pour plus de clarté                            break;         case
WctUnknownType:               //Code omis pour plus de clarté                             break;
          default:          break;       }  }

Application de Test

Avec cet article, vous retrouverez à la fois le code source ainsi que les binaires de deux applications.

L'une Analyze.exe qui permet d'analyser via WCT la seconde TestDL.exe qui simule une étreinte fatale.

  1. Lancez Analyze.exe
    Lancez Analyze.exe

  2. Appuyez sur le bouton "OpenProcess" pour démarrez le processus TestDL.Exe

  3. Le processus TestDL.EXE doit se charger
    Le processus TestDL.EXE doit se charger

  4. Revenez dans la fenêtre de l'application d'analyse, et appuyez sur le bouton Analyse
    Vous devriez voir la fenêtre suivante apparaitre

    Le thread N° 5912 dans notre cas (sans aucun doute un autre numéro pour vous), est bloqué. Ce qui est normal, car c'est le thread de l'interface graphique, qui est en attente d'événements utilisateurs.

  5. Allez dans l'application TestDL.EXE, et appuyez, une seule fois sur le bouton "Etreinte Fatale"

  6. Revenez sur l'application Analyze.Exe et appuyez une nouvelle fois sur le bouton "Analyse"
    Revenez sur l'application Analyze.Exe et appuyez une nouvelle fois sur le bouton "Analyse"
    Dans cette analyse, WCT nous reporte que d'autres threads ont été créées en plus du thread d'interface (4592 et 4816) et que le thread 4592 est bloqué par le mutex MX3 tenu par le thread 4816.
    L'interface graphique doit toujours répondre car son thread 5912, n'est pas encore atteint par le virus.

  7. Revenez dans l'application TestDL.exe, et appuyez une seconde fois sur le bouton "Etreinte Fatale", puis le bouton "Analyse" de l'application Analyze.exe
    Analyze.exe
    L'application TestDL.EXE ne doit plus répondre à aucune entrée utilisateur, vous devez la tuer via la croix rouge. L'interface ne répond plus, car son thread est bloqué. Voyons pourquoi.

    Si on analyse notre figure, le chainage de WCT nous indique que notre thread 5912 (Dernière ligne) est cette fois-ci en attente d'acquisition du mutex MX4 tenu par le thread 4592, qui est lui-même en attente du mutex MX3 tenu par le Thread 4816, qui est lui-même en attente du mutex MX5 tenu par le thread 5912. Tout le monde attend tout le monde, d’où l'étreinte fatale. Si on analyse les autres lignes, on obtient le même phénomène.

Conclusion

Bien que WCT soit réservé à des composants plutôt orientés débogage ou de rapport d'erreur comme Windows Error Reporting, rien ne vous empêche de le mettre en œuvre dans votre propre application.

En effet avec l'arrivée en masse de machine à plusieurs cœurs, il est raisonnable de penser que dans un avenir proche vous développerez des applications fortement parallèles. L'usage d'une API qui simplifie la détection des étreintes fatales (deadlock) vous aidera sans aucun doute à ajouter de la robustesse à votre application, et prendre les décisions qui s'imposent lorsque ce type d'erreur survient.

Dans notre prochain article, nous aborderons la nouvelle manière de créer des compteurs de performances avec la version 2.0 de Perflib.

Téléchargement

Téléchargez les sources