Obligement - L'Amiga au maximum

Samedi 31 mai 2025 - 15:47  

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

 


Programmation : Assembleur - les sémaphores
(Article écrit par Frédéric Delacroix et extrait d'Amiga News - septembre 1994)


Il est souvent commode de diviser un problème complexe en plusieurs tâches simples. Mais, alors qu'une tâche a le droit de faire ce qu'elle veut de ses données propres, un arbitrage doit être prévu pour des données communes. C'est ici qu'interviennent les sémaphores.

Mais d'abord, pourquoi un arbitrage ? Eh bien imaginez qu'une tâche est en train de modifier une zone de données communes (par exemple une liste globale), et qu'une autre commence à la lire avant même que la première ait terminé (rien de physique ne l'en empêche), elle a de fortes chances de trouver la liste dans un état intermédiaire, donc invalide. Il faut donc réglementer les accès à de telles zones, un peu à la manière des feux rouges qui réglementent l'accès aux carrefours.

La méthode la plus simple consiste à utiliser Forbid() et Permit(), mais ceci est aussi très gênant car cela arrête toutes les tâches (un peu comme si on paralysait une ville entière au lieu d'un seul carrefour). Exec offre à cela l'élégante solution des sémaphores : au lieu d'arrêter toutes les tâches du système, on n'arrête que celles qui sont nécessaires. Celles-ci sont alors mises en attente, ne consomment alors aucun temps processeur, et se réveillent dès que l'accès est libre.

Concrètement

Sur Amiga, les sémaphores sont implémentés au sein de l'exec.library, grâce à quelques fonctions bien utiles. Écartons tout de suite les sémaphores à l'ancienne mode, utilisés avant le Kickstart 3.0 (jusqu'à la version 38 incluse donc) par les deux fonctions Procure() et Vacate(). Ce système comportait un certain nombre de bogues et ces deux fonctions utilisent depuis le Kickstart 3.0 un système beaucoup plus sûr. Ceci n'affecte pas les autres fonctions dédiées aux sémaphores, qui restent compatibles.

On peut trouver dans le fichier include exec/semaphores.[hi] la définition d'une structure SignalSemaphore, utilisée par toutes les fonctions d'Exec :

Assembleur

En règle générale, on n'a pas trop à se soucier du contenu exact de cette structure. Examinons tout de même quelques champs importants :

ss_WaitQueue : une liste de toutes les tâches en attente de ce sémaphore. En fait, ce sont des pointeurs sur les tâches en attentes qui sont chaînés dans cette liste (les structures Task elles-mêmes sont déjà chaînées dans une autre liste système, Ready ou Waiting). Je présume qu'à partir de la V39, on peut aussi y trouver des SemaphoreMessages, utilisés par Procure()/Vacate().

ss_Owner : pointeur sur la structure Task qui possède le sémaphore.

ss_QueueCount : nombre de tâches en attente de ce sémaphore.

ss_NestCount : indique le nombre de fois que le sémaphore a été réservé. Le sémaphore n'est libre que si ce compteur est à 0.

Les types de sémaphores

Depuis le Kickstart 2.0, on distingue deux types d'accès aux sémaphores : les accès exclusifs et les accès partagés (avant, tous les sémaphores étaient exclusifs). La règle est simple : un sémaphore peut être possédé par un nombre quelconque de tâches en accès partagé, ou bien par une seule tâche en accès exclusif. Ça ne vous rappelle rien ? Mais si bien sûr ! Les "locks" (verrous) du DOS !

On peut assimiler un accès exclusif à une "écriture" et un accès partagé à une "lecture". Notons tout de suite que, bien que les sémaphores sont généralement utilisés pour protéger une zone mémoire commune à plusieurs tâches, ils peuvent être utilisés pour tout autre chose, et cette protection est uniquement une question de protocole respecté par les "clients" (rien de physique n'interdit à une tâche de violer l'accès d'une autre tâche). Voyons les différentes fonctions de base pour la gestion des sémaphores : InitSemaphore(), ReleaseSemaphore(), ObtainSemaphore(), ObtainSemaphoreShared() (V36), AttemptSemaphore(), AttemptSemaphoreShared() (V36), ObtainSemaphoreList(), ReleaseSemaphoreList(), Procure() (V39), Vacate() (V39).

Un sémaphore, de la même manière qu'un port message, peut être soit public (il a alors un nom et une priorité, appartient à une liste d'Exec, et peut servir de point de rendez-vous à plusieurs tâches), ou privé (les tâches qui l'utilisent doivent savoir où il est). C'est à peu près la même chose en ce qui concerne la gestion, il y a juste lors de l'initialisation des sémaphores publics un appel à AddSemaphore() et RemSemaphore(). Les sémaphores publics peuvent être trouvés par la fonction FindSemaphore() (qui doit absolument être encadrée par Forbid()/Permit()), mais les fonctions ObtainlReleaseSemaphoreList() (qui posent d'ailleurs d'autres problèmes) ne doivent pas être utilisés sur eux.

Un sémaphore doit d'abord être initialisé. Ceci se fait grâce à la fonction InitSemaphore(), qui prend en A0 un pointeur sur la structure SignalSemaphore et se charge de tout. Pour un sémaphore public, c'est fait automatiquement par AddSemaphore(). Une fois ceci accomplis, le sémaphore est prêt à l'emploi.

Une tâche désirant le sémaphore pour un accès exclusif peut utiliser soit ObtainSemaphore(), soit AttemptSemaphore(). La différence entre les deux réside dans le fait que la première met la tâche en attente dans le cas où le sémaphore n'est pas libre, tandis que la seconde retourne immédiatement et indique l'échec par un 0 en D0. La tâche qui a réussi à obtenir le sémaphore peut alors accéder aux données protégées. Quand elle a terminé, elle utilise ReleaseSemaphore() pour laisser sa place aux tâches qui attendent. Pour un accès partagé, c'est strictement identique, en remplaçant les fonctions par leurs homologues Obtain/AttemptSemaphoreShared(). Toutes les fonctions citées prennent en paramètre un pointeur sur la structure SignalSemaphore en A0.

Les choses sont différentes pour le mécanisme de Procure()/Vacate() (valable, rappelons-le, uniquement depuis le Kickstart 3.0). Ces fonctions permettent de réclamer un sémaphore de façon asynchrone. Pour cela, on fournit à Procure() un pointeur sur le SignalSemaphore en A0, mais aussi en A1 un pointeur sur une structure SemaphoreMessage. C'est une structure message un peu étendue, qui sera retournée par ReplyMsg() lorsque la tâche aura obtenu le sémaphore, avec son champ ssm_Semaphore pointant sur le sémaphore en question. Le champ ln_Name doit être mis à 0 pour un accès exclusif, ou à 1 pour un accès partagé. Pour libérer un sémaphore, ou annuler une demande, on utilise la fonction Vacate(), qui prend les mêmes paramètres et rend le sémaphore à l'usage public. Le message est alors retourné avec le champ ssm_Semaphore à 0.

Enfin, précisons que lorsqu'une tâche possède un sémaphore (aussi bien en exclusif qu'en partagé), il lui est possible de le demander une nouvelle fois sans pour autant provoquer de blocage, c'est à cela que sert le champ ss_NestCount. Si le sémaphore a été bloqué deux fois, il doit être libéré deux fois pour être accessible à d'autres tâches. Ceci permet d'utiliser des routines qui bloquent le sémaphore alors qu'il l'est déjà, sans trop s'en faire. Sur ce, nous pouvons passer à l'écriture d'un programme d'exemple.

L'exemple

Pas facile d'écrire un programme d'exemple simple... Je vous propose deux programmes pour le prix d'un, judicieusement nommés Prog1 et Prog2. Ils sont destinés à être utilisés simultanément dans deux Shell : en lançant Prog1 dans le premier Shell, on crée un sémaphore public. Prog1 attend alors un signal Ctrl-F pour le détruire. Prog2, lancé dans un autre (ou plusieurs autres) Shell, tente de réserver le sémaphore créé par Prog1, et le libère à la réception d'un signal Ctrl-C.

A la réception du Ctrl-F, Prog1 réserve le sémaphore (ObtainSemaphore()) avant de le détruire, et donc attend que toutes les occurrences de Prog2 l'aient libéré. Une fois que tous les Prog2 auront reçu leur Ctrl-C, Prog1 détruira le sémaphore et se terminera. Les accès par Prog2 se font en partagé, les accès par Prog1 en exclusif. Conséquence : Kickstart 2.0 ou plus requis.

Un détail qui montre la prudence requise pour ce genre de choses : imaginez que Prog1, ayant reçu son Ctrl-F, attende l'accès au sémaphore, et que l'utilisateur (vous !) lance un autre Prog2, qui sera lui aussi mis en liste d'attente (en supposant qu'il réclame cette fois-ci un accès exclusif, ce n'est pas le cas de notre exemple). Lorsque le sémaphore redeviendra libre, Prog1 libèrera sa mémoire, et le dernier Prog2 se trouvera en train d'accéder à une zone mémoire qui a été libérée, d'où un plantage quasi assuré !

La solution est, une fois que Prog1 a reçu son Ctrl-F, de rendre le sémaphore privé, en le retirant par RemSemaphore(). Le sémaphore est toujours valable pour les tâches qui connaissent son adresse, mais il est introuvable pour les nouveaux clients ! Prog1 peut donc attendre sa libération sans crainte de voir un autre client après lui. Bien entendu, entre la réception du Ctrl-F et le RemSemaphore(), il reste une chance pour qu'une tâche arrive à trouver le sémaphore, c'est pourquoi on englobe le tout entre Forbid() et Permit(). Le Forbid() est immédiatement "cassé" par Wait(), mais revient automatiquement à la réception du signal. Enfantin non ? Pas trop, je vous l'accorde.

Prog1

Assembleur
Assembleur
Assembleur

Prog2

Assembleur
Assembleur


[Retour en haut] / [Retour aux articles]