Obligement - L'Amiga au maximum

Vendredi 26 mai 2017 - 21:02  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

 · Accueil
 · A Propos
 · Articles
 · Galeries
 · Glossaire
 · Hit Parade
 · Liens
 · Liste jeux Amiga
 · Quizz
 · Téléchargements
 · Trucs et astuces


Articles

 · Actualité (récente)
 · Actualité (archive)
 · Comparatifs
 · Dossiers
 · Entrevues
 · Matériel (tests)
 · Matériel (bidouilles)
 · Points de vue
 · En pratique
 · Programmation
 · Reportages
 · Tests de jeux
 · Tests de logiciels
 · Tests de compilations
 · Articles divers

 · Articles in english
 · Articles in other languages


Twitter

Suivez-nous sur Twitter




Liens

 · Sites de téléchargements
 · Associations
 · Pages Personnelles
 · Moteurs de recherche
 · Pages de liens
 · Constructeurs matériels
 · Matériel
 · Autres sites de matériel
 · Réparateurs
 · Revendeurs
 · Presse et médias
 · Programmation
 · Développeurs logiciels
 · Logiciels
 · Développeurs de jeux
 · Jeux
 · Autres sites de jeux
 · Scène démo
 · Divers
 · Informatique générale


Jeux Amiga

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Trucs et astuces

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Glossaire

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Partenaires

Annuaire Amiga

Amedia Computer

Relec

Hit Parade


Soutien

N'hésitez pas à soutenir le projet Obligement



Contact

David Brunet

Courriel

 


Programmation : Assembleur - introduction à Exec (2e partie)
(Article écrit par Gilles Dridi et extrait d'Amiga News - mars 1997)


Communication inter-processus - choisir son mode de communication, son mode d'interruption

Les signaux

Avant de les utiliser il faut réserver son canal de communication par la fonction AllocSignal(n°), puis les libérer par FreeSignal(n°). Le signal : c'est un bit d'un long mot contenu dans le descripteur de tâche. Il est affirmé par la fonction Signal (descripteur_de_tâche, bits) et infirmé par la fonction Wait(bits). La fonction Signal(T, X) vérifie d'abord que les bits X sont attendus par la tâche T, place la tâche T dans la liste des tâches prêtes si elle n'y est pas. Par conséquent, la fonction Wait() n'est pas bloquante lorsque un des bits attendus aura déjà été affirmé par la fonction Signal(T, X). Enfin, si T est placée en tête de suite (sa priorité est la plus élevée), une demande de préemption est faite et T sera élue (voir annexe A). Pour cette raison, et parce qu'un signal est un booléen, un signal "ne mémorise" qu'un seul appel à la fonction Signal(T, X), les autres appels identiques peuvent être éventuellement sans effets, si X est déjà affirmé dans T ; ces signaux supplémentaires seront perdus.

Le signal d'exception : un signal est défini signal d'exception par la fonction SetExcept(val, pos). Il faut de plus, initialiser des champs spéciaux du descripteur de tâche pour y ficher une routine de traitement exceptionnelle du signal. La particularité du signal d'exception est lors de l'utilisation de la fonction Signal(T, X) qui fera appeler sa routine d'exception. Si T est déjà élue la routine est exécutée immédiatement. Si T est endormie, elle est mise en suite et la routine d'exception sera exécutée lorsqu'elle sera élue. Ainsi, on tire un bénéfice du signal d'exception, plutôt lorsque T est déjà élue. Lorsque la tâche est élue, une sauvegarde/restauration est faite sur la pile de cette tâche puisque son programme est interrompu. Contrairement à un signal classique, où son traitement n'arrive qu'à l'appel de Wait() ou de SetSignal() que nous verrons. Un signal d'exception peut donc arriver alors que l'on est entré dans une fonction. Si nous voulons utiliser cette fonction dans la routine d'exception, nous devons être sûr qu'elle est réentrante ou n'alloue pas de ressource ; voir le paragraphe sur les sémaphores.

Nota : le RKM nous met en garde contre l'utilisation de la plupart des fonctions systèmes dans une routine d'exception qui coexisteraient avec celles de la tâche.

Conseil de Messieurs Dittrich, Gelfand et Schemmel (Bible Amiga) : on peut éviter de tester régulièrement l'arrivée du signal par SetSignal(0,0), ce qui ralentit la boucle, avec un signal d'exception et sa routine. Voici les deux solutions que je vous propose à un programme qui simule un calcul simple (l'incrémentation d'un compteur) :

Introduction Exec
Introduction Exec
Introduction Exec
Introduction Exec

En conclusion, la boucle de "excp" va environ cinq fois plus vite. Pour être rigoureux, il faudrait comptabiliser le temps total des instructions des deux boucles, y compris l'appel à SetSignal() pour le premier programme. Ceci est surtout un exemple "d'Exec et de la programmation par objets avec C/C++". En effet, le temps du calcul est bien inférieur à celui du reste de la boucle. Aussi, les deux solutions devraient se valoir en durée lorsque le calcul est plus conséquent. La deuxième solution a l'avantage de ne pas appeler une fonction système de façon répétitive et utilise donc le fait qu'Exec gère un mécanisme d'interruption local à la tâche (processus de délégation). La mise au point est plus longue et nous a demandé des connaissances du noyau mais ces inconvénients sont négligeables quant à la réutilisabilité des classes "Contrôleur" ou "Compteur" ; il ne sera plus nécessaire d'expliciter la gestion du signal d'arrêt que par active()/desactive(). Cette délégation d'une gestion du signal d'exception, par le composant logiciel "Exception", met en évidence l'encapsulation ou masquage. Nous pourrons réutiliser ces composants, ici des classes C/C++, et maintenir plus facilement une application conçue ainsi.

Exercice 2 : lancer les commandes "exc" et "excp" dans deux Shells avec une même priorité (0) puis l'un avec une priorité de 1. Pourquoi ne peut-on pas interrompre le Shell avec la priorité nulle tant qu'on a pas appuyé sur Ctrl+E dans l'autre ?

Les messages

Voyons maintenant comment comptabiliser plusieurs communications par passage de messages. Celui-ci fonctionne à l'instar du système postal avec les notions de message, destinataire, expéditeur et "retour à l'envoyeur".

Le port

C'est le point d'entrée et d'accumulation des messages ou la boîte aux lettres. Le port peut-être public : il est alors dans une liste du noyau, ou privé (il n'est pas dans cette liste). Parce qu'il peut être public, il n'est pas d'office contenu dans le descripteur de tâche, contrairement au système de communication minimal que sont les signaux. Le port est constitué d'une partie "Signal" qui contient le numéro de bit à affirmer et la tâche à réveiller, là où se trouve le long mot.

Remarque 3 : la création d'un port ou boîte aux lettres est initiée par une tâche, aussi il faut avoir à l'esprit qu'un port est toujours associé à une tâche afin qu'à la réception d'un message sur ce port, une tâche puisse toujours le traiter.

L'autre partie est une file d'attente, où s'accumulent les messages. A chaque envoi d'un message est sous-jacent l'affirmation d'un signal (peut-être déjà affirmé) et l'enfilement du message. Souvenons-nous que la tâche qui traitera ces messages n'est peut-être pas la plus prioritaire et par conséquent, lorsqu'elle s'exécutera et ira regarder à ce port, elle devra traiter peut-être plusieurs messages. Maintenant nous avons notre mécanisme de mémorisation. Voici l'algorithme classique de traitement de messages à un port, proposé par le RKM.

Initialiser un port

Algorithme de réception (n°1)

Wait(port) ; attente du signal associé au port tant que la file du port n'est pas vide
Faire message <- GetMsg(port) ; défile un message du port de traitement
   Traiter ce message
   ;ReplyMsg() : renvoyer une réponse pour signifier que le message est traité
   ReplyMsg(message) ; enfile un message dans son port-réponse
Fin

Le message

C'est un élément de file. Il contient un champ qui donne l'adresse d'un port où l'on recevra le message en retour, c'est-à-dire une réponse. A l'instar de l'adresse de l'expéditeur sur une enveloppe ou en haut d'une lettre pour un "retour à l'envoyeur". De plus, la longueur du message. Voici l'algorithme classique d'envoi de messages.

Initialiser un port-réponse

Initialiser un message et son champ port-réponse.

Algorithme d'envoi (n°2)

PutMsg(port, message) ; enfile un message dans le port de traitement (algo, n°1)
Wait(portRep) ; si plusieurs envois de messages utiliser alors une boucle
GetMsg(portRep) : défile un message du port-réponse
; Réponse en retour à l'envoi, traiter éventuellement un code de retour d'erreur
; A partir d'ici le message peut-être réutilisé

Nous attendons à ce port une réponse ou le traitement du message. Le message envoyé au port de destination y est défilé, traité, puis enfilé dans notre port réponse et signalé vers la tâche à réveiller grâce à sa partie "Signal".

L'ensemble des deux algorithmes forme un protocole de communication.

Remarque 4 : dans le premier algorithme, si nous dupliquons le message, nous nous plaçons soit dans un contexte où l'on simule l'envoi d'un message par une liaison matérielle (+ signaux de contròle), soit le transfert d'informations entre deux types d'espace mémoire... Ce qu'il ne faut pas confondre avec le fait, que c'est toujours à l'expéditeur de prévoir le nombre de messages qu'il désire envoyer "en parallèle". Il doit donc préparer autant d'occurrences de message que d'envois qu'il veut initier de manière asynchrone.

Aparté

On dialoguera avec un périphérique logiciel ou "device" par passage de messages que l'on nomme requêtes. Pour un mode d'envoi asynchrone, on remplacera dans l'algorithme n°2, les fonctions PutMsg() par SendIO() et Wait()/GetMsg() par WaitIO(). Pour un mode synchrone, c'est DoIO() qui remplacera PutMsg(), Wait() et GetMsg().

Notons qu'un périphérique logiciel est basé sur une bibliothèque qui contient les vecteurs suivants : Open(), Close(), Epure(), Nulle(), BeginIO(), AbortIO(). Il contient une ou plusieurs unités selon le nombre d'occurrences d'un matériel de même nature, sur lequel il effectue les mêmes commandes. Par exemple, plusieurs lecteurs de disquette, plusieurs canaux audio. Une unité est un port de traitement, plus une tâche ; relire la remarque 3. Enfin, une unité ne sert à rien d'autre qu'au mécanisme de passage de messages. Puisqu'une opération est identique quelle que soit l'unité choisie et que l'adresse de l'unité est contenue dans la requête, on regroupera en général les fonctions de traitement au niveau du périphérique. La fonction SendIO() appelle simplement le vecteur BeginIO() qui envoie un message à l'unité spécifiée (dans la requête). L'unité implante l'algorithme de réception (n°1), BeginIO() celui de l'envoi (n°2). Toutefois, BeginIO() pourra éviter cet échange de messages et le réduire à l'appel de la routine de traitement correspondant à la requête. La demande est faite en affirmant un bit dans la requête. DoIO() affirme toujours ce bit, SendIO() l'infirme. C'est toujours le code de la fonction BeginIO() qui, selon la nature de la requête (l'opération demandée), prend la décision d'opérer immédiatement. Sinon, le bit est infirmé et la fonction BeginIO() revient alors à PutMsg(portUnite).

Ce mécanisme permet d'éviter un échange de messages pour des requêtes d'état par exemple. Un tome entier des RKM compile les différents devices de base de l'Amiga.

Exercice 3

Le "trackdisk.device" gère les lecteurs de disquette, il se trouve dans la liste de devices d'Exec. De plus, celui-ci peut avoir jusqu'à quatre unités qui chacune est une tâche du même nom et que l'on trouve dans la liste des tâches. Elles auraient pu s'appeler "trackdisk.unitx" ce qui aurait évité la confusion. Enfin, l'AmigaDOS s'appuie sur Exec, c'est le Disk Operating System de l'Amiga. Si vous disposez de plusieurs lecteurs de disquette, vous aurez autant d'instances d'unités mais aussi autant d'instances de tâches AmigaDOS ou processus : comme une unité, c'est une tâche avec un port de communication. Ils se nomment "DFx:". Tapons la commande "dir DF0:", le processus "DF0:" reçoît un message ou paquet (sous AmigaDOS), puis "DF0:" envoie une requête au "trackdisk.device" en spécifiant l'unité 0. Pour la nommer, nous dirons que c'est sur le port de la tâche "trackdisk.unit0" où est enfilée la requête si BeginIO() ne la traite pas immédiatement. Reprenons l'exercice 1 mais faisons "dir : all" sur deux lecteurs différents. Vous vous apercevrez alors que même en augmentant la priorité d'un Shell, la vitesse d'apparition des fichiers est pratiquement la même dans les deux Shells. Nous avons deux unités "en parallèle" qui traitent nos requêtes avec une même priorité (les processus AmigaDOS ne jouent pas). Il faudra changer la priorité d'une de ces unités pour que l'affichage soit plus rapide dans un Shell. Ceci n'est pas à faire, ou sinon délicat.

Le message d'interruption : lorsque nous recevons un message à un port, nous pouvons demander à Exec de déclencher une interruption de niveau 1 dite "interruption logicielle". La fonction Cause() est là pour ça. Prenons un port de réception, dans sa partie "Signal" à la place de la tâche à réveiller nous fichons alors l'adresse d'un structure d'interruption comme celles contenues dans la liste d'un serveur, que nous avons déjà vu. Le traitement de l'algorithme d'envoi (n°2) se simplifie. Après PutMsg() il n'est plus nécessaire d'attendre une réponse. En effet, la routine d'interruption est appelée lors du ReplyMsg() de l'algorithme de réception (n°1) et ne contient plus que GetMsg() de l'algorithme d'envoi (n°2).

Les sémaphores

Nous avons vu qu'avec le passage de messages, nous avions réalisé un partage d'informations entre tâches. Le sémaphore sert aussi ce partage. Le mécanisme est presque semblable au passage de messages. Il est implanté, dans Exec en utilisant les signaux, il contient un compteur et une file de tâches à réveiller. Le sémaphore est utilisé pour des problèmes connus comme celui des philosophes...

C'est un mécanisme qui permet de diminuer le temps d'utilisation des fonctions Forbid()/Permit, qui gèlent temporairement la commutation de tâches. Comme celui du passage de messages pour améliorer la simultanéité des communications.

Synthèse

L'Amiga est un système performant de multiprogrammation non seulement parce qu'il permet à plusieurs programmes de s'exécuter efficacement et "simultanément" grâce aux mécanismes que nous avons vu. Toutefois, l'Amiga ne serait pas ce qu'il est sans l'aspect matériel essentiel qui lui permet d'être aussi performant et versatile que sont les processeurs dédiés au transfert de données ou DMA. Ils déchargent le microprocesseur des opérations de transfert, Exec étant toujours au coeur de l'Amiga pour "servir" leurs interruptions de fin de transfert. Ce qui fait de l'Amiga non pas une machine parallèle mais plutôt spécialisée dans "le multimédia".

Épilogue

Nous n'avons pas présenté l'expansion.library qui permet avec les ressources, les périphériques logiciels ou devices et l'AmigaDOS, le "Plug & Play" depuis au moins neuf ans sur Amiga. Le HAL ou "Hardware Abstraction Layer" implanté par ce qu'Exec appelle des ressources. C'est encore une liste du noyau dont la plupart des éléments sont de "petites" bibliothèques de fonctions pour s'informer puis accéder d'une manière exclusive et directe au matériel, dans un contexte de multiprogrammation. Citons disk.resource, cia.resource, etc. et aussi FileSystem.resource. Grâce à cela, l'Amiga gère l'amorçage automatique ou "AutoBoot" et de multiples systèmes de fichiers. Il faut noter que les ressources sont assez exotiques et que le mécanisme d'autoconfiguration des cartes lié à celui des ressources, des périphériques logiciels et de l'AmigaDOS est aussi compliqué que l'est la compréhension de l'AmigaDOS et de son histoire. Nous terminons ici cette introduction à Exec.

Introduction Exec
Introduction Exec
Introduction Exec

Note concernant l'algorithme de réception (n°1) :

Pour les communications avec les pilotes système, tel qu'input.device et lorsqu'on attend des événements d'Intuition (le message peut ne pas nous appartenir si l'on s'est abonné à ceux-ci), il faut pouvoir répondre plus rapidement et ne pas faire attendre le système GUI (aussi parce qu'on est pas le seul à utiliser le système) une duplication des _champs_ du message, dans des variables locales au programme, avant de le traiter est nécessaire pour respecter le multitâche du système. Ce qui nous permet de répondre plus rapidement, à Intuition, "dans la foulée" et, d'être, très respectueux du système ("system/OS friendly").

D'où l'algorithme n°1 modifié :

Wait(port) ; attente du signal associé au port tant que la file du port n'est pas vide
Faire message <- GetMsg(port) ; défile un message du port de traitement
   ;Copier les champs Data du message dans des variables locales (ici des types int, WORD et short)
   int       monChamp1 = msg->champ1;
   WORD      monChamp2 = msg->champ2;
   short    monChpBool = msg->chpBool;
   ;ReplyMsg() : renvoyer une réponse pour signifier que le message est traité (pseudo-traitement)
   ReplyMsg(message) ; enfile un message dans son port-réponse
   Traiter ce message en utilisant nos variables mon/mesChamps
FinFaire

Enfin, dans le cas où l'on ne peut pas dupliquer les "Data" du message, il faut se mettre en attente sur un timer, patienter, ou mieux en faisant une nouvelle occurrence de passage de messages pour être dans l'esprit du système (c'est ce que devrait proposer, par défaut, les API d'AmigaOS).

Je n'ai pas vérifié, mais seules les ressouces devraient pouvoir verrouiller certaines parties du système au niveau de leur API (Open/Close) ou conceptuellement avec un sémaphore mutex, uniquement binaire si la ressource n'est pas multiple sinon je conseille d'utiliser l'allocation multiple "en pool" de sémaphores binaires (de verrous) ; plutôt qu'un sémaphore multivalué au-delà de la plage 0/1-).


[Retour en haut] / [Retour aux articles]