SYSTEME MULTITACHE

A. INTRODUCTION

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.

 

B. PRINCIPES DU SYSTEME ROUND-ROBIN

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.

 

C. LES ZONES D'USAGERS

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.

 

D. VOCABULAIRE USER ET PRIMITIVES DU MULTITACHE

USER ---
Vocabulaire contenant des homonymes utilisés pour définir les variables et vecteurs d'usager dépendants des tâches. Le vocabulaire USER contient les mots ALLOT CREATE VARIABLE DEFER dont les actions sont identiques aux mêmes ALLOT CREATE VARIABLE DEFER du vocabulaire FORTH, les différences portant sur l'adressage interne.

UP --- adr
Variable système (User-Pointer) contenant l'adresse de base de la zone usager en cours. Toutes les variables USER sont accédées par déplacement par rapport à l'adresse contenue dans UP.

#USER --- adr
Variable système conservant le nombre d'octets déjà réservés dans la zone USER pour les variables et vecteurs d'usager.

#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 ---
Version ALLOT du vocabulaire USER : réserve n octets dans la zone usager en cours.

CREATE
<nom> --- Version CREATE du vocabulaire USER : crée une entrée dans le dictionnaire pointant la première cellule libre dans la zone usager en cours.

VARIABLE <nom-variable> ---
Version VARIABLE du vocabulaire USER : mot de définition d'une variable d'usager tâche dépendante. A l'exécution <nom-variable> dépose sur la pile l'adresse où est rangée la valeur 16 bits de la variable, exactement comme une variable système habituelle. La différence vient du fait que cette adresse est située dans la zone USER en cours, calculée par déplacement, et qu'il peut y avoir autant de valeurs pour <nom-variable> que de tâches définies.

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> ---
Version DEFER du vocabulaire USER : mot de définition d'un mot d'exécution différée tâche dépendant. A l'exécution <nom-vecteur> exécute le mot dont le code exécutif est vectorisé dans son champ paramètreé exactement comme un vecteur d'exécution différée habituel (voir DEFER du vocabulaire FORTH). La différence vient du fait que l'adresse de vectorisation est située dans la zone USER en cours, calculée par déplacement, et qu'il peut y avoir autant de vecteurs d'exécution pour <nom-vecteur> que de tâches définies. La vectorisation d'un mot USER DEFER peut s'effectuer pour la tâche en cours par IS comme pour les autres mots d'exécution différée.

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
Variable USER (Top Of Stack) conservant le pointeur de la pile de paramètres de la tâche en cours, c'est-à-dire le registre interne SP. Comme première variable USER, l'adresse délivrée par TOS est aussi l'adresse de la zone usager en cours, ou encore l'adresse de base de la tâche en cours.

ENTRY --- adr
Variable USER utilisée comme vecteur d'entrée de la tâche en cours. ENTRY contient le code d'une instruction de saut soit dans le code exécutif de la tâche si celle-ci est éveillée, soit vers la tâche suivante si elle est endormie.

LINK --- adr
Variable USER pointant l'adresse d' ENTRY de la tâche suivante. Il s'agit en fait d'une adresse calculée par déplacement en raison du mode d'adressage relatif de l'instruction JMP placée dans ENTRY si la tâche est endormie. LINK est l'unique pointeur servant de maillon à la ronde des tâches.

(PAUSE) --- ip rp
Mot de bas niveau exécuté par PAUSE lorsque la gestion multitâche est active: interrompt l'exécution de la tâche en cours et passe le contrôle de l'interpréteur interne à la tâche suivante pointée par LINK. La pile actuelle conserve le pointeur d'interprétation interne et le pointeur de la pile de retour; la variable User TOS garde le pointeur de cette pile pour pouvoir tout récupérer au prochain tour.

RESTART ip rp ---
Mot de bas niveau réalisant l'opération inverse de (PAUSE): reprend l'exécution de la dernière tâche interrompue. Ce mot ne doit pas être exécuté ni compilé pour des raisons liées à la technique d'interruption logicielle choisie au plus bas niveau pour entrer dans le code d'une tâche.

INT# --- 128
Constante 80 en hexadécimal. Il s'agit du numéro de l'interruption logicielle utilisée par le moteur interne du multitâche. Lorsque le multitâche est activé (par MULTI), une interruption 80h exécute le mot RESTART. L'instruction INT 80h est codée dans la variable ENTRY de toutes les tâches éveillées. Quand une tâche est endormie, son ENTRY contient le code NOP JMP utilisant le LINK qui suit pour sauter directement à la tâche suivante.

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'
Convertit l'adresse d'une variable usager en celle de la même variable prise dans une autre zone usager tâche-dépendante.

exemple:		SPOOLER #LINE LOCAL OFF
met à zéro la variable User #LINE de la tâche SPOOLER
@LINK --- adr
Délivre l'adresse LINK de la prochaine tâche à exécuter.

!LINK adr-tâche ---
Inscrit dans la variable LINK en cours l'adresse d'une tâche comme étant la prochaine tâche à exécuter.

SET-TASK ip adr-tâche ---
Initialise une tâche définie avec un code exécutif. Ce mot place dans les piles de la tâche les premières valeurs nécessaires à son démarrage par RESTART, de la même façon qu'une PAUSE.

 

E. MOTS UTILISATEURS DU MULTITACHE

TASK: <nom-de-tâche> taille ---
Mot de création de l'en-tête d'une nouvelle tâche dans le dictionnaire et principale primitive du mot de définition BACKGROUND:. TASK: crée une entrée dans le dictionnaire avec le nom-de-tâche prélevé dans le flot d'interprétation et lui réserve le nombre d'octets précis, par le paramètre 'taille' sur la pile.

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> ---
Mot de définition et de compilation d'une nouvelle tâche. Ce mot exécute d'abord la création d'un en-tête de tâche par TASK: en réservant 400 octets pour la zone USER et les piles, puis entre en mode compilation deux-points du code exécutif de cette tâche. Exactement comme une définition deux-points classique, la séquence BACKGROUND: <NOM> doit être suivie par une suite de mots ou instructions et terminée par le mot point- virgule ';' de fin de compilation.

Deux mots sont à considérer dans la définition d'une tâche:
  1. 1) le mot PAUSE doit être présent pour laisser aux autres tâches une chance d'être exécutées. Pour une "longue" tâche, mieux vaut prévoir de fragmenter l'exécution sur plusieurs tours du Round- Robin en multipliant l'occurence de PAUSE. Le mot PAUSE n'est pas obligatoire si on utilise des mots d'entrées-sorties qui le contiennent implicitement.
  2. 2) Le mot STOP doit terminer l'exécution de la tâche en rendant la main aux autres tâches. Une tâche infinie peut se passer de STOP.
exemple:
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 ---
Place une tâche en éveil: la tâche "prend son tour" d'exécution chaque fois que le contrôle lui est passé.

exemple:	COMPTEUR WAKE
la tâche COMPTEUR compte...
SLEEP adr-tâche ---
Place une tâche en sommeil: la tâche "passe son tour" en transmettant directement le contrôle à la tâche suivante.

exemple:	COMPTEUR SLEEP
la tâche COMPTEUR cesse de compter...
PAUSE ---
Suspend la tâche en cours en transmettant le contrôle à la tâche suivante. PAUSE est le maître mot autorisant le multitâche: il doit être présent explicitement ou implicitement dans le code exécutif d'une tâche. Les mots d'entrées-sorties du clavier, de la console ou de l'imprimante comme KEY EMIT TYPE (PRINT) etc contiennent implicitement PAUSE.

Quand la gestion multitâche n'est pas activée par MULTI, le mot PAUSE est sans action.

STOP ---
Termine la tâche en cours: la tâche courante est mise en sommeil indéfiniment avant de passer le contrôle à la tâche suivante par une ultime PAUSE.

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 ---
Active une tâche définie en lui assignant le code exécutif qui suit. La tâche est aussitôt éveillée pour exécuter ce code. A n'utiliser qu'en compilation.

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 ---
Installe le mode multitâche. Le tour de rôle des tâches est activé et le mot PAUSE est redéfini pour autoriser les passages d'une tâche à une autre.

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 ---
Quitte la gestion multitâche pour retourner en mode monotâche usuel (dans la tâche en cours): le mot PAUSE redevient inactif.

 

 

-- hautdepage -- sommaire -- page d'accueil --