• Aucun résultat trouvé

Le revocable lock et le recoverable lock

4.3 Impl´ ementation des sections critiques

4.3.4 Le revocable lock et le recoverable lock

On peut ´egalement s’en servir quand on ´ecrit nos ordonnanceurs hi´erarchiques (ou mˆeme pour un ordonnanceur de thread utilisateur : si un ordonnanceur est pr´eempt´e avant de pouvoir donner du temps `a un thread, le contexte peut avoir ´et´e modifi´e, ainsi que le choix du thread `a ordonnancer.

Mais l’usage le plus courant de ces sections est pour l’impl´ementation des locks de plus haut niveau eux-mˆeme. En effet, d`es qu’on modifie un param`etre de la pr´eemption programmable, l’ex´ecution ne reprend plus d’o`u elle en ´etait lorsqu’il y a pr´eemption. Et il y a plusieurs param`etres `a changer pour prendre des locks tels que le rollforward lock. Pour contrer le probl`eme, tous nos algorithmes qui modifient les param`etres de la pr´eemption programmable se placent dans des sections restart. Note : Les sections critiques de [BRE92] sont diff´erentes en ce que la derni`ere

op´eration et la fin de la section restart sont simultan´ees. On peut ´egalement impl´ementer cette variante en modifiant l’upcall de reprise pour qu’il fasse une comparaison du program counter.

4.3.4 Le revocable lock et le recoverable lock

4.3.4.1 Revocable lock

Le m´ecanisme de restart lock a ´et´e g´en´eralis´e par Harris et Fraser sous le nom de

revocable lock [HF05]. La diff´erence par rapport au restart lock est que lorsque le revocable lock

verrou est r´evoqu´e, le thread r´evoqu´e a son program counter modifi´e pour ex´ecuter une fonction pr´ed´efinie, plutˆot que de seulement recommencer.

L’interface se constitue donc de deux fonctions :

• void revocable lock( lock t lock, function t revokee) • void revocable unlock( lock t lock)

Comparaison Notre impl´ementation des revocable lock pr´esente cependant un certain nombre de diff´erences avec celle de Harris et Fraser.

• La plus importante est qu’un lock n’est r´evoqu´e que si celui qui le d´etient ne fait plus aucun progr`es, i.e. est pr´eempt´e ou a d´eclench´e une exception. Par opposition, l’impl´ementation de Harris et Fraser permettent `a un thread de r´evoquer un lock n’importe quand. Cela a plusieurs cons´equences :

– Leur impl´ementation r´evoque des threads qui s’ex´ecutaient correctement, donc perdra du temps `a refaire un travail qui ´etait en train de se terminer. Si la contention est forte et la section critique assez longue, l’ex´ecution peut mˆeme ˆetre compl`etement bloqu´ee. Pour ´eviter ce probl`eme, Harris et Fraser ajoutent une attente active de dur´ee born´ee avant de r´evoquer un lock, mais cela ne marche pas dans tous les cas16

.

16

pour ´eviter tous les probl`emes, il faudrait que l’attente active soit toujours plus longue que le temps maximum pass´e dans la section critique, ce qui est dur `a garantir.

– Cette attente active est du temps perdu lorsque le thread qui d´etenait le verrou a ´et´e pr´eempt´e ; dans notre impl´ementation la r´evocation est directe.

– Dans notre impl´ementation la r´evocation (ni la prise de lock) ne demande pas d’appel syst`eme, donc est aussi plus rapide.

Ces am´eliorations rendent le lock plus efficace.

• Notre impl´ementation est non-bloquante, ce qui n’est pas toujours le cas pour celle de Harris et Fraser.

• Au niveau des inconv´enients, le lock occupe un mot entier plutˆot qu’un seul bit. Il est cependant possible d’ajouter des masques au m´ecanisme de pr´eemption programmable pour permettre la mˆeme chose.

• Dernier inconv´enient, notre impl´ementation requiert un m´ecanisme sp´ecial, la pr´eemption programmable ; cependant l’impl´ementation de Harris et Fraser utilise aussi des m´ecanismes OS pour impl´ementer la r´evocation.

4.3.4.2 Recoverable lock

Enfin, nous avons g´en´eralis´e notre impl´ementation en un verrou que nous appelons recoverable lock. Le recoverable lock permet au thread qui fait la r´evocation d’effectuer

recoverable lock

une fonction dite de recouvrement, qui permet au thread de r´etablir un ´etat sain avant d’ex´ecuter sa section critique.

L’interface se constitue donc de deux fonctions :

• void recoverable lock( lock, revoked fun, recover fun) ; • void recoverable unlock( lock) ;

Notons que la fonction de recouvrement peut elle-mˆeme ˆetre pr´eempt´ee sans pr´eavis. Notons aussi qu’on peut, dans la section critique, regarder si l’´etat est sain, et faire le recouvrement depuis l`a. C’est l’approche utilis´ee par les revocable locks de [HF05]. Cependant elle souffre de plusieurs probl`emes :

• il peut ˆetre impossible, ou trop long, de d´etecter les incoh´erences par tests sur les donn´ees. Par exemple, si la section critique doit ins´erer un ´el´ement dans une liste puis incr´ementer un compteur “nombre d’´el´ement” de cette liste ; • ces tests sont sp´ecifiques `a une utilisation ; tandis que le recoverable lock permet

d’avoir des strat´egies de recouvrement g´en´eriques, comme le rollforward lock, le rollback , le restart ou mˆeme le revocable lock. Tous ces locks sont impl´ement´es en tant que sp´ecialisations du recoverable lock ;

• ces tests sont des surplus, puisque lorsqu’un thread en r´evoque un autre, il le “sait” sans avoir besoin de test suppl´ementaire. Ainsi le recoverable lock permet d’´eviter ces tests inutiles.

4.3.4. Le revocable lock et le recoverable lock

Les exemples donn´es par Harris et Fraser dans [HF05] peuvent ˆetre impl´ement´es plus simplement (et efficacement) `a l’aide du recoverable lock. En particulier pour le two-handed emulation de Greenwald [GC96]. Aussi pr´ef´ererons nous ce dernier lock.

Notons que le recoverable lock peut s’impl´ementer sans la pr´eemption pro- grammable, et constituerait une modification simple de l’impl´ementation de [HF05]. Notons enfin que la fonction revoked fun, en argument du verrou, est simple, car tout le recouvrement est fait par la fonction recover fun. En pratique, revoked fun ne fait que retourner au d´ebut de la section critique ou `a la fin, selon que l’op´eration voulue ait ´et´e compl´et´ee ou non.

4.3.4.3 Impl´ementation

Il s’agit d’une simple g´en´eralisation du code du rollforward lock pr´esent´e Listing 4.3 page 170.

4.3.4.4 Usage

Le verrou est utilis´e pour prot´eger un ensemble de donn´ees. Comme le verrou peut ˆetre r´evoqu´e `a n’importe quel moment, il faut que les invariants sur les donn´ees soient v´erifi´es `a tout moment.

Impl´ementation de locks avec strat´egie de recouvrement g´en´erique Le recoverable lock permet d’impl´ementer des locks avec une strat´egie de recouvrement g´en´erique. Par exemple, le rollforward lock s’obtient approximativement en ayant pour fonction de recouvrement le rechargement des registres du thread pr´eempt´e. Le rollback peut s’obtenir en utilisant une fonction de recouvrement qui restaure les valeurs dans le journal ; etc...

Utilisation directe On peut donc utiliser les recoverable lock directement, comme on le ferait des revocable lock. Ce verrou prot`ege contre les op´erations simultan´ees et retard´ees(§ 4.1.1.3). Il prot`ege ´egalement contre les probl`emes de consistances des caches (§ 4.1.1.4), qui complexifient cette programmation pour le programmeur et obligent `a l’utilisation de barri`eres coˆuteuses17

.

Pour l’utiliser pour prot´eger une donn´ee d, il faut v´erifier une seule propri´et´e : apr`es chaque ´ecriture, tous les invariants sur d sont encore v´erifi´es. L’id´ee est que les op´erations gardent l’´etat des donn´ees continuellement “sain”, i.e. coh´erent. Cela impose des contraintes sur le code, mais qui sont bien plus simple `a respecter (car l’ex´ecution dans le lock est s´equentielle) que dans le cadre g´en´eral d’applications parall`eles. Cela a ´et´e d´etaill´e en section 4.1.3.

Certains invariants ne peuvent pas ˆetre assur´ees de mani`ere continue, d`es qu’il y a une ´equivalence `a assurer. Ce probl`eme peut ˆetre contourn´e, car il existe des solutions universelles pour r´esoudre ces probl`emes (e.g. utilisation du rollback lock). Mais l’avantage d’une solution adapt´ee est d’ˆetre plus efficace pour un probl`eme donn´e.

17

Toutefois, il faut prendre garde `a ce que le compilateur ne r´eordonne ni ne supprime des ´ecritures.

Exemple : la v´erification Ce verrou peut ˆetre utilis´e pour les lectures, ou lorsqu’on a peu de modifications `a faire.

Par exemple, beaucoup de code peut ˆetre amen´e `a v´erifier une condition sur une ressource avant de faire une action (comme modifier la ressource). Un code concurrent peut poser probl`eme, car la condition pourrait ne plus ˆetre v´erifi´ee. Le revocable lock, en assurant la s´equentialit´e des traitements, permet de pallier `a ce probl`eme.

Par exemple, on peut utiliser ce lock pour faire une sorte de semi NCAS, i.e. faire atomiquement :

if( o1 == v1 && o2 == v2 && ... && on == vn) *addr = value ;

Cela permet ´egalement de r´esoudre les probl`emes de s´equencement correct : on v´erifie qu’on est dans le bon ´etat de l’automate avant de faire la transition.

Autres utilisations Harris et Fraser [HF05] donnent d’autres exemples d’util- isation ; pour impl´ementer de la software transactional memory ou le “two-hand emulation” de Greenwald [GC96].

On peut ´egalement utiliser le revocable lock pour empˆecher les probl`emes de livelock dans les algorithmes obstruction-free, qui sont simple `a ´ecrire selon Herlihy [HLM03], et fournissent donc une application pratique.

4.4

Travaux en rapport

La plupart des travaux concern´es sur le probl`eme de la pr´eemption lors de section critiques se sont concentr´es sur la m´ethode du sursis d’ex´ecution, que nous avons rejet´ee. Certains ont un m´ecanisme analogue `a la pr´eemption programmable, et certains non.

M´ecanismes similaires `a la pr´eemption programmable

Scheduler activations L’upcall de reprise fait ressembler notre m´ecanisme aux scheduler activations [ABLL92]. La diff´erence principale est que les scheduler activa- tions utilisent un upcall de pr´eemption. Mˆeme si ce m´ecanisme n’est pas impl´ement´e par un sursis d’ex´ecution, il souffre de nombreux probl`emes : cet upcall pr´eempte un autre processeur, ce qui perturbe sa d´ecision d’ordonnancement et cause un certain overhead. D’autant plus que le recouvrement des sections critiques est fait de mani`ere imm´ediate ; dans Anaxagoros le recouvrement est fait de mani`ere paresseuse, ce qui r´eduit des besoins de recouvrement et r´eduit les interf´erences d’ex´ecution de threads non reli´es.

L’upcall de pr´eemption leur retire le besoin de la zone de non-sauvegarde sur pr´eemption, ainsi que le besoin de stocker les registres en espace utilisateur (ces derniers sont pass´es en param`etre de l’upcall). Enfin, il semble plus facile d’impl´ementer l’user-level scheduling avec, car cet upcall permet la mise `a jour de la liste des threads prˆets. Nous pensons cependant que cette mise `a jour peut se faire de mani`ere paresseuse au moment de la consultation de ces listes.

4.4. TRAVAUX EN RAPPORT

Une autre diff´erence est le traitement des appels syst`emes bloquants. Nous n’impl´ementons pas d’appel syst`eme bloquants ; notre plan est de pouvoir ´emuler appels syst`emes bloquants et non-bloquants par le biais d’un m´ecanisme de notifica- tion, permettant au service de pr´evenir les clients quand leurs donn´ees sont prˆetes. Ces notifications permettent ´egalement de mettre un thread dans l’´etat « prˆet » en pr´evenant son ordonnanceur.

Enfin, la derni`ere diff´erence concerne la motivation : les scheduler activations ont pour but d’am´eliorer les performances de l’ordonnancement de thread utilisateur sur multiprocesseur (M-to-N scheduling [Can96]). La pr´eemption programmable a pour but de cr´eer un m´ecanisme de synchronisation efficace et non-bloquant en ´evitant de modifier l’ordonnancement ; en particulier pour les services, pour lequel l’ordonnancement est 1-to-1. Le fait qu’on puisse faire de l’ordonnancement de thread utilisateur avec n’est qu’un bonus agr´eable.

Psyche Le m´ecanisme de threads utilisateur de premi`ere classe de Psyche [MSLM91], ´egalement con¸cu pour am´eliorer l’ordonnancement M -to-N , est similaire au notre en ce qu’il utilise de la m´emoire partag´ee entre le noyau et le thread pour guider ce que doit faire le noyau. Il diff`ere du notre en ce qu’il utilise l’approche probl´ematique du “sursis d’ex´ecution” pour r´esoudre les probl`emes de pr´eemption dans les sections critiques. Il n’y a pas non plus d’upcall de reprise.

Exokernel Les impl´ementations d’exokernels [EKJO95, KEG+97] ont des simil-

itudes avec les nˆotres. La sauvegarde et le rechargement du contexte est faite en espace utilisateur, au moins pour l’impl´ementation sur Alpha [EKJO95, § 5.3]. Mais la sauvegarde des registres se fait par un upcall [EKJO95, § 5.1.1], ce qui implique un aller-retour noyau/application suppl´ementaire. Cet upcall peut terminer les sec- tions critiques avec un sursis d’ex´ecution [EKJO95, § 5.1.1], ce qui pose ´egalement probl`eme (§ 4.1.2.1)

M´ecanismes de synchronisation

Symunix Symunix[ELS88] ne permet pas la gestion du contexte processeur par l’espace utilisateur, mais pr´esente le m´ecanisme de sursis d’ex´ecution, qui ´emule le masquage des interruptions en espace utilisateur.

Restartable atomic sequences Les restartable atomic sequences [BRE92] per- mettent de reprendre l’ex´ecution en arri`ere lorsque le program counter est `a une certaine valeur ; cela permet d’´eviter l’utilisation d’op´erations read-modify-write sur monoprocesseur (de type CAS), qui sont coˆuteuses.

La zone de non sauvegarde sur pr´eemption a des similitudes avec ce m´ecanisme, puisque la comparaison se fait ´egalement par program counter, et que la reprise se fait au pr´ec´edent point de sauvegarde si l’upcall de reprise est param´etr´e pour reprendre l’ex´ecution. Mais cette impl´ementation n’est pas optimis´ee, et ne permet pas de faire une ´ecriture en sortant de la section atomiquement, ce qui limite son utilit´e. On peut cependant impl´ementer les vraies restartable atomic sequences `a l’aide du m´ecanisme entier de pr´eemption programmable, par exemple en modifiant

l’upcall de pr´eemption pour qu’il reprenne l’ex´ecution conditionellement `a la valeur du program counter lors de la pr´eemption.

Notons que l’impl´ementation des recoverable lock/rollforward lock en monopro- cesseur n’a pas besoin d’utiliser d’op´eration read-modify-write atomique, ce qui est l’optimisation permise par les rollforward atomic sequences.

Rollforward lock D’autres papiers ont explor´e l’usage du rollforward lock. Le rollforward lock est mentionn´e dans [Ber93], mais non impl´ement´e compte `a cause des probl`emes de d´efaut de page et de la difficult´e d’une impl´ementation sˆure.

Mosberger et al. [DM96] ont impl´ement´e un rollforward lock. Leur impl´ementation diff`ere de la notre essentiellement en ce qu’ils terminent l’ex´ecution par l’interruption qui provoque la pr´eemption (i.e. utilisent un sursis d’ex´ecution). Cela pose des probl`emes de s´ecurit´e (e.g. la routine d’interruption doit scanner le code pour v´erifier qu’il termine et ne permet pas des op´erations interdites), augmente le temps de pr´eemption, et ne marche que pour le monoprocesseur.

Le m´ecanisme de « donation volontaire » de Ford et al. [FS96, § 3.4] permet de donner du temps `a un autre thread pour qu’il termine l’ex´ecution de la section critique ; cela ´etend le m´ecanisme classique d’h´eritage de priorit´e. La diff´erence principale avec notre m´ecanisme est que nous n’avons pas besoin de faire intervenir d’ordonnanceur, ce qui est plus efficace et facilite l’´ecriture des ordonnanceurs.

Le m´ecanisme de futex de Linux [FRK02] permet d’´eviter d’avoir `a faire des appels syst`emes dans le cas o`u le lock n’est pas pris. Mais pour d´ebloquer un lock pris, il est toujours n´ecessaire de faire plusieurs appels syst`emes.

Revocable locks Les revocable lock de Harris et Fraser [HF05] sont impl´ement´es diff´eremment. Ils ne savent pas quand un thread est pr´eempt´e dans une section critique et par cons´equence, font une attente active dans l’esp´erance qu’il en sortira, ce qui diminue les chances de r´evoquer un thread actif sans les ´eliminer. De plus la r´evocation ne se fait pas en temps constant, et a besoin d’un appel syst`eme coˆuteux.

Nos recoverable lock impl´ementent donc une interface am´elior´ee (avec un test g´en´erique pour savoir s’il y a eu r´evocation) de mani`ere plus l´eg`ere.

4.5

Conclusion et travaux futurs

Conclusion

´

Ecrire des services ind´ependants de toute politique et bas´es sur le prˆet de ressource pose des probl`emes concrets : les services sont multithread´es, les ordonnanceurs sont incontrˆolables voire malicieux, et la m´emoire rattach´ee aux threads peut disparaˆıtre `a tout moment. De plus, il est impossible d’utiliser tel quels les m´ecanismes de synchronisation habituels que sont le sleeplock et le spinlock. Cela complexifie la r´esolution des probl`emes de coh´erence.

Dans le noyau, la possibilit´e de masquer les interruptions facilite la r´esolution du probl`eme, mais cette possibilit´e n’est pas tol´erable pour des services en espace

4.5. CONCLUSION ET TRAVAUX FUTURS

utilisateur sans confiance. Nous avons donc d´evelopp´e un ensemble de primitives de synchronisations pour ce cas telles que :

• l’impl´ementation du noyau en soit peu affect´ee, pour que celui-ci reste simple et sˆur ;

• la synchronisation ne demande pas la coop´eration des ordonnanceurs, qui peuvent ˆetre malicieux ;

• les sections critiques soient non-bloquante, ce qui garantit la vivacit´e du syst`eme ; la perturbation dans l’ex´ecution des threads est minimis´ee ;

• la synchronisation fonctionne sur des machines multicœur, qui seront standard dans le futur, y compris pour les syst`emes temps r´eel.

Le r´esultat en est un m´ecanisme et une impl´ementation simple, ´el´egante et efficace de sections critiques non-bloquantes aux propri´et´es diff´erentes ; qui sont utiles pour l’ensemble des applications du syst`eme, et pas seulement les services. Nous avons donc des techniques de r´esolution des probl`emes de synchronisation qui permettent de minimiser la perturbation des d´ecisions de l’ordonnancement, en monoprocesseur et en SMP.

Leur utilisation syst´ematique nous permet de rejoindre la petite famille des syst`emes d’exploitations qui font seulement de la synchronisation non bloquantes ; et cela sans utiliser DCAS (comme [Mas92, GC96]) ni utiliser d’ordonnancement sp´ecifique [SWH05].

Travaux futurs

Bien que les verrous non-bloquants impl´ement´es dans cette section soient une r´eelle avanc´ee, il reste n´eanmoins plusieurs probl`emes `a r´egler.

Appels de service dans une section critique Les verrous que nous avons impl´ement´es permettent de synchroniser des donn´ees pour plusieurs threads dans le mˆeme espace d’adressage. Mais nous n’adressons pas le probl`eme de la synchronisa- tion entre plusieurs espaces d’adressages.

Des applications clientes peuvent se synchroniser de mani`ere diff´erentes, en faisant notamment intervenir leur(s) ordonnanceur(s). Les services ne peuvent pas faire cela, car ils doivent ˆetre ind´ependant des d´ecisions d’ordonnancements (§ 3.1.1). Concr`etement, le probl`eme qui se pose pour les services est celui de la synchronisation lorsqu’il y a un appel `a un autre service dans la section critique. Pour l’instant, nous ´evitons les appels de service r´ecursifs (qui sont de toute mani`ere lent et une introduction de complexit´e dans le service), mais c’est parfois un peu contraignant (en g´en´eral, on en a besoin pour faire des op´erations sur la m´emoire virtuelle).

Nous entrevoyons deux solutions diff´erentes `a ce probl`eme :

• le CPU inheritance. Ce moyen, d´ecrit par Ford et Susarla [FS96], est une sorte de g´en´eralisation de l’h´eritage de priorit´e ind´ependant des politiques d’ordonnancement. L’id´ee consiste `a faire ex´ecuter la section critique par un