Sophie

Sophie

distrib > * > 2010.0 > * > by-pkgid > a412ceb851151854794ced2a242192bb > files > 2727

howto-html-fr-20080722-1mdv2010.0.noarch.rpm

<html><head><META http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"><title>2.&nbsp;Linux sur SMP</title><link href="style.css" rel="stylesheet" type="text/css"><meta content="DocBook XSL Stylesheets V1.68.1" name="generator"><link rel="start" href="index.html" title="Le traitement en parall&egrave;le sous Linux"><link rel="up" href="index.html" title="Le traitement en parall&egrave;le sous Linux"><link rel="prev" href="ar01s01.html" title="1.&nbsp;Introduction"><link rel="next" href="ar01s03.html" title="3.&nbsp;Clusters de syst&egrave;mes Linux"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table summary="Navigation header" width="100%"><tr><th align="center" colspan="3">2.&nbsp;Linux sur SMP</th></tr><tr><td align="left" width="20%"><a accesskey="p" href="ar01s01.html">Pr&eacute;c&eacute;dent</a>&nbsp;</td><th align="center" width="60%">&nbsp;</th><td align="right" width="20%">&nbsp;<a accesskey="n" href="ar01s03.html">Suivant</a></td></tr></table><hr></div><div class="sect1" lang="fr"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="N102AE"></a>2.&nbsp;Linux sur SMP</h2></div></div></div><p>

Ce document donne un bref aper&ccedil;u de la mani&egrave;re dont on utilise <a href="http://www.linux.org.uk/SMP/title.html" target="_top">le SMP sous Linux</a> 
pour le traitement en parall&egrave;le. L'information la plus &agrave; jour concernant 
le SMP sous Linux est fort probablement disponible via la liste de 
diffusion du SMP Linux Project (N.D.T.&nbsp;: en anglais). Envoyez un 
courrier &eacute;lectronique &agrave;

<code class="email">&lt;<a href="mailto:majordomo CHEZ vger POINT rutgers POINT edu">majordomo CHEZ vger POINT rutgers POINT edu</a>&gt;</code>

avec le texte <code class="literal">subscribe linux-smp</code> pour rejoindre la 
liste.

</p><p>

Le SMP sous Linux fonctionne-t-il vraiment&nbsp;? En juin 1996, j'ai 
fait l'achat d'un bi-Pentium 100MHz flambant neuf. Le syst&egrave;me complet et 
assembl&eacute;, comprenant les deux processeurs, la carte-m&egrave;re Asus, 256 
kilo-octets de m&eacute;moire cache, 32 m&eacute;ga-octets de RAM, le disque dur d'1.6 
giga-octet, le lecteur de CD-ROM 6X, une carte Stealth 64 et un moniteur 
15'' Acer m'a co&ucirc;t&eacute; 1800 dollars. Cela ne fait que quelques centaines de 
dollars de plus qu'un syst&egrave;me monoprocesseur. Pour faire fonctionner le 
SMP sous Linux, il a suffi d'installer le Linux monoprocesseur 
d'origine, de recompiler le noyau en d&eacute;commentant la ligne 
<code class="literal">SMP=1</code> dans le <span class="emphasis"><em>Makefile</em></span> (bien que 
je trouve le fait de mettre <code class="literal">SMP</code> &agrave; 
<code class="literal">1</code> un peu ironique&nbsp;! ;-) ), et d'informer 
<code class="literal">lilo</code> de l'existence du nouveau noyau. Ce syst&egrave;me 
pr&eacute;sente une stabilit&eacute; et des performances suffisamment bonnes pour 
qu'il me serve depuis de station de travail principale. Pour r&eacute;sumer, le 
SMP sous Linux, &ccedil;a fonctionne&nbsp;!

</p><p>

La question qui se pr&eacute;sente alors est&nbsp;: existe-t-il suffisamment 
d'API de haut niveau permettant d'&eacute;crire et d'ex&eacute;cuter des programmes en 
parall&egrave;le et utilisant la m&eacute;moire partag&eacute;e sous Linux SMP&nbsp;? Courant 
1996, il n'y en avait pas beaucoup. Les choses ont chang&eacute;. Par exemple, 
il existe d&eacute;sormais une biblioth&egrave;que POSIX de gestion des 
<span class="foreignphrase"><em class="foreignphrase">threads</em></span><sup>[<a href="#ftn.N102D7" name="N102D7">2</a>]</sup> tr&egrave;s compl&egrave;te.

</p><p>

Bien que les performances soient moins &eacute;lev&eacute;es que celles des m&eacute;canismes 
de m&eacute;moire partag&eacute;e natifs, un syst&egrave;me Linux sur SMP peut aussi utiliser 
la plupart des logiciels de traitement en parall&egrave;le initialement 
d&eacute;velopp&eacute;s pour des <span class="foreignphrase"><em class="foreignphrase">clusters</em></span> de stations 
de travail en utilisant la communication par 
<span class="foreignphrase"><em class="foreignphrase">socket</em></span>. Les <span class="emphasis"><em>sockets</em></span> 
(voir section 3.3) fonctionnent &agrave; l'int&eacute;rieur d'une machine en SMP, et 
m&ecirc;me dans un <span class="foreignphrase"><em class="foreignphrase">cluster</em></span> de machines SMP 
reli&eacute;es en r&eacute;seau. Cependant, les <span class="foreignphrase"><em class="foreignphrase">sockets</em></span> 
engendrent beaucoup de pertes en temps inutiles pour du SMP. Cela 
complique le probl&egrave;me car Linux SMP n'autorise en g&eacute;n&eacute;ral qu'un seul 
processeur &agrave; la fois &agrave; se trouver dans le noyau et le contr&ocirc;leur 
d'interruption est r&eacute;gl&eacute; de fa&ccedil;on &agrave; ce que seul le processeur de 
<span class="foreignphrase"><em class="foreignphrase">boot</em></span><sup>[<a href="#ftn.N102EE" name="N102EE">3</a>]</sup> puisse traiter les interruptions. En d&eacute;pit de cela, 
l'&eacute;lectronique de communication typique des syst&egrave;mes SMP est tellement 
meilleure que la plupart des <span class="foreignphrase"><em class="foreignphrase">clusters</em></span> en 
r&eacute;seau que les logiciels pour <span class="foreignphrase"><em class="foreignphrase">cluster</em></span> 
fonctionneront souvent mieux sur du SMP que sur le 
<span class="foreignphrase"><em class="foreignphrase">cluster</em></span> pour lequel ils ont &eacute;t&eacute; con&ccedil;us.

</p><p>
Le reste de cette section traite de l'&eacute;lectronique contr&ocirc;lant le SMP,
passe en revue les m&eacute;canismes Linux de base partageant de la m&eacute;moire
&agrave; travers les diff&eacute;rents processus d'un programme en parall&egrave;le, fait
quelques remarques concernant l'atomicit&eacute;, la volatilit&eacute;, les verrous
et les lignes de cache, et donne enfin des r&eacute;f&eacute;rences vers d'autres
ressources de traitement en parall&egrave;le &agrave; m&eacute;moire partag&eacute;e.
</p><div class="sect2" lang="fr"><div class="titlepage"><div><div><h3 class="title"><a name="N102FD"></a>2.1.&nbsp;L'&eacute;lectronique SMP</h3></div></div></div><p>

Bien que les syst&egrave;mes SMP soit r&eacute;pandus depuis de nombreuses ann&eacute;es, 
jusque tr&egrave;s r&eacute;cemment, chaque machine tendait &agrave; impl&eacute;menter les 
fonctions de base d'une mani&egrave;re suffisamment diff&eacute;rente des autres pour 
que leur gestion par le syst&egrave;me d'exploitation ne soit pas portable. Les 
choses ont chang&eacute; avec la <span class="foreignphrase"><em class="foreignphrase">Intel's MultiProcessor 
Specification</em></span> (Sp&eacute;cification MultiProcesseurs d'Intel) 
souvent d&eacute;sign&eacute;e par <span class="emphasis"><em>MPS</em></span>. La sp&eacute;cification MPS 1.4 
est actuellement disponible sous forme de document PDF sur <a href="http://www.intel.com/design/intarch/MANUALS/242016.htm" target="_top">http://www.intel.com/design/intarch/MANUALS/242016.htm</a><sup>[<a href="#ftn.N1030A" name="N1030A">4</a>]</sup>,
mais gardez &agrave; l'esprit qu'Intel r&eacute;organise souvent son site web. Un large 
panel de constructeurs fabrique des syst&egrave;mes conformes &agrave; MPS pouvant 
recevoir jusqu'&agrave; quatre processeurs, mais en th&eacute;orie, MPS admet bien 
plus de processeurs.

</p><p>

Les seuls syst&egrave;mes non MPS et non IA32 reconnus par Linux SMP sont les 
machines SPARC multiprocesseurs de Sun4m. Linux SMP prend aussi en 
charge la plupart des machines Intel conformes &agrave; MPS 1.1 ou 1.4, 
comptant jusqu'&agrave; 16 processeurs 486DX, Pentium, Pentium MMX, Pentium Pro 
ou Pentium II. Parmi les processeurs IA32 non pris en charge (N.D.T.&nbsp;: 
par le SMP), on trouve les Intel 386 et 486SX/SLC (l'absence de 
coprocesseur math&eacute;matique interf&egrave;re sur les m&eacute;canismes du SMP) et les 
processeurs AMD et Cyrix (qui n&eacute;cessitent des circuits de gestion du SMP 
diff&eacute;rents et qui ne semblent pas &ecirc;tre disponibles &agrave; l'heure o&ugrave; ce 
document est &eacute;crit).

</p><p>
Il est important de bien comprendre que les performances de diff&eacute;rents
syst&egrave;mes conformes &agrave; MPS peuvent fortement varier. Comme l'on peut s'y
attendre, une des causes de diff&eacute;rence de performance est la vitesse
du processeur&nbsp;: Une horloge plus rapide tend &agrave; rendre les syst&egrave;mes plus
rapides, et un processeur Pentium Pro est plus rapide qu'un Pentium.
En revanche, MPS ne sp&eacute;cifie pas vraiment comment le mat&eacute;riel doit
mettre en &#339;uvre la m&eacute;moire partag&eacute;e, mais seulement comment cette
impl&eacute;mentation doit fonctionner d'un point de vue logiciel. Cela
signifie que les performances d&eacute;pendent aussi de la fa&ccedil;on dont
l'impl&eacute;mentation de la m&eacute;moire partag&eacute;e interagit avec les caract&eacute;ristiques
de Linux SMP et de vos applications en particulier.
</p><p>
La principale diff&eacute;rence entre les syst&egrave;mes conformes &agrave; MPS r&eacute;side
dans la mani&egrave;re dont ils impl&eacute;mentent l'acc&egrave;s &agrave; la m&eacute;moire physiquement
partag&eacute;e.
</p><div class="sect3" lang="fr"><div class="titlepage"><div><div><h4 class="title"><a name="N10317"></a>2.1.1.&nbsp;Chaque processeur poss&egrave;de-t-il sa propre m&eacute;moire cache de niveau 2 (L2)&nbsp;?</h4></div></div></div><p>
Certains syst&egrave;mes MPS &agrave; base de Pentium, et tous les syst&egrave;mes MPS
Pentium Pro et Pentium II ont des m&eacute;moires cache L2 ind&eacute;pendantes
(le cache L2 est embarqu&eacute; dans le module des Pentium Pro et Pentium II).
Les m&eacute;moires caches L2 dissoci&eacute;es sont g&eacute;n&eacute;ralement r&eacute;put&eacute;es augmenter
les performances de l'ordinateur, mais les choses ne sont pas si &eacute;videntes
sous Linux. La principale complication provient du fait que l'ordonnanceur
de Linux SMP n'essaie pas de maintenir chaque processus sur
le m&ecirc;me processeur, concept connu sous le nom d'<span class="emphasis"><em>affinit&eacute; processeur</em></span>.
Cela pourrait bient&ocirc;t changer. Un d&eacute;bat a r&eacute;cemment eu lieu sur ce sujet dans la
communaut&eacute; des d&eacute;veloppeurs Linux SMP, sous le titre &laquo;&nbsp;<span class="quote"><span class="foreignphrase"><em class="foreignphrase">processor
bindings</em></span></span>&nbsp;&raquo; (&laquo;&nbsp;<span class="quote">associations de processeurs</span>&nbsp;&raquo;). Sans affinit&eacute; processeur,
des caches L2 s&eacute;par&eacute;s peuvent introduire des d&eacute;lais non n&eacute;gligeables
lorsqu'un processus se voit allouer une tranche de temps d'ex&eacute;cution
sur un processeur qui n'est pas le m&ecirc;me que celui sur lequel il
s'ex&eacute;cutait juste avant.
</p><p>
Plusieurs syst&egrave;mes relativement bon march&eacute; sont organis&eacute;s de mani&egrave;re
&agrave; ce que deux processeurs Pentium puissent partager la m&ecirc;me m&eacute;moire
cache L2. La mauvaise nouvelle, c'est que cela cr&eacute;e des conflits &agrave;
l'utilisation de ce cache, qui d&eacute;gradent s&eacute;rieusement les performances
lorsque plusieurs programmes s&eacute;quentiels ind&eacute;pendants s'ex&eacute;cutent
simultan&eacute;ment. La bonne nouvelle, c'est que bon nombre de programmes
parall&egrave;les pourraient tirer profit de la m&eacute;moire cache partag&eacute;e, car
si les deux processeurs veulent acc&eacute;der &agrave; la m&ecirc;me ligne de m&eacute;moire
partag&eacute;e, seul un processeur doit aller la rapatrier dans le cache,
et l'on &eacute;vite des conflits de bus. Le manque d'affinit&eacute; processeur
peut aussi s'av&eacute;rer moins d&eacute;sastreux avec un cache L2 partag&eacute;. Ainsi,
pour les programmes parall&egrave;les, il n'est pas vraiment certain que
partager la m&eacute;moire cache L2 soit si pr&eacute;judiciable que l'on pourrait
le penser.
</p><p>
&Agrave; l'usage, notre bi-Pentium &agrave; m&eacute;moire cache partag&eacute;e
de 256Ko pr&eacute;sente une vaste &eacute;chelle de performances, d&eacute;pendantes du
niveau d'activit&eacute; noyau requis. Au pire, le gain en vitesse
n'atteint qu'un facteur de 1,2. En revanche, nous avons aussi
constat&eacute; une acc&eacute;l&eacute;ration de 2,1 fois la vitesse d'origine, ce qui
sugg&egrave;re que les calculs intensifs &agrave; base de SPMD tirent vraiment
profit de l'effet d'&laquo;&nbsp;<span class="quote">acquisition partag&eacute;e</span>&nbsp;&raquo; (&laquo;&nbsp;<span class="quote"><span class="foreignphrase"><em class="foreignphrase">shared fetch</em></span></span>&nbsp;&raquo;).
</p></div><div class="sect3" lang="fr"><div class="titlepage"><div><div><h4 class="title"><a name="N10331"></a>2.1.2.&nbsp;Configuration du bus&nbsp;?</h4></div></div></div><p>
La premi&egrave;re chose &agrave; dire est que la plupart des syst&egrave;mes modernes
relient le processeur &agrave; un ou plusieurs bus PCI qui &agrave; leur tour
sont &laquo;&nbsp;<span class="quote">pont&eacute;s</span>&nbsp;&raquo; vers un ou plusieurs bus ISA ou EISA. Ces ponts
engendrent des temps de latence, et l'ISA comme l'EISA offrent
g&eacute;n&eacute;ralement des bandes passantes plus r&eacute;duites que le PCI
(ISA &eacute;tant le plus lent). C'est pourquoi les disques, cartes vid&eacute;os et
autres p&eacute;riph&eacute;riques de haute performance devraient en principe
&ecirc;tre connect&eacute;s sur un bus PCI.
</p><p>
Bien qu'un syst&egrave;me MPS puisse apporter un gain en vitesse honorable
&agrave; plusieurs programmes parall&egrave;les de calcul intensif m&ecirc;me avec un
seul bus PCI, les op&eacute;rations d'entr&eacute;es/sorties, elles, ne sont pas
meilleures que sur un syst&egrave;me monoprocesseur. Elles sont peut-&ecirc;tre m&ecirc;me
un peu moins bonnes &agrave; cause des conflits de bus entre les processeurs.
Ainsi, si votre objectif est d'acc&eacute;l&eacute;rer les entr&eacute;es/sorties, prenez
soin de choisir un syst&egrave;me MPS comportant plusieurs bus PCI ind&eacute;pendants
et plusieurs contr&ocirc;leurs d'entr&eacute;es/sorties (par exemple&nbsp;: plusieurs
cha&icirc;nes SCSI). Il vous faudra &ecirc;tre prudent, et s&ucirc;r que Linux
reconna&icirc;t tout votre mat&eacute;riel. Gardez aussi &agrave; l'esprit le fait que
Linux n'autorise qu'un seul processeur &agrave; la fois &agrave; entrer en mode
noyau, aussi devrez-vous choisir des contr&ocirc;leurs qui r&eacute;duisent au
minimum le temps noyau n&eacute;cessaire &agrave; leurs op&eacute;rations. Pour atteindre
des performances vraiment tr&egrave;s &eacute;lev&eacute;es, il se pourrait m&ecirc;me qu'il
vous faille envisager d'effectuer vous-m&ecirc;me les op&eacute;rations d'entr&eacute;e/sortie
de bas niveau directement depuis les processus utilisateurs,
sans appel syst&egrave;me&hellip; ce n'est pas forc&eacute;ment aussi difficile que
cela en a l'air, et cela permet d'&eacute;viter de compromettre la s&eacute;curit&eacute; du
syst&egrave;me (voir la section 3.3 pour une description des techniques de
base).
</p><p>
Il est important de remarquer que la relation entre vitesse du bus
et vitesse du processeur est devenue tr&egrave;s floue ces derni&egrave;res ann&eacute;es.
Bien que la plupart des syst&egrave;mes utilisent maintenant la m&ecirc;me fr&eacute;quence
de bus PCI, il n'est pas rare de trouver un processeur rapide appari&eacute;
avec un bus lent. L'exemple classique est celui du Pentium 133 qui
utilise en g&eacute;n&eacute;ral un bus plus rapide que celui du Pentium 150, produisant
des r&eacute;sultats &eacute;tranges sur les logiciels bancs de tests (&laquo;&nbsp;<span class="quote"><span class="foreignphrase"><em class="foreignphrase">benchmarks</em></span></span>&nbsp;&raquo;).
Ces effets sont amplifi&eacute;s sur les syst&egrave;mes SMP, o&ugrave; il est encore plus
important d'utiliser un bus rapide.
</p></div><div class="sect3" lang="fr"><div class="titlepage"><div><div><h4 class="title"><a name="N10341"></a>2.1.3.&nbsp;Interfoliage de la m&eacute;moire et technologie DRAM</h4></div></div></div><p>
L'interfoliage de la m&eacute;moire n'a en fait absolument rien &agrave; voir
avec le MPS, mais vous verrez souvent cette mention accompagner
les syst&egrave;mes MPS car ceux-ci sont typiquement gourmands en
bande passante m&eacute;moire. Concr&egrave;tement, l'interfoliage en deux ou
en quatre voies organise la RAM de fa&ccedil;on &agrave; ce que l'acc&egrave;s &agrave; un
bloc de m&eacute;moire se fasse au travers de plusieurs bancs de RAM
plut&ocirc;t qu'un seul. Ceci acc&eacute;l&egrave;re grandement les acc&egrave;s &agrave; la
m&eacute;moire, particuli&egrave;rement en ce qui concerne le chargement et
l'enregistrement du contenu des lignes de cache.
</p><p>
Il faut toutefois souligner que ce fait n'est pas aussi &eacute;vident
qu'il y parait, car la DRAM EDO et les diff&eacute;rentes technologies
m&eacute;moire tendent &agrave; optimiser ce genre d'op&eacute;rations. Un excellent
aper&ccedil;u des diff&eacute;rentes technologies DRAM est disponible sur
<a href="http://www.pcguide.com/ref/ram/tech.htm" target="_top">http://www.pcguide.com/ref/ram/tech.htm</a>.
</p><p>
Ainsi, par exemple, mieux vaut-il avoir de la m&eacute;moire DRAM EDO
interfoli&eacute;e &agrave; 2 voies, ou de la m&eacute;moire SDRAM non interfoli&eacute;e&nbsp;?
C'est une tr&egrave;s bonne question et la r&eacute;ponse n'est pas simple,
car la m&eacute;moire interfoli&eacute;e comme les technologies DRAM exotiques
ont tendance &agrave; &ecirc;tre co&ucirc;teuses. Le m&ecirc;me investissement en m&eacute;moire
plus ordinaire vous apporte en g&eacute;n&eacute;ral une m&eacute;moire centrale bien
plus vaste. M&ecirc;me la plus lente des m&eacute;moire DRAM reste autrement
plus rapide que la m&eacute;moire virtuelle par fichier d'&eacute;change&hellip;
</p></div></div><div class="sect2" lang="fr"><div class="titlepage"><div><div><h3 class="title"><a name="N1034E"></a>2.2.&nbsp;Introduction &agrave; la programmation en m&eacute;moire partag&eacute;e</h3></div></div></div><p>
<span class="foreignphrase"><em class="foreignphrase">Okay</em></span>, donc vous avez d&eacute;cid&eacute; que le traitement 
en parall&egrave;le sur SMP, c'est g&eacute;nial&hellip; Par quoi allez-vous commencer&nbsp;? Eh bien,
la premi&egrave;re &eacute;tape consiste &agrave; en apprendre un peu plus sur le fonctionnement r&eacute;el
de la communication par m&eacute;moire partag&eacute;e.
</p><p>
A premi&egrave;re vue, il suffit qu'un processeur range une valeur en m&eacute;moire
et qu'un autre la lise. Malheureusement, ce n'est pas aussi simple.
Par exemple, les relations entre processus et processeurs sont tr&egrave;s
floues. En revanche, si nous n'avons pas plus de processus actifs que
de processeurs, les termes sont &agrave; peu pr&egrave;s interchangeables. Le reste
de cette section r&eacute;sume bri&egrave;vement les cas de figure typiques qui
peuvent poser de s&eacute;rieux probl&egrave;mes, si vous ne les connaissiez pas
d&eacute;j&agrave;&nbsp;: les deux diff&eacute;rents mod&egrave;les utilis&eacute;s pour d&eacute;terminer ce qui
est partag&eacute;, les probl&egrave;mes d'atomicit&eacute;, le concept de volatilit&eacute;,
les instructions de verrouillage mat&eacute;riel, les effets de la ligne
de cache, et les probl&egrave;mes pos&eacute;s par l'ordonnanceur de
Linux.
</p><div class="sect3" lang="fr"><div class="titlepage"><div><div><h4 class="title"><a name="N10358"></a>2.2.1.&nbsp;Partage Int&eacute;gral contre Partage Partiel</h4></div></div></div><p>
Il existe deux mod&egrave;les fondamentaux couramment utilis&eacute;s en programmation
en m&eacute;moire partag&eacute;e&nbsp;: le <span class="emphasis"><em>partage int&eacute;gral</em></span>
et le <span class="emphasis"><em>partage partiel</em></span>. Ces mod&egrave;les
permettent tous deux aux processeurs de communiquer en chargeant et
rangeant des donn&eacute;es depuis et dans la m&eacute;moire. La diff&eacute;rence r&eacute;side
dans le fait que le partage int&eacute;gral place toutes les structures en
m&eacute;moire partag&eacute;e, quand le partage partiel, lui, distingue les structures
qui sont potentiellement partageables et celles qui sont
<span class="emphasis"><em>priv&eacute;es</em></span>, propres &agrave; un seul processeur
(et oblige l'utilisateur &agrave; classer explicitement ses structures dans l'une
de ces cat&eacute;gories).
</p><p>
Alors quel mod&egrave;le de partage m&eacute;moire faut-il utiliser&nbsp;? C'est surtout
une affaire de chapelle. Beaucoup de gens aiment le partage int&eacute;gral
car ils n'ont pas sp&eacute;cialement besoin d'identifier les structures qui
doivent &ecirc;tre partag&eacute;es au moment de leur d&eacute;claration. On place simplement
des verrous sur les objets auxquels l'acc&egrave;s peut cr&eacute;er des conflits, pour
s'assurer qu'un seul processeur (ou processus) y acc&egrave;de &agrave; un moment donn&eacute;.
Mais l&agrave; encore, ce n'est pas aussi simple&hellip; aussi beaucoup d'autres
gens pr&eacute;f&egrave;rent, eux, le mod&egrave;le relativement s&ucirc;r du partage partiel.
</p><div class="sect4" lang="fr"><div class="titlepage"><div><div><h5 class="title"><a name="N10368"></a>2.2.1.1.&nbsp;Partage int&eacute;gral</h5></div></div></div><p>
Le bon cot&eacute; du partage int&eacute;gral est que l'on peut ais&eacute;ment reprendre
un programme s&eacute;quentiel existant et le convertir progressivement en
programme parall&egrave;le en partage int&eacute;gral. Vous n'avez pas &agrave; d&eacute;terminer
au pr&eacute;alable les donn&eacute;es qui doivent &ecirc;tre accessibles aux autres
processeurs.
</p><p>
Pos&eacute; simplement, le principal probl&egrave;me avec le partage int&eacute;gral vient
du fait qu'une action effectu&eacute;e par un processeur peut affecter les
autres processeurs. Ce probl&egrave;me ressurgit de deux mani&egrave;res&nbsp;:
</p><p>

<div class="itemizedlist"><ul type="disc"><li><p>
Plusieurs biblioth&egrave;ques utilisent des structures de donn&eacute;es qui
ne sont tout simplement pas partageables. Par exemple, la convention
UNIX stipule que la plupart des fonctions peuvent renvoyer un code
d'erreur dans une variable appel&eacute;e <code class="literal">errno</code>.
Si deux processus en partage int&eacute;gral font des appels divers, ils vont
interf&eacute;rer l'un sur l'autre car ils partagent la m&ecirc;me variable
<code class="literal">errno</code>. Bien qu'il existe d&eacute;sormais une
biblioth&egrave;que qui r&egrave;gle le probl&egrave;me de cette variable, ce probl&egrave;me se pr&eacute;sente toujours
dans la plupart des biblioth&egrave;ques comme par exemple X-Window qui, &agrave; moins de prendre
des pr&eacute;cautions tr&egrave;s sp&eacute;ciales, ne fonctionnera pas si diff&eacute;rents appels sont
pass&eacute;s depuis diff&eacute;rents processus en partage int&eacute;gral.
</p></li><li><p>
En temps normal, un programme qui utilise un pointeur ou un index
d&eacute;faillant provoque au pire l'arr&ecirc;t du processus qui contient le code corrompu.
Il peut m&ecirc;me g&eacute;n&eacute;rer un fichier <code class="literal">core</code> vous renseignant sur les conditions
dans lesquelles se sont d&eacute;roul&eacute;s les &eacute;v&eacute;nements. En programmation parall&egrave;le
&agrave; partage int&eacute;gral, il est fort probable que les acc&egrave;s ill&eacute;gaux provoquent
la <span class="emphasis"><em>fin d'un processus qui n'est pas le fautif</em></span>, rendant
la localisation et la correction de l'erreur quasiment impossibles.
</p></li></ul></div>

</p><p>
Aucun de ces deux probl&egrave;mes n'est courant dans le cas du partage
partiel, car seules sont partag&eacute;es les structures explicitement marqu&eacute;es
comme telles. De plus, il est trivial que le partage int&eacute;gral ne peut
fonctionner que si les processeurs ex&eacute;cutent exactement la m&ecirc;me image
en m&eacute;moire. On ne peut pas utiliser le partage int&eacute;gral entre des images
de code diff&eacute;rentes (c'est-&agrave;-dire que vous pourrez travailler en SPMD,
mais pas d'une mani&egrave;re g&eacute;n&eacute;rale en MIMD).
</p><p>

Les supports de programmation en partage int&eacute;gral existent le plus 
couramment sous la forme de <span class="emphasis"><em>biblioth&egrave;ques de 
<span class="foreignphrase"><em class="foreignphrase">threads</em></span></em></span>. Les <a href="http://liinwww.ira.uka.de/bibliography/Os/threads.html" target="_top"><span class="foreignphrase"><em class="foreignphrase">threads</em></span></a> 
sont essentiellement des processus &laquo;&nbsp;<span class="quote">all&eacute;g&eacute;s</span>&nbsp;&raquo; dont 
l'ex&eacute;cution peut ne pas &ecirc;tre planifi&eacute;e comme celle des processus UNIX 
normaux et qui, c'est le plus important, partagent tous la m&ecirc;me page 
m&eacute;moire. L'adaptation des <a href="http://www.mit.edu:8001/people/proven/IAP_2000/index.html" target="_top">Pthreads</a> 
POSIX a fait l'objet de nombreux efforts. La grande question est&nbsp;: 
ces adaptations parall&eacute;lisent-elles les 
<span class="foreignphrase"><em class="foreignphrase">threads</em></span> d'un programme en environnement 
Linux SMP (id&eacute;alement, en attribuant un processeur &agrave; chaque 
<span class="foreignphrase"><em class="foreignphrase">thread</em></span>)&nbsp;?. L'API POSIX ne l'impose 
pas, et certaines versions comme <span class="emphasis"><em>PCthreads</em></span> semblent 
ne pas impl&eacute;menter une ex&eacute;cution en parall&egrave;le des 
<span class="foreignphrase"><em class="foreignphrase">threads</em></span>&nbsp;: tous les 
<span class="foreignphrase"><em class="foreignphrase">threads</em></span> d'un programme sont conserv&eacute;s &agrave; 
l'int&eacute;rieur d'un seul processus Linux.

</p><p>
La premi&egrave;re biblioth&egrave;que de <span class="foreignphrase"><em class="foreignphrase">threads</em></span> &agrave; avoir pris en charge le parall&eacute;lisme
sous Linux SMP fut la d&eacute;sormais quelque peu obsol&egrave;te biblioth&egrave;que
<span class="emphasis"><em>bb_thread</em></span>, une toute petite biblioth&egrave;que qui utilisait l'appel Linux
<code class="literal">clone()</code> pour donner naissance &agrave; de nouveaux
processus Linux, planifi&eacute;s ind&eacute;pendamment les uns des autres, tous partageant
un m&ecirc;me espace d'adressage. Les machines Linux SMP peuvent lancer plusieurs
de ces &laquo;&nbsp;<span class="quote"><span class="foreignphrase"><em class="foreignphrase">threads</em></span></span>&nbsp;&raquo; car chaque &laquo;&nbsp;<span class="quote"><span class="foreignphrase"><em class="foreignphrase">thread</em></span></span>&nbsp;&raquo; est un processus Linux &agrave; part enti&egrave;re.
L'inconv&eacute;nient, c'est que l'on ne peut obtenir l'ordonnancement &laquo;&nbsp;<span class="quote">poids-plume</span>&nbsp;&raquo;
apport&eacute;e par les biblioth&egrave;ques de <span class="foreignphrase"><em class="foreignphrase">threads</em></span> d'autres syst&egrave;mes
d'exploitation. La biblioth&egrave;que utilisait un peu de code assembleur int&eacute;gr&eacute;
dans un code source en langage C pour mettre en place un bloc de m&eacute;moire pour la
pile de chaque <span class="foreignphrase"><em class="foreignphrase">thread</em></span> et fournir des fonctions d'acc&egrave;s atomiques &agrave; un tableau
de verrous (les <span class="emphasis"><em>mutex</em></span>). Sa documentation se r&eacute;sumait &agrave; un fichier
<code class="literal">LISEZMOI</code> et &agrave; un court programme d'exemple.
</p><p>
Plus r&eacute;cemment, une version de <span class="foreignphrase"><em class="foreignphrase">threads</em></span> POSIX utilisant <code class="literal">clone()</code>
a &eacute;t&eacute; d&eacute;velopp&eacute;e. Cette biblioth&egrave;que,
<a href="http://pauillac.inria.fr/~xleroy/linuxthreads/" target="_top">LinuxThreads</a>,
est clairement la biblioth&egrave;que en partage int&eacute;gral favorite pour l'utilisation
sous Linux SMP. Les <span class="foreignphrase"><em class="foreignphrase">threads</em></span> POSIX sont bien document&eacute;s, et les documents
<a href="http://pauillac.inria.fr/~xleroy/linuxthreads/README" target="_top">LinuxThreads&nbsp;README</a>
et <a href="http://pauillac.inria.fr/~xleroy/linuxthreads/faq.html" target="_top">LinuxThreads&nbsp;FAQ</a>
sont vraiment tr&egrave;s bien r&eacute;alis&eacute;s. A pr&eacute;sent, le principal probl&egrave;me est
que les <span class="foreignphrase"><em class="foreignphrase">threads</em></span> POSIX ont encore beaucoup de d&eacute;tails &agrave; r&eacute;gler, et que
LinuxThread est toujours un projet en cours d'&eacute;volution. D'autre part,
les <span class="foreignphrase"><em class="foreignphrase">threads</em></span> POSIX ont &eacute;volu&eacute; pendant dans leur phase de standardisation,
aussi devrez-vous &ecirc;tre prudent pour ne pas d&eacute;velopper en suivant une version obsol&egrave;te du standard.
</p></div><div class="sect4" lang="fr"><div class="titlepage"><div><div><h5 class="title"><a name="N103EE"></a>2.2.1.2.&nbsp;Partage Partiel</h5></div></div></div><p>

Le Partage Partiel consiste r&eacute;ellement &agrave; &laquo;&nbsp;<span class="quote">ne partager que ce qui 
doit &ecirc;tre partag&eacute;</span>&nbsp;&raquo;. Cette approche est valable pour le MIMD en 
g&eacute;n&eacute;ral (et pas simplement le SPMD) &agrave; condition de prendre soin 
d'allouer les objets partag&eacute;s aux m&ecirc;mes endroits dans le plan m&eacute;moire de 
chaque processeur. Plus important encore, le partage partiel facilite 
l'estimation et l'ajustage des performances, le d&eacute;bogage des sources, et 
c&aelig;tera. Les seuls probl&egrave;mes sont&nbsp;:

</p><p>

<div class="itemizedlist"><ul type="disc"><li><p>
D&eacute;terminer &agrave; l'avance ce qui doit &ecirc;tre partag&eacute; peut s'av&eacute;rer difficile.
</p></li><li><p>
L'allocation d'objets dans la m&eacute;moire partag&eacute;e peut en fait se r&eacute;v&eacute;ler
malais&eacute;, sp&eacute;cialement en ce qui concerne tout ce qui aurait du &ecirc;tre d&eacute;clar&eacute;
dans la pile. Par exemple, il peut &ecirc;tre n&eacute;cessaire d'allouer explicitement
des objets partag&eacute;s dans des segments de m&eacute;moire s&eacute;par&eacute;s, n&eacute;cessitant des
routines d'allocation m&eacute;moire s&eacute;par&eacute;es, et impliquant l'ajout de pointeurs
et d'indirections suppl&eacute;mentaires &agrave; chaque r&eacute;f&eacute;rence.
</p></li></ul></div>

</p><p>

Actuellement, il existe deux m&eacute;canismes similaires permettant aux 
groupes de processus sous Linux de poss&eacute;der des espaces m&eacute;moire 
ind&eacute;pendants, mais de tous partager un unique et relativement &eacute;troit 
segment de m&eacute;moire. En supposant que vous n'ayez pas b&ecirc;tement exclu 
l'option &laquo;&nbsp;<span class="quote">System V IPC</span>&nbsp;&raquo; lorsque que vous avez configur&eacute; 
votre syst&egrave;me Linux (N.D.T.&nbsp;: ici &agrave; la recompilation du noyau), Linux 
g&egrave;re un m&eacute;canisme tr&egrave;s portable devenu c&eacute;l&egrave;bre sous le nom de 
&laquo;&nbsp;<span class="quote">m&eacute;moire partag&eacute;e System V</span>&nbsp;&raquo;. L'autre alternative est une 
fonction de projection en m&eacute;moire dont l'impl&eacute;mentation varie grandement 
selon le syst&egrave;me UNIX utilis&eacute;&nbsp;: L'appel syst&egrave;me 
<code class="literal">mmap</code>. Vous pouvez &mdash; et devriez &mdash; 
apprendre le fonctionnement de ces primitives au travers des pages du 
manuel (<span class="foreignphrase"><em class="foreignphrase">man pages</em></span>)&hellip; mais vous 
trouverez quand m&ecirc;me un rapide aper&ccedil;u de chacune d'elles dans les 
sections 2.5 et 2.6 pour vous aider &agrave; d&eacute;marrer.

</p></div></div><div class="sect3" lang="fr"><div class="titlepage"><div><div><h4 class="title"><a name="N1040F"></a>2.2.2.&nbsp;Atomicit&eacute; et ordonnancement</h4></div></div></div><p>
Que vous utilisiez l'un ou l'autre des mod&egrave;les cit&eacute;s ci-dessus, le
r&eacute;sultat est &agrave; peu pr&egrave;s le m&ecirc;me&nbsp;: vous obtenez un pointeur sur un bloc de
m&eacute;moire en lecture/&eacute;criture accessible par tous les processus de votre
programme en parall&egrave;le. Cela signifie-t-il que je peux laisser mes
programmes acc&eacute;der aux objets partag&eacute;s comme s'ils se trouvaient en
m&eacute;moire locale ordinaire&nbsp;? Pas tout &agrave; fait&hellip;
</p><p>
L'<span class="emphasis"><em>atomicit&eacute;</em></span> d&eacute;signe une op&eacute;ration sur
un objet effectu&eacute;e en une s&eacute;quence indivisible et ininterruptible.
Malheureusement, les acc&egrave;s &agrave; la m&eacute;moire partag&eacute;e n'impliquent pas que
les toutes les op&eacute;rations sur les donn&eacute;es de cette m&eacute;moire se fassent
de mani&egrave;re atomique. A moins de prendre des pr&eacute;cautions sp&eacute;ciales, seules les
op&eacute;rations de lecture ou d'&eacute;criture s'accomplissant en une seule transaction
sur le bus (c'est-&agrave;-dire align&eacute;es sur une adresse multiple de 8, 16 ou 32
bits, &agrave; l'exclusion des op&eacute;rations 64 bits ou mal align&eacute;es) sont atomiques. Pire encore,
les compilateurs &laquo;&nbsp;<span class="quote">intelligents</span>&nbsp;&raquo; comme GCC font souvent des optimisations qui
peuvent &eacute;liminer les op&eacute;rations m&eacute;moire n&eacute;cessaires pour s'assurer que les autres processeurs
puissent voir ce que le processeur concern&eacute; a fait. Heureusement, ces probl&egrave;mes ont tous
deux une solution&hellip; en acceptant seulement de ne pas se soucier de la
relation entre l'efficacit&eacute; des acc&egrave;s m&eacute;moire et la taille de la ligne de
cache.
</p><p>
En revanche, avant de traiter de ces diff&eacute;rents cas de figure, il est
utile de pr&eacute;ciser que tout ceci part du principe que les r&eacute;f&eacute;rences &agrave;
la m&eacute;moire pour chaque processeur se produisent dans l'ordre o&ugrave; elles
ont &eacute;t&eacute; programm&eacute;es. Le Pentium fonctionne de cette mani&egrave;re, mais les
futurs processeurs d'Intel pourraient ne pas le faire. Aussi, quand
vous d&eacute;velopperez sur les processeurs &agrave; venir, gardez &agrave; l'esprit qu'il
pourrait &ecirc;tre n&eacute;cessaire d'encadrer les acc&egrave;s &agrave; la m&eacute;moire avec des
instructions provoquant l'ach&egrave;vement de toutes les acc&egrave;s &agrave; la m&eacute;moire
en suspens, provoquant ainsi leur mise en ordre.
L'instruction <code class="literal">CPUID</code> semble provoquer
cet effet.
</p></div><div class="sect3" lang="fr"><div class="titlepage"><div><div><h4 class="title"><a name="N10422"></a>2.2.3.&nbsp;Volatilit&eacute;</h4></div></div></div><p>
Pour &eacute;viter que l'optimiseur du GCC ne conserve les valeurs de la
m&eacute;moire partag&eacute;e dans les registres de processeur, tous les objets
en m&eacute;moire partag&eacute;e doivent &ecirc;tre d&eacute;clar&eacute;s avec l'attribut
<code class="literal">volatile</code>. Tous les acc&egrave;s en lecture ou &eacute;criture
ne n&eacute;cessitant l'acc&egrave;s qu'&agrave; un seul mot se feront alors de mani&egrave;re
atomique. Par exemple, en supposant que <span class="emphasis"><em>p</em></span> est un pointeur
sur un entier, et que ce pointeur comme l'entier qu'il pointe se trouvent en
m&eacute;moire partag&eacute;e, la d&eacute;claration en C ANSI ressemblera &agrave;&nbsp;:
</p><p>

<pre class="programlisting">
volatile int * volatile p;
</pre>

</p><p>

Dans ce code, le premier <code class="literal">volatile</code> concerne 
l'<code class="literal">int</code> que <code class="literal">p</code> pointe 
&eacute;ventuellement, quand le second <code class="literal">volatile</code> s'applique 
au pointeur lui-m&ecirc;me. Oui, c'est ennuyeux, mais c'est le prix &agrave; payer 
pour que GCC puisse faire des optimisations vraiment puissantes. En 
th&eacute;orie, l'option <code class="literal">-traditional</code> devrait suffire &agrave; 
produire du code correct au prix de quelques optimisations, car le 
standard C K&amp;R (N.D.T.&nbsp;: Kernigan &amp; Ritchie) pr&eacute; norme ANSI 
&eacute;tablit que toutes les variables sont volatiles si elles ne sont pas 
explicitement d&eacute;clar&eacute;es comme <code class="literal">register</code>. Ceci &eacute;tant 
dit, si vos compilations GCC ressemblent &agrave; <code class="literal">cc -O6 
&hellip;</code>, vous n'aurez r&eacute;ellement besoin de d&eacute;clarer les 
choses comme &eacute;tant volatiles qu'aux endroits o&ugrave; c'est n&eacute;cessaire.

</p><p>
Un rumeur a circul&eacute; &agrave; propos du fait que les verrous &eacute;crits en assembleur
signal&eacute;s comme modifiant tous les registres du processeur provoquaient de
la part du compilateur GCC l'enregistrement ad&eacute;quat de toutes les variables en
suspens, &eacute;vitant ainsi le code compil&eacute; &laquo;&nbsp;<span class="quote">inutile</span>&nbsp;&raquo; associ&eacute; aux objets d&eacute;clar&eacute;s
<code class="literal">volatile</code>. Cette astuce semble fonctionner pour
les variables globales statiques avec la version 2.7.0 de GCC&hellip; En revanche,
ce comportement n'est <span class="emphasis"><em>pas</em></span> une recommandation du standard
C ANSI. Pire encore, d'autres processus n'effectuant que des acc&egrave;s en lecture
pourraient conserver &eacute;ternellement les valeurs dans des registres, et ainsi
ne <span class="emphasis"><em>jamais</em></span> s'apercevoir que la vraie valeur stock&eacute;e en
m&eacute;moire partag&eacute;e a en fait chang&eacute;. En r&eacute;sum&eacute;, d&eacute;veloppez comme vous l'entendez,
mais seules les variables d&eacute;clar&eacute;es <code class="literal">volatile</code>
offrent un fonctionnement normal <span class="emphasis"><em>garanti</em></span>.
</p><p>
Notez qu'il est possible de provoquer un acc&egrave;s volatile &agrave; une variable
ordinaire en utilisant un transtypage (&laquo;&nbsp;<span class="quote"><span class="emphasis"><em>casting</em></span></span>&nbsp;&raquo;) imposant l'attribut
<code class="literal">volatile</code>. Par exemple, un
<code class="literal">int i;</code> ordinaire peut &ecirc;tre r&eacute;f&eacute;renc&eacute; en tant
que volatile par <code class="literal">*((volatile int *) &amp;i);</code> .
Ainsi, vous pouvez forcer la volatilit&eacute; et les co&ucirc;ts suppl&eacute;mentaires qu'elle engendre
seulement aux endroits o&ugrave; elle est critique.
</p></div><div class="sect3" lang="fr"><div class="titlepage"><div><div><h4 class="title"><a name="N1047A"></a>2.2.4.&nbsp;Verrous (<span class="foreignphrase"><em class="foreignphrase">Locks</em></span>)</h4></div></div></div><p>

Si vous pensiez que <code class="literal">++i;</code> aurait toujours incr&eacute;ment&eacute; 
une variable <code class="literal">i</code> sans probl&egrave;me, vous allez avoir une 
mauvaise surprise&nbsp;: m&ecirc;me cod&eacute;es en une seule instruction, le 
chargement et l'enregistrement du r&eacute;sultat sont deux transactions 
m&eacute;moire s&eacute;par&eacute;es, et d'autres processeurs peuvent acc&eacute;der &agrave; 
<code class="literal">i</code> entre ces deux transactions. Par exemple, deux 
processus effectuant chacun l'instruction <code class="literal">++i;</code> 
pourraient n'incr&eacute;menter la variable <code class="literal">i</code> que d'une 
unit&eacute; et non deux. Selon le &laquo;&nbsp;<span class="quote">Manuel de l'Architecture et de la 
Programmation</span>&nbsp;&raquo; du Pentium d'Intel, le pr&eacute;fixe 
<code class="literal">LOCK</code> peut &ecirc;tre employ&eacute; pour s'assurer que chacune des 
instructions suivantes soit atomique par rapport &agrave; l'adresse m&eacute;moire &agrave; 
laquelle elles acc&egrave;dent&nbsp;:

</p><p>

<pre class="programlisting">
BTS, BTR, BTC                     mem, reg/imm
XCHG                              reg, mem
XCHG                              mem, reg
ADD, OR, ADC, SBB, AND, SUB, XOR  mem, reg/imm
NOT, NEG, INC, DEC                mem
CMPXCHG, XADD
</pre>

</p><p>
En revanche, il n'est pas conseill&eacute; d'utiliser toutes ces op&eacute;rations.
Par exemple, <code class="literal">XADD</code> n'existait m&ecirc;me pas sur 386,
aussi l'employer en programmation peut poser des probl&egrave;mes de portabilit&eacute;.
</p><p>

L'instruction <code class="literal">XCHG</code> engendre <span class="emphasis"><em>toujours</em></span>
un verrou, m&ecirc;me sans le pr&eacute;fixe <code class="literal">LOCK</code>, et est ainsi
et indiscutablement l'op&eacute;ration atomique favorite pour construire d'autres op&eacute;rations
atomiques de plus haut niveau comme les s&eacute;maphores et les files d'attente partag&eacute;es.
Bien s&ucirc;r, on ne peut pas demander &agrave; GCC de g&eacute;n&eacute;rer cette instruction en &eacute;crivant
simplement du code C. Il vous faudra &agrave; la place &eacute;crire un peu de code assembleur en
ligne<sup>[<a href="#ftn.N104B6" name="N104B6">5</a>]</sup>. En prenant un objet volatile 
<span class="emphasis"><em>obj</em></span> et un registre du processeur 
<span class="emphasis"><em>reg</em></span>, tous deux de type <code class="literal">word</code> 
(longs de 16 bits), le code assembleur GCC sera&nbsp;:

</p><pre class="programlisting">
__asm__ __volatile__ ("xchgl %1,%0"
                      :"=r" (reg), "=m" (obj)
                      :"r" (reg), "m" (obj));
</pre><p>

Quelques exemples de programmes assembleur en ligne utilisant des 
op&eacute;rations bit-&agrave;-bit pour r&eacute;aliser des verrous sont disponibles dans le 
code source de la biblioth&egrave;que bb_threads.

</p><p>

Il est toutefois important de se souvenir que faire des transactions 
m&eacute;moire atomiques a un co&ucirc;t. Une op&eacute;ration de verrouillage engendre des 
d&eacute;lais suppl&eacute;mentaires assez importants et peut retarder l'activit&eacute; 
m&eacute;moire d'autres processeurs, quand des r&eacute;f&eacute;rences ordinaires auraient 
utilis&eacute; le cache local. Les meilleures performances s'obtiennent en 
utilisant les op&eacute;rations atomiques aussi <span class="emphasis"><em>peu</em></span> 
souvent que possible. De plus, ces instructions atomiques IA32 ne sont 
&eacute;videment pas portables vers d'autres syst&egrave;mes.

</p><p>

Il existe plusieurs alternatives permettant aux instructions ordinaires 
d'&ecirc;tre utilis&eacute;es pour mettre en &#339;uvre diff&eacute;rents types de 
synchronisation, y compris l'<span class="emphasis"><em>exclusion mutuelle</em></span>, 
qui garantit qu'au plus un seul processeur met &agrave; jour un objet partag&eacute; 
donn&eacute; &agrave; un moment pr&eacute;cis. La plupart des manuels des diff&eacute;rents syst&egrave;mes 
d'exploitation traitent d'au moins une de ces techniques. On trouve un 
tr&egrave;s bon expos&eacute; sur le sujet dans la quatri&egrave;me &eacute;dition des 
<span class="foreignphrase"><em class="foreignphrase">Operating System Concepts</em></span> (Principes des 
Syst&egrave;mes d'Exploitation), par Abraham Silberschatz et Peter B. Galvin, 
ISBN 0-201-50480-4.

</p></div><div class="sect3" lang="fr"><div class="titlepage"><div><div><h4 class="title"><a name="N104D6"></a>2.2.5.&nbsp;Taille de la ligne de cache</h4></div></div></div><p>

Encore une chose fondamentale concernant l'atomicit&eacute; et qui peut avoir 
des cons&eacute;quence dramatiques sur les performances d'un SMP&nbsp;: la 
taille de la ligne de cache. M&ecirc;me si le standard MPS impose que les 
r&eacute;f&eacute;rences soient coh&eacute;rentes quelque soit le cache utilis&eacute;, il n'en 
reste pas moins que lorsque qu'un processeur &eacute;crit sur une ligne 
particuli&egrave;re de la m&eacute;moire, chaque copie en cache de l'ancienne ligne 
doit &ecirc;tre invalid&eacute;e ou mise &agrave; jour. Ceci implique que si au moins deux 
processeurs &eacute;crivent chacun sur des portions diff&eacute;rentes de la ligne de 
cache, cela peut provoquer un trafic important sur le bus et le cache, 
pour au final transf&eacute;rer la ligne depuis le cache vers le cache. Ce 
probl&egrave;me est connu sous le nom de <span class="emphasis"><em>faux partage</em></span> 
(&laquo;&nbsp;<span class="quote"><span class="foreignphrase"><em class="foreignphrase">false sharing</em></span></span>&nbsp;&raquo;). La 
solution consiste uniquement &agrave; <span class="emphasis"><em>organiser les donn&eacute;es de telle 
mani&egrave;re que ce que les objets auxquels on acc&egrave;de en parall&egrave;le 
proviennent globalement de diff&eacute;rentes lignes de cache pour chaque 
processus</em></span>.

</p><p>

Vous pourriez penser que le faux partage n'est pas un probl&egrave;me quand on 
utilise un cache de niveau 2 partag&eacute;, mais souvenez-vous qu'il existe 
toujours des caches de niveau 1 s&eacute;par&eacute;s. L'organisation du cache et le 
nombre de niveaux s&eacute;par&eacute;s peut varier, mais la ligne de cache de premier 
niveau d'un Pentium est longue de 32 octets, et le cache externe typique 
tourne autour de 256 octets. Supposons que les adresses (physiques ou 
logiques) de deux objets soient <span class="emphasis"><em>a</em></span> et 
<span class="emphasis"><em>b</em></span>, et que la taille de la ligne de cache soit 
<span class="emphasis"><em>c</em></span>, que nous admettrons &ecirc;tre une puissance de 2. 
Pour &ecirc;tre tr&egrave;s pr&eacute;cis, si

<code class="literal">((int) a) &amp; &#732;(c-1)</code> est &eacute;gal &agrave; 
<code class="literal">((int) b) &amp; &#732;(c-1)</code>,

alors les deux r&eacute;f&eacute;rences se trouvent dans la m&ecirc;me ligne de cache. Une 
r&egrave;gle plus simple consiste &agrave; dire que si deux objets r&eacute;f&eacute;renc&eacute;s en 
parall&egrave;le sont &eacute;loign&eacute;s d'au moins <span class="emphasis"><em>c</em></span> octets, ils 
devraient se trouver dans des lignes de cache diff&eacute;rentes.

</p></div><div class="sect3" lang="fr"><div class="titlepage"><div><div><h4 class="title"><a name="N104FB"></a>2.2.6.&nbsp;Les probl&egrave;mes de l'ordonnanceur de Linux</h4></div></div></div><p>

Bien que tout l'int&eacute;r&ecirc;t d'utiliser de la m&eacute;moire partag&eacute;e pour les 
traitements en parall&egrave;le consiste &agrave; &eacute;viter les d&eacute;lais dus au syst&egrave;me 
d'exploitation, ces d&eacute;lais peuvent parfois provenir d'autres choses que 
les communications en elles-m&ecirc;mes. Nous avons d&eacute;j&agrave; remarqu&eacute; que le 
nombre de processus que l'on devrait cr&eacute;er doit &ecirc;tre inf&eacute;rieur ou &eacute;gal 
au nombre de processeurs de la machine. Mais comment d&eacute;cide-t-on 
exactement du nombre de processus &agrave; cr&eacute;er&nbsp;?

</p><p>

Pour obtenir les meilleures performances, <span class="emphasis"><em>le nombre de 
processus de votre programme en parall&egrave;le doit &ecirc;tre &eacute;gal au nombre de 
processus qui peuvent &ecirc;tre ex&eacute;cut&eacute;s simultan&eacute;ment, chacun sur son 
processeur</em></span>. Par exemple, si un syst&egrave;me SMP &agrave; quatre 
processeurs h&eacute;berge un processus tr&egrave;s actif pour un autre usage (par 
exemple un serveur <span class="foreignphrase"><em class="foreignphrase">web</em></span>), alors votre 
programme en parall&egrave;le ne devra utiliser que trois processus. Vous 
pouvez vous faire une id&eacute;e g&eacute;n&eacute;rale du nombre de processus actifs 
ex&eacute;cut&eacute;s sur votre syst&egrave;me en consultant la &laquo;&nbsp;<span class="quote">charge 
syst&egrave;me moyenne</span>&nbsp;&raquo; (&laquo;&nbsp;<span class="quote"><span class="foreignphrase"><em class="foreignphrase">load 
average</em></span></span>&nbsp;&raquo;) mise en &eacute;vidence par la commande 
<code class="literal">uptime</code>.

</p><p>

Vous pouvez en outre &laquo;&nbsp;<span class="quote">pousser</span>&nbsp;&raquo; la priorit&eacute; de vos processus 
de votre programme parall&egrave;le en utilisant, par exemple, la commande 
<code class="literal">renice</code> ou l'appel syst&egrave;me <code class="literal">nice()</code>. 
Vous devez &ecirc;tre privil&eacute;gi&eacute;<sup>[<a href="#ftn.N10520" name="N10520">6</a>]</sup> pour augmenter la priorit&eacute; d'un processus. L'id&eacute;e 
consiste simplement &agrave; &eacute;jecter les autres programmes des autres 
processeurs pour que votre programme puisse &ecirc;tre ex&eacute;cut&eacute; sur tous les 
processeurs simultan&eacute;ment. Ceci peut &ecirc;tre effectu&eacute; de mani&egrave;re un peu 
plus explicite en utilisant la version prototype de Linux SMP disponible 
sur <a href="http://www.fsmlabs.com/products/openrtlinux/" target="_top">http://www.fsmlabs.com/products/openrtlinux/</a> et qui 
propose un ordonnanceur en temps r&eacute;el (N.D.T.&nbsp;: il existe d&eacute;sormais un 
guide consacr&eacute; &agrave; RTLinux, accessible en ligne&nbsp;: <a href="http://www.traduc.org/docs/howto/lecture/RTLinux-HOWTO.html" target="_top">RTLinux 
HOWTO</a>).

</p><p>

Si vous n'&ecirc;tes pas le seul utilisateur employant votre syst&egrave;me SMP comme 
une machine en parall&egrave;le, il se peut que vous entriez en conflit avec 
les autres programmes en parall&egrave;le essayant de s'ex&eacute;cuter simultan&eacute;ment. 
La solution standard est l'<span class="emphasis"><em>ordonnancement de groupe</em></span> 
(&laquo;&nbsp;<span class="quote"><span class="foreignphrase"><em class="foreignphrase">gang scheduling</em></span></span>&nbsp;&raquo;), 
c'est-&agrave;-dire la manipulation de la priorit&eacute; d'ordonnancement de fa&ccedil;on &agrave; 
ce que seuls les processus d'un seul programme en parall&egrave;le s'ex&eacute;cutent 
&agrave; un moment donn&eacute;. Il est bon de rappeler, en revanche, que multiplier 
les parall&eacute;lismes tend &agrave; r&eacute;duire les retours et que l'activit&eacute; de 
l'ordonnanceur introduit des d&eacute;lais suppl&eacute;mentaires. Ainsi, par exemple, 
il sera s&ucirc;rement pr&eacute;f&eacute;rable, pour une machine &agrave; quatre processeurs, 
d'ex&eacute;cuter deux programmes contenant chacun deux processus, plut&ocirc;t que 
d'ordonnancer en groupe deux programmes de quatre processus chacun.

</p><p>

Il y a encore une chose dont il faut tenir compte. Supposons que vous 
d&eacute;veloppiez un programme sur une machine tr&egrave;s sollicit&eacute;e le jour, mais 
disponible &agrave; cent pour cent pendant la nuit pour le traitement en 
parall&egrave;le. Il vous faudra &eacute;crire et tester votre code dans les 
conditions r&eacute;elles, donc avec tous ses processus lanc&eacute;s, m&ecirc;me en sachant 
que des tests de jour risquent d'&ecirc;tre lents. Ils seront en fait 
<span class="emphasis"><em>tr&egrave;s</em></span> lents si certains de vos processus sont en 
&eacute;tat d'<span class="emphasis"><em>attente active</em></span> (&laquo;&nbsp;<span class="quote"><span class="foreignphrase"><em class="foreignphrase">busy 
waiting</em></span></span>&nbsp;&raquo;)<sup>[<a href="#ftn.N10544" name="N10544">7</a>]</sup>, guettant le changement de certaines valeurs en 
m&eacute;moire partag&eacute;e, changement cens&eacute; &ecirc;tre provoqu&eacute; par d'autres processus 
qui ne sont pas ex&eacute;cut&eacute;s (sur d'autres processeurs) au m&ecirc;me moment. Ce 
m&ecirc;me probl&egrave;me appara&icirc;t lorsque l'on d&eacute;veloppe et que l'on teste un 
programme sur un syst&egrave;me monoprocesseur.

</p><p>

La solution consiste &agrave; int&eacute;grer des appels syst&egrave;me &agrave; votre code l&agrave; o&ugrave; il 
peut se mettre en boucle en attendant une action d'un autre processeur, 
pour que Linux puisse donner une chance de s'ex&eacute;cuter &agrave; un autre 
processus. J'utilise pour cela une macro en langage C, appelons-la 
<code class="literal">IDLE_ME</code> (N.D.T.&nbsp;: 
<code class="literal">MetsMoiEnAttente</code>)&nbsp;: pour faire un simple test, 
compilez votre programme par

&laquo;&nbsp;<span class="quote"><code class="literal">cc -DIDLE_ME=usleep(1);&hellip;</code></span>&nbsp;&raquo;.

Pour produire un ex&eacute;cutable d&eacute;finitif, utilisez

&laquo;&nbsp;<span class="quote"><code class="literal">cc -DIDLE_ME={}&hellip;</code></span>&nbsp;&raquo;.

L'appel <code class="literal">usleep(1)</code> r&eacute;clame une pause d'une 
microseconde, qui a pour effet de permettre &agrave; l'ordonnanceur de Linux de 
choisir un nouveau processus &agrave; ex&eacute;cuter sur ce processeur. Si le nombre 
de processus d&eacute;passe le nombre de processeurs disponibles, il n'est pas 
rare de voir des programmes s'ex&eacute;cuter dix fois plus rapidement avec 
<code class="literal">usleep(1)</code> que sans.

</p></div></div><div class="sect2" lang="fr"><div class="titlepage"><div><div><h3 class="title"><a name="N10564"></a>2.3.&nbsp;bb_threads</h3></div></div></div><p>

La biblioth&egrave;que bb_threads (<span class="foreignphrase"><em class="foreignphrase">"Bare Bones" 
threads</em></span>) est une biblioth&egrave;que remarquablement simple qui 
fait la d&eacute;monstration de l'utilisation de l'appel syst&egrave;me Linux 
<code class="literal">clone()</code>. Le fichier <code class="literal">tar.gz</code> 
n'occupe que 7&nbsp;ko&nbsp;! Bien que cette biblioth&egrave;que ait &eacute;t&eacute; rendue 
pour l'essentiel obsol&egrave;te par la biblioth&egrave;que LinuxThreads, trait&eacute;e dans 
la section 2.4, bb_threads reste utilisable, et est suffisamment simple 
et peu encombrante pour former une bonne introduction &agrave; la gestion des 
<span class="foreignphrase"><em class="foreignphrase">threads</em></span> sous Linux. Il est beaucoup moins 
effrayant de se lancer dans la lecture de ce code source que dans celui 
de LinuxThreads. En r&eacute;sum&eacute;, la biblioth&egrave;que bb_threads forme un bon 
point de d&eacute;part, mais n'est pas vraiment adapt&eacute;e &agrave; la r&eacute;alisation de 
grands projets.

</p><p>

La structure de base des programmes utilisant la biblioth&egrave;que bb_threads 
est la suivante&nbsp;:

</p><div class="orderedlist"><ol type="1"><li><p>

Lancez le programme en tant que processus unique.

</p></li><li><p>

Il vous faudra estimer l'espace maximum dans la pile qui sera n&eacute;cessaire 
&agrave; chaque <span class="foreignphrase"><em class="foreignphrase">thread</em></span>. Pr&eacute;voir large est 
relativement sage (c'est &agrave; &ccedil;&agrave; que sert la m&eacute;moire virtuelle ;-), mais 
souvenez-vous que <span class="emphasis"><em>toutes</em></span> les piles proviennent d'un 
seul espace d'adressage virtuel, aussi voir trop grand n'est pas une 
id&eacute;e formidable. La d&eacute;mo sugg&egrave;re 64Ko. Cette taille est fix&eacute;e &agrave; 
<span class="emphasis"><em>b</em></span> octets par 
<code class="literal">bb_threads_stacksize(b)</code>.

</p></li><li><p>

L'&eacute;tape suivante consiste &agrave; initialiser tous les verrous dont vous aurez 
besoin. Le m&eacute;canisme de verrouillage int&eacute;gr&eacute; &agrave; cette biblioth&egrave;que 
num&eacute;rote les verrous de 0 &agrave; <code class="literal">MAX_MUTEXES</code>, et 
initialise un verrou <span class="emphasis"><em>i</em></span> par 
<code class="literal">bb_threads_mutexcreate(i)</code>.

</p></li><li><p>

La cr&eacute;ation d'un nouveau <span class="foreignphrase"><em class="foreignphrase">thread</em></span> 
s'effectue en appelant une routine de la biblioth&egrave;que recevant en 
arguments la fonction que le nouveau 
<span class="foreignphrase"><em class="foreignphrase">thread</em></span> doit ex&eacute;cuter, et les arguments 
qui doivent lui &ecirc;tre transmis. Pour d&eacute;marrer un nouveau 
<span class="foreignphrase"><em class="foreignphrase">thread</em></span> ex&eacute;cutant la fonction 
<span class="emphasis"><em>f</em></span> de type <code class="literal">void</code> et attendant un 
argument <span class="emphasis"><em>arg</em></span>, l'appel ressemblera &agrave; 
<code class="literal">bb_threads_newthread (f, &amp;arg)</code>, o&ugrave; 
<span class="emphasis"><em>f</em></span> devra &ecirc;tre d&eacute;clar&eacute; comme suit&nbsp;:

</p><pre class="programlisting">
void f (void *arg, size_t dummy)
</pre><p>

Si vous avez besoin de passer plus d'un argument &agrave; votre fonction, 
utilisez un pointeur sur une structure contenant les valeurs &agrave; 
transmettre.

</p></li><li><p>

Lancement du code en parall&egrave;le, en prenant soin d'utiliser 
<code class="literal">bb_threads_lock(n)</code> et 
<code class="literal">bb_threads_unlock(n)</code> o&ugrave; <span class="emphasis"><em>n</em></span> est un 
entier indiquant le verrou &agrave; utiliser. Notez que les op&eacute;rations de 
verrouillage et d&eacute;verrouillage sont des op&eacute;rations de blocage<sup>[<a href="#ftn.N105CD" name="N105CD">8</a>]</sup> tr&egrave;s primaires et utilisant des instructions 
atomiques de verrouillage du bus, lesquelles peuvent causer des 
interf&eacute;rences d'acc&egrave;s &agrave; la m&eacute;moire, et qui n'essaient en aucun cas 
d'agir &laquo;&nbsp;<span class="quote">proprement</span>&nbsp;&raquo;.

Le programme de d&eacute;monstration fourni avec bb_threads n'utilisait pas 
correctement les verrous pour emp&ecirc;cher <code class="literal">printf()</code> 
d'&ecirc;tre ex&eacute;cut&eacute; depuis les fonctions <code class="literal">fnn</code> et 
<code class="literal">main</code>, et &agrave; cause de cela, la d&eacute;mo ne fonctionne pas 
toujours. Je ne dis pas cela pour d&eacute;molir la d&eacute;mo, mais plut&ocirc;t pour bien 
mettre en &eacute;vidence le fait que ce travail comporte <span class="emphasis"><em>beaucoup de 
pi&egrave;ges</em></span>. Ceci dit, utiliser LinuxThreads ne se r&eacute;v&egrave;le que 
l&eacute;g&egrave;rement plus facile.

</p></li><li><p>

Lorsqu'un <span class="foreignphrase"><em class="foreignphrase">thread</em></span> ex&eacute;cute 
<code class="literal">return</code>, il d&eacute;truit le processus&hellip; mais la pile 
locale n'est pas automatiquement d&eacute;sallou&eacute;e. Pour &ecirc;tre plus pr&eacute;cis, 
Linux ne g&egrave;re pas la d&eacute;sallocation, et l'espace m&eacute;moire n'est pas 
automatiquement rendu &agrave; la liste d'espace libre de 
<code class="literal">malloc()</code>. Aussi, le processus parent doit-il 
r&eacute;cup&eacute;rer l'espace m&eacute;moire de chaque processus fils mort par 
<code class="literal">bb_threads_cleanup(wait(NULL))</code>.

</p></li></ol></div><p>

Le programme suivant, &eacute;crit en langage C, utilise l'algorithme trait&eacute; 
dans la section 1.3 pour calculer la valeur de Pi en utilisant deux 
<span class="foreignphrase"><em class="foreignphrase">threads</em></span> bb_threads.

</p><pre class="programlisting">
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/wait.h&gt;
#include "bb_threads.h"

volatile double pi = 0.0;
volatile int intervalles;
volatile int pids[2];      /* Num&eacute;ros de processus Unix des threads */

void
do_pi(void *data, size_t len)
{
  register double largeur, sommelocale;
  register int i;
  register int iproc = (getpid() != pids[0]);

  /* Fixe la largeur des intervalles */
  largeur = 1.0 / intervalles;

  /* Effectue les calculs locaux */
  sommelocale = 0;
  for (i=iproc; i&lt;intervalles; i+=2) {
    register double x = (i + 0.5) * largeur;
    sommelocale += 4.0 / (1.0 + x * x);
  }
  sommelocale *= largeur;

  /* Obtention des permissions, mise &agrave; jour de Pi, et d&eacute;verrouillage */
  bb_threads_lock(0);
  pi += sommelocale;
  bb_threads_unlock(0);
}

int
main(int argc, char **argv)
{
  /* R&eacute;cup&egrave;re le nombre d'intervalles */
  intervalles = atoi(argv[1]);

  /* Fixe la taille de la pile, et cr&eacute;e le verrou */
  bb_threads_stacksize(65536);
  bb_threads_mutexcreate(0);

  /* cr&eacute;e deux threads ... */
  pids[0] = bb_threads_newthread(do_pi, NULL);
  pids[1] = bb_threads_newthread(do_pi, NULL);

  /* nettoie derri&egrave;re les deux threads */
  /* (forme ainsi une barri&egrave;re de synchro) */

  bb_threads_cleanup(wait(NULL));
  bb_threads_cleanup(wait(NULL));

  /* Affiche le r&eacute;sultat */
  printf("Estimation de la valeur de Pi: %f\n", pi);

  /* Sortie avec code de SUCCES */
  exit(0);
}
</pre></div><div class="sect2" lang="fr"><div class="titlepage"><div><div><h3 class="title"><a name="N10601"></a>2.4.&nbsp;LinuxThreads</h3></div></div></div><p>

LinuxThreads (<a href="http://pauillac.inria.fr/~xleroy/linuxthreads/" target="_top">http://pauillac.inria.fr/~xleroy/linuxthreads/</a>) est une 
impl&eacute;mentation assez compl&egrave;te et bien construite en accord avec le 
standard de <span class="foreignphrase"><em class="foreignphrase">threads</em></span> POSIX 1003.1c. 
Contrairement aux autres adaptations d'impl&eacute;mentations de 
<span class="foreignphrase"><em class="foreignphrase">threads</em></span> POSIX, LinuxThreads utilise 
&eacute;galement l'appel <code class="literal">clone()</code> du noyau Linux, d&eacute;j&agrave; 
employ&eacute; par bb_threads. La compatibilit&eacute; POSIX implique qu'il est 
relativement ais&eacute; de faire l'adaptation de certaines applications 
provenant d'autres syst&egrave;mes, et diff&eacute;rents tutoriels et leur support 
sont disponibles. Bref, c'est incontestablement la biblioth&egrave;que &agrave; 
utiliser pour d&eacute;velopper des applications 
<span class="foreignphrase"><em class="foreignphrase">multi-threads</em></span> &agrave; grande &eacute;chelle sous 
Linux.

</p><p>

La structure de base d'un programme utilisant LinuxThreads suit ce 
mod&egrave;le&nbsp;:

</p><div class="orderedlist"><ol type="1"><li><p>

Lancement du programme en tant que processus unique.

</p></li><li><p>

Initialisation de tous les verrous dont vous aurez besoin.
Contrairement aux verrous de bb_threads qui sont identifi&eacute;s par
des num&eacute;ros, les verrous POSIX sont d&eacute;clar&eacute;s comme des variables de
type <code class="literal">pthread_mutex_t lock</code>.
Utilisez <code class="literal">pthread_mutex_init(&amp;lock,val)</code>
pour initialiser chacun des verrous que vous utiliserez.

</p></li><li><p>

Comme avec bb_threads, la cr&eacute;ation d'un nouveau 
<span class="foreignphrase"><em class="foreignphrase">thread</em></span> se fait par l'appel d'une fonction 
de la biblioth&egrave;que admettant des arguments sp&eacute;cifiant &agrave; leur tour la 
fonction que le nouveau <span class="foreignphrase"><em class="foreignphrase">thread</em></span> doit 
ex&eacute;cuter et les arguments que celle-ci re&ccedil;oit. Cependant, POSIX impose &agrave; 
l'utilisateur la d&eacute;claration d'une variable de type 
<code class="literal">pthread_t</code> pour identifier chaque 
<span class="foreignphrase"><em class="foreignphrase">thread</em></span>. Pour cr&eacute;er un 
<span class="foreignphrase"><em class="foreignphrase">thread</em></span> <code class="literal">pthread_t 
thread</code> ex&eacute;cutant la fonction <code class="literal">f()</code>, on 
appelle <code class="literal">pthread_create(&amp;thread,NULL,f,&amp;arg)</code>.

</p></li><li><p>

Lancement de la partie parall&egrave;le du programme, en prenant soin d'utiliser

<code class="literal">pthread_mutex_lock(&amp;lock)</code> et

<code class="literal">pthread_mutex_unlock(&amp;lock)</code>

comme il se doit.

</p></li><li><p>

Utilisation de <code class="literal">pthread_join(thread,&amp;retval)</code>
apr&egrave;s chaque <span class="foreignphrase"><em class="foreignphrase">thread</em></span> pour tout nettoyer.

</p></li><li><p>

Utilisation de <code class="literal">-D_REENTRANT</code> &agrave; la compilation de votre 
programme en C.

</p></li></ol></div><p>

Voici l'exemple du calcul de Pi en parall&egrave;le, s'appuyant sur 
LinuxThreads. L'algorithme de la section 1.3 est utilis&eacute; et, comme pour 
l'exemple de bb_threads, deux <span class="foreignphrase"><em class="foreignphrase">threads</em></span> 
s'ex&eacute;cutent en parall&egrave;le.

</p><pre class="programlisting">
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include "pthread.h"

volatile double pi = 0.0;    /* Approximation de pi (partag&eacute;e) */
pthread_mutex_t pi_lock;     /* Verrou de la variable ci-dessous */
volatile double intervalles; /* Combien d'intervalles ? */

void *
process(void *arg)
{
  register double largeur, sommelocale;
  register int i;
  register int iproc = (*((char *) arg) - '0');

  /* Fixe la largeur */
  largeur = 1.0 / intervalles;

  /* Fais les calculs locaux */
  sommelocale = 0;
  for (i=iproc; i&lt;intervalles; i+=2) {
    register double x = (i + 0.5) * largeur;
    sommelocale += 4.0 / (1.0 + x * x);
  }
  sommelocale *= largeur;

  /* Verrouille la variable pi en vue d'une mise &agrave; jour,
     effectue la mise &agrave; jour, puis d&eacute;verrouille Pi. */

  pthread_mutex_lock(&amp;pi_lock);
  pi += sommelocale;
  pthread_mutex_unlock(&amp;pi_lock);

  return(NULL);
}

int
main(int argc, char **argv)
{
  pthread_t thread0, thread1;
  void * retval;

  /* R&eacute;cup&egrave;re le nombre d'intervalles */
  intervalles = atoi(argv[1]);

  /* Initialise un verrou sur pi */
  pthread_mutex_init(&amp;pi_lock, NULL);

  /* Cr&eacute;e les deux threads */
  if (pthread_create(&amp;thread0, NULL, process, "0") ||
      pthread_create(&amp;thread1, NULL, process, "1")) {
    fprintf(stderr, "%s: Cr&eacute;ation des threads impossible\n", argv[0]);
    exit(1);
  }

  /* &laquo; Joint &raquo; (d&eacute;truit) les deux threads */
  if (pthread_join(thread0, &amp;retval) ||
      pthread_join(thread1, &amp;retval)) {
    fprintf(stderr, "%s: Erreur &agrave; la fusion des threads\n", argv[0]);
    exit(1);
  }

  /* Affiche le r&eacute;sultat */
  printf("Estimation de la valeur de Pi: %f\n", pi);

  /* Sortie */
  exit(0);
}
</pre></div><div class="sect2" lang="fr"><div class="titlepage"><div><div><h3 class="title"><a name="N1066C"></a>2.5.&nbsp;La m&eacute;moire partag&eacute;e de System V</h3></div></div></div><p>

La gestion des IPC (<span class="foreignphrase"><em class="foreignphrase">Inter-Process 
Communication</em></span>) System V s'effectue au travers d'un 
certain nombre d'appels syst&egrave;me fournissant les m&eacute;canismes des files de 
message, des s&eacute;maphores et de la m&eacute;moire partag&eacute;e. Bien s&ucirc;r, ces 
m&eacute;canismes ont &eacute;t&eacute; initialement con&ccedil;us pour permettre &agrave; plusieurs 
processus de communiquer au sein d'un syst&egrave;me monoprocesseur. Cela 
signifie n&eacute;anmoins que ces m&eacute;canismes devraient aussi fonctionner dans 
un syst&egrave;me Linux SMP, quelque soit le nombre de processeurs.

</p><p>

Avant d'aller plus loin dans l'utilisation de ces appels, il est 
important de comprendre que m&ecirc;me s'il existe des appels IPC System V 
pour des choses comme les s&eacute;maphores et la transmission de messages, 
vous ne les utiliserez probablement pas. Pourquoi&nbsp;? Parce ces 
fonctions sont g&eacute;n&eacute;ralement lentes et s&eacute;rialis&eacute;es sous Linux SMP. 
Inutile de s'&eacute;tendre.

</p><p>

La marche &agrave; suivre standard pour cr&eacute;er un groupe de processus partageant 
l'acc&egrave;s &agrave; un segment de m&eacute;moire partag&eacute;e est la suivante.

</p><div class="orderedlist"><ol type="1"><li><p>

Lancement du programme en tant que processus unique.

</p></li><li><p>

En temps normal, chaque instance de votre programme en parall&egrave;le devra 
avoir son propre segment de m&eacute;moire partag&eacute;e, aussi vous faudra-t-il 
appeler <code class="literal">shmget()</code> pour cr&eacute;er un nouveau segment de la 
taille souhait&eacute;e. Mais d'autre part, cet appel peut &ecirc;tre utilis&eacute; pour 
r&eacute;cup&eacute;rer l'identifiant d'un segment de m&eacute;moire partag&eacute;e d&eacute;j&agrave; existant. 
Dans les deux cas, la valeur de retour est soit l'identifiant du segment 
de m&eacute;moire partag&eacute;e, soit -1 en cas d'erreur. Par exemple, pour cr&eacute;er un 
segment de m&eacute;moire partag&eacute;e long de <span class="emphasis"><em>b</em></span> octets, on 
passe un appel ressemblant &agrave;

<code class="literal">shmid = shmget(IPC_PRIVATE, b, (IPC_CREAT | 0666))</code>.

</p></li><li><p>

L'&eacute;tape suivante consiste &agrave; attacher ce segment de m&eacute;moire partag&eacute;e au 
processus, c'est-&agrave;-dire l'ajouter &agrave; son plan m&eacute;moire. M&ecirc;me si l'appel 
<code class="literal">shmat()</code> permet au programmeur de sp&eacute;cifier l'adresse 
virtuelle &agrave; laquelle le segment doit appara&icirc;tre, cette adresse doit &ecirc;tre 
align&eacute;e sur une page (plus pr&eacute;cis&eacute;ment &ecirc;tre un multiple de la taille 
d'une page renvoy&eacute;e par <code class="literal">getpagesize()</code>, correspondant 
&agrave; 4096 octets), et recouvrera (prendra le pas sur) tout segment de 
m&eacute;moire s'y trouvant d&eacute;j&agrave;. Ainsi est-il plus sage de laisser le syst&egrave;me 
choisir une adresse. Dans les deux cas, la valeur de retour est un 
pointeur sur l'adresse virtuelle de base du segment fra&icirc;chement install&eacute; 
dans le plan m&eacute;moire. L'instruction correspondante est la 
suivante&nbsp;:

<code class="literal">shmptr = shmat(shmid, 0, 0)</code>.

Remarquez que vous pouvez allouer toutes vos variables statiques dans ce 
segment de m&eacute;moire partag&eacute;e en d&eacute;clarant simplement vos variables 
partag&eacute;es comme &eacute;tant les membres d'une structure de type 
<code class="literal">struct</code>, et en d&eacute;clarant <span class="emphasis"><em>shmptr</em></span> 
comme &eacute;tant un pointeur vers ce type de donn&eacute;es. Avec cette technique, 
une variable partag&eacute;e <span class="emphasis"><em>x</em></span> serait accessible par 
<span class="emphasis"><em>shmptr</em></span><code class="literal">-&gt;</code><span class="emphasis"><em>x</em></span>.

</p></li><li><p>

Comme ce segment de m&eacute;moire partag&eacute;e doit &ecirc;tre d&eacute;truit quand le
dernier processus &agrave; y acc&eacute;der prend fin ou s'en d&eacute;tache, il nous
faut appeler <code class="literal">shmctl()</code> pour configurer
cette action par d&eacute;faut. Le code correspondant ressemble &agrave;
<code class="literal">shmctl(shmid, IPC_RMID, 0)</code>.

</p></li><li><p>

Utiliser l'appel Linux <code class="literal">fork()</code><sup>[<a href="#ftn.N106BE" name="N106BE">9</a>]</sup> pour cr&eacute;er le nombre d&eacute;sir&eacute; de processus. 
Chacun d'eux h&eacute;ritera du segment de m&eacute;moire partag&eacute;e.

</p></li><li><p>

Lorsqu'un processus a fini d'utiliser un segment de m&eacute;moire
partag&eacute;e, il doit s'en d&eacute;tacher. On accomplit cela par un
<code class="literal">shmdt(shmptr)</code>.

</p></li></ol></div><p>

M&ecirc;me avec si peu d'appels syst&egrave;me, une fois le segment de m&eacute;moire 
partag&eacute;e &eacute;tabli, tout changement effectu&eacute; par un processeur sur une 
valeur se trouvant dans cet espace sera automatiquement visible par les 
autres processus. Plus important, chaque op&eacute;ration de communication sera 
exon&eacute;r&eacute;e du co&ucirc;t d'un appel syst&egrave;me.

</p><p>

Ci-apr&egrave;s, un exemple de programme en langage C utilisant les segments de 
m&eacute;moire partag&eacute;e System V. Il calcule Pi, en utilisant les algorithmes 
de la section 1.3.

</p><pre class="programlisting">
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/stat.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;sys/ipc.h&gt;
#include &lt;sys/shm.h&gt;

volatile struct shared { double pi; int lock; } * partage;

inline extern int xchg(register int reg,
volatile int * volatile obj)
{
  /* Instruction atomique d'&eacute;change */
__asm__ __volatile__ ("xchgl %1,%0"
                      :"=r" (reg), "=m" (*obj)
                      :"r" (reg), "m" (*obj));
  return(reg);
}

main(int argc, char **argv)
{
  register double largeur, sommelocale;
  register int intervalles, i;
  register int shmid;
  register int iproc = 0;;

  /* Alloue de la m&eacute;moire partag&eacute;e */
  shmid = shmget(IPC_PRIVATE,
                 sizeof(struct shared),
                 (IPC_CREAT | 0600));
  partage = ((volatile struct shared *) shmat(shmid, 0, 0));
  shmctl(shmid, IPC_RMID, 0);

  /* Fais les inits ... */
  partage-&gt;pi = 0.0;
  partage-&gt;lock = 0;

  /* Cr&eacute;e un fils */
  if (!fork()) ++iproc;

  /* R&eacute;cup&egrave;re le nombre d'intervalles */
  intervalles = atoi(argv[1]);
  largeur = 1.0 / intervalles;

  /* Fais les calculs locaux */
  sommelocale = 0;
  for (i=iproc; i&lt;intervalles; i+=2) {
    register double x = (i + 0.5) * largeur;
    sommelocale += 4.0 / (1.0 + x * x);
  }
  sommelocale *= largeur;

  /* Verrou d'attente atomique, ajout, et d&eacute;verrouillage ... */
  while (xchg((iproc + 1), &amp;(shared-&gt;lock))) ;
  shared-&gt;pi += sommelocale;
  shared-&gt;lock = 0;

  /* Fin du processus fils (barri&egrave;re de synchro) */
  if (iproc == 0) {
    wait(NULL);
    printf("Estimation de pi: %f\n", partage-&gt;pi);
  }

  /* Sortie en bonne et due forme */
  return(0);
}
</pre><p>
Dans cet exemple, j'ai utilis&eacute; l'instruction atomique d'&eacute;change
pour mettre le verrouillage en &#339;uvre. Pour de meilleures performances,
pr&eacute;f&eacute;rez-lui une technique de synchronisation &eacute;vitant les intructions
verrouillant le bus.
</p><p>
Pendant les phases de d&eacute;bogage, il est utile de se souvenir que la
commande <code class="literal">ipcs</code> renvoie la liste des
facilit&eacute;s des IPC System V en cours d'utilisation.
</p></div><div class="sect2" lang="fr"><div class="titlepage"><div><div><h3 class="title"><a name="N106D8"></a>2.6.&nbsp;Projection m&eacute;moire (<span class="foreignphrase"><em class="foreignphrase">Memory Map Call</em></span>)</h3></div></div></div><p>

L'utilisation des appels syst&egrave;me pour acc&eacute;der aux fichiers (les 
entr&eacute;es/sorties) peut revenir cher. En fait, c'est la raison pour 
laquelle il existe une biblioth&egrave;que de gestion des entr&eacute;es/sorties 
g&eacute;rant un tampon dans l'espace utilisateur 
(<code class="literal">getchar()</code>, <code class="literal">fwrite()</code>, et c&aelig;tera). 
Mais les tampons utilisateur ne remplissent pas leur fonction si 
plusieurs processus acc&egrave;dent au m&ecirc;me fichier ouvert en &eacute;criture. La 
solution Unix BSD &agrave; ce probl&egrave;me fut l'ajout d'un appel syst&egrave;me 
permettant &agrave; une portion d'un fichier d'&ecirc;tre projet&eacute;e en m&eacute;moire 
utilisateur, en utilisant principalement les m&eacute;canismes de la m&eacute;moire 
virtuelle pour provoquer les mises &agrave; jour. Le m&ecirc;me m&eacute;canisme a &eacute;t&eacute; 
utilis&eacute; pendant plusieurs ann&eacute;es dans les syst&egrave;mes de Sequent comme base 
de leur gestion du traitement parall&egrave;le en m&eacute;moire partag&eacute;e. En d&eacute;pit de 
commentaires tr&egrave;s n&eacute;gatifs dans la page de manuel (assez ancienne), 
Linux semble correctement effectuer au moins quelques unes des fonctions 
de base, et sait prendre en charge l'usage d&eacute;riv&eacute; de cet appel pour 
projeter un segment anonyme de m&eacute;moire pouvant &ecirc;tre partag&eacute; par 
plusieurs processus.

</p><p>

L'impl&eacute;mentation Linux de l'appel <code class="literal">mmap()</code> est en 
elle-m&ecirc;me une solution int&eacute;gr&eacute;e de remplacement des &eacute;tapes 2, 3 et 4 du 
sch&eacute;ma classique de m&eacute;moire partag&eacute;e System V, mis en &eacute;vidence dans la 
section 2.5. Pour cr&eacute;er un segment de m&eacute;moire partag&eacute;e anonyme&nbsp;:

</p><pre class="programlisting">
shmptr =
    mmap(0,                        /* Le syst&egrave;me choisit l'adresse */
         b,                        /* Taille du segment de m&eacute;moire partag&eacute;e */
         (PROT_READ | PROT_WRITE), /* droits d'acc&egrave;s, peuvent &ecirc;tre rwx */
         (MAP_ANON | MAP_SHARED),  /* anonyme, partag&eacute; */
         0,                        /* descripteur de fichier (inutilis&eacute;) */
         0);                       /* offset fichier (inutilis&eacute;) */
</pre><p>

L'&eacute;quivalent de l'appel de m&eacute;moire partag&eacute;e System V 
<code class="literal">shmdt()</code> est <code class="literal">munmap()</code>&nbsp;:

</p><pre class="programlisting">
munmap(shmptr, b);
</pre><p>

&Agrave; mon avis, on ne gagne pas grand chose &agrave; utiliser 
<code class="literal">mmap()</code> plut&ocirc;t que les m&eacute;canismes de gestion de la 
m&eacute;moire partag&eacute;e de System V.

</p></div><div class="footnotes"><br><hr align="left" width="100"><div class="footnote"><p><sup>[<a href="#N102D7" name="ftn.N102D7">2</a>] </sup>

N.D.T.&nbsp;: d&eacute;composition d'un programme en plusieurs processus 
distincts, mais travaillant simultan&eacute;ment et de concert.

</p></div><div class="footnote"><p><sup>[<a href="#N102EE" name="ftn.N102EE">3</a>] </sup>

N.D.T.&nbsp;: qui a d&eacute;marr&eacute; la machine avant de passer en mode SMP.

</p></div><div class="footnote"><p><sup>[<a href="#N1030A" name="ftn.N1030A">4</a>] </sup>
N.D.T.&nbsp;: le lien vers l'ancienne version 1.1, lui, n'existe plus. La 
documentation la plus r&eacute;cente se trouve &agrave; ce jour sur <a href="http://www.intel.com/design/Pentium4/documentation.htm" target="_top">http://www.intel.com/design/Pentium4/documentation.htm</a>.
</p></div><div class="footnote"><p><sup>[<a href="#N104B6" name="ftn.N104B6">5</a>] </sup>

N.D.T.&nbsp;: ins&eacute;r&eacute; au sein du code source, ici en C.

</p></div><div class="footnote"><p><sup>[<a href="#N10520" name="ftn.N10520">6</a>] </sup>
 
N.D.T.&nbsp;: soit, sous Unix, &ecirc;tre sous le compte <code class="literal">root</code>.

</p></div><div class="footnote"><p><sup>[<a href="#N10544" name="ftn.N10544">7</a>] </sup>

N.D.T.&nbsp;: temporisations introduites au sein d'un programme en 
utilisant par exemple des boucles et en consommant ainsi tout le temps 
machine allou&eacute; au processus plut&ocirc;t qu'en rendant la main au syst&egrave;me.

</p></div><div class="footnote"><p><sup>[<a href="#N105CD" name="ftn.N105CD">8</a>] </sup>

N.D.T.&nbsp;: &laquo;&nbsp;<span class="quote"><span class="foreignphrase"><em class="foreignphrase">spin 
locks</em></span></span>&nbsp;&raquo;&nbsp;: mise en &eacute;tat d'attente jusqu'&agrave; ce 
qu'une condition soit remplie.

</p></div><div class="footnote"><p><sup>[<a href="#N106BE" name="ftn.N106BE">9</a>] </sup>

N.D.T.&nbsp;: Il s'agit en fait d'un appel Unix standard.

</p></div></div></div><div class="navfooter"><hr><table summary="Navigation footer" width="100%"><tr><td align="left" width="40%"><a accesskey="p" href="ar01s01.html">Pr&eacute;c&eacute;dent</a>&nbsp;</td><td align="center" width="20%">&nbsp;</td><td align="right" width="40%">&nbsp;<a accesskey="n" href="ar01s03.html">Suivant</a></td></tr><tr><td valign="top" align="left" width="40%">1.&nbsp;Introduction&nbsp;</td><td align="center" width="20%"><a accesskey="h" href="index.html">Sommaire</a></td><td valign="top" align="right" width="40%">&nbsp;3.&nbsp;<span class="foreignphrase"><em class="foreignphrase">Clusters</em></span> de syst&egrave;mes Linux</td></tr></table></div></body></html>