Skip to content

MOOC NSI - fondamentaux.

Transcription de la vidéo

4.2.2.8 : Signaux II

[00:00:01]

Nous allons continuer à parler des signaux et nous allons voir comment on peut redéfinir les traitants des signaux. Nous avons vu qu'il existe plusieurs types de signaux et nous en avons listé quelques-uns qui sont souvent utilisés. Nous avons également vu que pour envoyer un signal à des processus, nous disposons de la primitive kill, donc d'une fonction fournie par le système d'exploitation, qui est accessible soit via l'interface programmatique, soit en ligne de commande. Maintenant, on va regarder comment redéfinir le traitant par défaut d'un signal. En Python, nous disposons de la fonction signal, qui est dans le module signal, qui prend deux arguments. Le premier spécifie le signal dont on voudra redéfinir le traitant et le deuxième donne le nom de la fonction qui va devenir le nouveau traitant. Nous avons déjà dit , il faut insister qu'il est impossible de redéfinir les traitant des deux signaux SIGKILL et SIGSTOP qui sont utilisés par le système d'exploitation afin de garantir la correction de comportement. Commençons par un premier exemple qui redéfinit le traitement du signal SIGINT. Je vous rappelle que le signal SIGINT est envoyé à un processus qui s'exécute au premier plan et quand on tape Ctrl-C pour le tuer. Donc, le traitement par défaut, c'est arrêter le processus. Ici, nous allons définir le nouveau traitant dans le programme Python qui est montré. Quand on lance notre programme, nous avons notre processus qui va exécuter main, dans main il fait un premier affichage et ensuite il utilise la fonction signal pour redéfinir le traitant.

[00:02:05]

On voit bien qu'ici en premier argument, nous avons bien signal.SIGINT, donc le nom symbolique c’est SIGINT qui est connu dans le module signal et ensuite le nouveau traitant va être la fonction handler_sigint qui est définie juste au-dessus. La fonction handler_sigint... (alors, les signatures des traitants de signaux sont imposées et donc prennent deux paramètres sig et f). et ce que nous faisons c'est juste un affichage qui dit que le processus a reçu un SIGINT et ensuite le processus se termine. Dans le main, après avoir redéfini le traitant du signal, le processus va faire signal.pause(). Alors, pause a pour résultat de faire arrêter le processus en attente de n'importe quel signal. Donc, une fois que ce processus reçoit un signal, il va continuer et il va se terminer. Donc, quand on fait une pause, le processus peut recevoir SIGINT mais il peut aussi recevoir un autre signal. A droite sur le carré, bleuâtre, une exécution schématique du programme. Donc, si je lance le programme sigint.py, je peux taper Ctrl-C et dans ce cas là, ce signal va être reçu, il va être traité, le nouveau traitant va être appliqué et donc nous allons voir l'affichage “ Je suis numéro de processus et j'ai reçu SIGINT”. C'est effectivement ce que nous voyons dans la trace d'exécution un peu plus bas. Donc, quand on lance sigint, il y a bien un affichage en premier du main .

[00:04:00]

“Je me lance et je suis 34589”. Ensuite, on voit bien le Ctrl-C avec le petit chapeau et le C, et on voit le traitant afficher “Je suis 3, 4, 5, 8, 9 et j'ai reçu SIGINT” Il nous montre que nous avons bien réussi à redéfinir en le traitant par défaut. Ici d'un autre traitant on a fait le processus s'arrêter, donc on a mis le exit. Mais il se peut très bien que cet exit ne soit pas là et que donc le processus continue à s'exécuter. Ce qui ferait qu'avec Ctrl-C, il ne sera plus possible de le terminer.

Passons maintenant à un deuxième exemple où nous allons jouer avec le signal SIGALRM. Pour envoyer un SIGALRM à un moment donné,nous avons la fonction alarm dans le même module signal. alarm prend en paramètre un entier, nombre de secondes, et a pour effet d' envoyer un signal d'alarme au bout d’environ nbSec secondes. Voici le programme que nous allons considérer. Nous avons donc notre processus qui démarre avec l'exécution de la fonction main et dans main, il redéfinit le traitement du signal SIGALRM en disant que le nouveau traitant va être la fonction handler. La fonction handler imprime “trop tard” et termine le processus. Après avoir redéfini le traitant, nous armonsune alarme, donc nous planifions une alarme qui devrait se déclencher au bout d'à peu près cinq secondes. Ensuite, nous demandons à l'utilisateur de rentrer une valeur.

[00:06:00]

Mais il faudrait qu'il se dépêche et que ça se passe avant cinq secondes. Pourquoi avant cinq secondes? Puisque nous avons mis une alarme pour dans 5 secondes et si l'utilisateur n'a pas réagi suffisamment vite, le signal va être déclenché, il va être reçu, on va exécuter le traitant qui va nous dire que c'est trop tard et va terminer le processus. Et effectivement, nous avons ici schématiquement, une exécution, on va dire correcte, intuitive, où nous armons l’arame au début, ici nous rentrons une valeur avant le délai de 5 secondes, cette chose désarme le minuteur et nous avons la suite du programme dans le main qui est “vous avez entré” suivi par la valeur que l'utilisateur a tapé. Et là, le processus se termine en ayant bien lu une valeur dans le temps imparti. Il se peut quand même que le délai s'écoule sans que l'utilisateur ait pu rentrer une valeur et dans ce cas là, il va y avoir le signal, exécution de l’handler et donc on va nous dire trop tard avant de se terminer. Voici une trace d'exécution dans les deux cas. Donc, dans le premier cas, je lance alarm.py et je rentre une valeur avant le délai de 5 secondes. Donc, le programme me répond bien. “vous êtes rentré 10” alors que dans le deuxième cas, je prends un petit peu plus de temps, je ne rentre pas de valeur avant les 5 secondes et le programme me dit bien qu'il est trop tard.

[00:07:49]

Passons enfin à un troisième exemple, important, qui joue sur la synchronisation père-fils, puisque nous avons vu que les pères doivent s'occuper de leur fifilslle et doivent s'occuper de leur terminaison pour ne pas laisser de processus zombis. Nous avons également vu que tous les fils, quand ils se terminent, envoient un signal SIGCHLD à leur père, mais que le traitement de base est vide, c'est-à-dire le signal est ignoré. Ce que nous pouvons faire, c'est donc programmer un traitement de ce signal pour pouvoir récupérer et donc nettoyer les processus fils de manière automatique. Le schéma de ce que nous allons faire est le suivant, montré dans le carré bleu. Le processus qui va créer des processus fils et qui voudrait récupérer ses fils de manière automatique va redéfinir le traitant par défaut pour le signal SIGCHLDen donnant une nouvelle fonction à exécuter. Ensuite, il va créer un certain nombre de fils, mais leur terminaison et leur attente vont être gérées par le traitant. Qu'est ce qu'on fait dans le traitant, donc dans la fonction handler? Nous avons l'appel à la fonction d'attente d'un fils, par contre, ici, quand on fait waitpid quand on attend un fils, au lieu d'utiliser une valeur particulière, on utilise -1 qui veut dire “J'attends n'importe lequel de mes fils”. “J’attends, je récupère et ensuite je peux faire des choses en utilisant les informations de terminaison. Mais en ayant exécuté waitpid,

[00:09:40]

le processus fissent n'a pas de risque à devenir zombie.

Regardons cette chose là en application. Ici, nous avons la première partie du programme Python correspondant ou nous avons le traitant. Donc le traitant c'est la fonction n handler qui commence par un affichage comme quoi “je suis le processus de tel numéro et j'ai reçu SIGCHLD”, c'est-à-dire j'ai reçu l'information qu'un processus fils à moi s'est terminé. Je récupère l'information sur un fils qui s'est terminé, un fils quelconque, je récupère le statut et derrière, je peux vérifier si le fils s'est terminé normalement ou s’il s'est terminé anormalement. La suite, c'est la fonction main où le père commence par un affichage, redéfinit le traitant du signal SIGCHLD, on dit bien que le nouveau traitant, c'est la fonction handler que nous avons vu sur le transparent précédent. Et là à la suite, nous allons créer deux fils. Alors, on va le faire de manière séquentielle. Il y a un premier fork. ensuite, on fait un test sur la valeur de retour. Donc, si c'est le fils, il va faire un affichage. Si c'est le père, il va refaire un fork pour créer un deuxième fils. Si c'est le deuxième fils, il va refaire un affichage. Et si c'est le père, il va faire une boucle où il va dormir pendant une seconde et ensuite va imprimer le numéro d'itération. A la fin, tous les processus, donc le père et le deux fils vont imprimer.

[00:11:26]

“J'ai terminé” et vont terminer leur exécution. Regardons une trace d'exécution. Alors, nous avons d'abord l'affichage du père qui dit “mon pid est 1701” et ensuite nous voyons l'affichage du premier fils qui dit que son pid est 1702. Le fils ne fait pas grand chose donc il arrive vite dans l'instruction qui dit que “j'ai terminé 1702”. Ensuite, on voit le fils , le deuxième fils, qui fait la même chose. Mais ensuite, on voit le traitement des signaux qui ont été envoyés. On voit bien que le père, 1701, a reçu un signal SIGCHLD et il affiche que son fils, 1702, a terminé normalement . La même chose pour le deuxième fils : il a reçu le signal SIGCHLD et 1703, a terminé normalement. Donc, cette chose là est faite automatiquement en utilisant le mécanisme de traitement des signaux et ensuite, le père peut continuer son exécution sans se préoccuper des attentes et des terminaisons de ses fils.

Dans cette deuxième séquence sur les signaux, nous avons vu qu'on peut redéfinir les traitants par défaut pour obtenir des interactions et des communications entre processus plus intéressantes. La redéfinition des traitants des signaux est possible à travers la fonction signal fourni par le système d'exploitation. La redéfinition n'est pas possible pour deux signaux particuliers, SIGKILL et SIGSTOP, qui sont réservés au système et lui permettent de garantir le bon fonctionnement des applications.