• Aucun résultat trouvé

Le sch´ema de synchronisation use/destroy

5.2 Synchronisation dans les services

5.2.4 Le sch´ema de synchronisation use/destroy

5.2.3.3 Conclusion

Nous avons vu les trois sortes de verrou mis `a disposition des services dans Anaxagoros. Malheureusement, il n’y a pas de bonne solution universelle, chacune ayant ses avantages et inconv´enients :

• le rollforward lock consomme de la m´emoire propre, et ne renvoie pas de r´esultat ; mais garantit la s´erialisation des sections critiques ;

• le rollback lock consomme ´egalement de la m´emoire propre, mais moins ; il peut renvoyer des r´esultats ; mais on ne peut pas toujours retourner en arri`ere dans l’ex´ecution (communication avec mat´eriel) ;

• le recoverable lock ne consomme quasiment pas de m´emoire propre, mais est souvent difficile `a utiliser.

Enfin, un dernier probl`eme de synchronisation est le fait que nous n’avons pas encore de moyen de synchronisation inter-service, mˆeme si nous avons discut´e de la possibilit´e d’en impl´ementer.

Une possibilit´e future serait d’examiner un verrou qui pourrait avoir certaines sec- tions critiques en rollforward et certaines en rollback. Cela devrait couvrir quasiment tous les cas d’utilisation.

Une autre possibilit´e serait de fournir un environnement de programmation de plus haut niveau pour faciliter le travail du programmeur. Ce framework pourrait ˆetre fond´e sur la g´en´eration automatique de drivers `a partir des sp´ecifications de l’OS et du mat´eriel, comme le permet Termite [RCK+

09]. Cela permettrait de n’avoir `a r´esoudre les probl`emes de synchronisation que par classe de drivers, et que cela s’applique `a tous les drivers de la classe.

La solution que nous avons choisi est de minimiser les besoins en synchronisation et de permettre au programmeur de comprendre finement ces besoins, afin qu’il utilise ces primitives judicieusement. Cette minimisation offre par ailleurs d’autres avantages, en terme de passage `a l’´echelle et de non-perturbation de l’ordonnancement.

5.2.4 Le sch´ema de synchronisation use/destroy

5.2.4.1 Pr´esentation

Probl`eme Le probl`eme que nous cherchons `a r´esoudre survient dans tout syst`eme multithread o`u les ressources sont allou´ees dynamiquement. Plusieurs threads peuvent utiliser une ressource lorsqu’un autre d´ecide de la d´etruire. Il est n´ecessaire d’utiliser un sch´ema de synchronisation pour s’assurer qu’un thread n’utilise pas une ressource d´etruite.

Ce sch´ema de synchronisation a des applications dans tout le syst`eme. C’est notamment grˆace `a lui que nous avons pu impl´ementer notre syst`eme de m´emoire virtuelle de mani`ere presque enti`erement wait-free. Il est ´egalement utile pour impl´ementer la r´evocation des ressources en espace utilisateur.

Solution Notre solution `a ce probl`eme consiste `a d´etruire la ressource en plusieurs phases, nomm´ement :

1. empˆecher de nouveaux threads d’utiliser la ressource ;

2. attendre ou faire en sorte que les threads qui utilisent d´ej`a la ressource ne l’utilisent plus ;

3. nettoyer ou d´etruire la ressource ;

4. marquer la ressource comme r´eutilisable.

On note ´egalement que la destruction d’une ressource est un ´ev`enement assez peu fr´equent compar´e `a son utilisation. Typiquement, une ressource est allou´ee/cr´e´ee, utilis´ee plusieurs fois, avant d’ˆetre d´esallou´ee/d´etruite. En particulier, il n’y a pas de raison d’empˆecher l’utilisation parall`ele de la ressource ; il faut juste empˆecher l’utilisation lorsque la destruction est entam´ee.

Si on se permet d’attendre pour pouvoir d´etruire la ressource, il faut toutefois que ce temps d’attente ne soit pas trop long, i.e. qu’il puisse ˆetre born´e. Ceci est impos´e par le principe de r´evocation imm´ediate (§ 3.1.2.2). Par ailleurs, si l’attente ´etait infinie, il serait possible pour des threads de monopoliser une ressource sans possibilit´e de leur retirer, ce qui est une faille de s´ecurit´e ´evidente.

Th´eorie Plus g´en´eralement, ce sch´ema de synchronisation fournit une autre so- lution au respect d’invariants sous forme canonique P (a) ⇒ P′

(b) (voir § 4.1.3.2). Les threads « utilisateurs » doivent s’assurer que P′

(b) est vrai quand ils modifient a, tandis que le thread « destructeur » veut rendre P′

(b) faux. Dans l’exemple pr´ec´edent, P′

(b) est donc simplement “la ressource est prˆete”.

Concepts reli´es Le sch´ema de synchronisation readers/writer est un concept proche du notre : tous deux permettent `a un ensemble de threads de s’assurer d’une condition (respectivement, que la m´emoire n’est pas r´eutilis´ee pour le use/destroy lock, que les donn´ees ne sont pas modifi´ees pour le reader/writer lock). Nous proposons d’ailleurs diff´erentes impl´ementation du sch´ema use/destroy, qui peuvent ˆetre utilis´ees pour impl´ementer le sch´ema readers/writer de mani`ere diff´erente que le traditionnel readers/writer lock.

Le typestable memory management (TSM) de Greenwald [GC96] est ´egalement un concept rapproch´e de notre sch´ema de synchronisation. Cependant dans le cas des TSM, il n’y a pas de synchronisation explicite, mais seulement une assurance (par le temps) que les threads qui utilisent une structure ne sont pas en train d’acc´eder `a de la m´emoire lib´er´ee (ou r´eallou´ee `a un autre usage). Ainsi il est impossible de r´eutiliser de la m´emoire en train d’ˆetre acc´ed´ee d’utilisation imm´ediatement comme dans notre impl´ementation.

5.2.4.2 Impl´ementations

Pr´esentation Il y a plusieurs impl´ementations possibles de ce sch´ema. La premi`ere propos´ee utilise un verrou pour r´ealiser des exclusions mutuelles conform´ement au sch´ema d´ecrit pr´ec´edemment. En particulier :

5.2.4. Le sch´ema de synchronisation use/destroy • un thread qui veut d´etruire la structure est en exclusion mutuelle avec tous

les autres ;

• les threads qui veulent utiliser la structure ne sont pas en exclusion mutuelle. Ce sch´ema ressemble beaucoup au readers/writer lock, avec l’exception que les threads qui utilisent la structure peuvent la modifier. Comme il n’y a pas d’exclusion mutuelle entre les threads qui utilisent la structure, il faut soit utiliser un autre verrou, soit faire les modifications de mani`ere non-bloquante.

Impl´ementation th´eorique L’id´ee est la suivante. Chaque ressource poss`ede un compteur d’utilisateurs (i.e. du nombre de threads qui utilisent la ressource), incr´ement´e de 1 quand un thread utilise la ressource, d´ecr´ement´e de 1 quand il ne l’utilise plus.

Chaque ressource poss`ede ´egalement un indicateur de destruction, un bool´een qui dit si un thread veut d´etruire la ressource. Quand cet indicateur est `a 1, alors aucun nouveau thread ne peut toucher la ressource. Le thread qui veut d´etruire la ressource n’a alors plus qu’`a attendre que tous les threads qui utilisaient d´ej`a la ressource partent, i.e. que le compteur d’utilisateurs passe `a 0. L’impl´ementation th´eorique est pr´esent´ee en Figure 5.2.4.2 (atomic indique des op´erations `a effectuer atomiquement). Diff´erentes impl´ementations r´eelles sont donn´ees en annexes B.1 et C.2.2.2.

error_t use( resource_t r) { atomic{ if ( r->lock.destroyer) return EACCESS ; else r->lock.users ++ ; } ... // utilise ou modifie atomic{ r->lock.users -- ; } }

error_t destroy( resource_t r) { atomic{ if ( r->lock.destroyer) return ECONCURRENCY ; else r->lock.destroyer = 1 ; } while( r->lock.users != 0) ; ... // nettoie atomic{ r->lock.destroyer = 0} ; }

Fig. 5.2 – Impl´ementation th´eorique du use/destroy lock.

Notons l’usage de ECONCURRENCY, qui permet de d´etecter des acc`es concurrents en renvoyant un erreur au lieu d’attendre, comme expliqu´e section 5.2.1.1. EACCESS est retourn´e lorsqu’on tente d’utiliser une ressource en cours de destruction, ou d´etruite.

Le probl`eme principal de cette impl´ementation th´eorique est l’attente pour le thread qui veut d´etruire la ressource, que tous les threads utilisateurs aient termin´e d’utiliser la ressource. Il est en g´en´eral rare de d´etruire une ressource en cours d’utilisation (c’est utilis´e par exemple pour forcer la destruction d’un programme), donc le fait d’attendre un peu ne cr´ee pas un overhead significatif ; mais une attente infinie occasionne une faille de s´ecurit´e. Nous voyons comment ce probl`eme est trait´e dans diff´erents cas d’utilisation.

Utilisation dans le noyau Dans le noyau, on peut demander au thread qui utilise la ressource de le faire dans une section non pr´eemptible, et de ne pas l’utiliser pour tr`es longtemps. Par exemple, lorsqu’un thread veut modifier une entr´ee dans la table des pages, il acqui`ere un use/destroy lock sur la table des pages, modifie l’entr´ee de mani`ere atomique, puis relˆache le verrou. L’utilisation est extrˆemement simple, l’overhead petit, et est l’une des raisons pour laquelle nous pr´esageons d’excellentes performances pour notre syst`eme de gestion de la m´emoire virtuelle (dont l’impl´ementation est d´ecrite en annexe C).

Si on doit modifier plusieurs entr´ees dans la mˆeme table, on peut ´eviter l’overhead de prendre et relˆacher le verrou d’utilisation : il suffit de v´erifier directement si lock.destroyer == 1 sans relˆacher le verrou. Si c’est le cas, cela signifie que la ressource est en instance de destruction, et on relˆache le verrou. Cette technique de “polling” est souvent utilisable lorsque le thread doit accomplir une tˆache longue et r´ep´etitive, et peut ˆetre coupl´ee avec l’introduction des points de pr´eemption explicite. Utilisation `a travers le noyau Tant que l’utilisation de la ressource se fait dans le noyau, la technique pr´ec´edente fonctionne : il suffit de v´erifier si la ressource doit ˆetre d´etruite, aux moments o`u on fait des v´erifications pour les points de pr´eemption explicite. Mais il peut arriver qu’on utilise une ressource pendant un certain temps sans pouvoir faire cette v´erification, lorsqu’on sort du noyau pour ex´ecuter une application utilisateur.

Par exemple, si on veut utiliser un domaine, on va prendre un verrou d’utilisation sur ce domaine, puis poursuivre l’ex´ecution de ce domaine en espace utilisateur. Simultan´ement, un thread (sur un autre CPU) veut d´etruire le domaine ex´ecut´e.

Si on ne fait rien, le thread qui veut d´etruire ce domaine ex´ecut´e va attendre pendant longtemps. Pour rem´edier `a cela, ce thread va envoyer un IPI au processeur qui ex´ecute le domaine, afin qu’il retourne dans le noyau et arrˆete d’utiliser le domaine plus tˆot. Il faut faire cela avant d’attendre que tous les threads relˆachent la section critique.

Ainsi, au « polling » de la section pr´ec´edente, on substitue un m´ecanisme « d’in- terruption » pour pr´evenir les threads d’arrˆeter d’utiliser la ressource.

Au niveau de l’impl´ementation, il ne suffit plus maintenant de compter les processeurs qui utilisent la ressource, mais il faut pouvoir les identifier. Cela peut se faire en rempla¸cant le compteur thread.users par un bitfield, ou en indiquant pour chaque processeur quelles sont les ressources qu’il est en train d’utiliser. Le thread qui veut d´etruire la ressource envoie alors un IPI `a tous ces CPUs entre le moment o`u il met son indicateur `a 1 et celui o`u il attend que plus personne n’utilise le verrou. Les impl´ementations r´eelles sont pr´esent´ees plus en d´etail en annexes B.1 et C.2.2.2.

Utilisation en espace utilisateur Notons que l’impl´ementation pr´ec´edente retire le besoin d’ex´ecuter le code dans une section non pr´eemptible. Cela peut permettre d’utiliser le use/destroy lock en espace utilisateur.

Le fait d’avoir un upcall de reprise suffit pour impl´ementer le sch´ema use/destroy en monoprocesseur : il suffit de param´etrer cet upcall pour qu’il regarde si la ressource n’a pas ´et´e d´etruite avant de reprendre son ex´ecution. L’exemple le plus important de

5.2.5. Conclusion

l’utilisation de cette m´ethode est lorsque nous v´erifions que les dates de la capacit´e et de la ressource correspondent encore lors de la reprise d’ex´ecution dans le service (§ 3.3.3.2).

En multiprocesseur, les IPIs sont n´ecessaires : si un thread d´etruit une ressource, il faut empˆecher les autres threads de l’utiliser. Cela se fait comme dans le noyau, `a la diff´erence que l’appel syst`eme de la section 4.2.2 ne permet d’envoyer un IPI qu’aux threads qui partagent le mˆeme domaine.

5.2.4.3 Conclusion

Le use/destroy pattern est un sch´ema de synchronisation extrˆemement int´eressant. Il permet `a diff´erents CPU d’utiliser la mˆeme ressource sans aucune restriction sur le parall´elisme, tout en permettant une r´evocation de la ressource en un temps born´e (principe 3.1.2.2). Ces deux propri´et´es sont int´eressantes `a la fois pour les syst`emes temps r´eel dur, mais aussi pour les syst`emes best-effort.

Grˆace `a lui, notre syst`eme de m´emoire virtuelle fait toutes ses op´erations en parall`ele, sans copie, en op´erant directement sur les donn´ees, et sans boucle ; donc de mani`ere wait-free, sauf pour les quelques attentes lorsqu’un thread veut forcer la r´ecup´eration d’une page m´emoire. Cela devrait le rendre extrˆemement scalable pour la plupart des op´erations. Cette impl´ementation pourrait ˆetre reprise par d’autres exokernels ou VMMs, dont les impl´ementations actuelles utilisent un certain nombre de verrous ; ou mˆeme dans la couche basse de noyaux monolithiques.

Enfin, cette technique est tr`es importante pour l’impl´ementation du principe de r´evocation rapide (principe 3.1.2.2), puisque la r´evocation peut se faire en temps constant.

Travaux futurs L’impl´ementation du sch´ema use/destroy pattern ne peut impl´e- menter des ressources qui n’ont que deux ´etats : invalide/inutilisable et valide/utilis- able. Il serait int´eressant (et a priori simple) d’´etendre ce sch´ema pour g´erer des ´etats plus complexes, si n´ecessaire. C’est ce qui est fait avec les pages m´emoires, dont l’automate des ´etats est plus complexe (voir annexe C) ; il serait int´eressant de voir comment g´en´eraliser cette approche.

5.2.5 Conclusion

Nous avons expliqu´e comment utiliser diff´erentes primitives pour faire la synchroni- sation `a l’int´erieur des services. Ces primitives ne sont pas fonci`erement difficiles `a utiliser pour un programmeur habitu´e `a la programmation syst`eme, mais demandent n´eanmoins une r´eflexion. C’est la raison pour laquelle nous recommandons de ne pas trop se soucier de l’impl´ementation des synchronisations dans un premier temps.

Mais avant de tenter de r´esoudre des probl`emes de synchronisation, il vaut mieux les ´eviter ; i.e. ne pas synchroniser `a moins que cela soit n´ecessaire. Les sections suivantes se penchent sur ces questions.

5.3

Minimisation des probl`emes de