|
||||||||||||||||||||||||||||||||||||||||||||||
|
Le partage d'une fonction non réentrante Après l'article d'introduction à Exec (noyau exécutif du système d'exploitation de l'Amiga), nous allons aborder des notions de gestion de ressources. D'une part, nous réutilisons la classe Exception que nous avons définie avec le langage C++ et qui regroupe les fonctions de gestion du signal d'exception. D'autre part, nous allons résoudre le partage d'une fonction de la bibliothèque C, que l'on considérera comme non réentrante : printf(). Nous allons essayer de la partager entre le programme principal : la fonction main() et la fonction routine() de la classe Exception. Nous verrons les techniques de gestion de ressources : par masquage d'interruption, de moniteur, d'exclusion mutuelle et nous conclurons par le modèle client-serveur. Nous donnons, en plus, un exemple de conception logicielle orientée par les objets avec le langage C++. Une étude Avantage et inconvénient du signal d'exception L'avantage évident du signal d'exception tel que nous l'exploitons est d'avoir un mécanisme d'interruption locale à la tâche : le cours normal du programme est interrompu, les registres de travail sauvegardés et la routine de la classe Exception s'exécutent en mode utilisateur et pile du programme. Nous sommes donc dans un contexte d'interruption mais locale à la tâche : la commutation de tâches (le multitâche) est autorisée. Le programme a pu être interrompu alors qu'il exécutait une fonction système ou autre. Nous ne devons pas réutiliser cette fonction dans la routine à moins qu'elle ne soit réentrante. Et pour cela, nous avons explicité la section qui n'est pas critique nous avons encadré la boucle de la classe Contrôleur, par la paire active()/desactive() afin de n'autoriser le signal d'exception que dans cette partie du programme. Dans l'exemple de l'article d'introduction à Exec, nous fixons juste un booléen nommé "fin". Par conséquent, remarquez l'utilisation du mot-clé volatile qui permet en principe de stipuler que la variable ne sera pas dans un registre. Nous verrons plus tard le pourquoi. Comme nous pouvons redéfinir la fonction virtuelle routine() dans la classe Contrôleur, héritière de la classe Exception : que se passe-t-il si nous voulons en plus faire un affichage à partir de cette routine, sur la console avec la fonction printf() ? Le Gourou ? (Guru) Définition : en Inde, chef religieux d'un village. Pratiquement, si on utilise la fonction printf() à la fois dans le main() et dans la routine() on obtient la méditation du Guru (voir définition ci-dessus). On pourrait tout de suite se dire qu'il est impossible que deux processus puissent faire des affichages simultanément à l'écran et donc, se partager la console. Ceci est faux (voir exercice ci-dessous). Il montre deux processus AmigaDOS, c'est-à-dire deux tâches avec leurs ports de communication. L'un et l'autre ont ouvert le périphérique logiciel console ou "console.device" et dialoguent par requêtes (passage de messages) avec celui-ci. Nous venons de donner une solution générale au problème de gestion de ressource. Toutefois, comme elle est construite à partir des connaissances des mécanismes de communication d'Exec, nous allons "l'oublier" et essayer de résoudre le problème autrement. Exercice : lancez dans le même Shell, les deux commandes suivantes (le même Shell, c'est par analogie aux affichages du programme que l'on souhaite réaliser) :
On voit apparaître un mélange fait de textes en mode vidéo inversé et normal, respectivement de la première et la seconde commande "list". Pour arrêter l'exécution, ouvrir un autre Shell et taper la commande "status". Vous aurez la liste des processus et pour chacun, le nom de la commande lancée et un numéro. Tapez alors "break numéro C", où "numéro" est celui de la ligne où le nom de la commande "list" apparaît. Quelques explications sur les séquences de contrôle d'une console ANSI Les significations des champs sont <CSI>style;couleur_caractère;couleur_cellule>couleur_fondm. Le <ControlSequenceIntroducer> est de deux caractères : <Esc>[ pour 0x1B et 0x5B en valeurs hexadécimales, c'est le cas ici. Plus rapide à transmettre, le <CSI> peut être d'un caractère Ox9B. Les plus connues de ces séquences de contrôle ont un caractère. Par exemple, en fin d'une chaine "C" comme paramètre d'un printf(), on utilise souvent le LINEFEED (\n), CARRIAGE RETURN (\r), HORIZONTAL TAB (\t). D'autres séquences pour contrôler une console au standard ANSI sont introduites par le <CSI> (voir RKM à console.device ou IFF Specification FTXT). La séquence pour rétablir les valeurs par défaut, utilisée après "%s%s" dans la première commande "list", est introduite par <Esc>[ : <Esc>[0;39:39>0m. Mais pour "réinitialiser" la console, ce n'est pas un <CSI>, la séquence est juste introduite par <Esc> : <Esc>c. Le RKM nous explique qu'une sortie texte à l'écran utilise la ressource système "blitter". Clairement, c'est un problème d'interblocage, d'après lui. Nous ne nous attarderons pas sur le terme de ressource "blitter" puisque cela pourrait faire le sujet d'un article entier. Juste que le "BLock Image Transfert"ER ou "Bitmap Lump Image Transfert"ER est vu par la graphics.library comme une ressource et que la bibliothèque graphique propose de le gérer soit par exclusion mutuelle soit par des files d'attente (FIFO). Alors, sachant qu'il ne faut pas utiliser deux fois la même fonction, nous nous proposons d'utiliser la fonction printf() uniquement dans la routine. Même comme cela, nous n'avons pas un programme qui fonctionne correctement. Nous pensons que la fonction printf() provoque un appel à la fonction système Wait() (et/ou SetSignal()?) et que nous utilisons aussi cette fonction système dans le main(). Wait() est-elle réentrante ? Vis-à-vis des autres tâches, oui, puisque c'est une fonction d'Exec : système multitâche. On peut alors supposer que la fonction Wait() modifie une ou des variables du descripteur de tâche qui ne sont plus locales à la tâche mais partagées ou globales au regard des deux appels de Wait() ; par le Wait() du printf() dans la routine(), et par le Wait() dans le main(). Par conséquent, c'est l'utilisation, que nous en faisons, qui ne rend plus réentrante la fonction système Wait(). Nous considérerons, dans la suite de cet article, que la fonction printf() n'est pas réentrante. Qu'est-ce qu'une fonction réentrante ? Nous devrions utiliser les mots "code ou programme réentrant", puisque si l'on pense à réutiliser du code, une fonction sera par définition, réentrante. L'idée vient des sous-routines qui doivent permettre au programmeur de réutiliser du code. Plaçons-nous d'abord dans un système monotâche. Une fonction peut être vue par ses entrées et ses sorties mais aussi par les "données" qu'elle modifie. Or, on comprend qu'avec un langage machine ou assembleur, on soit obligé de faire attention à ne pas écraser certains registres de travail, qui pourraient constituer une partie du niveau extérieur à l'appel de la sous-routine (ou fonction). Lorsqu'on utilise un langage structuré, le compilateur devra se charger de sauvegarder ces registres et donc de préserver l'environnement avant l'appel de la fonction, et de restaurer celui-là après. On peut donc dire que toute fonction écrite avec un compilateur est réentrante une fois, plusieurs fois mais dans un système monotâche. Plaçons-nous maintenant dans un système multitâche. Étant donné que nous n'empêchons pas notre fonction d'accéder et de modifier des variables globales (Cf. les "données" qu'elle modifie), il se peut que notre code soit interrompu (les registres de travail sont, tout de même, sauvegardés par le commutateur) et qu'un autre appel à notre fonction ait lieu, celle-ci peut écraser ces variables, dites alors globales, et la fonction n'est plus réentrante. Pour palier à ce problème, nous pouvons définir des fonctions auxquelles "on ajoute" un ou plusieurs paramètres supplémentaires qui seront des variables locales, non plus globales. Cela signifie qu'il faudra réserver un environnement et cantonner la fonction dans celui-ci. Avec un langage structuré, on peut préalablement à l'appel de la fonction, réserver et passer comme paramètre l'adresse de cet espace mémoire (tas ou pile) contenant la ou les variables (c'est toutes les structures à allouer puis à passer comme paramètres aux fonctions des bibliothèques de l'Amiga). Avec un système ou langage fonctionnel, nous sommes "contraints", par le style de programmation, à utiliser plutôt des variables locales. Alors, on dira que si on définit une fonction avec un langage ou système de la programmation fonctionnelle, utilisé comme tel, elle est réentrante y compris dans un système multitâche. Attention, nous avons la possibilité qu'un langage puisse permettre à une fonction de s'appeler elle-même. Ceci est à différencier de la notion de réentrance même si on peut dire que cette fonction est "réentrante", elle l'est de son propre point de vue et pas de l'extérieur : elle s'appelle elle-même. Ainsi, comme nous venons de le voir, la fonction sera réentrante dans un système monotâche, mais cela n'implique pas qu'elle le soit dans un système multitâche. Enfin, n'oublions pas que nous avions défini des "sous-routines", pas uniquement pour réduire la taille du code mais surtout pour réutiliser du code (Cf. début du paragraphe). Attention, nous avons parlé du codage uniquement, pas de spécifications (invariants, pré/post-conditions) et encore moins d'analyse (fonctionnelle, objet, événementielle). Conclusion de l'étude Nous pouvons dire que nous avons deux fonctions, représentées par main() et Exception::routine() qui font appel à une même fonction printf() que l'on a considérée comme non réentrante. En particulier, remarquons que la routine ne peut être interrompue ; elle fait partie de la même tâche, du même descripteur de tâche. De plus, un autre signal d'exception ne peut pas non plus l'interrompre, ceci, grâce à la conception du signal d'exception d'Exec. Une conception Partie commune aux solutions des programmes exa.cp, exb.cp et exbb.cp. Rappel : le passage des paramètres en C se fait par la pile. Ce qui change dans le code ci-dessus c'est Exception_routineVerif() en Exception2_routineVerif(). Maintenant, nous allons réutiliser la classe Exception en la plaçant dans un fichier d'inclusion. Nous nous apercevons d'un défaut, qui empêche la réutilisabilité : la variable "n", précédemment dans une section privée. Attention, pour réduire le texte, nous avons défini la fin de chacun des programmes dans deux fichiers d'inclusion supplémentaires. Ils utilisent la classe Moniteur qui ne sera définie que dans chacun des programmes. Parties communes : Nous vous laissons avec une solution qui ne fonctionne pas, mais en vous disant qu'il faut entourer le printf() de afficheValeur() appelée par la fonction main(), par active()/desactive().
|