4. Temporisation de haute précision

4.1. Temporisations

Avant toutes choses, il est important de préciser l'impossibilité de garantir un contrôle précis des temps d'exécution de processus en mode utilisateur du fait de la nature multitâche du noyau Linux. Votre processus peut être mis en sommeil à n'importe quel moment pour une durée allant de 10 millisecondes à quelques secondes (sur un système dont la charge est très importante). Malgré tout, pour la plupart des applications utilisant les ports d'entrées / sorties, cela n'est pas très important. Si vous voulez minimiser cet inconvénient, vous pouvez donner à votre processus une priorité plus haute (reportez-vous à la page de manuel de nice(2)) ou faire appel à l'ordonnancement temps-réel (voir ci-après).

Si vous souhaitez obtenir une précision de temporisation plus élevée que celle qu'offre les processus en mode utilisateur usuels, sachez qu'il existe des possibilités de support « temps-réel » en mode utilisateur. Les noyaux Linux de la série 2.x permettent de travailler en quasi temps-réel. Pour les détails, reportez-vous à la page de manuel de sched_setscheduler(2). Il existe également des versions spéciales du noyau offrant un vrai ordonnancement temps-réel.

4.1.1. Avec sleep() et usleep()

Commençons tout d'abord par les appels de temporisation les plus simples. Pour des temporisation de plusieurs secondes, le meilleur choix est probablement la fonction sleep(). Pour des durées au minimum de l'ordre de dizaines de millisecondes (10 millisecondes semblent être la durée minimum), usleep() devrait s'avérer suffisant. Ces fonctions libèrent l'accès au microprocesseur pour d'autres processus, évitant ainsi le gaspillage du temps machine. Les pages de manuel de sleep(3) et usleep(3) vous donneront plus de précisions.

Pour des temporisations de moins de 50 millisecondes (en fonction de la cadence du microprocesseur, de la machine ainsi que de la charge du système), redonner le processeur aux autres processus prend énormément de temps. En effet l'ordonnanceur des tâches du noyau Linux (en tout cas pour les microprocesseurs de la famille x86) prend généralement au moins 10 à 30 millisecondes avant de rendre le contrôle au processus. De ce fait, dans les temporisations de courte durée, usleep(3) effectue en réalité une pause plus longue que celle spécifiée en paramètre, prenant au moins 10 millisecondes supplémentaires.

4.1.2. nanosleep()

Dans les noyaux Linux de la série 2.0.x est apparu l'appel système nanosleep() (voir la page de manuel de nanosleep(2)) permettant d'endormir ou de retarder un processus pendant un laps de temps très court (quelques microsecondes ou plus).

Pour des attentes ≤ 2 millisecondes, si (et seulement si) votre processus fonctionne en ordonnancement quasi temps-réel (au moyen de sched_setscheduler()), nanosleep() fait appel à une boucle d'attente ; si tel n'est pas le cas, le processus s'endort simplement tout comme avec usleep().

La boucle d'attente utilise udelay() (une fonction interne au noyau utilisée par beaucoup de pilotes), la durée de celle-ci étant calculée en fonction du nombre de BogoMips. La vitesse de ce type de boucle d'attente est une des grandeurs que les BogoMips permettent de mesurer de façon précise. Voyez /usr/include/asm/delay.h pour plus de détails quant à son fonctionnement.

4.1.3. Temporisations grâce aux ports d'entrée / sortie

Les accès aux ports d'entrée / sortie sont un autre moyen d'obtenir des temporisations. L'écriture ou la lecture d'un octet sur le port 0x80 (voir ci-dessus la procédure à suivre) devrait avoir pour conséquence un retard d'une microseconde, indépendamment du type et de la cadence du microprocesseur. Vous pouvez donc procéder de la sorte afin d'obtenir un retard de quelques microsecondes. L'écriture sur ce port ne devrait pas avoir d'effets secondaires sur une machine classique, pour preuve certains pilotes du noyau font appel à cette méthode. C'est également de cette manière que {in|out}[bw]_p() effectue une pause (voir asm/io.h).

Plus précisément, une opération de lecture ou d'écriture sur la plupart des ports dans l'intervalle 0x000-0x3ff prend 1 microseconde. Par exemple, si vous utilisez directement le port parallèle, il suffit d'utiliser des inb() additionnels sur ce port pour obtenir une temporisation.

4.1.4. Temporisations en assembleur

Si vous connaissez le type et la fréquence du processeur de la machine sur laquelle votre programme va s'exécuter, vous pouvez coder en dur les temporisations en faisant appel à certaines instructions assembleur. Rappelez-vous cependant qu'à tout moment votre processus peut-être mis en attente par l'ordonnanceur et, de ce fait, les temporisations peuvent s'avérer plus longues que souhaitées.

Pour les données du tableau ci-dessous, la fréquence interne du microprocesseur détermine le nombre de cycles d'horloge consommés. Par exemple, pour un microprocesseur à 50 Mhz (un 486DX-50), un cycle d'horloge dure 1/50000000 de seconde (soit 200 nanosecondes).

Instructioncycles d'horloge i386cycles d'horloge i486
xchg %bx,%bx33
nop31
or %ax,%ax21
mov %ax,%ax21
add %ax,021

Les cycles d'horloges du Pentium devraient être les mêmes que ceux du 486, à l'exception du Pentium Pro / II, dont l'instruction add %ax, 0 peut ne consommer qu'un demi cycle d'horloge. Cette instruction peut-être parfois être combinée avec une autre (cependant, du fait de l'algorithme d'exécution hors de séquence (out-of-order), il n'est pas nécessaire qu'il s'agisse d'une instruction consécutive dans le flot).

Les instructions nop et xchg du tableau ne devraient pas avoir d'effets secondaires. Les autres, en revanche, peuvent modifier le registre d'état, mais cela reste sans gravité puisque gcc devrait le détecter. xchg %bx,%bx reste un bon compromis comme instruction de temporisation.

Pour utiliser ces instructions, placez un appel asm("instruction") dans votre programme. La syntaxe d'appel de ces instructions est telle qu'énumérée dans le tableau ci-dessus. Si vous préférez grouper plusieurs instructions dans le même appel à asm, il vous suffit de les séparer par des points-virgules. Par exemple asm("nop ; nop ; nop ; nop") exécute quatre fois l'instruction nop, effectuant une temporisation de quatre cycles d'horloge sur un i486 ou un Pentium (ou douze cycles sur un i386).

Les instructions asm() sont directement intégrées au code par gcc, évitant ainsi la perte de temps que pourrait engendrer un appel de fonction classique.

Les temporisations de moins d'un cycle d'horloge sont impossibles sur les architectures x86 d'Intel.

4.1.5. rdtsc pour Pentium

Avec les microprocesseurs Pentium, vous avez la possibilité de connaître le nombre de cycles d'horloge écoulés depuis le dernier redémarrage avec le code C suivant (qui fait appel à l'instruction appelée RDTSC) :

	    extern __inline__ unsigned long long int rdtsc()
 {
 unsigned long long int x;
 __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
 return x;
 }

Vous pouvez scruter cette valeur dans une boucle d'attente afin d'obtenir un retard correspondant au nombre de cycles d'horloge que vous souhaitez.

4.2. Mesure du temps

Pour des durées de la précision d'une seconde, il est certainement plus simple d'utiliser la fonction time(). Pour obtenir plus de précision, gettimeofday() a une précision d'environ une microseconde (mais rappelez-vous de l'ordonnancement déjà évoqué précédemment). Pour les Pentium, le fragment de code rdtsc ci-dessus est précis au cycle d'horloge près.

Si vous souhaitez que votre processus reçoive un signal après un certain laps de temps, utilisez setitimer() ou alarm(). Voyez les pages de manuel de ces fonctions pour plus de détails.