Délais, alarmes et timers sous Unix

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 14/09/2000 - Dernière mise à jour : 28/09/2001

Remarques préliminaires :

Sommaire

Introduction
Découverte des délais
Classe CTimevalBase
Classe CTimespecBase
Utilisation de la classe CTimevalBase : fonction système select()       exo_01

Fonctions sleep() et nanosleep() - Classes CTimeval et CTimespec

exo_02
Première amélioration
Seconde amélioration
Fonction alarm() - Timer       exo_03

Sablier cyclique

Classe CItimervalBase
Classes CTimer...
exo_04
Mesure de durées par un timer
Temps réel, temps virtuel  exo_05
Mode user, mode kernel   exo_06

Introduction

    Unix offre deux structures permettant de manipuler des délais : struct timeval , et struct timespec ,[1].

    La première, de conception ancienne, est essentiellement utilisée par les timers Unix. Elle permet de manipuler des délais jusqu'à la micro-seconde (pourvu que la machine et la configuration le permettent). La dernière, d'introduction récente, permet de définir des délais de façon beaucoup plus fine et est conforme à la norme POSIX. Elle est en particulier utilisée par la fonction système nanosleep().

    Malheureusement, aucune des deux n'offre de réelle sécurité dans son utilisation. La documentation indique que la valeur de la donnée membre tv_usec d'un timeval ne doit pas excéder 999 999, que celle de la donnée membre d'un timespec ne doit pas excéder 999 999 999. Mais rien ne permet de garantir le respect de cette contrainte, puisque les données membres sont publiques (c'est une struct), ce qui peut provoquer des erreurs. Considérons par exemple la séquence suivante :
 
timeval tv1 = {3, 1500000};   // le mot "struct" devant timeval serait nécessaire
                              // s'il existait une fonction timeval()
timeval tv2 = {2, 2500000};

bool egal = timercmp (tv1, tv2, <);  // timercmp[2] est une macro permettant de comparer 2 timeval

    Le booléen egal sera faux après exécution de ces instructions.

    De plus, les opérations arithmétiques sur ces structures ne sont pas complètes.

Sommaire


Découverte des délais

Masquage de l'information par dérivation

     Afin de manipuler de façon plus sûre les délais lors des exercices suivants, nous mettons à votre disposition deux classes, CTimevalBase et CTimespecBase, dans l'espace des noms nsSysteme, dérivées respectivement de struct timeval et struct timespec. Elles sont décrites dans les fichiers TimeBase.h, TimeBase.hxx et TimeBase.cxx. Une autre classe, CItimervalBase, est fournie. Elle sera utilisée ultérieurement.

    Chargez-les dans vos propres répertoires include et util. Afin de pouvoir les utiliser correctement ultérieurement, passez un peu de temps à étudier leurs spécifications. L'analyse du code peut aussi vous apprendre des choses : voir Rq1 !

Classe CTimevalBase

    La classe CTimevalBase appartient à l'espace de noms nsSysteme. Elle est obtenue par double dérivation :     Elle offre les fonctionnalités suivantes :     Une exception CException doit être levée en cas de durée négative.

    Le constructeur par recopie, et l'opérateur d'affectation, gracieusement offerts par le compilateur, sont utilisables car les données membres ne contiennent pas de pointeurs.

Classe CTimespecBase

    La classe CTimespec est très semblable à la classe CTimevalBase. Il suffit de remplacer les micro-secondes par des nano-secondes. L'accesseur GetMuSec() est remplacé par GetNSec().
 

Utilisation de la classe CTimevalBase : fonction système select()

    La fonction select() est appelée fonction de multiplexage d'entrées/sorties. Rappelons que les "fichiers" (au sens large d'Unix) peuvent être classés en deux catégories selon que la fonction système read() renvoie 0 lorsqu'ils sont vides (c'est le cas des fichiers disques "normaux") ou qu'elle bloque (normalement) le processus qui l'appelle lorsque le "fichier" est vide (c'est le cas du "fichier" clavier, des sockets, des pipes). Rappelons aussi que la fonction select() est bloquante jusqu'à ce qu'au moins un événement attendu arrive, ou que le délai fixé en dernier paramètre arrive à expiration.
 


exo_01

    Ajouter à nsSysteme le wrapper de la fonction système select(), en modifiant légèrement ses paramètres : remplacer le type timeval du dernier paramètre par le type CTimevalBase, et donner aux trois derniers pointeurs la valeur 0 par défaut (pourquoi seulement aux trois derniers ?). Le premier paramètre de la fonction select() reprèsente la valeur maximale du file descriptor à considérer dans les masques. Si elle n'est pas connue, utiliser la fonction système getdtablesize() qui renvoie le nombre maximal de file descriptors que peut gérer Unix (c'est-à-dire la taille de la table des file descriptors).

    Mettre à jour les fichiers MakeUtil et INCLUDE_H.

    Ecrire un programme qui répète 5 fois une boucle dans laquelle il attend soit une frappe au clavier, soit l'écoulement d'un délai de 5 secondes. Si l'événement est une frappe au clavier, afficher sous la forme suivante (par exemple) :
 
f : caractere lu au bout d'un délai de [1:380000]

le caractère frappé (qui sera lu soit par l'extracteur ">>", soit par la fonction membre get() de la classe istream : faire les deux essais). Si la sortie de la fonction Select() est due à l'épuisement du délai, afficher le message "timeout atteint".

Rq2

    Mettre à jour le fichier Makefile. Compiler et tester les deux possibilités.

Corrigés : exo_01.cxx    -    Makefile    -    nsSysteme.h    -    nsSysteme.hxx    -    MakeUtil    -    INCLUDE_H

Sommaire


Fonctions sleep() et nanosleep() - Classes CTimeval et CTimespec

exo_02

    La fonction C sleep() peut être utilisée pour suspendre le processus pendant un certain temps (en secondes). En rechargeant la fonction avec la valeur renvoyée, il est possible d'imposer un délai de suspension fixé, malgré les éventuelles interruptions. En même temps, des événements urgents peuvent être pris en compte. Un inconvénient est que le nombre de secondes est entier, donc arrondi. Le délai cumulé des suspensions est donc approximatif.

    Dans la fonction ppal() du fichier exo_02.cxx, faire une boucle jusqu'à ce que le délai, passé en argument de la commande, soit épuisé. Pour cela, appeler la fonction sleep(), en récupérant le délai restant.

    Pour permettre d'interrompre la fonction sleep() sans terminer le programme, dérouter le signal SIGINT avant d'entrer dans la boucle.

    Afficher un message à chaque retour de la fonction sleep(), par exemple :
 
SIGINT reçu; il reste     5 secondes

    Compiler et tester.

    La fonction système nanosleep() a un comportement très analogue à la fonction C. Cependant, elle permet une granularité du temps plus fine, et utilise pour cela la structure struct timespec.

    Ecrire le wrapper Nanosleep() de la fonction nanosleep(), en remplaçant le type timespec par CTimespecBase. Contrairement à la fonction système nanosleep(), il est souhaitable que son wrapper remette à zéro le temps restant lorsque nanosleep() se termine normalement.

    Ajouter à ppal() une nouvelle boucle utilisant Nanosleep(), jusqu'à ce que le délai, passé en argument de la commande [3], soit épuisé. Comme précédemment, afficher le délai restant à chaque retour de la fonction Nanosleep().

    Compiler et tester.

Première amélioration
    Il serait souhaitable de disposer d'une constante de type CTimespecBase, qu'on appellerait par exemple CstZero, à laquelle n'importe quel délai pourrait être comparé. On devrait pouvoir écrire :
 
CTimespecBase Delai (5);
...
if (CstZero == Delai)
    ...

    Une première solution, triviale, consiste à laisser le soin à l'utilisateur de la construire lui-même :
 
const CTimespecBase CstZero (0); // ou simplement  : const CTimespecBase CstZero; 
...
if (CstZero == Delai)
    ...

    En réalité, les constantes d'une classe doivent être proposées par la classe elle-même. Il suffit de les déclarer et les définir comme membres statiques de la classe.

    En conception objet, une dérivation correspond à une spécialisation : on dérive la classe mammifère en une classe chat par exemple. Dans la pratique de la programmation objets, la dérivation est parfois simplement utilisée pour ajouter quelques commodités à une classe de base. Peu recommandable dans le cas général, car en particulier génératrice de hiérarchies de classes trop volumineuses, cette possibilité est cependant nécessaire, comme nous allons le montrer ici.

    La classe CTimespecBase vous étant fournie, vous ne devez pas en modifier le code. Il ne vous reste plus qu'à la dériver en CTimespec, dans le fichier Time.h placé dans le répertoire include, afin de lui ajouter la déclaration de cette constante. On en profitera pour la doter de quelques possibilités supplémentaires. Dans le fichier Time.cxx correspondant, du répertoire util, ajouter la définition de CstZero (une définition de constante ne doit jamais se trouver dans un fichier inclus, pour éviter d'éventuelles définitions multiples).

    Son utilisation peut être la suivante :
 
CTimespec Delai (5);
...
if (CTimespec::CstZero == Delai)
    ...

    Lorsqu'une classe est dérivée, la nouvelle classe ne possède que les constructeurs par défaut et par recopie. Si sa classe mère ne les possède pas, il peut être nécessaire de les supprimer. Si sa classe mère possède d'autres constructeurs, il peut être nécessaire de donner les mêmes à la classe fille. C'est le cas ici : la classe CTimespec devra proposer deux constructeurs et un destructeur virtuel.

    Effectuer la même modification pour la classe CTimevalBase, dérivée en CTimeval, à laquelle est ajoutée la constante CstZero.

    Dans le fichier exo_02.cxx ajouter une nouvelle boucle en utilisant cette possibilité.

    Mettre à jour les fichiers Makefile, INCLUDE_H et MakeUtil.

    Compiler et tester.

Seconde amélioration
    En C/C++, la valeur 0 est considérée comme faux, toute autre valeur est considérée comme vrai, qui est défini par true = !false. L'origine en est l'assembleur et les deux instructions de rutpure de séquence conditionnelle : JZ et JNZ, respectivement Jump if Zero et Jump if Not Zero, très utiles pour scruter un registre. Dans la pratique, les programmeurs ont l'habitude d'élargir la signification de false à invalide, opération qui a échoué, ou tout simplement nul ou vide et inversement pour true. Par exemple :
 
for (int n = argc; --n; ) ...  // tant que compteur non nul

    ou

if (Read (fdSource ...)) ...   // si lecture non vide

    ou

for (; cin >> i; ) ...         // tant que opération valide

    ou

char pZone = 0;
...
if (pZone) ...                 // si pointeur valide

    Dans cet esprit, il est normal de doter de cette possibilité toute classe dont un représentant peut être considéré comme nul. C'est le cas de CTimespec et CTimeval. Il faut donc pouvoir écrire par exemple :
 
CTimespec Delai (5);
    ...
if (Delai) ...

    Le C++ offre pour cela une très élégante solution, qui consiste à définir dans ces classes l' opérateur de conversion en booléen.

    Après avoir introduit ces modifications dans les deux classes, ajouter une nouvelle boucle en utilisant cette possibilité, puis compiler et tester.

Corrigés : exo_02.cxx    -    Makefile    -    nsSysteme.h    -    nsSysteme.hxx    -    Time.h    -    Time.hxx    -    Time.cxx    -    INCLUDE_H    -    MakeUtil

Sommaire


Fonction alarm() - Timer

exo_03

    La fonction système alarm() arme un sablier (timer) non cyclique. A échéance du délai, un signal SIGALRM est reçu par le processus. La fonction ne renvoie pas d'erreur donc il est inutile d'en écrire un wrapper.

    Recopier le fichier exo_02.cxx dans exo_03.cxx. Ecrire la fonction ppal() qui :

    Compiler et tester.

    Dans l'espace de noms anonyme, écrire la fonction Chrono(), de profil :
 
void Chrono (ostream & os, const string & Msg, int Periode, int NbreCycles);

qui répète NbreCycles  fois la boucle formée de la séquence :

    Si NbreCycles est négatif en entrée, la boucle doit être infinie.

    Dans ppal(), ajouter la séquence qui :

    Compiler et tester. On constate qu'il n'y a pas d'interférence visible entre les délais gérés par alarm() et par sleep().

Corrigé : exo_03.cxx

Sommaire


Sablier cyclique

    Unix offre à chaque processus la possibilité d'utiliser trois sabliers logiques cycliques (habituellement appelés simplement timers Unix), dénotés par les trois macros ITIMER_REAL, ITIMER_VIRTUAL et ITIMER_PROF (revoir si nécessaire le cours correspondant). Ils utilisent une structure itimerval, qui présente les mêmes inconvénients que les structures timeval et timespec étudiées ci-dessus.

Classe CItimervalBase

    La classe CItimervalBase est mise à votre disposition dans les fichiers TimeBase.* que vous avez récupérés. Elle est obtenue par double dérivation :     Elle offre les fonctionnalités suivantes :     De nouveau, passez un peu de temps à étudier les spécifications et l'implémentation de cette classe.

 


Classes CTimer...

    Comme indiqué plus haut, il ne peut y avoir qu'un seul timer par type (VIRTUAL, REAL ou PROF). Cette sorte de classes est appelée singleton dans la méthodologie d'analyse objet appelée "design pattern". L'implémentation que nous en ferons est légèrement différente de la solution habituellement adoptée dans de telles circonstances.

    Cependant, les trois timers fonctionnent de la même façon. Il est donc élégant de fournir un "modèle" de timer fournissant le prototype (classe abstraite). On ne peut pas parler ici de classe d'interface (réviser le cours de Java) car certaines des fonctions de la classe abstraite peuvent déjà être implémentées.

    Cette classe CTimerAbstr vous est fournie dans les fichiers CTimerAbstr.h et CTimerAbstr.hxx (à récupérer dans les chemins d'accès aux sources des corrigés).

    Obtenue par dérivation protégée de la classe CItimervalBase, elle fournit :

    Aucune de ces fonctions n'est sensée lever d'exception.

    De plus, aucune sorte de timer ne doit autoriser la duplication, tant par le constructeur par recopie que par l'opérateur d'affectation. Il suffit donc que la classe mère ne les autorise pas : les classes filles auront bien ces fonctions par défaut, mais toute tentative d'utilisation appellera les fonctions correspondantes de la classe mère, ce qui déclanchera une erreur de compilation.

    Dans les fichiers CTimer.h, CTimer.hxx et CTimer.cxx (répertoires include et util), créer les classes CTimerVirtual, CTimerProf et CTimerReal dérivées de CTimerAbstr. Lever une exception CException de code CstTropTimers = 143 pour toute tentative de création d'un timer déjà existant.

    Compléter les fichiers INCLUDE_H (macros CTIMERABSTR_H, CTIMERABSTR_HXX, CTIMER_H et CTIMER_HXX), MakeUtil (compilation de CTimer.cxx) et Makefile (ajout de CTimer.o).

Corrigés : CTimerAbstr.h     -     CTimerAbstr.hxx     -     CTimer.h     -     CTimer.hxx     -     CTimer.cxx     -     CstCodErr.h     -     INCLUDE_H     -     MakeUtil     -     Makefile

Sommaire


exo_04

    Dans l'espace de noms anonyme, écrire la fonction Delete(), qui détruit un timer passé en paramètre, de profil suivant :  
 
void Delete (CTimerAbstr * p);
    Compiler et tester. Comparer les fréquences de réception des deux signaux. Avez-vous remarqué le polymorphisme ? Si vous continuez à recevoir des signaux après destruction des timers, c'est qu'il y a un problème dans vos destructeurs !

Corrigé : exo_04.cxx

Sommaire


Mesure de durées par un timer

Temps réel, temps virtuel

    Cet exercice est destiné à mesurer la durée d'exécution d'un programme au moyen de timers VIRTUAL et REAL.

    Recopier le fichier exo_04.cxx dans exo_05.cxx. Ecrire la fonction ppal() qui :

    Compiler et tester.

Corrigé : exo_05.cxx

Sommaire


Mode user, mode kernel

exo_06

    L'objectif de cet exercice est de calculer le temps que passe un processus à effectuer les commutations de contexte et à exécuter des fonctions système. Pour cela, utiliser le timer PROF (temps cumulé écoulé en mode user et en mode kernel) et le timer VIRTUAL (temps écoulé en mode user seulement).

    Recopier le fichier exo_05.cxx dans exo_06.cxx. Supprimer le namespace anonyme et son contenu, tout ce qui concerne les signaux et le traitement final de l'exception. Remplacer les timers dynamiques par des timers "automatiques". Remplacer le timer REAL par un timer PROF. Comme ci-dessus, initialiser les deux timers au moyen de la même constante CTimeval, de valeur importante (par ex. 100 secondes), l'intervalle étant fixé à 0.

    Remplacer la boucle par :

    Compiler et tester. Relancer plusieurs fois l'exécution et comparer les temps moyens des différentes opérations.

Remarques :

  1. La granularité des timers est assez importante sur cette installation (de l'ordre de 10 ms). Donc des durées inférieures ne peuvent pas être mesurées. En conséquence, prendre un fichier source volumineux.
  2. Il existe un moyen beaucoup plus simple d'obtenir ces informations, ainsi que beaucoup d'autres concernant le déroulement d'un processus, grâce à la fonction système getrusage(), que vous pouvez trouver dans le man correspondant.
Corrigés : exo_06.cxx

Sommaire


Téléchargement des corrigés :

tptimer.zip

Chemins d'accès aux sources des corrigés :

~mathieu/PARTAGE/src/tp/tpsys/tptimer/dirtimer
~mathieu/PARTAGE/src/tp/tpsys/tptimer/util
~mathieu/PARTAGE/src/tp/tpsys/tptimer/include


[1] Il règne sous Linux une grande confusion dans la localisation dans les fichiers inclus des identificateurs concernant le temps :

    La solution que nous préconisons est donc d'inclure <sys/time.h> de préférence à <time.h>.

[2] le fichier <sys/time.h> comporte deux macros permettant de convertir des timeval en timespec et inversement.
 
#define TIMEVAL_TO_TIMESPEC(tv, ts) {                                   \
        (ts)->tv_sec = (tv)->tv_sec;                                    \
        (ts)->tv_nsec = (tv)->tv_usec * 1000;                           \
}

#define TIMESPEC_TO_TIMEVAL(tv, ts) {                                   \
        (tv)->tv_sec = (ts)->tv_sec;                                    \
        (tv)->tv_usec = (ts)->tv_nsec / 1000; 

Il contient aussi des macros de manipulation de timeval : timercmp, timeradd, timersub, etc...

[3] Rappel : une chaîne de caractères (NTCTS) peut être transformée en entier grâce à aux fonctions C atoi() ou strtol()

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique