Suivez-nous sur X

|
|
|
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
|
|
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
|
|
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
|
|
A propos d'Obligement
|
|
David Brunet
|
|
|
|
Programmation : Assembleur - exemple de programme avec Exec (CheckMem 2.0)
(Article écrit par Gilles Soulet et extrait d'Amiga News - juin 1993)
|
|
Ce mois-ci, comme promis dans le précédent article,
nous allons encore avancer dans la découverte d'Exec et d'Intuition, avec une nouvelle version
de notre utilitaire de "surveillance" des vecteurs du système, le bien nommé CheckMem.
Une mise à jour indispensable
CheckMem a beau être un petit outil discret, qui ne se manifeste que très rarement,
il n'est pas question d'en rester à la version 1.0 du point de vue de "l'interface"
(ou de son absence !). Afficher un dégradé de couleurs, c'est peut-être joli, mais l'Amiga
ne peut plus se contenter d'un tel effet, et il est grand temps de commencer à communiquer
réellement avec l'utilisateur.
La bibliothèque concernée est bien évidemment Intuition, et elle propose un nombre
incroyable de fonctions pour gérer le dialogue entre l'homme et sa machine. Cette
bibliothèque, écrite en C et assembleur, représente une grosse partie du système
d'exploitation de l'Amiga. Nous allons découvrir Intuition petit à petit, car la
route est longue et semée d'embûches. Nous allons aussi en profiter pour approfondir
nos connaissances sur Exec, en particulier sur les tâches et les signaux.
Avant de s'attaquer aux gros morceaux de cette bibliothèque, nous allons voir
aujoud'hui une fonction très pratique et très simple à utiliser : la fameuse fonction
DisplayAlert() que vous connaissez bien puisque c'est elle qui affiche l'infâme gourou !
Cette fonction est utilisée pour avertir l'utilisateur qu'il se passe quelque chose
de grave dans le système, et qu'il faut prendre des mesures d'urgence. Lorsqu'elle est
invoquée, l'Amiga interrompt le cours normal de son fonctionnement et affiche un texte
rouge encadré d'un rectangle clignotant du plus bel effet. Les interruptions ne sont pas
inhibées, et la commutation de tâches n'est pas arrêtée, mais l'utilisateur est prié de
cliquer sur un bouton de la souris pour sortir de cette situation.
Il existe deux types d'alertes : l'alerte récupérable (recoverable alert) qui se
signale par le texte en orange, et dont on peut sortir en cliquant sur un bouton souris,
et l'alerte fatale (dead_end alert), dont la couleur rouge indique une future mise à
mort du système. Dans ce cas, cliquer sur un bouton ne doit conduire qu'à redémarrer
complètement l'Amiga !
La fonction DisplayAlert() est utilisée en cas de grave erreur système, par exemple,
si la MemList n'est pas correcte. Dans ce cas, Exec interdit la commutation de tâches,
mais pas les interruptions. La fonction DisplayAlert() prend dans le registre d0
le type de l'alerte que l'on veut envoyer. "0" correspond à une alerte récupérable,
tandis que "1" correspond à une alerte fatale. Le registre d1 contient la hauteur
(en pixels) du rectangle clignotant que l'on veut afficher, et a0
pointe sur une zone mémoire contenant les informations sur le texte encadré par le rectangle.
On trouve tout d'abord dans cette zone un WORD (non aligné) donnant l'abscisse de la première
ligne de caractères, puis un OCTET donnant l'ordonnée, et ensuite la première ligne de texte,
terminée par 0. Ensuite, un octet de "continuation" indique, s'il est à 1,
que le texte contient une autre ligne, et s'il est à 0, qu'il n'y a plus de lignes.
S'il y a une autre ligne, on trouve alors à nouveau un WORD d'abscisse puis un OCTET
d'ordonnée, puis une chaîne, puis à nouveau un octet de continuation, etc.
On peut ainsi afficher un texte de plusieurs lignes sur tout l'écran si on veut.
Si l'alerte est récupérable, la fonction DisplayAlert() renvoie (dans le registre d0)
0 si l'utilisateur a cliqué sur le bouton droit, et 1 s'il a cliqué sur le bouton gauche.
Si l'alerte est fatale, DisplayAlert() renvoie toujours O. Cette fonction permet donc de consulter
rapidement et simplement l'utilisateur. Elle est bien adaptée à notre cas :
nous voulons savoir si, un vecteur ayant été modifié, il faut impérativement le corriger, ou plutôt
prendre en compte cette modification. DisplayAlert() est faite pour nous !
Oui, mais...
En effet, "oui mais", car c'est ici que les choses se gâtent. Nous n'allons pas pouvoir
utiliser DisplayAlert() depuis notre interruption, car cette merveilleuse fonction ouvre
un écran (noir). Donc elle alloue de la mémoire.
Et il est absolument interdit
de procéder à une allocation mémoire pendant une interruption, car les routines de gestion
des MemLists n'inhibent pas les interruptions. Donc si on alloue de la mémoire dans une
interruption, on s'expose à ce que ceci ce fasse pendant qu'une tâche était justement
en train d'allouer ! Ceci provoquerait immédiatement de gros problèmes dans la gestion de la MemList...
C'est pour cela que je vous disais qu'il fallait trouver un moyen de "sortir" de l'interruption.
Le seul moyen possible est de créer une nouvelle tâche qui va se charger de prévenir l'utilisateur.
Mais quand cette nouvelle tâche doit-elle être créée ? Pendant l'interruption, au moment où l'on
trouve un vecteur modifié ? Non ! Il est également interdit de créer une nouvelle tâche sous
interruption (décidément !) car après l'appel à la fonction AddTask(), Exec procède à un ReSchedule()
qui bidouille lui aussi avec les interruptions ! Ne cherchez pas d'informations sur les
fonctions Schedule() et ReSchedule() : Commodore n'en a presque pas donné !
Disons qu'elles servent à gérer la répartition du temps machine entre les différentes tâches.
Par contre, on peut très bien créer une tâche au départ, qui va s'endormir aussitôt, puis réveiller
ensuite cette tâche avec un signal. Ce signal sera envoyé depuis l'interruption lorsqu'un vecteur
est modifié, et la tâche réveillée se chargera d'appeler DisplayAlert(). Voilà la solution !
La création d'une tâche
Notre petit programme CheckMem va donc procéder dès le départ à la création d'une
tâche destinée à avertir l'utilisateur, sauf si elle s'y trouve déjà (rappelons que
ce programme fonctionne comme un interrupteur On/Off). Cette petite tâche
agit comme un "démon". Ce n'est pas un processus, mais une simple tâche (Task)
qui reste endormie, invisible. Elle ne prend pas de temps machine, et n'est réveillée
qu'en cas de nécessité.
La création se fait au moyen de la fonction AddTask(). Cette fonction prend trois
paramètres en entrée : A2 pointe sur une zone qui contient le code machine à exécuter
dès le départ (Initial PC). A3 pointe sur le code à exécuter lorsque la tâche rencontrera le
dernier RTS. Si A3 est nul, le système place au sommet de la pile de la tâche l'adresse
standard de fin de programme, qui libère la mémoire allouée par AllocEntry() et élimine
la tâche par un RemTask(). Enfin, on place dans A1 l'adresse d'une nouvelle structure "Task"
correctement initialisée. Voici le détail de cette structure :
struct Task {
struct Node tc_Node;
UBYTE tc Flags; drapeaux (utilisés par le système)
UBYTE tc_State; état (waiting, running, ready, etc.)
BYTE tc_iDNestCnt; compteur pour disable()
BYTE tc_TDNestCnt; compteur pour forbid()
ULONGSigAlloc; signaux alloués
ULONG tc_SigWait; signaux attendus
ULONG tc_SigRervd; signaux reçus
ULONG tc_SigExcept; signaix provoquant une exception
UWORD tc_TrapAlloc; traps alloués (1 bit par trap)
UWORD tc_TrapAble; traps autorisés (16 traps possibles)
APTR tc_ExceptData; -> données pour exception
APTR tc_ExceptCode; -> code pour exception
APTR te TrapData; -> données pour trap
APTR tc_TrapCode; -> code pour trap
APTR tc_SPReg; pointeur de pile task
APTR tc_SPLower; -> limite basse de la pile task
APTR tc_SPUpper; -> limite haute
VOID (*tc_Switch)(); -> routine invoquée avant perte processeur
VOID (*tcLaunch)(); -> routine invoquée avant gain processeur
struct List tc_MemEntry; liste de zones allouées avec AllocEntry()
APTR tc_UserData; pointeur disponible pour le programmeur
|
La signification des différents champs est relativement claire, mais
certains d'entre eux doivent être explicités. Ainsi, tc_ExceptData
et tc_ExceptCode sont utilisés en cas d'exception, au sens Exec (et non
au sens de l'exception du 68000). Une exception Exec est provoquée par
un signal (soft) qui déroute la tâche vers un traitement particulier,
par exemple quand on tape "control C" pour arrêter un programme.
tc_TrapData et tc_TrapCode sont utilisés en cas d'exception 68000,
c'est-à-dire une erreur logicielle (division par zéro, violation de privilège,
etc.) ou un appel à l'instruction TRAP du 68000.
Les champs tc_IDNestCnt et tc_TDNestCnt servent respectivement à compter
le nombre d'appels aux fonctions disable()/enable() et forbid()/permit().
Une dernière précision sur les champs tc_Switch et tc_Launch :
ils pointent chacun vers une région mémoire qui contient une routine devant
être exécutée lorsque la tâche perd ou reprend le contrôle du 68000 lors de
la commutation de tâche (task-switching).
Peu de champs doivent être initialisés au départ : tc_SPUpper doit
pointer vers la limite supérieure de la pile de la tache, tc_SPLower
pointe sur la limite inférieure de cette pile, et tc_SPReg est initialisé
avec l'adresse de départ du pointeur de pile task, qui se trouve bien
évidemment entre les deux limites précédentes ! Il faut donc avoir préalablement
alloué de la mémoire pour la pile task. Une valeur de 1000 octets est souvent
suffisante. Le CLI exécute toutes ses commandes avec une pile standard de 4000 octets.
Les signaux
Après avoir créé notre nouvelle tâche et ajouté l'interruption qui espionne
certains vecteurs, notre programme se termine et quitte le système.
La nouvelle tâche créée par AddTask() s'exécute immédiatement après sa création.
Elle possède sa propre pile et sa propre zone de code, qui ont toutes deux
été recopiées par le programme principal dans une zone mémoire allouée. Cette
nouvelle tâche ne doit pas consommer de temps machine : elle attend simplement
d'être réveillée par l'interruption si un vecteur est modifié. Elle sera réveillée
à la réception d'un signal envoyé depuis l'interruption.
Exec gère un système de signaux pour les tâches : chacune peut se réserver
jusqu'à 32 signaux différents, et attendre un ou plusieurs signaux à la fois.
On utilisera la fonction AllocSignal() qui permet soit d'allouer un signal bien
spécifié, dont le numéro (de 0 à 31) est transmis dans d0, ou bien le premier
signal disponible sur le masque, lorsqu'on met "-1" dans d0.
Nous laisserons
à Exec le choix du signal que l'on va utiliser, en mettant "-1" dans d0. Ceci
nous garantit au moins l'obtention d'un signal, c'est pourquoi nous ne testerons
pas si AllocSignal() a planté. Le résultat renvoyé est le numéro du signal alloué,
que nous transformerons en masque binaire qui sera stocké dans la case Signal Alloue,
visible depuis l'interruption.
Ensuite, la tâche n'a plus qu'à s'endormir paisiblement en attendant d'être
réveillée par l'interruption. Dès le réveil, elle s'assure qu'elle a bien été
réveillée par l'interruption, en comparant le masque reçu avec celui attendu, puis
elle déclenche la fonction DisplayAlert() pour avertir l'utilisateur.
Les adresses des vecteurs modifiés et originels sont transmises dans les
cases VectMod et VectOrg par l'interruption. Cette dernière case nous sert
également de sémaphore : si elle est différente de zéro, c'est qu'on a déjà
réveillé la tâche et qu'on vient de lui transmettre les vecteurs. Dans ce cas,
on ne fait rien. Ceci permet de n'envoyer depuis l'interruption qu'un seul
signal de réveil, et non 50 signaux par seconde, ce qui finirait par planter tout le système !
Petit exercice...
Je ne vous en dis pas plus pour aujourd'hui. Regardez attentivement
le listing du programme : il est abondamment commenté et très facile
à comprendre. Il comporte aussi une petite erreur de conception, assez
facile à déceler, et qui pourra nous gêner sur les prochaines versions
de CheckMem. Un numéro gratuit d'Amiga News du mois juillet sera offert
au premier lecteur qui trouve l'erreur !
Listing
|