• Aucun résultat trouvé

Sommeil utilisant les temporisations de précision

Dans le document Programmation système en C sous (Page 105-109)

return (0);

}

L'exécution suivante nous montre qu'à un niveau macroscopique (la seconde), la précision est conservée, même sur une durée relativement longue comme une minute, avec une charge système assez faible.

$ ./exemple_nanosleep 13:04:05

13:05:05

$

Bien sûr dans notre cas, le gestionnaire de signaux n'effectuait aucun travail. Si le gestionnaire consomme vraiment du temps processeur, et si la précision du délai est critique, on se reportera au principe évoqué avec sleep( ), en recadrant la durée restante régulièrement grâce à la fonction time( ).

Notons que depuis Linux 2.2, des attentes de faible durée (inférieures à 2 ms) sont finalement devenues possibles de manière précise avec nanosleep( ) en utilisant un ordonnancement temps-réel du processus. Dans ce cas, le noyau effectue une boucle active. L'attente est toute-fois prolongée jusqu'à la milliseconde supérieure.

Sommeil utilisant les temporisations de précision

Nous avons vu dans les chapitres précédents le fonctionnement de l'appel-système alarm(

), qui déclenche un signal SIGALRM au bout du nombre de secondes programmées. Il existe en fait trois temporisations qui fonctionnent sur un principe similaire, mais avec une plus grande précision (tout en étant toujours limitées à la résolution de l'horloge interne à 1/HZ seconde, soit 10 ms sur architecture PC).

De plus, ces temporisations peuvent être configurées pour redémarrer automatiquement au bout du délai prévu. Les trois temporisations sont programmées par l'appel-système setitimer( ). On peut également consulter la programmation en cours grâce à l'appel getitimer( ).

Le prototype de setitimer( ) est déclaré ainsi dans <sys/time.h> : int setitimer (int laquelle, const struct itimerval * valeur, struct itimerval * ancienne);

Le premier argument permet de choisir quelle temporisation est utilisée parmi les trois constantes symboliques suivantes :

Nom Signification

ITIMER_REAL Le décompte de la temporisation a lieu en temps« réel», et lorsque le compteur arrive à zéro, le signal SIGALRM est envoyé au processus.

ITIMER_VIRTUAL La temporisation ne décroît que lorsque le processus s'exécute en mode utilisateur. Un signal SIGVTALRM lui est envoyé à la fin du décompte.

ITIMER_PROF Le décompte a lieu quand le processus s'exécute en mode

L'utilisation de la temporisation ITIMER_REAL est la plus courante. Elle s'apparente globale-ment au même genre d'utilisation que la fonction alarm( ) , mais offre une plus grande précision et un redémarrage automatique en fin de comptage.

ITIMER_VIRTUAL s'utilise surtout conjointement à ITIMER_PROF, car ces temporisations permettent. par une simple soustraction, d'obtenir des statistiques sur le temps d'exécution passé par le processus en mode utilisateur et en mode noyau.

La temporisation ITIMER_PROF permet de rendre compte du déroulement du processus indépendamment des mécanismes d'ordonnancement, et donc d'avoir une indication quantitative de la durée d'une tâche quelle que soit la charge système. On peut utiliser cette technique pour comparer par exemple les durées de plusieurs algorithmes de calcul.

Pour lire l'état de la programmation en cours, on utilise getitimer( ) : int getitimer (int laquelle, struct itimerval * valeur);

La structure itimerval servant à stocker les données concernant un timer est définie dans

<sys/time.h> avec les deux membres suivants :

Type Nom Signification

struct timeval it_interval Valeur à reprogrammer lors de l'expiration du timer

struct timeval it_value Valeur décroissante actuelle

La structure timeval que nous avons déjà rencontrée dans la présentation de wait3( ) est utilisée pour enregistrer les durées, avec les membres suivants :

Type Nom Signification

time_t tv_sec Nombre de secondes

time_t tv_usec Nombre de microsecondes

La valeur du membre it_value est décrémentée régulièrement suivant les caractéristiques de la temporisation. Lorsque cette valeur atteint zéro, le signal correspondant est envoyé. Puis, si la valeur du membre it_interval est non nulle, elle est copiée dans le membre it_value, et la temporisation repart.

La bibliothèque GlibC offre quelques fonctions d'assistance pour manipuler les structures timeval . Comme le champ tv_usec d'une telle structure doit toujours être compris entre 0 et 999.999, il n'est pas facile d'ajouter ou de soustraire ces données. Les fonctions d'aide sont les suivantes :

void timerclear (struct timeval * temporisation);

qui met à zéro les deux champs de la structure transmise.

void timeradd (const struct timeval * duree_1, const struct timeval * duree_2, struct timeval * duree resultat);

additionne les deux structures (en s'assurant que les membres tv_usec ne dépassent pas 999.999) et remplit les champs de la structure résultat, sur laquelle on passe un pointeur

dernier argument. Une structure utilisée en premier ou second argument peut aussi servir pour récupérer le résultat, la bibliothèque C réalisant correctement la copie des données.

void timersub (const struct timeval * duree_1, const struct timeval * duree_2, struct timeval * duree_resultat);

soustrait la deuxième structure de la première (en s'assurant que les membres tv_usec ne deviennent pas négatifs) et remplit les champs de la structure résultat.

int timerisset (const struct timeval * temporisation);

est vraie si au moins l'un des deux membres de la structure est non nul.

ATTENTION Nous avons présenté ici des prototypes de fonctions, mais en réalité elles sont toutes les quatre implémentées sous forme de macros, qui évaluent plusieurs fois leurs arguments. Il faut donc prendre les précautions adéquates pour éviter les effets de bord.

Le premier exemple que nous allons présenter avec setitimer( ) va servir à implémenter un sommeil de durée précise, même lorsque le processus reçoit de nombreuses interruptions parallèlement à son sommeil. Pour cela, nous utiliserons le timer ITIMER_REAL. Nous allons créer une fonction sommeil_precis( ), prenant en argument le nombre de secondes, suivi du nombre de microsecondes de sommeil voulu. La routine sauvegarde tous les anciens paramètres, qu'elle modifie pour les rétablir en sortant. Elle renvoie 0 si elle réussit, ou -1 sinon. On utilise la méthode de blocage des signaux employant sigsuspend( ) , que nous avons étudié dans le chapitre précédent.

La routine sommeil_precis( ) créée ici est appelée depuis les deux processus père et fils que nous déclenchons dans main( ). Le fils utilise un appel sur une longue durée (60 s), et le père une multitude d'appels de courte durée (20 ms). Le processus père envoie un signal SIGUSR1 à son fils entre chaque petit sommeil.

Les deux processus invoquent la commande date au début et à la fin de leur exécution pour afficher l'heure.

exemple_setitimer_1.c #include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <signal.h>

#include <errno.h>

#include <sys/time.h>

#include <sys/wait.h>

static int temporisation_ecoulee;

void

gestionnaire_sigalrm (int inutile) {

temporisation_ecoulee = 1;

} int

sommeil_precis (long nb_secondes, long nb_microsecondes) {

struct sigaction action;

struct sigaction ancienne_ action;

sigset_t masque_sigalrm;

sigset_t ancien_masque;

int sigalrm_dans_ancien_masque = 0;

struct itimerval ancien_timer;

struct itimerval nouveau_timer;

int retour = 0;

/* Préparation du timer */

timerclear (& (nouveau_timer . it_interval));

nouveau_timer . it_value . tv_sec = nb_secondes;

nouveau_timer . it_value . tv_usec = nb_microsecondes;

/* Installation du gestionnaire d'alarme */

action . sa_handler = gestionnaire sigalrm;

sigemptyset (& (action . sa_mask));

action . sa_flags = SA_RESTART;

if (sigaction (SIGALRM, & action, & ancienne_action) != 0) return (-1);

/* Blocage de SIGALRM avec mémorisation du masque en cours */

sigemptyset (& masque_sigalrm);

sigaddset (& masque_sigalrm, SIGALRM);

if (sigprocmask (SIG_BLOCK, & masque_sigalrm, & ancien_masque) != 0) { retour = -1;

goto reinstallation_ancien_gestionnaire;

}

if (sigismember (& ancien_masque, SIGALRM)) { sigalrm_dans_ancien_masque = 1;

sigdelset (& ancien_masque, SIGALRM);

}

/* Initialisation de la variable globale */

temporisation_ecoulee = 0;

/* Sauvegarde de l'ancien timer */

if (getitimer (ITIMER_REAL, & ancien_timer) != 0) { retour = -1;

goto restitution_ancien_masque;

}

/* Déclenchement du nouveau timer */

if (setitimer (ITIMER REAL, & nouveau_timer, NULL) != 0) { retour = -1;

goto restitution_ancien_timer;

}

/* Boucle d'attente de la fin du sommeil */

while (! temporisation_ecoulee) {

if ((sigsuspend (& ancien_masque) != 0) &&

(errno != EINTR)) { retour = -1;

break;

} }

restitution_ancien_timer:

if (setitimer (ITIMER_REAL, & ancien_timer, NULL) != 0) { retour = -1;

restitution_ancien_masque :

if (sigalrm_dans_ancienmasque) {

sigaddset (& ancien_masque, SIGALRM);

}

if (sigprocmask (SIG_SETMASK, & ancien_masque, NULL) != 0) { retour = -1;

reinstallation_ancien_gestionnaire

if (sigaction (SIGALRM, & ancienne_action, NULL) != 0) { retour = -1;

return (retour);

} void

gestionnairesigusr1 (int inutile) {

} int main (void) {

pid_t pid;

struct sigaction action;

int i;

if ((pid = fork ( )) < 0) {

fprintf (stderr, "Erreur dans fork \n");

exit (1);

}

action . sa_handler = gestionnaire_sigusrl;

sigemptyset (& (action . sa_mask));

action . sa_flags = SA_RESTART;

if (sigaction (SIGUSR1, & action, NULL) != 0) { fprintf (stderr, "Erreur dans sigaction \n");

exit (1);

}

if (pid == 0) {

system ("date +\"Fils : %H:%M:%S\"");

if (sommeil_precis (60, 0) != 0) {

fprintf (stderr, "Erreur dans sommeil_precis \n");

exit (1);

}

system ("date +\"Fils %H:%M:%S\"");

} else {

sommeil_precis (2, 0);

system ("date +\"Père : %H:%M:%S\"");

for (i = 0; i < 3000; 1 ++) {

sommeil_precis (0, 20000); /* 1/50 de seconde */

kill (pid, SIGUSR1);

}

system ("date +\"Père : %H:%M:%S\"");

waitpid (pid, NULL, 0);

}

return (0);

}

Nous voyons que la précision du sommeil est bien conservée, tant sur une longue période que sur un court intervalle de sommeil :

$ ./exemple_setitimer_1 Fils : 17:50:34

Père : 17:50:36 Fils : 17:51:34 Père : 17:51:36

$

Les temporisations n'expirent jamais avant la fin du délai programmé, mais plutôt légèrement après, avec un retard constant dépendant de l'horloge interne du système. Si on désire faire des mesures critiques, il est possible de calibrer ce léger retard.

Avec la temporisation ITIMER_REAL, lorsque le signal SIGALRM est émis, le processus n'est pas nécessairement actif (contrairement aux deux autres temporisations). Il peut donc s'écouler un retard avant l'activation du processus et la délivrance du signal. Avec la temporisation ITIMER_PROF, le processus peut se trouver au sein d'un appel-système, et un retard sera égale-ment possible avant l'appel du gestionnaire de signaux.

ATTENTION Il serait illusoire d'attendre des timers une résolution meilleure que celle de l'ordonnanceur (de période 1/HZ, c'est-à-dire 10 ms sur PC).

Notre second exemple va utiliser conjointement les deux timers ITIMER_VIRTUAL et ITIMER_PROF pour mesurer les durées passées dans les modes utilisateur et noyau d'une routine qui fait une série de boucles consommant du temps processeur, suivie d'une série de copies d'un fichier vers le périphérique /dev/null pour exécuter de nombreux appels-système.

Le gestionnaire de signaux commun aux deux temporisations départage les signaux, puis incrémente le compteur correspondant. Les temporisations sont réglées pour envoyer un signal tous les centièmes de seconde. Une routine d'affichage des données est installée par atexit( ) afin d'être invoquée en sortie du programme.

exemple_setitimer_2.c #include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <signal.h>

#include <sys/time.h>

#include <sys/wait.h>

unsigned long int mode_utilisateur;

unsigned long int mode_utilisateur_et_noyau:

void gestionnaire_signaux (int numero);

void fin_du_suivi (void);

void action_a_mesurer (void);

int main (void) {

struct sigaction action;

struct itimerval timer;

/* Préparation du timer */

timer . it_value . tv_sec = 0; /* 1/100 s. */

timer . it_value . tv_usec = 10000;

timer . it_interval . tv_sec = 0; /* 1/100 s. */

timer . it_interval . tv_usec = 10000;

/* Installation du gestionnaire de signaux */

action . sa_handler = gestionnaire_signaux;

sigemptyset (& (action . sa_mask));

action . sa_flags = SA_RESTART;

if ((sigaction (SIGVTALRM, & action, NULL) != 0) ||(sigaction (SIGPROF, & action, NULL) != 0)) { fprintf (stderr, "Erreur dans sigaction \n");

return (-1);

}

/* Déclenchement des nouveaux timers */

if ((setitimer (ITIMERVIRTUAL, & timer, NULL) != 0) ||(setitimer (ITIMERPROF, & timer, NULL) != 0)) { fprintf (stderr, "Erreur dans setitimer \n");

return (-1);

}

/* Installation de la routine de sortie du programme */

if (atexit (fin_du_suivi) != 0) {

fprintf (stderr, "Erreur dans atexit \n");

return (-1);

}

/* Appel de la routine de travail effectif du processus */

action_a_mesurer( ) ; return (0);

} void

gestionnaire_signaux (int numero) {

switch (numero) { case SIGVTALRM : mode_utilisateur++;

break;

case SIGPROF :

mode_utilisateur_et_noyau++;

break;

} } void

fin_du_suivi (void) {

sigset_t masque;

/* Blocage des signaux pour éviter une modification */

/* des compteurs en cours de lecture. */

sigemptyset (& masque);

sigaddset (& masque, SIGVTALRM);

sigaddset (& masque, SIGPROF);

sigprocmask (SIG_BLOCK, & masque, NULL);

/* Comme on quitte à présent le programme, on ne * restaure pas l'ancien comportement des timers, * mais il faudrait le faire dans une routine de * bibliothèque.

*/

fprintf (stdout, "Temps passé en mode utilisateur : %ld/100 s \n", mode_utilisateur);

fprintf (stdout, "Temps passé en mode noyau %ld/100 s \n", mode_utilisateur_et_noyau - mode_utilisateur);

} void

action_a_mesurer (void) {

int i, j;

FILE * fp1, * fp2;

double x;

x = 0.0;

for (i = 0; i < 10000; i++) for (j = 0; j < 10000; j++) x += i * j;

for (i = 0; i < 500; i++) {

if ((fp1 = fopen ("exemple_setitimer_2", "r")) != NULL) { if ((fp2 = fopen ("/dev/null", "w")) != NULL) {

while (fread (& j, sizeof (int), 1, fp1) == 1) fwrite (& j, sizeof (int), 1, fp2);

fclose (fp2);

}

fclose (fpl);

} } }

L'exécution affiche les résultats suivants :

$ ./exemple_setitimer_2

Temps passé en mode utilisateur : 542/100 s Temps passé en mode noyau : 235/100 s

$ ./exemple_setitimer_2

Temps passé en mode utilisateur : 542/100 s Temps passé en mode noyau : 240/100 s

$ ./exemple_setitimer_2

Temps passé en mode utilisateur : 554/100 s Temps passé en mode noyau : 223/100 s

$

Nous voyons bien là les limites du suivi d'exécution sur un système multitâche, même si les ordres de grandeur restent bien constants. Nous copions à présent ce programme dans exemple setitimer_3.c en ne conservant plus que la routine de travail effectif, ce qui nous donne cette fonction main( ) :

int main (void) {

action_a_mesurer( ) ; return (0);

}

Nous pouvons alors utiliser la fonction « times » de bash 2, qui permet de mesurer les temps cumulés d'exécution en mode noyau et en mode utilisateur du shell et des processus qu'il a lancés.

$ sh -c "./exemple_setitimer_3 ; times"

0m0.00s 0m0.00s 0m5.21s 0m2.19s

$ sh -c "./exemple_setitimer_3 ; times"

0m0.00s 0m0.01s 0m5.07s 0m2.41s

$ sh -c "./exemple_setitimer_3 ; times"

0m0.01s 0m0.00s 0m5.04s 0m2.34s

$

Nous voyons que les résultats sont tout à fait comparables, même s'ils présentent également une variabilité due à l'ordonnancement multitâche.

Dans le document Programmation système en C sous (Page 105-109)