La gestion multitâche (multitasking) consiste à faire faire à un ordinateur plusieurs choses en même temps. Dans le cas particulier d'un ordinateur ne possédant qu'un processeur, le multitâche est une technique qui doit se comprendre du seul point de vue de l'utilisateur: l'ordinateur "semble" faire plusieurs choses à la fois même si à chaque instant précis il n'y a qu'un seul processus en cours d'exécution.
Multitâche s'entend donc comme un partage du temps: pour donner l'impression que plusieurs processus sont exécutés en même temps, le système passe rapidement de l'un à l'autre. Si rapidement que chaque processus indépendant, chaque tâche, semble se dérouler de façon continue pour le ou les utilisateurs.
Tout système monoprocesseur mais multipériphérique utilise peu ou prou le multitâche au plus bas niveau pour gérer simultanément ses périphériques. Il s'agit généralement d'un système d'interruptions matérielles ou logicielles transparent pour l'utilisateur. Au plus haut niveau toutefois il faut un système d'exploitation multitâche pour permettre à l'utilisateur de partager son temps-machine entre plusieurs applications.
La simple division du temps réel entre plusieurs processus n'est cependant pas un but en soi car on conçoit aisément que cette manipulation multitâche, avec notamment toutes les sauvegardes et restitutions des environnements de chaque tâche, soit elle-même dévoreuse de temps. Il faut faire intervenir en plus de cette division du temps la notion d'une véritable "optimisation" du temps. Celle-ci repose sur la constatation qu'un processus comporte généralement de fréquents "temps morts" ou attentes qui peuvent être mis à profit pour exécuter d'autres processus. Ainsi la gestion multitâche trouve-t-elle dans l'organisation du temps et des ressources son véritable intérêt.
De par son caractère "système", le langage FORTH a été très tôt pressenti comme un excellent outil de développement multitâche. Certaines implémentations du langage ont d'ailleurs été proposées spécifiquement dans ce sens. En ce qui concerne TURBO-FORTH 83, les outils de gestion multitâche de base restent assez simples: il s'agit d'un petit module (une quinzaine de mots) mettant rapidement et facilement en oeuvre une technique classique de 'Round-Robin' ou 'A la ronde'. Ce module reprend celui du F83 tel qu'il a été proposé par H. Laxen et M. Perry.
La technique adoptée par F83 repose sur un enchaînement circulaire, asynchrone et coopératif des tâches. C'est une gestion qui a pour elle la simplicité à défaut d'être toujours la plus "juste".
Supposons que nous ayons quatre tâches à exécuter simultanément:- Tfche 1: programme principal sur la console - Tfche 2: déclencher un signal sonore à une heure fixée - Tfche 3: enregistrer les messages en provenance du port série - Tfche 4: gérer une file d'attente pour l'imprimante (Notez comme une tâche est fréquemment liée à l'usage d'un périphérique)
La gestion multitâche va activer chacune de ces quatre tâches à tour de rôle selon un enchaînement circulaire: tâche 1 puis tâche 2, tâche 3, tâche 4 puis de nouveau tâche 1 etc...Chaque tâche pointe sur une et une seule tâche suivante, la dernière pointant la première. Il n'est pas possible de passer de la tâche 2 à la tâche 4 sans passer par la tâche 3.
Cette ronde reste cependant assez souple car une tâche peut y être "éveillée" ou "endormie": une tâche éveillée (WAKE) est exécutée quand vient son tour; une tâche endormie (SLEEP) passe son tour au profit de la suivante. Il est donc possible de passer de la tâche 2 à la tâche 4 par l'intermédiaire d'une tâche 3 endormie.
Il est toujours possible d'ajouter une tâche dans la ronde: pour cela, on ouvre la ronde au niveau du pointeur de la tâche en cours pour lui faire pointer la nouvelle tâche puis on referme la ronde sur la tâche suivante. La nouvelle tâche est inserrée à l'état endormi dans la ronde si bien que cette intrusion n'a pas de conséquence immédiate dans l'exécution des tâches en cours.
L'enchaînement des tâches est asynchrone ce qui veut dire que le temps alloué à chaque tâche est variable de zéro à l'infini: une tâche endormie passe son tour immédiatement tandis qu'une tâche éveillée peut rester indéfiniment active au détriment des autres tâches qui sont contraintes à une perpétuelle attente!
Le système est coopératif: ce sont les tâches elles-mêmes qui décident de s'interrompre au profit de la tâche suivante. Il n'y a pas de superviseur chargé de distribuer plus ou moins équitablement les tours de rôles. Une tâche doit donc avoir un certain "fair-play" vis-à-vis des autres tâches. Il incombe au programmeur de prévoir l'auto-interruption de sa tâche pour laisser du temps aux autres tâches.
Cette coopération à l'amiable repose sur un seul mot: PAUSE. Lorsqu'une tâche doit exécuter PAUSE, elle passe son tour à la tâche suivante. Il suffit donc de placer judicieusement des PAUSEs dans le code d'une tâche pour qu'elle soit coutoise vis-à-vis des autres. Une tâche sans PAUSE accapare tout le temps pour elle!
Le mot PAUSE est déjà présent dans un certain nombre de mots Forth qui sont: (KEY) (PRINT) (CONS) et par conséquent dans ceux qui utilisent ces derniers comme KEY EXPECT EMIT TYPE STOP? etc
Tous ces mots sont ceux qui agissent au niveau des entrées-sorties clavier et console, là où bien souvent le système gaspille du temps. Comme nous l'avons déjà signalé, la gestion multitâche se doit d'optimiser le temps: l'attente d'une touche au clavier par KEY est idéale pour exécuter au passage un peu d'une autre tâche. Bien entendu il n'est pas nécessaire de prévoir PAUSE dans le code d'une tâche utilisant déjà un de ces mots.
Le gros problème en matière de gestion multitâche après le partage du temps est le partage de la mémoire. Chaque tâche a besoin de conserver son propre environnement, ses propres variables et pointeurs à l'abri des manipulations des autres tâches. Certaines données, certaines portions de code peuvent être partagées ou échangées entre diverses tâches mais jusqu'à un certain point seulement car il ne faudrait pas qu'une tâche fasse perdre ses repères à une autre et l'empêche de fonctionner correctement.
Prenons un exemple bien compréhensible pour Forth: la pile des paramètres. Supposons deux tâches utilisant évidemment la pile pour prélever et déposer des paramètres comme il est de règle en Forth. De deux choses l'une; ou bien les deux tâches utilisent la même pile et dans ce cas il est impératif que chacune laisse celle-ci dans l'état exact où elle l'a trouvée. Imaginez les catastrophes et plantages garantis si une tâche se retrouve en cours d'exécution avec une pile modifiée! Ou bien, deuxième solution, chaque tâche utilise une pile différente et dans ce cas il n'y a pas de conflit possible mais au prix d'un système multipile plus complexe.
Chaque tâche dispose d'un espace qui lui est propre, appelée zone USER, contenant les pointeurs et variables d'usager de la tâche. Dans cette table nous trouvons des variables que nous connaissons bien comme les bases SP0 et RP0 des piles paramètres et retour, le pointeur DP du dictionnaire, la BASE numérique en vigueur ou le HANDLE du fichier courant. On y trouve également un vecteur d'exécution différée bien connu: EMIT.
C'est avec le multitâche qu'on comprend enfin l'intérêt de ces variables USER ou des mots vectorisés par USER DEFER. En effet ces mots dits d'usagers n'ont pas des comportements différents des variables et vecteurs ordinaires. S'ils sont regroupés dans une table et adressés par un déplacement dans cette table au lieu d'un adressage direct, c'est pour pouvoir être facilement permutés en bloc: il suffit de prendre une autre adresse de zone USER pour tout changer d'un coup. Avec ces variables et vecteurs d'usagers chaque tâche dispose d'un environnement personnalisé.
Lors de la définition d'une nouvelle tâche, il est réservé d'emblée un espace USER qui est initialisé par défaut avec les mêmes valeurs que celles de la zone USER de la tâche en cours. Le programmeur doit ensuite bien faire attention à ses variables USER, d'autant que la synonymie ne facilite pas les choses: exécuter HEX dans la tâche 1 n'a aucun effet sur la BASE de la tâche 4 car chaque tâche possède sa propre BASE numérique. Il faudra parfois au contraire créer de nouvelles variables USER pour éviter d'avoir à partager une variable globale sensible.
Les toutes premières variables USER sont directement impliquées dans la gestion multitâche: ce sont les variables TOS, ENTRY, LINK auxquelles s'ajoutent les bases SP0 et RP0 des piles de paramètres et de retour de chaque tâche.
USER | --- |
UP | --- adr |
#USER | --- adr |
#USER ? affiche 28
ce qui correspond aux 14 mots 16 bits des 13
variables user TOS, ENTRY, LINK, SP0, RP0, DP, #OUT, #LINE, HANDLE, BASE,
HLD, PRINTING, ECHO et du vecteur EMIT.
ALLOT | n --- |
CREATE |
VARIABLE | <nom-variable> --- |
Exemple: USER VARIABLE TERMINAL FORTH définit une variable TERMINAL tâche-dépendante.
Note: La variable TERMINAL ne peut être utilisée (par @ ou ! par exemple) que dans les tâches disposant d'une zone USER suffisamment dimensionnée; ce n'est pas le cas de la zone USER initiale qui s'arrête à la cellule EMIT.
DEFER | <nom-vecteur> --- |
EMIT (voir ce mot) est le seul vecteur USER déjà défini.
Exemple: USER DEFER ALARM FORTH définit un verbe ALARM tâche-dépendant.
Note: Le verbe ALARM ne peut être vectorisé par IS puis exécuté que dans les tâches disposant d'une zone USER suffisamment dimensionnée; ce n'est pas le cas de la zone USER initiale qui s'arrête à la cellule EMIT.
TOS | --- adr |
ENTRY | --- adr |
LINK | --- adr |
(PAUSE) | --- ip rp |
RESTART | ip rp --- |
INT# | --- 128 |
Note: la technique Round-Robin, relativement simple dans son principe, a été appliquée en utilisant les particularités du microprocesseur 8088: sauvegarde sur la pile de l'adresse de retour lors d'une interruption, mode d'adressage 16 bits des instructions de saut et même une "astuce" peu orthodoxe de patch de code-machine dans ENTRY pour n'avoir besoin que d'un pointeur 16 bits. Efficacité, lisibilité, transportabilité: le choix s'est porté plutôt sur l'efficacité.
LOCAL | adr-tâche adr --- adr' |
exemple: SPOOLER #LINE LOCAL OFF met à zéro la variable User #LINE de la tâche SPOOLER
@LINK | --- adr |
!LINK | adr-tâche --- |
SET-TASK | ip adr-tâche --- |
TASK: | <nom-de-tâche> taille --- |
Cet espace mémoire est ainsi agencé: les adresses basses forment la zone USER de la tâche; l'adresse la plus haute est la base de la pile de retour de la tâche; 256 octets plus bas débute la pile des paramètres de la tâche (les piles progressent vers les adresses basses). La taille de l'espace réservé à une tâche doit donc être supérieure à 256 octets, le supplément étant partagé entre la quinzaine de variables USER et la pile. Les 400 octets réservés par défaut dans BACKGROUND: peuvent se révéler trop justes pour certaines tâches.
La zone USER nouvellement créée est d'abord copiée sur celle actuellement en cours. Les variables d'usager SP0 et RP0 des piles sont initialisées aux nouvelles valeurs précisées ci dessus. La variable d'usager DP (pointeur du dictionnaire) est initialisée pour pointer la fin des variables USER: ceci peut parfois provoquer des conflits avec la pile locale. Les liens entre les tâches sont formés: le LINK en cours pointe la nouvelle ENTRY et le nouveau LINK pointe l'ancienne valeur du LINK en cours. La nouvelle ENTRY est initialisée en tâche dormante.
A l'exécution, le mot créé par TASK: c'est-à-dire de nom-de-tâche se comporte comme une variable en laissant l'adresse marquant le début de la zone USER de cette tâche.
TASK: est surtout une primitive multitâche puisqu'il manque encore le code exécutif pour la tâche qu'il prépare. Mais c'est aussi un mot utilisateur quand on prévoit d'user du mot ACTIVATE (voir plus loin).
BACKGROUND: | <nom-de-tâche> --- |
VARIABLE TOURS BACKGROUND: COMPTEUR BEGIN PAUSE TOURS 1+! AGAIN ;
Ceci est la définition d'une tâche COMPTEUR qui incrémente à chaque tour du Round-Robin une variable globale TOURS (ce type de tâche très simple est souvent utile pour gérer d'autres tâches, par exemple limiter une tâche à un tour sur N).
Notez que la tâche COMPTEUR est une boucle sans fin BEGIN...AGAIN contenant l'indispensable PAUSE permettant aux autres tâches de travailler mais dispensée ici du mot STOP.
Après la définition de la tâche COMPTEUR, il faudra exécuter: COMPTEUR WAKE pour éveiller la tâche compteur MULTI pour activer le multitâche
WAKE | adr-tâche --- |
exemple: COMPTEUR WAKE la tâche COMPTEUR compte...
SLEEP | adr-tâche --- |
exemple: COMPTEUR SLEEP la tâche COMPTEUR cesse de compter...
PAUSE | --- |
Quand la gestion multitâche n'est pas activée par MULTI, le mot PAUSE est sans action.
STOP | --- |
Le mot STOP doit absolument terminer la définition d'une tâche sauf si cette tâche n'a pas de fin comme le cas d'une boucle BEGIN...AGAIN.
ACTIVATE | adr-tâche --- |
Voici un exemple hyper-classique simplifié d'impression de fichiers en multitâche ou pseudo-spooling (le véritable spooling suppose en outre un tampon servant de file d'attente).
Définissons d'abord la tâche SPOOLER: 500 TASK: SPOOLER Initialisons le vecteur EMIT du spooler avec (PRINT) : ' (PRINT) SPOOLER ' EMIT >IS LOCAL ! Définissons le mot SPOOL d'utilisation du spooler: 40 STRING SPOOLING : SPOOL ( <nom-de-fichier> --- ) " LIST " SPOOLING $! BL WORD COUNT SPOOLING APPEND$ SPOOLER ACTIVATE SPOOLING $EXECUTE STOP ;
Il suffit d'écrire par exemple MULTI SPOOL CLOCK.FTH WORDS pour que l'imprimante imprime le fichier CLOCK.FTH "en même temps" que s'affiche le dictionnaire à l'écran. Le verbe SPOOL peut ensuite être réutilisé pour imprimer d'autres fichiers tant que le mode MULTI est actif. Ce spooler simplifié ne gère pas les erreurs pouvant survenir dans l'une ou l'autre des tâches.
ACTIVATE peut servir également à redéfinir une tâche de fond définie par BACKGROUND:.
MULTI | --- |
Après les définitions des tâches, le mot MULTI doit être exécuté au moins une fois pour lancer l'exécution multitâche: en mode MULTI, toutes les tâches éveillées sont exécutées simultanément. Après un retour en monotâche par SINGLE, MULTI permet de réarmer le multitâche.
SINGLE | --- |
-- hautdepage -- sommaire -- page d'accueil --