Le but de ce TP est d’illustrer les concepts suivants :
- Identification des défaillances, identification de la faute et élimination
- Identification d’une nouvelle défaillance possible puis utilisation de la tolérance aux fautes
Vous trouverez le code des exercices à cette adresse : archive du tp,
I Identification et élimination
Contexte :
Cette partie a pour objectif de vous faire implémenter un petit programme jouet qui est tout de même représentatif des composants d’un systèmes asservi supervisé par un humain. Le but est d’illustrer le problème très classique de « data race » ou « race hazard ». (https://en.wikipedia.org/wiki/Race_condition).
En bref, une situation de data race se produit lors de la conception d’une application constituée de plusieurs séquences d’instruction lisant et modifiant des variables partagées. La race condition est dite critique si le bon fonctionnement du programme est remis en cause. Cela se produira si l’on rencontre certains entrelacements de séquences d’accès en lecture puis écriture.
Nous allons dans un premier temps lancer un programme souffrant d’une telle race condition (critique). Vous pourrez constater que la faute ne s’active pas pour toutes les exécutions du programme, on parle alors de faute transitoire.
Vous pourrez constater que si l’on « aide » pas le programme, la condition ne se produit que très rarement (en gros vous ne devriez pas la voir). C’est la question 1). Puis nous allons activer des macros (code ajouté au programme via la macro #ifdef) afin d’insérer des temporisations dans le code pour augmenter la probabilité d’activation de la race condition (qui est une faute). Attention il sera nécessaire de recompiler le programme.
Le but de la question 2 sera d’utiliser les sémaphore afin d’empêcher l’entrelacement posant problème. Pour cela vous utiliserez deux appels systèmes : sem_wait(…), et sem_post(…).
Pour finir, en utilisant un programme à lancer dans un autre terminal nous verrons que malgré l’usage d’appel système de synchronisation la race condition peut se reproduire (impossible si les appels systèmes fonctionnent correctement)…
Allez dans le répertoire synchfault-3
Description du programme étudié et du système modélisé
Nous allons étudier deux fonctions d’une application qui pilote et permet la supervision du déplacement d’un chariot dans le plan. Une première fonction est en charge de mettre à jour les coordonnées du chariot (le chariot se déplace aux coordonnées indiquées). Une seconde fonction s’exécutant en parallèle permet de visualiser un rapport des dernières coordonnées occupées par le chariot. Chaque rapport affiche une lecture de l’abscisse x, de l’ordonnée y, et de la valeur x²+y². Le mouvement attendu du chariot est un déplacement le long du cercle de centre 0, et de rayon 1 (coordonnées cartésiennes). Le comportement attendu est que l’affichage soit cohérent à partir dès que x ou y sont non nuls. (première ligne est donc à ignorer)
Le programme systeme.c contient le code permettant de simuler le déplacement dans le plan d’un point sur un cercle de rayon 1 et de centre 0 (cf figure).
- void simu_systeme(){
- long valeur_suivante;
- while(cont>0)
- {
- valeur_suivante=(long)time((time_t*)NULL);
- coord_x=cos(sens*vitesse*(valeur_suivante-origine)/36.0*2*3.14);
- // temporisation pour simuler la difficulté du calcul
- simulate_exec_time (charge_calculs*1000000);
- coord_y= sin(sens*vitesse*(valeur_suivante-origine)/36.0*2*3.14);
- usleep(periode_calculs*1000);
- }
- }
La ligne 5 calcul le temps écoulé depuis le lancement du système. Cette valeur sert à calculer coord_x et coord_y en conséquence. La ligne 10 est la pour modéliser un comportement périodique. On répète 5-9 toutes les periode_calculs ms environ.
En parallèle de ce programme nous exécutons le code suivant de supervision :
- void afficheur () {
- while(cont>0){
- printf(« X : %f; »,coord_x);
- simulate_exec_time(charge_affichage*1000000);
- printf( » Y : %f; »,coord_y);
- printf( » R : %f;\n »,coord_x*coord_x+coord_y*coord_y);
- usleep(periode_affichage*1000);
- }
Ce code doit permettre d’afficher périodiquement un rapport de la position du chariot (a priori toujours sur le cercle). La ligne 7 sert ici aussi à avoir un comportement « périodique ». En gros, on répète les lignes 3 à 6 périodiquement. La ligne 7 sert à garantir une fréquence d’exécution maximale bornée.
Le code présent dans le main permet de lancer les thread exécutant chaque programme vous n’avez la possibilité que d’ajouter des lignes dans cette partie du code. Ce code prend 4 paramètres : la période de l’affichage (entier indiquant le nombre de ms), la charge lié à l’affichage, la période des calculs, la charge de ces calculs.
Lancez ce programme avec 2000 1 1000 1 comme paramètres. Le programme s’exécute-t-il correctement ?
Question 1 :
a) Lancez ce programme avec 2000 1 1000 1 comme paramètres. Le programme s’exécute-t-il correctement ?
b) Lancez make testhighfail, que se passe t il pourquoi ?
Question 2 : Proposez une correction du code en introduisant une synchronisation reposant sur un sémaphore (ceci est une contrainte de réalisation imposée).
Aide : un sémaphore est similaire à un tourniquet pour lequel on peut configurer un certain nombre « d’entrée ». L’appel sem_init() permet d’initialiser le sémaphore. Ceci doit être fait au tout début du programme. Dans notre cas, nous souhaitons qu’un seul contexte d’exécution puisse accéder à x et y lors d’une itération de mise à jour ou d’affichage. Le sémaphore doit être initialisé à 1 (un seul contexte accède à x/y).
Ainsi la solution consiste à synchroniser les sections critiques que sont les intervalles de lignes 3 à 6 dans l’affichage et 6 à 9 dans la simulation. Attention ce sont les numéros des encarts de code fournis dans le sujet (il peut y avoir une légère différence avec le fichier source du TP à vous d’adapter le principe étant de garder cohérentes les groupes de lectures cohérentes pour x y, idem pour les écritures
Avant chaque section critique, on prend un jeton sur le sémaphore : appel à sem_wait(..), quand on a fini d’exécuter les accès ne devant pas souffrir de race condition, on relâche le jeton : sem_post(…).
Sur internet ou en ligne de commande tapez man semaphore.h et parcourez la description des fonctions disponibles. En priorité, vous utiliserez sem_init(), sem_wait() et sem_post.
Question 3
Vérifiez que votre solution règle le problème (en utilisant make testhighfail).
Question 4
Nous allons maintenant illustrer le fait que l’élimination de fautes est insuffisante. Pour cela nous allons »activer une faute ».
exécutez pgrep -w faulty-system en parallèle de parallèle de faulty-system pour récupérer le PID des thread exécutant le code des fonction d’affichage et mise à jour. Pourquoi a-t-on trois PID affichés ?
Sans arrêter faulty system, envoyer un signal USR1 au premier, second et troisième PID que se passe t il?
Le script benchit automatise l’envoi de signaux au premier PID, exécutez make inject-error et constater que le programme s’exécute normalement.
Modifiez benchit pour envoyer cette fois USR1 sur le dernier PID créé par faulty-system. (on ajoute juste -w après pgrep dans la première commande du fichier). Exécutez make inject-error.
Le comportement du programme correspond il à l’attendu (le comportement attendu est un affichage périodique, les conditions d’usages raisonnables comprennent la réception de signaux par le processus exécutant l’application).
Réalisez un raisonnement arrière pour déterminer comment l’état défaillant peut être atteint.
Question 5
Proposez une approche par enveloppe permettant de tolérer l’activation de la faute de synchronisation détectée de manière selective (i.e. seule le mode de défaillance déclenché par benchit devra être traité par enveloppe).