Bien que cela ne fasse pas encore tout à fait l’unanimité dans la communauté informatique et industrielle, l’expression système temps réel a une signification plus précise que lors de son apparition. Il y a plusieurs définitions du temps réel. Plusieurs d’entre eux sont contradictoires. Malheureusement le sujet est controversé et il ne semble pas y avoir un consensus sur cette terminologie.
Les applications utilisées en milieu industriel sont très souvent des applications temps réel très différentes des applications de gestion à plusieurs points de vue:
|
Application temps réel |
Application de gestion |
Entrées et |
|
|
Délais |
|
|
Environnement |
|
|
Fiabilité |
|
|
Architecture de l’application |
|
|
Les applications industrielles, médicales et militaires sont souvent des applications en temps réel, car elles comportent plusieurs des caractéristiques de la première colonne. Dans l’optique du cours actuel, les applications suivantes ne sont pas des applications temps réel, quoi qu’en dise le langage courant:
Dans ces derniers cas, c’est plutôt l’ergonomie (confort) plutôt que les contraintes technologiques qui commande un temps de traitement rapide. On devrait dans ce cas (pour être plus juste) parler d’application interactive ou à réponse rapide.
On a parlé de système déterministe. Que veut-on dire au juste par déterminisme ? En termes simples, le déterminisme d’un système est la propriété qui nous permet de prédire son comportement dans toutes ses situations d’exécution. De façon générale, cette propriété est extrêmement importante dans tous les secteurs de l'informatique. Si le système se met à dérailler de façon imprévisible, il n’est pas déterministe. Demandons-nous si les logiciels distribués à grande échelle sont déterministes… Un guichet automatique (qui n’est pas un système temps réel, mais est un système informatique tout de même) doit être déterministe. Son comportement doit avoir été prévu afin de répondre adéquatement à chaque situation qui se présente. Un tel système informatique doit pouvoir réagir de façon adéquate lorsque l’utilisateur dérobe à la séquence normale des opérations : Entrer la carte, Entrer son NIP, Choisir une opération…. Le système doit en effet pouvoir traiter la séquence d’actions suivantes sans broncher : Entrer la carte, Choisir une opération, Entrer des nombres au hasard… … et le concepteur doit même pouvoir dire dans quel état doit se trouver le système après une séquence donnée d’opérations quelconques.
Dans un système en temps réel, le déterminisme inclut la capacité de prédire le temps d’exécution maximal des tâches impliquées. Il faut savoir à l’avance que telle ou telle séquence d’opération se fera à l’intérieur de tel ou tel délai.
Prenons l’exemple d’un réservoir hermétique muni d’un capteur de pression et d’une soupape de sécurité. Le délai entre
la lecture d’une pression trop élevée et
l’ouverture d’une soupape de sécurité
doit être connu pour savoir si les catastrophes seront toujours évitées. Ici, tous les processus requis, tant matériels que logiciels, doivent avoir été minutés soigneusement avant la mise en marche du système, et dans plusieurs cas de figure. Il est souvent impossible de connaître les délais exacts ; il est important dans ce cas de disposer d’une borne supérieure estimée sur le délai, et de savoir si ce temps maximal de réaction est inférieur au temps que prend la catastrophe pour se produire.
On a donc les relations suivantes entre les délais impliqués, dans le cas des traitements relatifs à une situation donnée :
Pour affirmer qu’un système est déterministe (qu'il respecte ses délais), il faut qu'en tout temps :
t < Test et Test < T
C’est la seule manière de s’assurer que t < T et de garantir que les situations critiques sont prises en charge à temps. Ces calculs sont souvent difficiles à réaliser. Les résultats obtenus dépendent non seulement des algorithmes utilisés dans les traitements, mais de pratiquement tout ce qui a contribué ou contribue au système :
Nous reviendrons sur ces concepts lorsque nous parlerons du traitement des interruptions, qui sont souvent les mécanismes les plus critiques dans un système.
Avant d’en arriver aux systèmes d’exploitation temps réel et exécutifs sophistiqués d’aujourd’hui, les concepteurs de systèmes ont tenté de concevoir des applications qui rencontraient leurs délais à l’intérieur de systèmes monotâches. Il fallait faire en sorte que chaque « tâche » puisse obtenir un peu de temps pour utiliser les ressources du processeur. Un bon moyen de le faire est de concevoir une tâche en boucle - que l'on nomme aussi un exécutif cyclique - qui fait appel à plusieurs sous-programmes. Nous verrons que pour peu que le problème se complexifie, la solution en boucle devient difficile à réaliser.
On écrit une application qui doit simuler un terminal. Des données à afficher proviennent d’un port série RS-232. La seule fonction du terminal est d’afficher les caractères reçus à l’écran, et de diriger les caractères lus au clavier sur le port série. Le port série a une vitesse maximale de 960 caractères par seconde, le clavier, une vitesse maximale de 10 caractères par seconde et l'écran 1000 caractères par seconde. L’illustration donne un bref aperçu du système. On identifie deux tâches indépendantes dans ce système :
RecevoirEtAfficherCaracteres() { Si caractère disponible sur le port série { Lire le caractère sur le port série Afficher le caractère à l’écran } } LireEtEnvoyerCaracteres() { Si caractère disponible dans le tampon du clavier { Lire le caractère dans le tampon Écrire le caractère sur le port série } } void main() { while( !Fin ) { RecevoirEtAfficherCaractere(); LireEtEnvoyerCaractere(); } } |
Les tâches se succédent afin que chacune obtienne la possibilité de s’exécuter. Il est important que les fonctions soient non-bloquantes pour laisser la chance à la fonction suivante de s'exécuter. La technique utilisée ici pour déterminer la disponibilité d’un caractère sur le port série ou dans le tampon du clavier est appelé scrutation (en anglais : polling). Dans cette boucle il y a de l'attente active car le processeur est utilisé même s'il n'y aucun échange de caractères. Ce n’est vraiment pas le moyen le plus efficace de procéder si d’autres programmes doivent exécuter sur le même système! En effet, la boucle exécutera en consommant les ressources du processeur, tandis qu’il est possible d’attendre sagement que le système nous avise qu’un caractère est disponible. Cette dernière solution est le mécanisme des interruptions.
Question: Qu'elle est la contrainte de temps de ce terminal?
Puisqu'en un tour de boucle l'exécutif cyclique doit traiter les deux entrées, la boucle doit suivre l'entrée la plus rapide soit le port série. La boucle doit donc s'exécuter au moins 960 fois par seconde. 960 tours par seconde est un fréquence. Le temps étant l'inverse de la fréquence, T = 1/f = 1/960 = 1,04 ms.
void main() { while( !Fin ) { if(TempsEcoule) { RecevoirEtAfficherCaractere(); LireEtEnvoyerCaractere(); TempsEcoule = FAUX; } } }Cette deuxième solution, bien que régulée, a toutes les caractéristiques d'un exécutif cyclique: les fonctions doivent être non-bloquantes, elle utilise la scrutation et il y a de l'attente active sur la variable TempsEcoule.
Avantages:
Désavantages:
La solution idéale consiste à séparer le problème en deux threads, un exécutif cyclique distincts par thread, et de réguler chacune des boucles à l'aide d'interruptions. Chacune des boucles pourra donc être régulée à des vitesses différentes, la première sera régulée à une vitesse de 1000 tours par seconde (la vitesse maximale du port série étant 960 caractères par seconde) tandis que la deuxième sera régulée à 10 tours par seconde (la vitesse maximale du clavier étant 10 caractères par seconde):
RecevoirEtAfficherCaracteres() { Demander au RTOS de générer des interruption à toutes les millisecondes while( !Fin ) { Si caractère disponible sur le port série { Lire le caractère sur le port série Afficher le caractère à l’écran } Attendre la prochaine interruption } } LireEtEnvoyerCaracteres() { Demander au RTOS de générer des interruption à toutes les 100 millisecondes while( !Fin ) { Si caractère disponible dans le tampon du clavier { Lire le caractère dans le tampon Écrire le caractère sur le port série } Attendre la prochaine interruption } } void main() { DémarrerThread( RecevoirEtAfficherCaracteres ); DémarrerThread( LireEtEnvoyerCaracteres ); Attendre fin de l'application }
Cette solution a plusieurs avantages:
Mais il faudra prendre garde aux désavantages classiques de la programmation multitâche: