Définition d'un système temps réel

Temps partagé et temps réel

La gestion du temps est l'un des problèmes majeurs des systèmes d'exploitation. La raison est simple : les système d'exploitation modernes sont tous multitâche, or ils utilisent du matériel basé sur des processeurs qui ne le sont pas, ce qui oblige le système à partager le temps du processeur entre les différentes tâches. Cette notion de partage implique une gestion du passage d'une tâche à l'autre qui est effectuée par un ensemble d'algorithmes appelé ordonnanceur (scheduler en anglais).

Un système d'exploitation classique comme UNIX, LINUX ou Windows utilise la notion de temps partagé, par opposition au temps réel. Dans ce type de système, le but de l'ordonnanceur est de donner à l'utilisateur une impression de confort tout en s'assurant que toutes les tâches demandées sont finalement exécutées. Ce type d'approche entraîne une grande complexité dans la structure même de l'ordonnanceur qui doit tenir compte de notions comme la régulation de la charge du système ou la date depuis laquelle une tâche donnée est en cours d'exécution. De ce fait, on peut noter plusieurs limitations par rapport à la gestion du temps.

Tout d'abord, la notion de priorité entre les tâches est peu prise en compte, car l'ordonnanceur a pour but premier le partage équitable du temps entre les différentes tâches du système (on parle de quantum de temps ou tick). Notez que sur les différentes versions d'UNIX dont LINUX, la commande nice permet de modifier la priorité de la tâche au lancement.

Ensuite, les différentes tâches doivent accéder à des ressources dites partagées, ce qui entraîne des incertitudes temporelles. Si une des tâches effectue une écriture sur le disque dur, celui-ce n'est plus disponible aux autres tâches à un instant donné et le délai de disponibilité du périphérique n'est donc pas prévisible.

En outre, la gestion des entrées/sorties peut générer des temps morts car une tâche peut être bloquée en attente d'accès à un élément d'entrée/sortie. La gestion des interruptions reçues par une tâche n'est pas optimisée. Le temps de latence - soit le temps écoulé entre la réception de l'interruption et son traitement - n'est pas garanti par le système.

Enfin, l'utilisation du mécanisme de mémoire virtuelle peut entraîner des fluctuations importante s dans les temps d'exécution des tâches.

Notion de temps réel

Le cas des systèmes temps réel est différent. Il existe un grande nombre de définition d'un système dit temps réel mais une définition simple d'un tel système pourra être la suivante :


Un système temps réel est une association logiciel/matériel où le logiciel permet, entre autre, une gestion adéquate des ressources matérielles en vue de remplir certaines tâches ou fonctions dans des limites temporelles bien précises.


Un autre définition pourrait être :


"Un système est dit temps réel lorsque l'information après acquisition et traitement reste encore pertinente".


Ce qui signifie que dans le cas d'une information arrivant de façon régulière (sous forme d'une interruption périodique du système), les temps d'acquisition et de traitement doivent rester inférieurs à la période de rafraîchissement de cette information.


Il est évident que la structure de ce système dépendra de ces fameuses contraintes. On pourra diviser les systèmes en deux catégories :


  1. Les systèmes dits à contraintes souples ou molles (soft real time). Ces systèmes acceptent des variations dans le traitement des données de l'ordre de la demi-seconde (ou 500 ms) ou la seconde. On peut citer l'exemple des systèmes multimédia : si quelques images ne sont pas affichées, cela ne met pas en péril le fonctionnement correct de l'ensemble du système. Ces systèmes se rapprochent fortement des systèmes d'exploitation classiques à temps partagé. Ils garantissent un temps moyen d'exécution pour chaque tâche. On a ici une répartition égalitaire du temps CPU entre processus.

  2. Les systèmes dits à contraintes dures (hard real time) pour lesquels une gestion stricte du temps est nécessaire pour conserver l'intégrité du service rendu. On citera comme exemples les contrôles de processus industriels sensibles comme la régulation des centrales nucléaires ou les systèmes embarqués utilisés dans l'aéronautique. Ces systèmes garantissent un temps maximum d'exécution pour chaque tâche. On a ici une répartition totalitaire du temps CPU entre tâches.


Les systèmes à contraintes dures doivent répondre à trois critères fondamentaux :


  1. Le déterminisme logique : les mêmes entrées appliquées au système doivent produire les mêmes effets.

  2. Le déterminisme temporel : un tâche donnée doit obligatoirement être exécutée dans les délais impartis, on parle d'échéance.

  3. La fiabilité : le système doit être disponible. Cette contrainte est très forte dans le cas d'un système embarqué car les interventions d'un opérateur sont très difficiles voire même impossibles. Cette contrainte est indépendante de la notion de temps réel mais la fiabilité du système sera d'autant plus mise à l'épreuve dans le cas de contraintes dures.


Un système temps réel n'est pas forcément plus rapide qu'un système à temps partagé. Il devra par contre satisfaire à des contraintes temporelles strictes, prévues à l'avance et imposées par le processus extérieur à contrôler. Une confusion classique est de mélanger temps réel et rapidité de calcul du système donc puissance du processeur (microprocesseur, micro-contrôleur, DSP). On entend souvent :


« Être temps réel, c'est avoir beaucoup de puissance : des MIPS voire des MFLOPS


Ce n'est pas toujours vrai. En fait, être temps réel dans l'exemple donné précédemment, c'est être capable d'acquitter l'interruption périodique (moyennant un temps de latence d'acquittement d'interruption imposé par le matériel), traiter l'information et le signaler au niveau utilisateur (réveil d'une tâche ou libération d'un sémaphore) dans un temps inférieur au temps entre deux interruptions périodiques consécutives. On est donc lié à la contrainte de délai entre deux interruptions générées par le processus extérieur à contrôler.

Si cette durée est de l'ordre de la seconde (pour le contrôle d'une réaction chimique par exemple), il ne sert à rien d’avoir un système à base de Pentium IV ! Un simple processeur 8 bits du type micro-contrôleur Motorola 68HC11, Microchip PIC, Scenix, AVR... ou même un processeur 4 bits fera amplement l'affaire, ce qui permettra de minimiser les coûts sur des forts volumes de production.

Si ce temps est maintenant de quelques dizaines de microsecondes (pour le traitement des données issues de l'observation d'une réaction nucléaire par exemple), il convient de choisir un processeur nettement plus performant comme un processeur 32 bits Intel x86, StrongARM ou Motorola ColdFire.

L'exemple donné est malheureusement idyllique (quoique fréquent dans le domaine des télécommunications et réseaux) puisque notre monde interagit plutôt avec un système électronique de façon apériodique.

Il convient donc avant de concevoir ledit système de connaître la durée minimale entre deux interruptions, ce qui est assez difficile à estimer voire même impossible. C'est pour cela que l’on a tendance à concevoir dans ce cas des systèmes performants (en terme de puissance de calcul CPU et de rapidité de traitement d’une interruption) et souvent sur-dimensionnés pour respecter des contraintes temps réel mal cernées à priori. Ceci induit en cas de sur-dimensionnement un sur-coût non négligeable.


En résumé, on peut dire qu'un système temps réel doit être prévisible (predictible en anglais), les contraintes temporelles pouvant s'échelonner entre quelques micro-secondes (µs) et quelques secondes.


La figure suivante permet d'illustrer la notion de temps réel sur le cas particulier de l'exécution d'une tâche périodique. Idéalement, une tâche périodique doit être exécutée toutes les m secondes (son temps d'exécution reste inférieur à m). Dans le cas d'un système non temps réel, un temps de latence apparaît avant l'exécution effective de la tâche périodique. il varie fortement au cours du temps (charge du système...). Dans le cas d'un système temps réel, ce temps de latence doit être borné et garanti inférieur à une valeur fixe et connue à l'avance. Si ce n'est pas le cas, il y a un défaut de fonctionnement pouvant causer le crash du système. Les éditeurs de systèmes temps réel donnent généralement cette valeur qui est fixe (si le système est bien conçu) quelle que soit la charge du système ou du nombre de tâches en fonction du processeur utilisé.




Figure 1. Tâche périodique et temps réel


Une petite expérience

L'expérience décrite sur la figure ci-dessous met en évidence la différence entre un système classique et un système temps réel. Elle est extraite d'un mémoire sur le temps réel réalisé par William Blachier à l'ENSIMAG en 2000.




Figure 2. Test comparatif temps réel/temps partagé

Le but de l'expérience est de générer un signal périodique sortant du port parallèle du PC. Le temps qui sépare deux émissions du signal sera mesuré à l'aide d'un compteur. Le but est de visualiser l'évolution de ce délai en fonction de la charge du système. La fréquence initiale du signal est de 25 Hz (Hertz) ce qui donne une demi-période T/2 de 20 ms.

Sur un système classique, cette demi-période varie de 17 à 23 ms, ce qui donne une variation de fréquence entre 22 Hz et 29 Hz. La figure ci-dessous donne la représentation graphique de la mesure sur un système non chargé puis à pleine charge :




Figure 3. Représentation sur un système classique

Sur un système temps réel, la demi-période varie entre 19,990 ms et 20,015 ms, ce qui donne une variation de fréquence de 24,98 Hz à 25,01 Hz. La variation est donc beaucoup plus faible. La figure donne la représentation graphique de la mesure :




Figure 4. Représentation sur un système temps réel

Lorsque la charge est maximale, le système temps réel assure donc une variation de +/- 0,2 Hz alors que la variation est de +/- 4 Hz dans le cas d'un système classique.


Ce phénomène peut être reproduit à l'aide du programme square.c écrit en langage C.


#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <asm/io.h>

#define LPT 0x378

int ioperm();

int main(int argc, char **argv)
{
    setuid(0);

    if (ioperm(LPT, 1, 1) < 0) {
	perror("ioperm()"); 
	exit(-1);
    }

    while(1) {
    	outb(0x01, LPT);
	usleep(50000);
	
    	outb(0x00, LPT);
	usleep(50000);
    }
    return(0);
}

Le signal généré sur la broche 2 (bit D0) du port parallèle est théoriquement un signal périodique carré de demi-période T/2 de 50 ms. On observe à l'oscilloscope le signal suivant sur un système non chargé (AMD Athlon 1500+).





Figure 5. Génération d'un signal carré sous Linux non chargé


On remarque que l'on n'a pas une période de 100 ms mais de 119,6 ms dû au temps supplémentaire d'exécution des appels système. Dès que l'on stresse le système (écriture répétitive sur disque d'un fichier de 50 Mo), on observe le signal suivant :









Figure 6. Génération d'un signal carré sous Linux chargé

On observe maintenant une gigue (jitter) sur le signal généré. La gigue maximale sur la durée de l'expérience est de 17,6 ms. La forme du signal varie maintenant au cours du temps, n'est pas de forme carrée mais rectangulaire. LINUX n'est donc plus capable de générer correctement ce signal. Il faut noter aussi que le front montant sur la figure précédente apparaît sans gigue car il a servi comme front de synchronisation de l'oscilloscope. La gigue observée est donc à voir comme la contribution de la gigue sur front montant et sur front descendant. Si l'on diminue la valeur de la demi-période, la gigue devient aussi importante que cette dernière et dans ce cas, Linux ne génère plus aucun signal !


Cette expérience met donc en évidence l'importance des systèmes temps réel pour certaines applications critiques.

Préemption et commutation de contexte

Le noyau (kernel) est le composant principal d'un système d'exploitation multitâche moderne. Dans un tel système, chaque tâche (ou processus) est décomposée en threads (processus léger ou tâche légère) qui sont des éléments de programmes coopératifs capables d'exécuter chacun une portion de code dans un même espace d'adressage. Chaque thread est caractérisé par un contexte local contenant la priorité du thread, ses variables locales et l'état de ses registres. Le passage d'un thread à un autre est appelé changement de contexte (context switch). Ce changement de contexte sera plus rapide sur un thread que sur un processus car les threads d'un processus évoluent dans le même espace d'adressage ce qui permet le partage des données entre les threads d'un même processus. Dans certains cas, un processus ne sera composé que d'un seul thread et le changement de contexte s'effectuera sur le processus lui-même.

Dans le cas d'un système temps réel, le noyau est dit préemptif, c'est à dire qu'un thread peut être interrompu par l'ordonnanceur en fonction du niveau de priorité et ce afin de permettre l'exécution d'un thread de plus haut niveau de priorité prêt à être exécuté. Ceci permet d'affecter les plus hauts niveaux de priorité à des tâches dites critiques par rapport à l'environnement réel contrôlé par le système. La vérification des contextes à commuter est réalisée de manière régulière par l'ordonnanceur en fonction de l'horloge logicielle interne du système, ou tick timer système.

Dans le cas d'un noyau non préemptif - comme le noyau LINUX - un thread sera interrompu uniquement dans le cas d'un appel au noyau ou d'une une interruption externe. La notion de priorité étant peu utilisée, c'est le noyau qui décide ou non de commuter le thread actif en fonction d'un algorithme complexe.