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.
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.
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.
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.
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).
Instruction | cycles d'horloge i386 | cycles d'horloge i486 |
---|---|---|
xchg %bx,%bx | 3 | 3 |
nop | 3 | 1 |
or %ax,%ax | 2 | 1 |
mov %ax,%ax | 2 | 1 |
add %ax,0 | 2 | 1 |
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.
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.
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.