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 - 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 :
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
Prog2
|