1. Présentation des signaux (légers rappels de cours)
Les signaux ont des origines diverses, ils peuvent être :
- retransmis par le noyau : division par zéro, overflow, instruction interdite,
- envoyés depuis le clavier par l’utilisateur ( touches : <CTRL>Z, <CTRL>C, …)
- émis par la commande kill depuis le shell ou par la fonction kill depuis un programme écrit en C
Emission d’un signal:
- En C: kill (num_du_processus_destinataire, num_du_signal)
- En Shell: kill -num_du_signal num_du_processus
Remarque : l’émetteur ne peut pas savoir si le destinataire a reçu ou non le signal.
Réception:
comportements possibles du destinataire du signal :
ignorer le signal | C’est ce qui se produit si le processus destinataire a exécuté le code:
signal(num_du_signal,SIG_IGN) |
repositionner le traitement par défaut | C’est ce qui se produit par défaut, ou si le processus destinataire a exécuté le code:
signal(num_du_signal,SIG_DFL) |
définir un traitement spécifique | C'est ce qui se produit si le processus destinataire a exécuté le code suivant (où fonction est l'identifiant d'une fonction C): signal(num_du_signal,fonction) |
L’ensemble des signaux est décrit dans /usr/include/sys/signal.h ou dans /usr/include/bits/signum.h.
Remarques :
- Sur les UNIX de la famille Berkeley, par exemple Solaris de Sun, après exécution de la fonction spécifique définie par l’utilisateur (3ème cas dans le tableau ci-dessus), l’option par défaut est rétablie. Si on veut conserver l’option spécifique il faut rappeler signal(num_sig, fonction) dans le code de fonction. Ceci n'est pas utile sous LINUX, qui est un UNIX à base System V.
- On n’a pas le droit de changer le traitement par défaut de certains signaux, tel que SIGKILL.
Sur une machine multi-processeur on peut avoir à forcer l’exécution sur un seul processeur, dans ce cas il faut utiliser la commande taskset, par exemple :
$taskset -c 0 ./exo1
En exécutant la commande ci-dessus, on force l’exécution du programme exo1 à n’utiliser qu’un seul coeur. Rq: avant le nom du programme, c’est un zéro et pas un ‘o’.
2.Ignorer les signaux
Ecrire un programme qui ignore TOUS les signaux.
Le canevas de code à compléter est donné ci-dessous. Le rôle du while(1) est de boucler pour attendre la réception d’un signal.
#include <stdio.h> #include <signal.h> #include <unistd.h> int main(int argc, char *argv[]){ int Nb_Sig; for(Nb_Sig = 1; Nb_Sig < NSIG ; Nb_Sig ++) { ... ... } while(1) { sleep(5); } /* Wait for receiving signals */ return 0; }
A FAIRE :
- Tester la valeur de retour de la fonction signal pour relever les numéros des (rares) signaux qu’on ne peut ignorer.
La commande /bin/kill -L donne la liste des signaux et de leurs mnémoniques
On peut aussi consulter /usr/include/sys/iso/signal_iso.h (ou /usr/include/bits/signum.h) qui est inclus par #include <signal.h> pour identifier le signal en fonction de son numéro (ici Nb_Sig). - Quels sont les signaux que le processus ne peut ignorer?
- Faites <CTRL>C dans la fenêtre où tourne le programme. Envoyez également des signaux vers ce programme en utilisant kill depuis une autre fenêtre (la commande kill -l donne la liste de tous les signaux et de leurs mnémoniques).
- Trouvez une solution pour terminer le programme sans fermer le terminal. Indice: constatez que SIGKILL (signal numéro 9, envoyé par kill -9) n’est pas ignoré par le programme et permet de l’arrêter.
3. Traitement spécifique des signaux
On va modifier le programme précédent : les signaux ne seront plus ignorés, mais traités par une fonction spéciale, que l’on appellera on_signal_reception.
Le canevas de code à compléter est donné ci-dessous. Le rôle du while(1) est de boucler pour attendre la réception d’un signal.
#include <signal.h> #include <unistd.h> void on_signal_reception (int sig_number); int main(int argc, char *argv[]) { int Nb_Sig; for(Nb_Sig = 1; Nb_Sig < NSIG ; Nb_Sig ++) { ... ... } while(1) { sleep(5); } /* Wait for receiving signals */ } /************* Function to deal with signals **************/ void on_signal_reception (int sig_number) { printf("Here we go, signal %d received!\n", sig_number); ... }
Faire comme précédemment, c’est à dire :
- Tester la valeur de retour de la fonction signal pour relever les (rares) signaux pour lesquels on ne peut définir un traitement spécifique.
- Faire <CTRL>C dans la fenêtre où tourne le programme. Envoyer des signaux vers ce programme en utilisant kill depuis une autre fenêtre. Constater que le signal SIGKILL (signal numéro 9) termine ce programme.
4. Traitement des erreurs sur les accès mémoire
Une tentative d’accès à un zone mémoire interdite se traduit par l’envoi d’un signal SIGSEGV ou SIGBUS au processus fautif. La réception de l’un de ces signaux provoque la fin du processus. Pour éviter cette fin, mais avertir l’utilisateur des éventuelles erreurs, nous allons traiter de façon spécifique les signaux reçus. Nous testerons le programme en simulant un accès mémoire erroné.
Un canevas de code vous est fournit dans le fichier TP_SELC/TP13/material/error_recovery.c
Instructions :
- Dans main :
- Utilisez la fonction signal pour changer le comportement associé à la réception d’un signal SIGSEGV ou SIGBUS.
- Positionnez le point de reprise en utilisant sigsetjmp plutôt que setjmp (cf. man sigsetjmp). Lors de l’appel à la fonction sigsetjmp on mettra le second paramètre à 1.
- Complétez la fonction de traitement des signaux on_memory_access_error pour que :
- Le retour de cette fonction se fasse au sigsetjmp se trouvant dans main, en utilisant la fonction siglongjmp. Lors de l’appel à la fonction siglongjmp, on initialisera le second paramètre avec le numéro du signal qui a provoqué l’erreur.
- Exécutez le programme et vérifiez que l’on passe bien par Traite_Sig pour chaque occurence de l’erreur,
- Utilisez la valeur de retour de sigsetjmp pour avertir l’utilisateur en cas d’erreur par un message du type « an error just occured, make sure you respect the
usage condition of this program ».
5. Utilisation de SIGUSR1 et SIGUSR2
Ecrire un programme (canevas donné plus bas) qui :
- Affiche son numéro (pid) via l’appel à getpid(),
- Traite tous les signaux par une fonction fonc qui se contente d’afficher le numéro du signal reçu.
- Traite le signal SIGUSR1 par une fonction on_sigusr1 et le signal SIGUSR2 par on_sigusr2 :
- on_sigusr1 affiche le numéro du signal reçu et la liste des utilisateurs de la machine (appel à who par system("who"))
- on_sigusr2 affiche le numéro du signal reçu et l’espace disque utilisé sur la machine (appel à df . par system("df ."))
A FAIRE :
- Lancer le programme et lui envoyer des signaux, dont SIGUSR1 et SIGUSR2, depuis une autre fenêtre, à l’aide de la commande kill,
- Vérifier que les signaux sont bien reçus,
- Pourquoi le signal 17 (SIGCHLD) est-il aussi reçu lorsqu’on envoie USR1 et USR2 (indication : lire ce que fait la commande system en faisant "$ man system") ?
Schéma du programme :
#include <signal.h> #include <unistd.h> /*************** main fonc **************/ int main (int argc, char *argv[]){ /* on_sigusr1 is the function executed when receiving * SIGUSR1*/ // TO BE COMPLETED /* on_sigusr2 is the function executed when receiving * SIGUSR2 */ // TO BE COMPLETED /* on_other_signals is the function executed when receiving * other signals */ // TO BE COMPLETED while (1) { printf("main : pid %d with waiting for signals \n", (int)getpid()); sleep(5); } /* Waiting for signals */ } /*************** on_sigusr1 function **************/ void on_sigusr1 (int NumSignal) { // TO BE COMPLETED: display an acknowledgement message // TO BE COMPLETED: execute command "who" } /*************** on_sigusr2 function **************/ void on_sigusr2 (int NumSignal) { // TO BE COMPLETED: display an acknowledgement message // TO BE COMPLETED: execute command "df ." } /*************** on_others function **************/ void on_others (int NumSignal) { // TO BE COMPLETED: display an acknowledgement message }
Fin du TP.