Obligement - L'Amiga au maximum

Vendredi 06 juin 2025 - 12:25  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

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

Articles in English


Réseaux sociaux

Suivez-nous sur X




Liste des 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,
ALL


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


Galeries

Menu des galeries

BD d'Amiga Spécial
Caricatures Dudai
Caricatures Jet d'ail
Diagrammes de Jay Miner
Images insolites
Fin de jeux (de A à E)
Fin de Jeux (de F à O)
Fin de jeux (de P à Z)
Galerie de Mike Dafunk
Logos d'Obligement
Pubs pour matériels
Systèmes d'exploitation
Trombinoscope Alchimie 7
Vidéos


Téléchargement

Documents
Jeux
Logiciels
Magazines
Divers


Liens

Associations
Jeux
Logiciels
Matériel
Magazines et médias
Pages personnelles
Réparateurs
Revendeurs
Scène démo
Sites de téléchargement
Divers


Partenaires

Annuaire Amiga

Amedia Computer

Relec


A Propos

A propos d'Obligement

A Propos


Contact

David Brunet

Courriel

 


Dossier : La bibliothèque Exec
(Article écrit par Gilles Soulet et extrait d'Amiga News - décembre 1992)


Après un premier chapitre introductif, nous allons attaquer ce mois-ci les choses sérieuses et nous intéresser de près à une bibliothèque très particulière ; elle s'appelle "exec.library", et son adresse de base est toujours stockée à l'adresse 4 de l'Amiga. C'est en quelque sorte le "coeur" ou le "noyau" du système d'exploitation. Dans cette bibliothèque sont regroupées toutes les fonctions primitives du système : gestion des listes, gestion de la mémoire, gestion du multitâche, etc.

Comprendre Exec, c'est comprendre comment marche un Amiga ! Aussi, nous allons commencer aujourd'hui par une description d'Exec et de ses principaux fondements.

Exec et les concepts de base

L'Amiga se programme en utilisant fréquemment un certain nombre de petites structures qu'il est bon de connaître. Pour commencer, voici la structure la plus utilisée dans le système :

Exec

Cette structure n'est pratiquement jamais utilisée seule. Elle est placée en tête d'une structure plus importante, afin de pouvoir chaîner celle-ci avec d'autres structures, dans une liste. Ainsi, dans l'article précédent, vous aviez pu remarquer que la structure d'une bibliothèque commençait justement par une structure de type noeud. C'est précisément parce que les bibliothèques sont toutes chaînées les unes aux autres dans la liste système "LibList".

Exec

Les listes jouent un rôle fondamental. Par exemple, à chaque tâche correspond une structure "Task" qui commence par une structure "Node". Toutes les tâches (ou processus) présentes dans le système sont placées dans deux listes : "TaskReady" contient la liste des tâches "actives", c'est-à-dire les tâches qui effectuent un travail et ont donc besoin de temps machine, tandis que "TaskWait" contient la liste des tâches "endormies", en attente d'un événement extérieur, et qui n'ont donc pas un besoin immédiat d'utiliser le processeur.

Les listes de l'Amiga sont dites "doublement chaînées", car chaque élément possède un pointeur sur le suivant et un pointeur sur le précédent. Cette technique consomme un peu plus de mémoire, mais facilite beaucoup la recherche, l'insertion ou la suppression d'un élément dans une liste.

Une liste se compose d'une "tête", qui contient les pointeurs sur le premier et le dernier élément (ou noeuds), ainsi qu'un octet donnant le type des éléments de la liste. La structure d'une tête de liste est la suivante :

Exec

Exec Exec

Lorsque la liste est vide, le champ "lh_Head" pointe sur le champ "lh_Tail", tandis que le champ "lh_tailPred" pointe sur "lh_Head". On peut s'interroger sur l'utilité du champ "lh_Tail". En effet, celui-ci contient toujours 0 ! Nous avons vu que le champ "ln_Succ" de la structure "Node" contenait l'adresse du noeud suivant. Or, une liste n'est jamais infinie ! Il faut bien qu'elle finisse quelque part. La plupart du temps, dans une liste chaînée d'éléments, lorsqu'on arrive au dernier élément, le pointeur sur l'élément suivant est mis à zéro. Sur Amiga, au niveau du dernier élément, le pointeur sur l'élément suivant contient l'adresse du champ "lh_Tail" de la tête de la liste, qui contient â son tour zéro. Il y a donc une indirection supplémentaire.

Exec

Cette structure peut paraître compliquée, mais elle présente en fait trois gros avantages. En premier lieu, elle facilite la détection d'une fin de liste. On utilise un seul registre d'adresse pour balayer la liste, en faisant par exemple "MOVE.L (A0),A0" pour passer à l'élément suivant, et faire un test indirect, grâce à l'instruction "TST.L (A0)" pour détecter la fin de la liste (il n'aurait pas été possible d'utiliser un "TST" directement sur A0, car l'instruction "TST.L A0" n'existe pas en 68000. De plus, le bit d'état "Z" n'est pas positionné lorsqu'on affecte la valeur 0 à un registre d'adresse).

Autre avantage, la recherche ne nécessite qu'un seul registre d'adresse, et il n'est pas nécessaire de garder un pointeur car le dernier élément lorsqu'on veut insérer un élément en fin de liste ! En effet, lorsque toute la liste a été balayée, et que A0 pointe sur le champ "lh_tail", il suffit de regarder le champ suivant (quatre octets plus loin) pour retrouver le dernier élément !

Enfin, dernier avantage, il n'y a pas de traitement particulier à faire lorsqu'on insère un élément dans une liste vide ou qu'on enlève le dernier élément d'une liste. On évite ainsi des tests supplémentaires qui pourraient ralentir les routines de manipulation des listes, qui sont parmi les plus utilisées dans la ROM de l'Amiga.

Voici un petit programme qui recherche dans une liste le premier élément d'une priorité donnée dans D0 et si un tel élément n'existe pas, insère alors un noeud pointé par A1 en fin de liste. Remarquez que la recherche ne consomme qu'un seul registre : A0.

Exec

Vous vous en doutez, les listes sont extrêmement utilisées dans l'Amiga. Exec contient dans sa structure un certain nombre de têtes de listes fondamentales :
  • MemList : liste des zones mémoire (physiques) présentes.
  • ResourceList : liste des ressources.
  • DeviceList : liste des périphériques logiques.
  • IntrList : liste des serveurs et handlers d'interruptions.
  • LibList : liste des bibliothèques ouvertes dans le système.
  • PortList : liste des ports de communication (notamment ARexx).
  • TaskReady : liste des tâches actives.
  • TaskWait : liste des tâches endormies.
  • SemaphoreList : liste des sémaphores.
De façon générale, la plupart des bibliothèques contiennent des têtes de listes pour accéder aux ressources qu'elles gèrent. Par exemple, dans la bibliothèque Intuition, on trouve une liste des écrans ou verts dans le système. Chaque fois que vous créez une nouvelle tâche, que vous ouvrez une bibliothèque, un écran ou une fenêtre, etc., vous rajoutez sans le savoir un noeud dans une liste, de façon transparente. Le système, lui, a besoin de fonctions pour manipuler les listes. De plus, le programmeur peut avoir besoin de gérer ses propres listes. C'est pourquoi Exec contient les fonctions nécessaires pour manipuler simplement et rapidement les listes. En voici une description rapide :
  • AddHead(Liste, Noeud) : insère un noeud en tête de liste.
  • RemHead(Liste) : enlève le premier élément d'une liste.
  • AddTail(Liste, Noeud) : insère un noeud en fin de liste.
  • RemTail(Liste) : enlève le dernier élément d'une liste.
  • Inser(Liste, Noeud, Pred) : insère un noeud après l'élément "pred".
  • Remove(Noeud) : enlève un noeud d'une liste.
  • Enqueure(Liste, Noeud) : insère un noeud suivant sa priorité.
  • FindName(Liste, Nom) : trouve un noeud d'après son nom.
Ces fonctions sont très efficaces, car écrites entièrement en assembleur, comme toutes les fonctions d'Exec dans AmigaOS 1.3 !

Exec

Exec et le multitâche

Ah ! Le multitâche ! C'est en utilisant un PC ou même un Mac, alors qu'on a l'habitude de travailler sur AmigaOS, qu'on se rend compte de la puissance du multitâche ! Et pourtant, il faut savoir qu'écrire un véritable système d'exploitation multitâche, sans utiliser les possibilités offertes par les microprocesseurs les plus récents (mode "maître" ou "protégé", changement automatique de contexte d'exécution, MMU, etc.) est une chose très difficile à faire.

C'est une véritable prouesse qu'ont réalisée les concepteurs d'Exec ! Certes, Windows 3 ou OS/2 (sur PC) permettent eux aussi de faire du multitâche, mais ces systèmes utilisent en réalité les possibilités des processeurs 386/486 pour faire de la "commutation de contexte", qui est une solution "matérielle", consistantes quelque sorte à "isoler" les tâches les unes des autres, tout en leur attribuant des segments de mémoire privés.

En aucun cas, ces systèmes ne permettent de faire du multitâche sur un simple processeur 8086. Seul GEOS, sur PC, se rapproche un peu d'un véritable système multitâche.

Apple essaie, en vain, depuis plusieurs années, d'écrire un tel système pour le Mac : on attend toujours ! Et même si un système multitâche existe un jour sur Mac, il faudra nécessairement posséder un 68030 pour l'utiliser. La tâche est rendue d'autant plus difficile que certains programmes sur Mac ou sur PC ont pris dès le début de "mauvaises habitudes", rendant leur cohabitation avec d'autres programmes très difficile, voire impossible. Ce n'est heureusement pas le cas sur notre machine, où des règles strictes de bonne conduite ont été imposées dès le départ, afin que plusieurs programmes cohabitent en utilisant les mêmes ressources, les mêmes bibliothèques, sans se gêner les amies autres.

Car il faut bien voir qu'il y a quand même de gros problèmes ! Que se passe-t-il lorsque deux programmes accèdent en même temps à une unité de disque, à une imprimante, à un fichier ouvert, ou même simplement à la mémoire ? Il existe beaucoup de situations critiques ; le système doit savoir les gérer, et pouvoir mettre en attente des processus. Un processus doit aussi faire preuve d'une certaine dose de civisme : avertir les autres processus quand il est en train d'utiliser une ressource, et ne pas la monopoliser indéfiniment, ne pas écrire n'importe où dans la mémoire, ne pas consommer inutilement du temps machine, etc.

Quoi qu'il en soit, c'est Exec qui gère entièrement le multitâche sur l'Amiga, et voici en gros comment ça se passe. Le temps machine est partagé entre les différents processus actifs, et il existe un système de priorités pour ces tâches ; encore une fois, les listes sont utilisées pour cela. Les tâches actives sont triées, selon la priorité de leur noeud, dans la liste "TaskReady". Exec choisira toujours la ou les tâches les plus prioritaires, et leur cèdera le contrôle du processeur pendant un certain temps. Ainsi, une tâche active de forte priorité passera toujours avant une tâche active de priorité plus faible.

Au niveau d'une tâche, la commutation est totalement transparente : une tâche ne s'aperçoit pas qu'elle est interrompue régulièrement, pas plus qu'elle ne peut savoir quand elle va être interrompue ! Elle effectue son travail paisiblement, sans se douter de rien !

Plus concrètement, toutes les deux millisecondes, une interruption est déclenchée par un chronomètre (timer). Le processeur suspend le cours normal de son exécution, passe en mode superviseur, et traite la routine Switch() d'Exec. Il doit avant tout préserver le contexte d'exécution de la tâche qui vient d'être interrompue. Pour cela, tous les registres machine sont empilés sur la pile utilisateur de la tâche. En effet, chaque tâche possède sa propre pile pour stocker ses données ou ses adresses de retour de JSR. PC (compteur ordinal = adresse d'exécution du code machine) et SR (registre d'état) sont également empilés. Ensuite, Exec consulte la liste "TaskReady". Si une autre tâche active possède une priorité au moins égale à la tâche qui vient d'être suspendue, elle prend le contrôle du processeur. Sinon, c'est la tâche suspendue qui reprend le contrôle du processeur.

Les registres sont alors dépilés de la pile de la tâche sélectionnée, SR et PC sont aussi restaurés, et Switch() se termine par un magnifique RTE qui permet à l'exécution de se poursuivre. Il peut arriver qu'il n'y ait plus de tâche active dans le système (liste TaskReady vide). Dans ce cas, Switch() arrête le processeur, en attente d'une nouvelle interruption, grâce à un STOP #2000.

Quand vous lancez une commande avec "Run", ou que vous lancez un programme depuis le Workbench, une tâche est rajoutée dans le système au moyen de la fonction "AddTask()". On passe à cette fonction trois paramètres : une structure de type "Tank", qui contient les informations générales sur la tâche (taille et adresse de sa pile, priorité, etc.), une adresse "Initial_PC" qui donne l'adresse du début du programme, et une adresse "Final_PC" qui donne l'adresse de fin du programme. Si Final_PC est donné à zéro, c'est la routine standard qui est utilisée : elle libère les segments du programme ainsi que la mémoire allouée avec AllocEntry(), puis élimine la tâche du système, au moyen de la fonction RemTask(). L'exécution s'arrête avec la fonction DisPatch() qui provoque un arrêt du processeur, en attente d'une nouvelle interruption. Remarquez que la fonction RemTask() peut être utilisée pour éliminer n'importe quelle tâche du système, ce qui peut être très dangereux !

Les tâches endormies sont placées dans une autre liste : "TaskWait". Une tâche est endormie lorsqu'elle attend un événement extérieur, et qu'elle n'a donc pas besoin d'utiliser le processeur. C'est le cas, par exemple, quand un programme ouvre une fenêtre de dialogue, et attend la réponse de l'utilisateur, ou quand un éditeur de texte attend qu'une touche clavier soit tapée. Dans tous ces cas, il serait idiot que le processeur tourne "à vide" en attendant.

Exec autorise une tâche à attendre gentiment un événement, au moyen des fonctions Wait() et WaitPort(). De nos jours, tous les programmes utilisent ce principe, et c'est ce qui rend le multitâche de l'Amiga si agréable et si efficace. Quand une tâche est réveillée, son noeud est transféré par Exec dans la liste TaskReady, et elle sera alors exécutée, à moins qu'une autre tâche de priorité plus importante ne monopolise le processeur.

Il existe aussi des cas où une tâche ne doit absolument pas être interrompue. Par exemple, dans une portion de code très "critique", qui modifie des routines système (au moyen de SetFonction() par exemple) ou quand on manipule des ressources communes, des interruptions, etc. Pour interdire la commutation entre tâches, on utilise la fonction Forbid(). Après un Forbid(), une tâche est assurée qu'elle ne sera pas interrompue par une commutation. Par contre, des interruptions continuent à être générées. Si une tâche ne veut même pas être dérangée par une interruption, elle utilise alors la fonction Disable().

Pour restaurer le multitâche et les interruptions, on utilise respectivement les fonctions Permit() et Enable(). Disable() et Enable() doivent être utilisées avec précaution. En particulier, le système gère assez mal que les interruptions soient interdites pendant "assez longtemps".

Exec et la mémoire

La gestion de la mémoire est l'un des aspects les plus problématiques d'un système multitâche. Sur un bon vieux C64, les problèmes d'allocation mémoire ne se posaient pas : un programme était toujours logé à la même place et utilisait toujours les mêmes cases mémoire, car il était toujours tout seul ! Sur un Amiga, plusieurs tâches peuvent se partager la mémoire. Il faut pouvoir charger un programme n'importe où dans la mémoire, sans en écraser un autre, et ce même programme doit pouvoir s'octroyer des zones de travail, sans gêner les autres tâches présentes dans le système !

Pour résoudre tous ces problèmes, Exec gère un système "d'allocation" de la mémoire. Au démarrage, presque toute la mémoire est disponible pour tout le monde. On dit que la mémoire est "libre". Ensuite, chaque fois qu'une tâche est chargée dans le système ou qu'une tâche besoin d'une zone de travail, la mémoire doit être "allouée". Dans le cas d'une nouvelle tâche qui vient s'installer, c'est la fonction LoadSeg() qui s'occupe de charger le code en mémoire et d'allouer les zones correspondantes. Si une tâche a besoin de s'octroyer une partie de la mémoire, il existe plusieurs fonctions (dans Exec) pour le faire : AllocMem(), AllocEntry(), AlloccAbs()...

Une tâche ne peut pas savoir à l'avance où elle va être chargée, ni à quelle adresse le système peut lui allouer une zone mémoire. C'est le système qui le décide en fonction de la mémoire disponible. La seule exception est la fonction AllocAbs() qui permet à une tâche de tenter une allocation à une certaine adresse, sachant que cette allocation peut très bien échouer si la zone est déjà allouée.

Une zone de mémoire allouée n'est plus disponible pour une nouvelle allocation : il ne faut donc pas oublier de la libérer après utilisation, pour qu'elle puisse à nouveau être utilisée, sans quoi, elle est irrémédiablement perdue. Par rapport à d'autres systèmes existants, la gestion mémoire sur Amiga est assez simple et "laxiste". La mémoire n'est ni "segmentée" ni "privée". En clair, n'importe qui peut faire n'importe quoi ! Rien n'empêche une tâche d'aller écrire n'importe où en mémoire, par exemple sur le code d'une autre tâche ou sur des vecteurs du système !

Sous Unix, chaque processus possède sa propre zone mémoire pour ses besoins. La mémoire est rendue "privée" grâce à des solutions matérielles, par exemple une MMU (Memory Management Unit). Une MMU interdira à un processus d'écrire à une adresse non allouée, ce qui sécurise beaucoup le système. De même, grâce à une MMU, il est facile de repérer toutes les allocations mémoire produites par un processus, et de libérer cette mémoire en cas de plantus ou en cas "d'oubli".

Enfin, grâce à une MMU, on peut éviter le phénomène de fragmentation. La fragmentation est le gros point noir de la gestion mémoire de l'Amiga. Il faut dire que quand cette machine a été conçue, il n'était pas concevable d'y mettre une MMU à cause du coût. La fragmentation, c'est le fait de voir se créer peu à peu une succession de zones mémoire libres et allouées, ces zones étant de plus en plus petites. Ce phénomène se produit dans tout système d'allocation/libération de mémoire. Imaginons que vous chargiez un premier programme. Le système l'installe au début de la mémoire libre. Ensuite, vous lancez une deuxième application. Elle sera logée juste après la première. Si le premier programme se termine, la mémoire qu'il occupait est libérée. Donc le système possède à présent une zone libre, puis une zone allouée, puis le reste de la mémoire libre. Imaginons qu'une troisième application soit lancée. Si elle est trop importante pour être logée dans la première zone libre, le système sera obligé de la charger après la seconde application, diminuant encore ainsi un peu plus la taille de la plus importante zone libre. Si, par contre, elle est plus "petite" que la première application, un "trou" de mémoire libre sera créé entre deux zones allouées.

N'oublions pas non plus qu'un programme a presque toujours besoin de faire des allocations mémoire, ce qui aggrave encore les problèmes. Petit à petit, la mémoire va devenir de plus en plus morcelée, et la taille des zones libres, de plus en plus petite : c'est la fragmentation. Au bout du compte, il se peut qu'un programme ne puisse pas se charger, non pas parce que la quantité totale de mémoire libre est insuffisante, mais parce que le plus gros bloc de mémoire libre est trop petit pour contenir le programme.

Pour tenter de limiter ce problème, les concepteurs de l'Amiga ont imaginé de "segmenter" les programmes en plusieurs morceaux (les fameux "hunks", ou segments). Ainsi, un exécutable de 300 ko pourra être divisé par exemple en trois segments de 100 ko, et être logé en mémoire même si le plus large bloc disponible fait moins de 300 ko. Malheureusement, ce système aggrave encore la fragmentation !

Avec une MMU, on peut "ramasser les miettes" (garbage collecting) en modifiant logiquement la cartographie mémoire au niveau du processeur. Cela revient à trafiquer les adresses des zones libres de façon à les mettre les unes à la suite des autres. Mais il faut alors une cartographie mémoire par processus ! On peut aussi forcer une application à réserver d'entrée une zone mémoire suffisante pour son code, ses données et toute autre mémoire dont elle a besoin pour travailler, puis libérer toute la zone lorsque le programme se termine ou plante. C'est la solution utilisée sur Macintosh, et elle présente l'avantage de limiter la fragmentation. Un tel système n'est pas utilisable sur Amiga, car une application peut installer en mémoire des ressources qui seront utilisées ensuite pas d'autres applications : bibliothèques, ports messages, écrans publics, etc.

Espérons malgré tout qu'avec la généralisation du processeur 68030 et de sa MMU, les futures versions d'Exec pourront remédier à ces problèmes. L'Amiga en a grandement besoin si l'on veut qu'un jour le système devienne aussi robuste qu'Unix (et encore...).

Voilà, j'en ai fini avec Exec pour ce mois-ci. La prochaine fois, nous appliquerons tout ceci grâce à un petit programme très simple, ce qui nous permettra de découvrir une autre bibliothèque fondamentale de l'Amiga : Intuition.


[Retour en haut] / [Retour aux articles] [Article précédent]