• Aucun résultat trouvé

Techniques usuelles de r´esolution

3.6 Conclusion

4.1.2 Techniques usuelles de r´esolution

Nous analysons maintenant diff´erentes techniques usuelles de r´esolution des probl`emes de concurrence, et analysons si elles sont compatibles avec le principe d’ind´ependance des politiques d’allocation (§ 3.1.1).

4.1.2.1 Les m´ecanismes bloquants

Les techniques de r´esolution des probl`eme de concurrence peuvent ˆetre catalogu´ees en deux : bloquantes ou non-bloquantes [Fra04]. Un m´ecanisme est bloquant lorsque

bloquant

l’arrˆet d’un thread peut retarder ou ind´efiniment empˆecher l’ex´ecution d’un autre ; non-bloquant lorsque ce n’est pas le cas4

. Les techniques bloquantes, les plus

non-bloquant

usit´ees, posent probl`eme dans Anaxagoros car elles ne respectent pas le principe d’ind´ependance de l’ordonnancement. Mais c’est en les analysant en d´etail que nous trouvons les racines des probl`emes que nous r´esolvons par la suite.

Section critique L’utilisation de section critique permet de r´epondre aux 4

section critique

probl`emes du mˆeme coup. Il s’agit de synchroniser les diff´erents threads afin qu’au plus un d’entre eux soit dans une certaine portion de code, qui est la section critique. Ainsi, cette portion de code s’ex´ecute de mani`ere s´equentielle.

On utilise souvent une variable partag´ee pour synchroniser ces diff´erents acc`es, appel´e verrou, ou mutex lock (verrou d’exclusion mutuelle).

verrou

mutex lock Il y a plusieurs fa¸cons d’impl´ementer une section critique, chacune ayant ses

avantages et ses d´efauts.

Spinlock La mani`ere la plus simple de cr´eer une section critique est le spinlock,

spinlock

3

Une exception courante est le cache des entr´ees de la table des pages (TLB) : le Pentium, par exemple, n’invalide pas les entr´ees automatiquement, ce qui requiert une synchronisation entre les processeurs, appel´ee TLB shootdown.

4

Notons que non-bloquant ´etait un synonyme de lock-free dans des papiers plus anciens, mais nous utilisons le vocabulaire actuel [Fra04, p. 9]

4.1.2. Techniques usuelles de r´esolution

ou verrou par attente active. Le thread qui rentre dans la section critique prend le verrou en modifiant la valeur du spinlock, et le lib`ere `a la fin de la section ; les autres bouclent en lisant la variable et attendant que le verrou se lib`ere. Il y a quantit´e de possibilit´es d’impl´ementation de ce m´ecanisme, comme le « Bakery algorithm » de Lamport [Lam74] ou l’algorithme de Peterson [Pet81] ; les impl´ementations modernes utilisent des instructions processeurs comme Compare-and-swap qui permettent une impl´ementation simple et efficace.

Le probl`eme principal des sections critiques par attente active est la pr´eemption pendant la section critique. Les autres threads sont alors bloqu´es jusqu’`a ce que le thread reprenne la main pour terminer cette section critique, donc pour un temps long, voire infini. De plus, comme les threads sont en attente active, le temps o`u ils sont bloqu´es est perdu.

Les deux approches classiques pour r´esoudre ce probl`eme sont soit d’empˆecher les pr´eemptions, soit d’´eviter de perdre le temps o`u le thread est bloqu´e en faisant autre chose (appel `a l’ordonnanceur).

Empˆechement des pr´eemptions Une section de code pour laquelle on empˆeche

la pr´eemption de survenir est appel´ee section non pr´eemptible. Lorsque le code section non pr´eemptible

ex´ecut´e pour la prise d’un spinlock et sa section critique sont ex´ecut´es `a l’int´erieur d’une section non pr´eemptible, le thread qui ex´ecute la section critique ne peut plus ˆetre pr´eempt´e, et les autres threads ne seront donc bloqu´es que pour un temps born´e (sous r´eserve qu’il n’y a pas de d´efaillance lors de l’ex´ecution de la section critique). Plus g´en´eralement, une section non pr´eemptible permet en un certain sens de transformer des algorithmes bloquants en algorithmes non-bloquants.

Notons qu’en monoprocesseur, une section non pr´eemptible est toujours une section critique, puisque le thread est garanti d’ˆetre seul `a utiliser le processeur. De plus, un spinlock est toujours inutile en monoprocesseur : lorsqu’un thread attend pour un spinlock, donc monopolise le processeur, le spinlock ne peut pas se lib´erer.

Il y a deux moyens pour empˆecher les pr´eemptions, a priori ou a posteriori. Masquage des interruptions On empˆeche les pr´eemptions a priori en masquant les interruptions. Ce m´ecanisme a un overhead faible, mais demande de faire confiance au programme qui l’emploie puisque celui-ci peut bloquer le processeur ind´efiniment5

. Dans Anaxagoros, cette technique est r´eserv´ee au (petit) noyau.

Sursis d’ex´ecution L’autre moyen est a posteriori : si une pr´eemption doit survenir, on l’enregistre mais on accorde au programme un d´elai suppl´ementaire. Cela peut ˆetre impl´ement´e sous diff´erentes formes : les exonoyaux [EKJO95, § 5.1.1] accordent un sursis de mani`ere syst´ematique. Psyche [MSLM91, § 3.2] utilise une notification d’une pr´eemption imminente : le temps entre cette notification et la pr´eemption peut ˆetre vu comme un sursis d’ex´ecution. Symunix [ELS88, § 3.2] utilise

5

On pourrait ´eventuellement rajouter un timeout, mais cela est contraire `a notre refus de coder du temps en dur (§ 3.1.4.3).

une variable par thread permettant de signaler `a l’ordonnanceur qu’il a besoin d’un sursis ; ce sursis est accord´e lorsque cette variable est `a 1.

Le probl`eme qui se pose est celui de la dur´ee du sursis d’ex´ecution.

S’il est fini, on prend le risque que la section critique ne soit pas termin´ee quand le thread est r´eellement pr´eempt´e. S’il est long, ce risque diminue, mais le temps de pr´eemption augmente, ainsi que l’interf´erence avec les d´ecisions d’ordonnancement, ce qui pose probl`eme pour les programmes temps r´eel. De plus on ne respecte pas notre refus de coder du temps en dur (§ 3.1.4.3). S’il est infini, cela demande de la confiance comme pour le masquage des interruptions.

Dans Anaxagoros, tout le noyau s’ex´ecute dans une section non pr´eemptible, impl´ement´ee `a l’aide d’un sursis d’ex´ecution infini ; on fait confiance au code du noyau pour v´erifier r´eguli`erement qu’il n’y a pas eu pr´eemption. Cette impl´ementation per- met `a la routine d’interruption de continuer `a s’ex´ecuter (e.g. pour compter le temps pass´e dans le noyau, ou impl´ementer un watchdog), et permet une impl´ementation efficace des points de pr´eemption explicites6

(il suffit de v´erifier une variable).

Sleeplock Pour ´eviter de perdre du temps `a attendre que la section critique soit lib´er´ee comme pour le spinlock, il est commun que les threads bloqu´es effectuent un appel `a l’ordonnanceur (e.g. par l’appel syst`eme sleep(), d’o`u le nom de sleeplock).

sleeplock

En particulier, si le thread dans la section critique ´etait pr´eempt´e, l’ordonnanceur pourra ordonnancer ce thread l`a, et ainsi lib´erer le verrou.

Dans le cas g´en´eral, l’utilisation de sleeplock perturbe l’ordonnancement de mani`ere impr´evisible et empˆeche l’ordonnancement d´eterministe. Cela va `a l’encontre du principe d’ind´ependance des politiques (§3.1.1.1).

Cependant, certains syst`emes permettent de “diriger” l’ordonnanceur afin qu’il ordonnance sp´ecifiquement le thread qui d´etient le verrou, le temps qu’il le lib`ere. Dans les syst`emes par priorit´e, on appelle cela h´eritage de priorit´e (e.g. [SWH05]), mais ce syst`eme peut ˆetre impl´ement´e de mani`ere g´en´erale, comme l’ont montr´e Ford et Susarla[FS96]. Ainsi, l’attente pour le verrou est born´e `a la longueur de la section critique 7. Ce m´ecanisme semble donc acceptable pour des sections critiques

courtes, et des verrous non r´ecursifs.

Mais mˆeme ainsi, il y a des d´esavantages : il y a perturbation de la d´ecision d’ordonnancement ; lorsqu’un processus peut d´etenir de multiples lock, l’ordonnance- ment peut devenir incompr´ehensible (en plus de pouvoir souffrir de deadlocks) ; enfin l’overhead de ce m´ecanisme est haut : un appel `a l’ordonnanceur pour commuter vers le thread qui d´etient la section critique, et un appel pour en revenir.

Dans Anaxagoros, nous avons d´ecid´e d’´eviter ce m´ecanisme au maximum (il n’est actuellement pas utilis´e, mais envisag´e en section 4.5). Si n´ecessaire, nous l’impl´ementerons, et l’utiliseront en prenant soin de n’utiliser qu’un seul lock simul- tan´ement, et pour des sections critiques tr`es courtes afin que la perturbation de l’ordonnancement ne soit pas remarquable.

6

[FHL+

99] utilise ´egalement des points de pr´eemption explicites.

4.1.2. Techniques usuelles de r´esolution

Autres probl`emes des section critiques Les sections critiques ont ´egalement d’autres probl`emes. Lorsqu’utilis´ees pour des sections de code longs, elles restreignent le parall´elisme et perturbent l’ex´ecution des threads bloqu´es. Il faut pour contrer cela utiliser des sections critiques courtes, ce qu’on appelle verrouillage `a grain fin. Mais cela augmente l’overhead pour entrer et sortir de la section critique ; de plus la multiplication des verrous augmente le risque d’interblocage. Dans Anaxagoros, on limite toutes les sections critiques `a des s´equences d’instruction tr`es courtes pour ne pas perturber l’ex´ecution des autres tˆaches. De plus, on interdit la prise de plusieurs verrous simultan´ement pour empˆecher les interblocages. Cela ne nous empˆeche pas d’ˆetre scalable.

Probl`eme de contention et famine Le fait de bloquer en attendant l’ex´ecu- tion de la fin de la section critique (quelque soit le m´ecanisme) g´en`ere des d´elais dans l’ex´ecution et des possibilit´es de famine, que l’on peut analyser th´eoriquement. S’il y a M threads en comp´etition pour rentrer dans une section critique dont l’ex´ecution dure au plus t, alors le temps d’attente maximal pour un thread pour rentrer dans la section critique est de (M − 1) ∗ t. Il y a possibilit´e de famine si un thread a besoin de moins de (M − 1) ∗ t pour ˆetre `a nouveau en attente du verrou (on suppose que le thread qui obtient le verrou est choisi al´eatoirement). Dans ce cas, certains threads peuvent ne jamais obtenir ce verrou. Pour ´eviter la famine, il faut minimiser le temps pass´e dans les sections critiques.

Notons que dans une section non pr´eemptible ou pour un algorithme lock-free, il y a au plus “nombre de processeur” threads simultan´ement en train d’acc´eder au verrou, donc un nombre petit.

Autres m´ecanismes bloquants Il y a d’autres m´ecanismes bloquants, donc inutilisables dans les services d’Anaxagoros. On peut bri`evement citer :

• le readers-writer lock, qui est simplement une version optimis´ee du mutex lock classique. Il permet `a plusieurs lecteurs un acc`es concurrent, mais souffre des mˆemes probl`emes. On consid´erera donc ce verrou comme analogue au mutex lock ;

• les s´emaphores [Dij68], qui d´eclenchent par nature un appel `a l’ordonnanceur, ce qui est contraire `a notre but d’ind´ependance de la politique d’ordonnancement (principe 3.1.1.1) ;

• les compteurs de version de Lamport[Lam77]. Ce m´ecanisme permet de synchro- niser un ´ecrivain et plusieurs lecteurs (les ´ecrivains peuvent ˆetre synchronis´es autrement). On utilise deux compteurs de versions v1 et v2, qui sont modifi´e

en d´ebut et en fin d’´ecriture ; le lecteur recommence `a lire tant que v16= v2,

et recommence donc tant que l’´ecrivain est pr´eempt´e. Il s’agit donc d’une synchronisation sans verrou, mais bloquante. Le probl`eme est ´evit´e si l’´ecrivain ne peut pas ˆetre pr´eempt´e vis `a vis du lecteur (e.g. s’ex´ecute dans une section non pr´eemptible, ou est de plus haute priorit´e quand l’ordonnancement est `a priorit´e fixe).

4.1.2.2 La programmation non-bloquante

La technique g´en´erale pour obtenir des algorithmes non-bloquants est d’agencer son code de fa¸con `a ce qu’il soit fonctionnel quelque soient les entrelacements des diff´erents threads.

Un expos´e complet sur l’´etat de l’art des diff´erentes techniques de programma- tion non-bloquante sortirait du cadre de cette th`ese ; nous nous contenterons de pointer vers l’excellent ´etat de l’art de Fraser [Fra04] et d’expliquer les raisons pour lesquelles ces techniques ne peuvent pas s’appliquer dans les services d’Anaxagoros. Concr`etement, les probl`emes qui se posent sont le fait de recommencer l’ex´ecution, qui introduit une interf´erence dans l’ex´ecution des tˆaches, et l’allocation dynamique de m´emoire, qui peut permettre des d´enis de ressource sur la m´emoire.

Typologie Il existe trois cat´egories d’algorithmes non-bloquants :

• un algorithme wait-free garantit `a tout moment que tous les threads pr´esents finiront leur op´eration dans un temps maximum ;

• un algorithme lock-free garantit `a tout moment qu’un thread parmi tous les threads pr´esents finira son op´eration en un temps maximum (et pas simplement qu’il n’y a pas de lock) ;

• un algorithme obstruction-free garantit `a tout moment qu’un thread terminera son op´eration en un temps maximum, s’il est seul.

Les algorithmes lock-free et obstruction-free demandent des pr´ecautions pour un usage pour le temps r´eel, car le temps d’ex´ecution des algorithmes doit ˆetre born´e. Mais en monoprocesseur, l’emploi de ces primitives ne pose pas de probl`eme pourvu que les threads soient ordonnanc´es pour un quantum de temps suffisamment long (sup´erieur `a la dur´ee d’ex´ecution de l’algorithme), car un thread est toujours tout seul `a s’ex´ecuter.

En multiprocesseur, les algorithmes obstruction-free peuvent souffrir du probl`eme du livelock, et ne jamais terminer. Il existe des solutions pour ´eviter ce probl`eme, comme d’attendre pendant un temps probabiliste, mais nous ne souhaitons pas employer ces solutions pour des programmes temps r´eel critiques. Les algorithmes lock-free souffrent de la contention (§ 4.1.2.1) et ´eventuellement de famine. Ce probl`eme n’est pas remarquable si on boucle sur des sections courtes et qu’on utilise ces primitives de mani`ere “peu fr´equente”, ce qui est g´en´eralement le cas donc ces algorithmes peuvent en g´en´eral ˆetre employ´es.

En r´esum´e, la programmation non-bloquante semble bien adapt´ee pour r´esoudre les probl`emes de synchronisation dans les services d’Anaxagoros.

Probl`eme de l’allocation m´emoire Malheureusement, une bonne partie des algorithmes non-bloquants demande une allocation m´emoire dynamique.

Beaucoup fonctionnent sur ce principe : « alloue de la m´emoire, accessible seulement au thread, ´ecrit les nouvelles valeurs dedans, et remplace atomiquement le pointeur sur ces valeurs ». Dans les services partag´es, cela demande de l’allocation

4.1.2. Techniques usuelles de r´esolution

de m´emoire propre par un client, et est donc un d´eni de ressource m´emoire potentiel (le nombre de clients ´etant non born´e). Entre autre algorithmes bas´es sur ce principe, on peut citer les constructions universelles de Herlihy [Her90, Her93], ou la STM de Fraser et Harris [Fra04].

L’autre utilisation de m´emoire allou´ee dynamiquement vient des lecteurs si- multan´es. Quand une nouvelle version est install´ee, il peut y avoir des threads qui lisent en parall`ele l’ancienne version. Souvent, on ne r´ecup`ere pas la m´emoire tant qu’il en reste un lecteur, ce qui occasionne aussi une possibilit´e de d´eni de service. Entre autres algorithmes qui sont bas´es sur ce principe, il y a aussi les constructions universelles de Herlihy [Her90, Her93], et le m´ecanisme RCU utilis´e dans le noyau Linux [BC05, p. 207]. Ce probl`eme est plus difficile `a r´esoudre, mais il existe ´egalement des solutions (par exemple en notifiant les lecteurs de ne plus utiliser la donn´ee, comme le fait notre sch´ema de synchronisation use/destroy 5.2.4).

Ces probl`emes empˆeche la r´eutilisation directe de beaucoup d’algorithmes exis- tants, d`es qu’ils demandent une allocation dynamique de la m´emoire.

Probl`eme sp´ecifique aux syst`emes d’exploitation Quand on ´ecrit un service de syst`eme d’exploitation, on doit communiquer avec le mat´eriel qui choisit ses structures de donn´ees (e.g. format des tables de pages). Souvent la programmation non-bloquante demande `a r´eorganiser ses donn´ees afin de pouvoir faire une mise `a jour atomique, mais ce n’est souvent pas possible. Entre autre, l’utilisation de ports IO (communication avec des instruction particuli`ere) ou de buffers `a des emplacement m´emoires fixes restreignent la possibilit´e d’utiliser les algorithmes habituels.

Peu de syst`emes d’exploitations ont ´et´e impl´ement´e de mani`ere non bloquante. Cache [GC96] et Synthesis [Mas92] utilisent tout deux l’instruction DCAS, qui ´etait pr´esente sur le processeur 68k mais ne l’est plus dans les processeurs actuels. OASIS est non-bloquant, mais la majorit´e des ressources sont allou´ees statiquement. Exception notables Il y a des exceptions notables, pour lesquels on peut faire de la programmation parall`ele sans verrou ni allocation dynamique. Entre autres choses que nous avons utilis´ees, on trouve le “single-word protocol” de Herlihy [Her90], la lecture et ´ecriture concurrente de Lamport [Lam90], et une modification de l’algorithme pour les listes de Harris [Har01] pour l’utiliser comme pile pour pouvoir faire de l’allocation FCFS. Enfin, avec un peu d’astuce on peut facilement trouver des algorithmes ad-hoc (e.g. annexe A.1.3).

4.1.2.3 Un chemin interm´ediaire

En r´esum´e, les m´ecanismes usuels pour la synchronisation dans les programmes par- all`eles demandent souvent `a choisir entre plusieurs maux : les algorithmes bloquants posent des probl`emes lorsqu’ils sont pr´eempt´es dans leur section critique, ce qui demande soit de faire confiance au service soit qu’ils puissent modifier l’ordonnance- ment. Ils restreignent aussi souvent inutilement le parall´elisme. Les algorithmes non bloquants r´eclament souvent une allocation dynamique de m´emoire, ce qui conduit tr`es facilement `a un d´eni de ressource m´emoire.

`

A cause de ces probl`emes, nous avons d´ecid´e d’explorer ´egalement un chemin interm´ediaire : i.e. de se pr´emunir contre certains des probl`emes li´es `a la programma- tion parall`ele par le code, et des autres par l’emploi de primitives de synchronisation. Notre but est de fournir des m´ecanismes pour permettre des synchronisations qui ne demandent pas d’allocation dynamique, qui soient non-bloquantes, et qui permette au programmeur d’´ecrire facilement du code parall`ele efficace.

Cette voie a ´et´e ouverte notamment par le m´ecanisme de revocable lock de Harris et Fraser [HF05] ; nous avons men´e une recherche plus exhaustive sur cette voie interm´ediaire.