REALISATION d'un ORDONNANCEUR à ECHEANCES
I- PRÉSENTATION... 3 II. DESCRIPTION DU NOYAU ORIGINEL... 4 II.1- ARCHITECTURE... 4 II.2 - SERVICES... 4 III. IMPLÉMENTATION DE L'ORDONNANCEUR À ÉCHÉANCES... 6 III.1- CHOIX RETENUS... 6 III.2- NOUVEAU DESCRIPTEUR DE TÂCHES... 7 III.3- PRIMITIVES CRÉÉES OU MODIFIÉES... 8 III.3.1- PRIMITIVE CRÉER_TÂCHE... 8 III.3.2- PRIMITIVE «FIN DE L OCCURRENCE D UNE TÂCHE PÉRIODIQUE»... 8 III.3.3- PROGRAMME HORLOGE... 9 III.3.4- PRIMITIVE "DÉTRUIRE_TÂCHE"... 9 III.3.5- AUTRES PRIMITIVES... 9 III.4.ALGORITHMES... 10 IV- PROGRAMME DE DÉMONSTRATION... 14 V- CONCLUSION... 15
I- Présentation Nous nous proposons d implémenter un ordonnanceur à échéances afin d étudier la faisabilité, la complexité et les performances d un tel système. Notre tâche est simplifiée du fait que nous avons précédemment développé un noyau préemptif avec un algorithme d ordonnancement classique à priorités. Ce noyau s exécute sur micro-ordinateur compatible PC et offre tous les services classiques de base d un exécutif temps réel, de nombreux modules sont réutilisables sans modifications notamment tout ce qui concerne les I/O, les boîtes aux lettres, les sémaphores et les mécanismes à mettre en oeuvre lors des commutations de contexte. On notera que ce noyau présente déjà une originalité par rapport aux OS du marché : la majorité des requêtes est possible pour des tâches immédiates, c est à dire des tâches déclenchées par interruptions matérielles et dont le fonctionnement est totalement différent de celui des tâches applicatives. Des limitations peuvent toutefois survenir du fait que ce noyau a été développé en assembleur 16 bits, ce qui ne facilite pas la programmation d algorithmes compliqués ni l utilisation de variables adaptées (par exemple, la taille des timers). Les principales révisions à faire concernent le gestionnaire d interruption de l horloge système car c est là que s effectue principalement le travail de l ordonnanceur ainsi que la mise à jour de toutes les variables temporelles. De même, le bloc de contrôle d une tâche initialisé lors de la création d une tâche comprend de nouveaux paramètres, notamment temporels, comme délai critique, période et date de déclenchement. Le traitement des tâches périodiques est également à reconsidérer. Avec l ordonnanceur original, les tâches sont construites comme des boucles infinies mais ici, on doit considérer qu une tâche périodique ayant terminé son travail avant le début de sa période suivante doit être mise en attente périodique. Nous nous proposons donc d'intégrer dans un premier temps les fonctions de base d'un ordonnanceur à échéances, c'est à dire sans fonction de garantie mais avec un mécanisme permettant d'effectuer un traitement d'exception si une tâche vient à commettre une faute temporelle.
II. Description du noyau originel II.1- Architecture Le noyau originel est un exécutif multitâche fonctionnant sur compatible PC dans l'environnement MS-DOS. L'ordonnancement s'effectue selon la politique des priorités empiriques fixées par le programmeur aux tâches applicatives en cours de conception. Sont disponibles 256 niveaux de priorité et on peut modifier la priorité d'une tâche en cours d'exécution. Une application utilisant ce noyau comprend des tâches applicatives qui font appel aux services du noyau par des primitives ou appels-système au moyen d' un mécanisme d'interruption logicielle, le noyau lui-même et des tâches matérielles, toujours prioritaires et déclenchées par interruptions matérielles. Chacun de ces éléments ayant été compilé, les fichiers.obj résultants sont liés en un seul programme exécutable. Les tâches systèmes matérielles implémentées concernent les gestionnaire d'interruption horloge et clavier. Ces tâches sont souvent qualifiées d'isr pour Interrupt Service Request. Un mécanisme d'imbrication permet de chaîner ces tâches matérielles en cas d'appels d'interruption multiples, le réordonnancement du système n'étant effectué qu'après la terminaison des appels d' ISR. Ces tâches matérielles ont la possibilité d'accéder à tous les services du noyau, sauf ceux qui les bloqueraient elles-mêmes. Elles accèdent à ces services par le même mécanisme que les tâches applicatives. Deux appels spéciaux signalent au noyau l'entrée et la sortie d'un programme d' ISR. Un contrôle strict de la validité des paramètres et de la cohérence (au niveau des états) est intégré au noyau. Si une erreur ou une incohérence est détectée par le noyau, le service n'est pas effectué et le noyau renvoie à la tâche appelante un code signalant le type de l'erreur. Dans le cas de paramètres corrects, on renvoie la valeur Code_OK. II.2 - Services Outre les services de base comme Créer_Une_Tâche, Mettre_Une_Tâche_En_Attente,etc.., le noyau offre des mécanismes de communication entre tâches ainsi qu'entre tâches et extérieur.
Les structures implémentées pour aider à la communication et au partage de ressources entre les tâches sont des Boîtes aux Lettres, des Sémaphores et des Evénements. Les Boîtes aux Lettres permettent aux tâches de déposer et de recevoir des messages. La réception d'un message peut être bloquante ou non bloquante si la Boîte aux Lettres est vide. Dans le cas d'un appel bloquant, les descripteurs des tâches mises en attente sont rangées dans une file FIFO. Les Sémaphores sont des sémaphores classiques à compte. Les tâches bloquées sont rangées dans une file ordonnée selon les niveaux de priorité et on peut spécifier un délai maximum d'attente au-delà duquel le tâche est obligatoirement réactivée. Les événements sont des mécanismes de communication sous forme d'un groupe de 16 bits. La mise à 1 ou à 0 d'un bit est significative d'un événement et une tâche peut se mettre en attente d'un groupe d'événements de façon partielle (réveil à la réception d'un seul parmi le groupe d'événement) ou totale (réveil à la réception de tous les événements). En ce qui concerne les communications avec l'extérieur, les tâches peuvent se mettre en attente d'un caractère quelconque entré au clavier, une seule tâche peut se mettre en attente d'un caractère spécial. En ce qui concerne l'affichage, les tâches peuvent demander au noyau d'afficher un caractère à l'écran. L'affichage s'effectue directement en mémoire vidéo en mode alphanumérique. On trouvera en annexe les spécifications complètes des différents services. Le fichier Init.inc liste les états d'une tâche et le code des différentes primitives.
III. Implémentation de l'ordonnanceur à échéances III.1- Choix retenus Dans un premier temps, nous nous proposons de construire un système minimal avec les appels modifiés "Créer_Tâche", "Détruire_Tâche", le nouveau programme d'horloge et la nouvelle primitive "Fin_Occurrence_Périodique". En ce qui concerne Earliest Deadline lui-même, nous avons choisi d'ordonner les files à priorité selon les délais critiques dynamiques croissants. Ce choix est motivé par plusieurs raisons : 1) La taille des compteurs et registres car, transmettre une date d échéance en paramètre nécessiterait des registres dont nous ne disposons pas. 2) Il est plus facile au programmeur de fixer des délais relatifs à une date de déclenchement plutô0t que de calculer des dates d échéance par rapport à un zéro universel. 3) On élimine le besoin de recalculer les dates d échéance pour les tâches périodiques. 4) On intègre ainsi plus facilement un mécanisme de régularisation, moins couteux qu une fonction de garantie mais tout aussi efficace (bien qu à postériori) et plus facilement modifiable par le programmeur à l aide de procédures d exception comme nous l expliquons ci-dessous. 5) La longueur du programme horloge est nettement plus conséquente, (en lignes de code) mais la plupart des instructions sont des décrémentations, incrémentations très rapides (deux cycles processeur soit 2.10-8 pour un processeur à 100 MHZ ). Ce n est que dans le cas d un nombre très important de tâches à traiter ou fautives que l on pourrait avoir un débordement temporel du programme horloge. Comme nous l avons dit précédemment, aucune fonction de garantie n'est implémentée, mais un mécanisme d'exception est mis en place au cas où une tâche commettrait une faute temporelle. Le programmeur peut associer un code d'exception à chaque tâche. Dans le cas où le programmeur n'a demandé aucune procédure d'exception, le noyau détectant une faute temporelle effectue les opérations suivantes : détruire l'occurrence en cours de la tâche si celle-ci est périodique, détruire la tâche si elle est occasionnelle. Le mécanisme d'exception fonctionne sur le même principe que celui lié aux interruptions matérielles.
Les tâches sont susceptibles de connaître deux nouvels états : "Non_Déclenchée" et "En_Attente_Périodique". Une file à double entrée peut contenir les descripteurs des tâches non déclenchées et des tâches mises en attente périodique. On peut envisager de transformer la primitive «Changer_Priorité» en «Changer_Délai_Critique», après avoir posé le moment où ce changement sera effectué pour une tâche périodique en cours d exécution, c est à dire soit au cours de la période courante (si cela est possible) soit au cours de la période suivante (ce qui implique une mémorisation de la demande). De même, en ce qui concerne une demande de changement de période. Par ailleurs, on doit envisager de développer des fonctions d aide à la connaissance du déroulement des tâches comme «Lire_Délai_Critique», «Lire_Temps_Ecoulé_Depuis_Début_Exécution»,etc... III.2- Nouveau descripteur de tâches On doit compléter notre descripteur de tâche en y intégrant les éléments suivants :. Délai critique initial : on doit enregistrer la valeur initiale d'un délai critique afin de réinitialiser chaque tâche au débût de chaque occurence périodique.. Délai critique dynamique : cette valeur est décrémentée à chaque appel d'horloge.. Date de déclenchement : si cette valeur est non nulle, la tâche est à déclenchement retardé.. Temps d'exécution initial : bien que n'utilisant pas encore cet élément, nous l'avons intégré à notre descripteur.c est une estimation du programmeur.. Temps d'exécution dynamique : même remarque que précédemment.. Importance : nous n'utilisons pas encore ce critère mais 256 niveaux d'importance sont prévus.. Temps_Ecoulé : ce compteur permet de mesurer le temps écoulé depuis le débût d'exécution d'une tâche.. Période : ce paramètre est nul pour une tâche apériodique.. Segment de code du programme d'exception ou la valeur nulle si aucun programme adapté à la tâche n'est prévu.
III.3- Primitives créées ou modifiées III.3.1- Primitive Créer_Tâche Lorsqu on crée une tâche, on doit fournir les paramètres suivants :. Le numéro identificateur de la tâche.. La période de la tâche ou la valeur nulle pour une tâche apériodique.. Le niveau d Importance de la tâche.. La date de déclenchement de la tâche ou la valeur nulle pour une tâche à déclenchement immédiat.. Le délai critique de la tâche.. Le temps d exécution estimé de la tâche (non utilisé).. Le segment de code de la tâche.. Le segment d exception de la tâche ou la valeur nulle pour une tâche sans traitement d exception. Le noyau, à la réception de cette requête doit effectuer les opérations suivantes:. Contrôler la validité des paramètres reçus et retourner à la tâche appelante avec un code d'erreur en cas de paramètre non valide car tout paramètre non valide peut «planter le programme».. Initialiser le Bloc de Contrôle de la tâche.. Si la tâche n est pas à déclenchement immédiat, l insérer dans la file d attente prévue à cet effet et retourner à la tâche appelante.. Si la tâche est à déclenchement immédiat, l insérer dans la file des tâches prêtes.. Tester si la priorité de la nouvelle tâche est supérieure à celle de la tâche en cours d exécution. Si oui, on commute vers la nouvelle tâche sinon on retourne à la tâche appelante avec le code de retour Code_OK. III.3.2- Primitive «Fin de l occurrence d une tâche périodique» Lorsqu une tâche signale qu elle a fini son occurrence, on doit la mettre en attente périodique. Pour ceci, on effectue les opérations suivantes :. On réinitialise le Bloc de Contrôle de la tâche.. On calcule le temps durant lequel la tâche doit rester en attente.. On insère la tâche dans la file d attente prévue à cet effet.
. On met la nouvelle tâche prioritaire en exécution ou on commute vers la tâche de fond. Une tâche faisant cet appel ne doit fournir aucun paramètres. Le seul contrôle effectué concerne le type de la tâche : elle doit être périodique. III.3.3- Programme Horloge La requête liée à l'interruption Timer accomplit les fonctions les plus fondamentales pour l'ordonnanceur, soit :. Incrémenter l'horloge temps réel.. Décrémenter les délais critiques dynamiques de toutes les tâches créées, ce qui implique de parcourir toutes les files d attente non vides.. Incrémenter les compteurs du temps écoulé depuis le début d'exécution pour toutes les tâches créées sauf celles non déclenchées ou en attente périodique.. Tester si une tâche risque une faute temporelle.si oui, appeler le programme d'exception s il existe ou effectuer le traitement par défaut.. Décrémenter les compteurs de toutes les tâches mises en attente d'un délai et réactiver celles-ci si nécessaire. Ceci concerne les tâches mises en attente d'un délai, les tâches en attente d'un sémaphore avec temporisateur, les tâches non déclenchées et les tâches en attente périodique. III.3.4- Primitive "Détruire_Tâche" Il suffit ici de réinitialiser les descripteurs et d'ajouter le traitement des tâches en attente périodique et non déclenchées à la routine initiale. III.3.5- Autres primitives On doit modifier dans toutes les autres primitives le test portant sur la commutation de tâches. On compare maintenant des délais critiques sur 16 bits et non des priorités d un octet, ce qui implique de reconsidérer l utilisation des registres. On notera que la commutation entre la tâche en cours d exécution et une tâche réactivée ne s effectue que si le délai critique de la tâche réactivée est strictement inférieure à celui de la tâche en cours.
III.4.ALGORITHMES. Algorithme de la primitive "Créer_Tâche" DEBUT.On doit contrôler les paramètres donnés en entrée Controler_ Paramètres SI Paramètres_Incorrects ALORS Code_Retour = Code_D'Erreur_De_La_Première_Erreur_Détectée Retourner_A_La_Tâche_Appelante SINON. On doit initialiser le Bloc de Contrôle de la tâche Initialiser_Bloc_Contrôle. On teste si la tâche est à déclenchement immédiat SI Tâche_A_Déclenchement_Non_Immédiat ALORS. La tâche est à déclenchement non immédiat Délai_Attente = Date_De_Déclenchement - Date_Courante Status = Tâche_Non_Déclenchée Insérer_Le_Bloc_de_Contrôl _De_La _Tâche dans la File_ Attente_Des_Tâches_Non_Déclenchées SINON. On insère le Bloc de Contrôle de la tâche dans la File_Des_Tâches_Prêtes SI Tâche_Exec.Priorité > Tâche.Priorité ALORS (Ceci équivaut à Tâche_Exec.Délai_Critique_Dynamique<Tâche.Délai_Critique_Dynamique) Tâche.Status = Prête Insérer le Bloc_Contrôle_Tâche dans la File_Des_Tâches_Prêtes Code_Retour = Code_OK Retourner_A_La_Tâche_Appelante SINON. On commute vers la nouvelle tâche Insérer le Bloc_Contrôle_Tâche en tête de la File_Des_Tâches_Prêtes Sauvegarder_ Le Contexte_ De_La Tâche_Appelante Tâche_Exec = Tâche.IDF Tâche.Status = En_Exécution Exécuter_La_ Nouvelle_ Tâche FIN
. Algorithme de la primitive "Fin_D'Occurrence_Périodique" DEBUT Si Tâche_Non_Périodique ALORS Code_Erreur = Erreur_Status Retourner_A_La_Tâche_Appelante Sinon. On réinitialise le Bloc de Contrôle de la tâche Délai_Critique_Dynamique = Délai_Critique_Initial Temps_Exécution_Dynamique = Temps_Exécution_Maximum_Initial Status = Attente_Périodique Délai_Attente = Période - Temps_Ecoulé_Depuis_Le_Début_Exécution Temps_Ecoulé_Depuis_Le_Début_Exécution = 0.On retire le Bloc de Contrôle de la tâche de la tête de la file des tâches prêtes Tache_Exec = Tache.ptr_next Tache.ptr_next = NULL Sauvegarder_Le_Contexte_De_La_Tâche_Mise_En_Attente.On Insére le Bloc de Contrôle de la tâche dans la file des tâches en Attente Périodique Tâche = Tâche_En_Tête_De_La_File_Des_Tâches_En_Attente_Périodique TANT QUE Tâche # NULL FAIRE Tâche = Tâche.ptr_next FIN TANT QUE Tâche.ptr_next = IDF_De_La_Tâche_A_Insérer Tâche_A_Insérer.ptr_prec = Tâche_IDF.On met la tâche en tête dans la file des tâches prêtes en exécution ou, si la file est vide, on exécute la Tâche de Fond SI Tâche_Exec = NULL ALORS Commuter_Vers_La_Tâche_De_Fond SINON Restituer_Le_Contexte_De_La_Tâche Commuter_Vers_La_Nouvelle_Tâche FIN
.Algorithme du programme horloge. Incrémenter l'horloge système HORLOGE = HORLOGE + 1. Mettre à jour les blocs de contrôle de tâches dans la file des tâches prêtes Tâche = Tâche_Exec TANT QUE Tâche # NULL FAIRE Tâche.Délai_Critique_Dynamique = Tâche.Délai_Critique_Dynamique - 1 Tâche.Temps_Exécution_Dynamique = Tâche.Temps_Exécution_Dynamique - 1 Tâche.Temps_Ecoulé = Tâche.Temps_Ecoulé + 1 SI Tâche.Délai_Critique_Dynamique <= 0 ALORS EXECUTER_PROCEDURE_EXCEPTION Tâche =Tâche.Ptr_Next FINTANTQUE. Mettre à jour les blocs de contrôle des tâches suspendues SI Attente # NULL ALORS Test = 01 Num_Tâche = 01 TANT QUE Num_Tâche <= MAX_Tâches FAIRE SI Test AND Attente #0 ALORS Tâche.Délai_Critique_Dynamique = Tâche.Délai_Critique_Dynamique - 1 Tâche.Temps_Exécution_Dynamique = Tâche.Temps_Exécution_Dynamique - 1 Tâche.Temps_Ecoulé = Tâche.Temps_Ecoulé + 1 SI Tâche.Délai_Critique_Dynamique <= 0 ALORS EXECUTER_PROCEDURE_EXCEPTION Test = Test * 2 Num_Tâche = Num_Tâche + 1 FINTANTQUE SINON.Mettre à jour les blocs de contrôle des tâches en attente aux boîtes à lettres File_Boite = 1 TANT QUE File_Boîte <= MAX_BOITES FAIRE Tâche = File_Boite.Tête TANT QUE Tâche # NULL FAIRE Tâche.Délai_Critique_Dynamique = Tâche.Délai_Critique_Dynamique - 1 Tâche.Temps_Exécution_Dynamique = Tâche.Temps_Exécution_Dynamique - 1 Tâche.Temps_Ecoulé = Tâche.Temps_Ecoulé + 1 SI Tâche.Délai_Critique_Dynamique <= 0 ALORS EXECUTER_PROCEDURE_EXCEPTION
Tâche = Tâche.Ptr_Next FINTANTQUE File_Boîte = File_Boîte + 1 FINTANTQUE. Contrôler et réveiller les tâches en attente d'un délai SI Attente_Délai # NULL ALORS Test = 01 Num_Tâche = 01 TANT QUE Num_Tâche <= MAX_Tâches FAIRE SI Test AND Attente # NULL ALORS Tâche.Délai_Critique_Dynamique = Tâche.Délai_Critique_Dynamique - 1 Tâche.Temps_Exécution_Dynamique = Tâche.Temps_Exécution_Dynamique -1 Tâche.Temps_Ecoulé = Tâche.Temps_Ecoulé + 1 SI Tâche.Délai_Critique_Dynamique <= 0 ALORS EXECUTER_PROCEDURE_EXCEPTION SINON Tâche.Tempo_Délai = Tâche.Tempo_Délai -1 SI Tâche.Tempo_Délai = 0 ALORS Réveiller_Tâche Test = Test * 2 Num_Tâche = Num_Tâche + 1 FINTANTQUE SINON. On va tester les tâches en attente d'un caractère, d'un caractère spécial ou d'un événement. En ce qui concerne les tâches en attente aux sémaphores, on doit également tester les tâches avec attente temporisée.. On va tester les tâches non déclenchées et en attente périodique afin de les réveiller si nécessaire. Tâche = Tâche_En_Tête_De_La_File_Des_Tâches_En_Attente_Périodique TANT QUE Tâche # NULL FAIRE Tâche.Tempo_Délai = Tâche_Tempo.Délai - 1 SI Tâche_Tempo.Délai = NULL ALORS Insérer_Tâche_Dans_File_Des_Tâches_Prêtes Tâche.Status = Prête Tâche = Tâche.ptr_next FIN TANT QUE. Si des programmes d'exception sont programmés, on lance le premier. FIN
IV- Programme de démonstration Le programme de démonstration n utilise pas toutes les primitives de l ordonnanceur mais permet de bien mettre en évidence toutes les caractéristiques du nouveau ordonnanceur, notamment le fait qu il est très difficile de prévoir l ordre exact de déroulement exact des tâches pour le programmeur à moins d utiliser des outils de synchronisation. A noter que la mise au point est assez pénible et laborieuse si on ne dispose pas d outils pour cela, car il est impossible d utiliser un debugger normal. Un outil de simulation est donc indispensable pour une mise au point correct. Dans notre programme de démonstration, on crée quatre tâches. C est la tâche 1, créée par le programme d initialisation qui crée les trois autres tâches. La tâche 1, non périodique, a été créée avec un grand délai critique et nous rappelle qu il faut également prévoir de traiter des tâches non critiques pour un fonctionnement logique et complet d un ordonnanceur. Les trois autres tâches ayant été créées, la tâche 1 se met en attente d un caractère spécial; la réception de ce caractère arrête le système qui retourne au DOS. Les trois autres tâches sont des tâches périodiques qui effectuent à peu près le même traitement, soit afficher leur numéro à l écran, se mettre en attente pour une certaine durée, réafficher leur numéro à l écran puis se mettre en attente périodique. Ces mêmes tâches servent à créer plusieurs exécutables, les paramètres temporels d initialisation seuls différant.
V- Conclusion La réalisation de cet ordonnanceur à échéances m a permis de mieux appréhender les difficultés de choix et d implémentation liés à la conception d un tel système. Il m est apparu qu un tel système n était pas utilisable seul et que la programmation effective d applications concrètes et opératives, offrant toutes les qualités de sécurité de fonctionnement indispensables nécessitait des outils complémentaires d aide à la mise au point et à la simulation. A noter qu il n a jamais été question d implanter un outil directement utilisable (surtout en assembleur) mais de vérifier qu une implémentation de certains choix était possible et d évaluer alors les possibilités et limitations inhérentes à ce choix.