diff --git a/Theorie/C/S2-src/char-addr.c b/Theorie/C/S2-src/char-addr.c index cf456f08b19285cc5a203ac42aecbf4335f3e4a2..667a1f2b944c03623ea68a40679086cc04b3e2f2 100644 --- a/Theorie/C/S2-src/char-addr.c +++ b/Theorie/C/S2-src/char-addr.c @@ -9,9 +9,6 @@ #include <stdlib.h> - - - ///BBB int main(int argc, char *argv[]) { diff --git a/Theorie/C/S2-src/foobar.c b/Theorie/C/S2-src/foobar.c index 48eb7413ec5dd6e7c668d01b539c8712e4de5359..c54fe40d0e88999c739aff1f8cf84b72dbcfd54f 100644 --- a/Theorie/C/S2-src/foobar.c +++ b/Theorie/C/S2-src/foobar.c @@ -9,13 +9,15 @@ #include <stdlib.h> void foo(void) { int a = 42; + printf("%d\n", a); } void bar(void) { +// cppcheck-suppress uninitvar ; int a; printf("%d\n", a); } - +// cppcheck-suppress uninitvar // int main(int argc, char *argv[]) { foo(); bar(); diff --git a/Theorie/C/S2-src/strlen.c b/Theorie/C/S2-src/strlen.c index 25494881469955ca13cbf4374a9ddf80cd4b638c..a623e5b6ddaa0e85a280872e674c821bcab382bc 100644 --- a/Theorie/C/S2-src/strlen.c +++ b/Theorie/C/S2-src/strlen.c @@ -14,8 +14,9 @@ int length(char str[]) { int i = 0; - while (str[i] != 0) // '\0' et 0 sont égaux + while (str[i] != 0) {// '\0' et 0 sont égaux i++; + } return i; } @@ -29,7 +30,7 @@ int main(int argc, char *argv[]) // char name2[] = "SINF1252"; printf("Longueur de name1 [%s] : %d\n", name1, length(name1)); - for (i = 0; i < 10; i++) { + for (i = 0; i < length(str); i++) { printf("%c", name1[i]); } diff --git a/Theorie/C/datatypes.rst b/Theorie/C/datatypes.rst index a0b68f8338a548e15891aa3dbf7a3231df7406c5..ceb4e4d0f3a72fc116a79c862455cd40c9605bc6 100644 --- a/Theorie/C/datatypes.rst +++ b/Theorie/C/datatypes.rst @@ -1,12 +1,12 @@ .. -*- coding: utf-8 -*- -.. Copyright |copy| 2012 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal +.. Copyright |copy| 2012, 2019 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal .. Ce fichier est distribué sous une licence `creative commons <http://creativecommons.org/licenses/by-sa/3.0/>`_ Types de données ================ -Durant la première semaine, nous avons abordé quelques types de +Dans les sections précédentes, nous avons abordé quelques types de données de base dont les ``int`` et les ``char``. Pour utiliser ces types de données à bon escient, il est important de comprendre en détail la façon dont ils sont supportés par le compilateur et leurs limitations. Celles-ci dépendent souvent de leur représentation en mémoire et durant cette semaine nous allons commencer à analyser de façon plus détaillée comment la mémoire d'un ordinateur est structurée. @@ -29,8 +29,8 @@ nombre entier :math:`\sum_{i=0}^{n-1} b_i \times 2^i`. Par convention, le bit est appelé le :term:`bit de poids faible`. Les suites de bits sont communément écrites dans l'ordre descendant des indices :math:`b_{n-1} ... b_i ... b_0`. À titre d'exemple, la suite de bits ``0101`` correspond à l'entier non signé représentant -la valeur cinq. Le bit de poids fort (resp. faible) de cette séquence de quatre bits -(ou :term:`nibble`) est ``0`` (resp. ``1``). La table ci-dessous reprend les +la valeur cinq. Le bit de poids fort de cette séquence de quatre bits +(ou :term:`nibble`) est ``0``. Le bit de poids faible est ``1``. La table ci-dessous reprend les différentes valeurs décimales correspondant à toutes les séquences de quatre bits consécutifs. ======= ===== =========== ======= @@ -54,7 +54,6 @@ binaire octal hexadécimal décimal 1111 17 F 15 ======= ===== =========== ======= -.. todo cafe, deadbeef adresses ipv6 http://www.qa.com/about-qa/blogs/2011/november/ipv6-the-return-of-badbeef-and-5adcafe/ Écrire une séquence de bits sous la forme d'une suite de ``0`` et de ``1`` peut s'avérer fastidieux. La représentation décimale @@ -150,7 +149,7 @@ pourrait se convertir via la formule :math:`(-1)^{b_{n-1}} \times \sum_{i=0}^{n-2} b_i \times 2^i`. En pratique, cette notation est rarement utilisée pour les nombres -entiers car elle rend l'implémentation des circuits électroniques de +entiers car elle rend la réalisation des circuits électroniques de calcul plus compliquée. Un autre inconvénient de cette notation est qu'elle utilise deux séquences de bits différentes pour représenter la valeur zéro (``00...0`` et ``10...0``). La représentation la plus courante pour les @@ -323,7 +322,7 @@ En langage C, les tableaux permettent d'agréger des données d'un même type. I :start-after: ///AAA :end-before: ///BBB -Les premières versions du langage C ne permettaient que la définition de tableaux dont la taille est connue à la compilation. Cette restriction était nécessaire pour permettre au compilateur de réserver la zone mémoire pour stocker le tableau. Face à cette limitation, de nombreux programmeurs définissaient la taille du tableau via une directive ``#define`` du pré-processeur comme dans l'exemple ci-dessus. Cette directive permet d'associer une chaîne de caractères quelconque à un symbole. Dans l'exemple ci-dessus, la chaîne ``10`` est associée au symbole ``N``. Lors de chaque compilation, le préprocesseur remplace toutes les occurences de ``N`` par ``10``. Cela permet au compilateur de ne traiter que des tableaux de taille fixe. +Les premières versions du langage C ne permettaient que la définition de tableaux dont la taille est connue à la compilation. Cette restriction était nécessaire pour permettre au compilateur de réserver la zone mémoire pour stocker le tableau. Face à cette limitation, de nombreux programmeurs définissaient la taille du tableau via une directive ``#define`` du pré-processeur comme dans l'exemple ci-dessus. Cette directive permet d'associer une chaîne de caractères quelconque à un symbole. Dans l'exemple ci-dessus, la chaîne ``10`` est associée au symbole ``N``. Lors de chaque compilation, le préprocesseur remplace toutes les occurrences de ``N`` par ``10``. Cela permet au compilateur de ne traiter que des tableaux de taille fixe. Un tableau à une dimension peut s'utiliser avec une syntaxe similaire à celle utilisée par Java. Dans un tableau contenant ``n`` éléments, le premier se trouve à l'indice ``0`` et le dernier à l'indice ``n-1``. L'exemple ci-dessous présente le calcul de la somme des éléments d'un vecteur. @@ -362,9 +361,7 @@ bits et des caractères. :rfc:`20` contient la table des caractères ASCII représentés sur 7 bits. À titre d'exemple, le chiffre `0` correspond à l'octet `0b00110000` et le chiffre `9` à l'octet `0b00111001`. La lettre `a` correspond à l'octet `0b01100001` et la -lettre `A` à l'octet `0b01000001`. - -.. todo:: inclure table ASCII +lettre `A` à l'octet `0b01000001`. De nombreux détails sur la table ASCII sont disponibles sur la page Wikipedia: https://en.wikipedia.org/wiki/ASCII Les inventeurs du C se sont appuyés sur la table ASCII et ont choisi de représenter un caractère en utilisant un octet. Cela correspond au @@ -383,7 +380,6 @@ caractère représentant la majuscule correspondante peut s'écrire : :start-after: ///AAA :end-before: ///BBB -.. todo:: ref unicode En pratique, l'utilisation de la table ASCII pour représenter des caractères souffre d'une limitation majeure. Avec `7` ou `8` bits il @@ -391,9 +387,9 @@ n'est pas possible de représenter exactement tous les caractères écrits de toutes les langues. Une table des caractères sur `7` bits est suffisante pour les langues qui utilisent peu de caractères accentués comme l'anglais. Pour le français et de nombreuses langues -d'Europe occidentale, la table sur `8` bits est suffisante et la norme +en Europe occidentale, la table sur `8` bits est suffisante et la norme ISO-8859_ contient des tables de caractères `8` bits pour de -nombreuses langues. La norme Unicode va plus loin en permettant de +nombreuses langues. La norme :term:`Unicode` va plus loin en permettant de représenter les caractères écrits de toutes les langues connues sur Terre. Une description détaillée du support de ces types de caractères sort du cadre de ce cours sur les systèmes @@ -401,25 +397,24 @@ informatiques. Il est cependant important que vous soyez conscient de cette problématique pour pouvoir la prendre en compte lorsque vous développerez des applications qui doivent traiter du texte dans différentes langues. + À titre d'exemple, la fonction `toupper(3)`_ qui est implémentée dans -les versions actuelles de Linux est nettement plus complexe que celle +les versions récentes de Linux est nettement plus complexe que celle que nous avons vue ci-dessus. Tout d'abord, la fonction `toupper(3)`_ prend comme argument un ``int`` et non un ``char``. Cela lui permet d'accepter des caractères dans n'importe quel encodage. Ensuite, le traitement qu'elle effectue dépend du type d'encodage qui a été défini via `setlocale(3)`_ (voir `locale(7)`_). -.. todo:: fournir un exemple plus tard - .. see http://en.wikipedia.org/wiki/List_of_binary_codes -Dans la suite du cours, nous supposerons qu'un caractère +Dans la suite de ce document, nous supposerons qu'un caractère est toujours représentable en utilisant le type ``char`` permettant de stocker un octet. En C, les chaînes de caractères sont représentées sous la forme d'un -tableau de caractères. Une chaîne de caractères peut s'initialiser de +tableau de caractères. Une chaîne de caractères peut être initialisée de différentes façons reprises ci-dessous. .. code-block:: c @@ -429,8 +424,6 @@ différentes façons reprises ci-dessous. char name3[] = "Unix"; -.. todo:: gcc ne semble pas poser de problème pour char wrong[4] = -.. { 'U', 'n', 'i', 'x' }; mais certains compilateurs pourraient Lorsque la taille de la chaîne de caractères n'est pas indiquée à l'initialisation (c'est-à -dire dans les deux dernières lignes @@ -490,7 +483,7 @@ stockés en mémoire et le :term:`garbage collector` retire de la mémoire les objets qui ne sont plus utilisés. En C, un programme peut aussi réserver des zones pour stocker de l'information en mémoire. Cependant, comme nous le verrons plus tard, c'est le -programmeur qui doit explicitement allouer et désallouer la mémoire. +programmeur qui doit explicitement allouer et libérer la mémoire. Les `pointeurs` sont une des caractéristiques principales du langage C par rapport à de nombreux autres langages. Un :term:`pointeur` est @@ -501,7 +494,7 @@ un ordinateur. D'un point de vue abstrait, la mémoire d'un ordinateur peut être vue sous la forme d'une zone de stockage dans laquelle il est possible de lire ou d'écrire de l'information. Chaque zone permettant de stocker de l'information est identifiée par une -:term:`adresse`. La mémoire peut être vue comme l'implémentation de +:term:`adresse`. La mémoire peut être vue comme une implémentation de deux fonctions C : - ``data read(addr)`` est une fonction qui, sur base @@ -521,7 +514,7 @@ figure et elles croîtront vers le haut. Considérons l'initialisation ci-dessous et supposons qu'elle est stockée dans une mémoire où les adresses sont encodées sur `3` -bits. Une telle mémoire dispose de huit slots permettant chacun de +bits. Une telle mémoire dispose de huit zones permettant chacune de stocker un octet. .. code-block:: c @@ -681,7 +674,7 @@ L'exécution de ce fragment de code produit une sortie qu'il est intéressant d' 2 est à l'adresse 0x7fff5fbff752 1 est à l'adresse 0x7fff5fbff753 -Tout d'abord, l'initialisation du pointeur ``ptr_char`` a bien stocké dans ce pointeur l'adresse en mémoire du premier élément du tableau. Ensuite, comme ``ptr_char`` est un pointeur de type ``unsigned char *``, l'expression ``*ptr_char`` a retourné la valeur de l'octet se trouvant à l'adresse ``0x7fff5fbff750``. L'incrémentation du pointeur ``ptr_char`` s'est faite en respectant l'arithmétique des pointeurs. Comme ``sizeof(unsigned char)`` retourne ``1``, la valeur stockée dans ``ptr_char`` a été incrémentée d'une seule unité par l'instruction ``ptr_char++``. En analysant les quatre ``unsigned char`` se trouvant aux adresses ``0x7fff5fbff750`` à ``0x7fff5fbff753``, on retrouve bien l'entier ``0x01020304`` qui avait été placé dans ``tab[0]``. +Tout d'abord, l'initialisation du pointeur ``ptr_char`` a bien stocké dans ce pointeur l'adresse en mémoire du premier élément du tableau. Ensuite, comme ``ptr_char`` est un pointeur de type ``unsigned char *``, l'expression ``*ptr_char`` a retourné la valeur de l'octet se trouvant à l'adresse ``0x7fff5fbff750``. Le pointeur ``ptr_char`` a été incrémenté en respectant l'arithmétique des pointeurs. Comme ``sizeof(unsigned char)`` retourne ``1``, la valeur stockée dans ``ptr_char`` a été incrémentée d'une seule unité par l'instruction ``ptr_char++``. En analysant les quatre ``unsigned char`` se trouvant aux adresses ``0x7fff5fbff750`` à ``0x7fff5fbff753``, on retrouve bien l'entier ``0x01020304`` qui avait été placé dans ``tab[0]``. .. todo:: exemples @@ -716,7 +709,7 @@ Dans les premières versions du langage C, une structure devait nécessairement Les structures sont utilisées dans différentes librairies et appels système sous Unix et Linux. Un exemple classique est la gestion du temps sur un système Unix. Un système informatique contient généralement une horloge dite `temps-réel` qui est en pratique construite autour d'un cristal qui oscille à une fréquence fixée. Ce cristal est piloté par un circuit électronique qui compte ses oscillations, ce qui permet de mesurer le passage du temps. Le système d'exploitation utilise cette horloge `temps réel` pour diverses fonctions et notamment la mesure du temps du niveau des applications. -Un système de type Unix maintient différentes structures qui sont associées à la mesure du temps [#ftimelibc]_. La première sert à mesurer le nombre de secondes et de microsecondes qui se sont écoulées depuis le 1er janvier 1970. Cette structure, baptisée ``struct timeval`` est définie dans `sys/time.h`_ comme suit : +Un système de type Unix maintient différentes structures qui sont associées à la mesure du temps [#ftimelibc]_. La première sert à mesurer le nombre de secondes et de microsecondes qui se sont écoulées depuis le premier janvier 1970. Cette structure, baptisée ``struct timeval`` est définie dans `sys/time.h`_ comme suit : .. code-block:: c @@ -725,11 +718,11 @@ Un système de type Unix maintient différentes structures qui sont associées suseconds_t tv_usec; /* and microseconds */ }; -Cette structure est utilisée par des appels système tels que `gettimeofday(2)`_ pour notamment récupérer l'heure courante ou les appels de manipulation de timers tels que `getitimer(2)`_ / `setitimer(2)`_. Elle est aussi utilisée par la fonction `time(3posix)`_ de la librairie standard et est très utile pour mesurer les performances d'un programme. +Cette structure est utilisée par des appels système tels que `gettimeofday(2)`_ pour notamment récupérer l'heure courante ou les appels de manipulation de temporisateurs (`timers` en anglais) tels que `getitimer(2)`_ / `setitimer(2)`_. Elle est aussi utilisée par la fonction `time(3posix)`_ de la librairie standard et est très utile pour mesurer les performances d'un programme. Les structures sont également fréquemment utilisées pour représenter des formats de données spéciaux sur disque comme le format des répertoires [#fdirent]_ ou les formats de paquets qui sont échangés sur le réseau [#freseau]_. -La définition de ``struct timeval`` utilise une fonctionnalité fréquemment utilisée du C : la possibilité de définir des alias pour des noms de type de données existants. Cela se fait en utilisant l'opérateur ``typedef``. En C, il est possible de renommer des types de données existants. Ainsi, l'exemple ci-dessous utilise ``typedef`` pour définir l'alias ``Entier`` pour le type ``int`` et l'alias ``Fraction`` pour la structure ``struct fraction``. +La définition de ``struct timeval`` utilise une fonctionnalité fréquemment utilisée du C : la possibilité de définir des alias pour des noms de type de données existants. Cela se fait en utilisant l'opérateur ``typedef``. En C, il est possible de renommer des types de données existants. Ainsi, l'exemple ci-dessous utilise ``typedef`` pour définir ``Entier`` comme alias pour le type ``int`` et ``Fraction`` pour la structure ``struct fraction``. .. literalinclude:: /C/S2-src/typedef.c :encoding: utf-8 @@ -742,9 +735,9 @@ Les types ``Entier`` et ``int`` peuvent être utilisés de façon interchangeabl .. note:: ``typedef`` en pratique - Le renommage de types de données a des avantages et des inconvénients dont il faut être conscient pour pouvoir l'utiliser à bon escient. L'utilisation de ``typedef`` peut faciliter la lecture et la portabilité de certains programmes. Lorsqu'un ``typedef`` est associé à une structure, cela facilite la déclaration de variables de ce type et permet le cas échéant de modifier la structure de données ultérieurement sans pour autant devoir modifier l'ensemble du programme. Cependant, contrairement aux langages orientés objet, des méthodes ne sont pas directement associées aux structures et la modification d'une structure oblige souvent à vérifier toutes les fonctions qui utilisent cette structure. L'utilisation de ``typedef`` permet de clarifier le rôle de certains types de données ou valeurs de retour de fonctions. À titre d'exemple, l'appel système `read(2)`_ qui permet notamment de lire des données dans un fichier retourne le nombre d'octets qui ont été lus après chaque appel. Cette valeur de retour est de type ``ssize_t``. L'utilisation de ces types permet au compilateur de vérifier que les bons types de données sont utilisés lors des appels de fonctions. + Renommer des types de données a des avantages et des inconvénients dont il faut être conscient pour pouvoir l'utiliser à bon escient. L'utilisation de ``typedef`` peut faciliter la lecture et la portabilité de certains programmes. Lorsqu'un ``typedef`` est associé à une structure, cela facilite la déclaration de variables de ce type et permet le cas échéant de modifier la structure de données ultérieurement sans pour autant devoir modifier l'ensemble du programme. Cependant, contrairement aux langages orientés objet, des méthodes ne sont pas directement associées aux structures et la modification d'une structure oblige souvent à vérifier toutes les fonctions qui utilisent cette structure. L'utilisation de ``typedef`` permet de clarifier le rôle de certains types de données ou valeurs de retour de fonctions. À titre d'exemple, l'appel système `read(2)`_ qui permet notamment de lire des données dans un fichier retourne le nombre d'octets qui ont été lus après chaque appel. Cette valeur de retour est de type ``ssize_t``. L'utilisation de ces types permet au compilateur de vérifier que les bons types de données sont utilisés lors des appels de fonctions. - ``typedef`` est souvent utilisé pour avoir des identifiants de type de données plus court. Par exemple, il est très courant d'abrévier les types ``unsigned`` comme ci-dessous. + ``typedef`` est souvent utilisé pour avoir des identifiants de types de données plus courts. Par exemple, il est très courant de remplacer le types ``unsigned`` par les abréviations ci-dessous. .. literalinclude:: /C/S2-src/typedef.c :encoding: utf-8 @@ -752,7 +745,7 @@ Les types ``Entier`` et ``int`` peuvent être utilisés de façon interchangeabl :start-after: ///EEE :end-before: ///FFF - Soyez prudent si vous utilisez des ``typedef`` pour redéfinir des pointeurs. En C, il est tout à fait valide d'écrire les lignes suivantes. + Soyez prudents si vous utilisez des ``typedef`` pour redéfinir des pointeurs. En C, il est tout à fait valide d'écrire les lignes suivantes. .. literalinclude:: /C/S2-src/typedef.c :encoding: utf-8 @@ -777,7 +770,7 @@ Les pointeurs sont fréquemment utilisés en combinaison avec des structures et Les fonctions ------------- -Comme la plupart des langages, le C permet de modulariser un programme +Comme la plupart des langages, le C permet de faciliter la compréhension d'un programme en le découpant en de nombreuses fonctions. Chacune réalise une tâche simple. Tout comme Java, C permet la définition de fonctions qui ne retournent aucun résultat. Celles-ci sont de type ``void`` comme l'exemple trivial ci-dessous. @@ -825,7 +818,11 @@ Les fonctions peuvent évidemment recevoir également des tableaux comme argumen :start-after: ///AAA :end-before: ///BBB -Tout comme cette fonction peut accéder au ième caractère de la chaîne passée en argument, elle peut également et sans aucune restriction modifier chacun des caractères de cette chaîne. Par contre, comme le pointeur vers la chaîne de caractères est passé par valeur, la fonction ne peut pas modifier la zone mémoire qui est pointée par l'argument. +.. spelling:: + + ième + +Tout comme cette fonction peut accéder au `ième` caractère de la chaîne passée en argument, elle peut également et sans aucune restriction modifier chacun des caractères de cette chaîne. Par contre, comme le pointeur vers la chaîne de caractères est passé par valeur, la fonction ne peut pas modifier la zone mémoire qui est pointée par l'argument. Un autre exemple de fonctions qui manipulent les tableaux sont des fonctions mathématiques qui traitent des vecteurs par exemple. @@ -862,7 +859,7 @@ Ces deux fonctions peuvent être utilisées par le fragment de code ci-dessous : warning: passing argument 1 of ‘plusun’ makes integer from pointer without a cast warning: passing argument 2 of ‘plusun’ makes pointer from integer without a cast - De nombreux programmeurs débutants ignorent souvent les warnings émis par le compilateur et se contentent d'avoir un programme compilable. C'est la source de nombreuses erreurs et de nombreux problèmes. Dans l'exemple ci-dessus, l'exécution de l'appel ``plusun(vecteur,N)`` provoquera une tentative d'accès à la mémoire dans une zone qui n'est pas allouée au processus. Dans ce cas, la tentative d'accès est bloquée par le système et provoque l'arrêt immédiat du programme sur une :term:`segmentation fault`. Dans d'autres cas, des erreurs plus subtiles mais du même type ont provoqué des problèmes graves de sécurité dans des programmes écrits en langage C. Nous y reviendrons ultérieurement. + De nombreux programmeurs débutants ignorent souvent les warnings émis par le compilateur et se contentent d'avoir un programme compilé. C'est la source de nombreuses erreurs et de nombreux problèmes. Dans l'exemple ci-dessus, l'exécution de l'appel ``plusun(vecteur,N)`` provoquera une tentative d'accès à la mémoire dans une zone qui n'est pas allouée au processus. Dans ce cas, la tentative d'accès est bloquée par le système et provoque l'arrêt immédiat du programme sur une :term:`segmentation fault`. Dans d'autres cas, des erreurs plus subtiles mais du même type ont provoqué des problèmes graves de sécurité dans des programmes écrits en langage C. Nous y reviendrons ultérieurement. Pour terminer, mentionnons que les fonctions écrites en C peuvent utiliser des structures et des pointeurs vers des structures comme arguments. Elles peuvent aussi retourner des structures comme résultat. Ceci est illustré par deux variantes de fonctions permettant d'initialiser une fraction et de déterminer si deux fractions sont égales [#fegal]_. @@ -932,6 +929,11 @@ A B A XOR B 1 1 0 === === ============ +.. spelling:: + + De Morgan + + Ces opérations peuvent être combinées entre elles. Pour des raisons technologiques, les circuits logiques implémentent plutôt les opérations NAND (qui équivaut à AND suivi de NOT) ou NOR (qui équivaut à OR suivi de NOT). Il est également important de mentionner les lois formulées par De Morgan qui peuvent se résumer par les équations suivantes : - :math:`\neg{(A \wedge B)}=\neg{A} \vee \neg{B}` @@ -943,7 +945,7 @@ Ces opérations binaires peuvent s'étendre à des séquences de bits. Voici que :encoding: utf-8 :language: console -En C, ces expressions logiques s'utilisent comme dans le fragment de code suivant. En général, elles s'utilisent sur des representations non signées, souvent des ``unsigned char`` ou des ``unsigned int``. +En C, ces expressions logiques s'utilisent comme dans le fragment de code suivant. En général, elles s'utilisent sur des représentations non signées, souvent des ``unsigned char`` ou des ``unsigned int``. .. literalinclude:: /C/S2-src/exprbin.c :encoding: utf-8 @@ -959,6 +961,10 @@ En pratique, les opérations logiques sont utiles pour effectuer des manipulatio :start-after: ///CCC :end-before: ///DDD +.. spelling:: + + Vernam + L'opération XOR joue un rôle important dans certaines applications. La plupart des méthodes de chiffrement et de déchiffrement utilisent de façon extensive cette opération. Une des propriétés intéressantes de l'opération XOR est que :math:`(A \oplus B) \oplus B=A`. Cette propriété est largement utilisée par les méthodes de chiffrement. La méthode développée par Vernam au début du vingtième siècle s'appuie sur l'opération XOR. Pour transmettre un message `M` de façon sûre, elle applique l'opération XOR bit à bit entre tous les bits du message `M` et une clé `K` doit avoir au moins le même nombre de bits que `M`. Si cette clé `K` est totalement aléatoire et n'est utilisée qu'une seule fois, alors on parle de *one-time-pad*. On peut montrer que dans ce cas, la méthode de chiffrement est totalement sûre. En pratique, il est malheureusement difficile d'avoir une clé totalement aléatoire qui soit aussi longue que le message à transmettre. Le programme ci-dessous implémente cette méthode de façon triviale. La fonction `memfrob(3)`_ de la librairie :term:`GNU` utilise également un chiffrement via un XOR. .. literalinclude:: /C/S2-src/xor.c @@ -1007,6 +1013,10 @@ Ces opérations de décalage permettent différentes manipulations de bits. À t .. [#fdirent] Voir notamment `fs(5)`_ pour des exemples relatifs aux systèmes de fichiers. Une analyse détaillée des systèmes de fichiers sort du cadre de ce cours. -.. [#freseau] Parmi les exemples simples, on peut citer la structure ``struct ipv6hdr`` qui correspond à l'entête IPv6 et est définie dans `linux/ipv6.h`_. +.. spelling:: + + IP + +.. [#freseau] Parmi les exemples simples, on peut citer la structure ``struct ipv6hdr`` qui correspond à l'entête du protocole IP version 6 et est définie dans `linux/ipv6.h`_. .. [#fegal] Cette définition de l'égalité entre fractions suppose que les fractions à comparer sont sous forme irréductible. Le lecteur est invité à écrire la fonction générale permettant de tester l'égalité entre fractions réductibles. diff --git a/Theorie/C/intro-C.rst b/Theorie/C/intro-C.rst index 4600b3f44ff026ce8ec199219a158ebc2d249784..566d3e500b2bb2fc32d4886fafef0d7251ea8125 100644 --- a/Theorie/C/intro-C.rst +++ b/Theorie/C/intro-C.rst @@ -1,5 +1,5 @@ .. -*- coding: utf-8 -*- -.. Copyright |copy| 2012 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal +.. Copyright |copy| 2012, 2019 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal .. Ce fichier est distribué sous une licence `creative commons <http://creativecommons.org/licenses/by-sa/3.0/>`_ Le langage C @@ -10,7 +10,7 @@ Différents langages permettent au programmeur de construire des programmes qui .. commentaire Sur certains processeurs, ces séquences ont une taille fixe (par exemple 32 ou 64 bits). C'est le cas par exemple sur certains processeurs de type :term:`RISC`. D'autres processeurs supportent des instructions en langage machine qui sont encodées sous la forme d'un nombre variables de bits. C'est le cas de processeurs de type :term:`CISC` et notamment les processeurs de la famille :term:`x86` développés initialement par intel et qui sont largement utilisés de nos jours. -Le langage machine est peu adapté aux humains et il est extrêmement rare qu'un informaticien doive manipuler des programmes directement en langage machine. Par contre, pour certaines tâches bien spécifiques, comme par exemple le développement de routines spéciales qui doivent être les plus rapides possibles ou qui doivent interagir directement avec le matériel, il est important de pouvoir efficacemment générer du langage machine. Cela peut se faire en utilisant un langage d'assemblage. Chaque famille de processeurs a un langage d'assemblage qui lui est propre. Le langage d'assemblage permet d'exprimer de façon symbolique les différentes instructions qu'un processeur doit exécuter. Nous aurons l'occasion de traiter à plusieurs reprises des exemples en langage d'assemblage dans le cadre de ce cours. Cela nous permettra de mieux comprendre la façon dont le processeur fonctionne et exécute les programmes. Le langage d'assemblage est converti en langage machine grâce à un :term:`assembleur`. +Le langage machine est peu adapté aux humains et il est extrêmement rare qu'un informaticien doive manipuler des programmes directement en langage machine. Par contre, pour certaines tâches bien spécifiques, comme par exemple le développement de routines spéciales qui doivent être les plus rapides possibles ou qui doivent interagir directement avec le matériel, il est important de pouvoir efficacement générer du langage machine. Cela peut se faire en utilisant un langage d'assemblage. Chaque famille de processeurs a un langage d'assemblage qui lui est propre. Le langage d'assemblage permet d'exprimer de façon symbolique les différentes instructions qu'un processeur doit exécuter. Nous aurons l'occasion de traiter à plusieurs reprises des exemples en langage d'assemblage dans le cadre de ce cours. Cela nous permettra de mieux comprendre la façon dont le processeur fonctionne et exécute les programmes. Le langage d'assemblage est converti en langage machine grâce à un :term:`assembleur`. Le langage d'assemblage est le plus proche du processeur. Il permet d'écrire des programmes compacts et efficaces. C'est aussi souvent la seule façon d'utiliser des instructions spéciales du processeur qui permettent d'interagir directement avec le matériel pour par exemple commander les dispositifs d'entrée/sortie. C'est essentiellement dans les systèmes embarqués qui disposent de peu de mémoire et pour quelques fonctions spécifiques des systèmes d'exploitation que le langage d'assemblage est utilisé de nos jours. La plupart des programmes applicatifs et la grande majorité des systèmes d'exploitation sont écrits dans des langages de plus haut niveau. @@ -69,7 +69,7 @@ Ensuite, un programme écrit en langage C comprend principalement des expression #define EXIT_FAILURE 1 #define EXIT_SUCCESS 0 - - inclure du code sur base de la valeur d'une constante définie par un ``#define``. Ce contrôle de l'inclusion de code sur base de la valeur de constantes est fréquemment utilisé pour ajouter des lignes qui ne doivent être exécutées que lorsque l'on veut débugger un programme. C'est aussi souvent utilisé pour faciliter la portabilité d'un programme entre différentes variantes de Unix, mais cette utilisation sort du cadre de ce cours. + - inclure du code sur base de la valeur d'une constante définie par un ``#define``. Ce contrôle de l'inclusion de code sur base de la valeur de constantes est fréquemment utilisé pour ajouter des lignes qui ne doivent être exécutées que lorsque l'on veut déboguer un programme. C'est aussi souvent utilisé pour faciliter la portabilité d'un programme entre différentes variantes de Unix, mais cette utilisation sort du cadre de ce cours. .. code-block:: c @@ -82,7 +82,7 @@ Ensuite, un programme écrit en langage C comprend principalement des expression Il est également possible de définir des macros qui prennent un ou plusieurs paramètres [CPP]_. -Les headers standards sont placés dans des répertoires bien connus du système. Sur la plupart des variantes de Unix ils se trouvent dans le répertoire ``/usr/include/``. Nous aurons l'occasion d'utiliser régulièrement ces fichiers standards dans le cadre du cours. +Les `headers` standards sont placés dans des répertoires bien connus du système. Sur la plupart des variantes de Unix ils se trouvent dans le répertoire ``/usr/include/``. Nous aurons l'occasion d'utiliser régulièrement ces fichiers standards dans le cadre du cours. Le langage Java a été largement inspiré du langage C et de nombreuses constructions syntaxiques sont similaires en Java et en C. Un grand nombre de mots clés en C ont le même rôle qu'en Java. Les principaux types de données primitifs supportés par le C sont : @@ -106,7 +106,7 @@ Les compilateurs récents qui supportent le type booléen permettent de déclare Au-delà des types de données primitifs, Java et C diffèrent et nous aurons l'occasion d'y revenir dans un prochain chapitre. Le langage C n'est pas un langage orienté objet et il n'est donc pas possible de définir d'objet avec des méthodes spécifiques en C. C permet la définition de structures, d'unions et d'énumérations sur lesquelles nous reviendrons. -En Java, les chaînes de caractères sont représentées grâce à l'objet ``String``. En C, une chaîne de caractères est représentée sous la forme d'un tableau de caractères dont le dernier élément contient la valeur ``\0``. Alors que Java stocke les chaînes de caractères dans un objet avec une indication de leur longueur, en C il n'y a pas de longueur explicite pour les chaînes de caractères mais le caractère ``\0`` sert de marqueur de fin de chaîne de caractères. Lorsque le language C a été développé, ce choix semblait pertinent, notamment pour des raisons de performance. Avec le recul, ce choix pose question [Kamp2011]_ et nous y reviendrons lorsque nous aborderons certains problèmes de sécurité. +En Java, les chaînes de caractères sont représentées grâce à l'objet ``String``. En C, une chaîne de caractères est représentée sous la forme d'un tableau de caractères dont le dernier élément contient la valeur ``\0``. Alors que Java stocke les chaînes de caractères dans un objet avec une indication de leur longueur, en C il n'y a pas de longueur explicite pour les chaînes de caractères mais le caractère ``\0`` sert de marqueur de fin de chaîne de caractères. Lorsque le langage C a été développé, ce choix semblait pertinent, notamment pour des raisons de performance. Avec le recul, ce choix pose question [Kamp2011]_ et nous y reviendrons lorsque nous aborderons certains problèmes de sécurité. .. literalinclude:: src/string.c :language: c @@ -144,7 +144,7 @@ Par convention, en C le premier argument (se trouvant à l'indice ``0`` du table .. literalinclude:: src/cmdline.out :language: console -Outre le traitement des arguments, une autre différence importante entre Java et C est la valeur de retour de la fonction ``main``. En C, la fonction ``main`` retourne un entier. Cette valeur de retour est passée par le système d'exploitation au programme (typiquemment un :term:`shell` ou interpréteur de commandes) qui a demandé l'exécution du programme. Grâce à cette valeur de retour il est possible à un programme d'indiquer s'il s'est exécuté correctement ou non. Par convention, un programme qui s'exécute sous Unix doit retourner ``EXIT_SUCCESS`` lorsqu'il se termine correctement et ``EXIT_FAILURE`` en cas d'échec. La plupart des programmes fournis avec un Unix standard respectent cette convention. Dans certains cas, d'autres valeurs de retour non nulles sont utilisées pour fournir plus d'informations sur la raison de l'échec. +Outre le traitement des arguments, une autre différence importante entre Java et C est la valeur de retour de la fonction ``main``. En C, la fonction ``main`` retourne un entier. Cette valeur de retour est passée par le système d'exploitation au programme (typiquement un :term:`shell` ou interpréteur de commandes) qui a demandé l'exécution du programme. Grâce à cette valeur de retour il est possible à un programme d'indiquer s'il s'est exécuté correctement ou non. Par convention, un programme qui s'exécute sous Unix doit retourner ``EXIT_SUCCESS`` lorsqu'il se termine correctement et ``EXIT_FAILURE`` en cas d'échec. La plupart des programmes fournis avec un Unix standard respectent cette convention. Dans certains cas, d'autres valeurs de retour non nulles sont utilisées pour fournir plus d'informations sur la raison de l'échec. En pratique, l'échec d'un programme peut être dû aux arguments incorrects fournis par l'utilisateur ou à des fichiers qui sont inaccessibles. À titre d'illustration, le programme :download:`src/failure.c` est le programme le plus simple qui échoue lors de son exécution. @@ -200,9 +200,14 @@ Toutes les tables de caractères placent les chiffres ``0`` à ``9`` à des posi - les pages de manuel de `FreeBSD <http://www.freebsd.org/cgi/man.cgi>`_ - les pages de manuel de `MacOS <http://developer.apple.com/documentation/Darwin/Reference/ManPages/index.html>`_ - Dans la version online de ces notes, toutes les références vers un programme Unix, un appel système ou une fonction de la librairie pointent vers la page de manuel Linux correspondante. + Dans la version en-ligne de ces notes, toutes les références vers un programme Unix, un appel système ou une fonction de la librairie pointent vers la page de manuel Linux correspondante. -Il existe de nombreux livres consacrés au langage C. La référence la plus classique est [KernighanRitchie1998]_, mais certains éléments comments à dater. Un tutorial intéressant a été publié par Brian Kernighan [Kernighan]_. [King2008]_ propose une présentation plus moderne du langage C. +.. spelling:: + + Kernighan + Ritchie + +Il existe de nombreux livres consacrés au langage C. La référence la plus classique est [KernighanRitchie1998]_, mais certains éléments commencent à dater. Un tutoriel intéressant a été publié par Brian Kernighan [Kernighan]_. [King2008]_ propose une présentation plus moderne du langage C. .. rubric:: Footnotes @@ -213,6 +218,12 @@ Il existe de nombreux livres consacrés au langage C. La référence la plus cla .. [#fmain] Il est également possible d'utiliser dans un programme C une fonction ``main`` qui ne prend pas d'argument. Sa signature sera alors ``int main (void)``. +.. spelling:: + + Darwin + MacOS + Windows + .. [#fenvp] En pratique, le système d'exploitation passe également les variables d'environnement à la fonction ``main``. Nous verrons plus tard comment ces variables d'environnement sont passées du système au programme et comment celui-ci peut y accéder. Sachez cependant que sous certaines variantes de Unix, et notamment Darwin/MacOS ainsi que sous certaines versions de Windows, le prototype de la fonction ``main`` inclut explicitement ces variables d'environnement (``int main(int argc, char *argv[], char *envp[])``) diff --git a/Theorie/C/linker.rst b/Theorie/C/linker.rst index 45ad70893630f438351b02d84726806d46880f9b..41112ec1386d5e7043aaf317d8633406edcb8ded 100644 --- a/Theorie/C/linker.rst +++ b/Theorie/C/linker.rst @@ -1,5 +1,5 @@ .. -*- coding: utf-8 -*- -.. Copyright |copy| 2012 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal +.. Copyright |copy| 2012, 2019 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal .. Ce fichier est distribué sous une licence `creative commons <http://creativecommons.org/licenses/by-sa/3.0/>`_ .. _complementsC: @@ -17,7 +17,7 @@ Pointeurs Les pointeurs sont très largement utilisés dans les programmes écrits en langage C. Nous avons utilisé des pointeurs vers des types de données primitifs tel que les ``int``, ``char`` ou ``float`` et des pointeurs vers des structures. En pratique, il est possible en C de définir des pointeurs vers n'importe quel type d'information qui est manipulée par un programme C. -Un premier exemple sont les pointeurs vers des fonctions. Comme nous l'avons vu dans le chapitre précédent, une fonction est une séquence d'instructions assembleur qui sont stockées à un endroit bien précis de la mémoire. Cette localisation précise des instructions qui implémentent la fonction permet d'appeler une fonction avec l'instruction ``calll``. En C, il est parfois aussi souhaitable de pouvoir appeler une fonction via un pointeur vers cette fonction plutôt qu'en nommant la fonction directement. Cela peut rendre le code plus flexible et plus facile à adapter. Nous avons déjà utilisé des pointeurs vers des fonctions sans le savoir lorsque nous avons utilisé ``printf("fct : %p\n",f)`` où ``f`` est un nom de fonction. L'exemple ci-dessous montre une autre utilisation intéressante des pointeurs vers des fonctions. Lorsque l'on écrit du code C, il est parfois utile d'ajouter des commandes qui permettent d'afficher à l'écran des informations de debugging. L'exemple ci-dessous est une application qui supporte trois niveaux de debugging. Rien n'est affiché au niveau ``0``, une ligne s'affiche au niveau ``1`` et des informations plus détaillées sont affichées au niveau ``2``. Lors de son exécution , l'application affiche la sortie suivante. +Un premier exemple sont les pointeurs vers des fonctions. Comme nous l'avons vu dans le chapitre précédent, une fonction est une séquence d'instructions assembleur qui sont stockées à un endroit bien précis de la mémoire. Cette localisation précise des instructions qui implémentent la fonction permet d'appeler une fonction avec l'instruction ``calll``. En C, il est parfois aussi souhaitable de pouvoir appeler une fonction via un pointeur vers cette fonction plutôt qu'en nommant la fonction directement. Cela peut rendre le code plus flexible et plus facile à adapter. Nous avons déjà utilisé des pointeurs vers des fonctions sans le savoir lorsque nous avons utilisé ``printf("fct : %p\n",f)`` où ``f`` est un nom de fonction. L'exemple ci-dessous montre une autre utilisation intéressante des pointeurs vers des fonctions. Lorsque l'on écrit du code C, il est parfois utile d'ajouter des commandes qui permettent d'afficher à l'écran des informations de débogage. L'exemple ci-dessous est une application qui supporte trois niveaux de débogage. Rien n'est affiché au niveau ``0``, une ligne s'affiche au niveau ``1`` et des informations plus détaillées sont affichées au niveau ``2``. Lors de son exécution , l'application affiche la sortie suivante. .. code-block:: console @@ -31,7 +31,7 @@ Un premier exemple sont les pointeurs vers des fonctions. Comme nous l'avons vu debug: Hello g=1 -Cette application qui supporte plusieurs niveaux de debugging utilise pourtant toujours le même appel pour afficher l'information de debugging : ``(debug_print[debug_level])(...);``. Cet appel profite des pointeurs vers les fonctions. Le tableau ``debug_print`` est un tableau de pointeurs vers des fonctions qui chacune prend comme argument un ``char *``. La variable globale ``debug_level`` est initialisée sur base de l'argument passé au programme. +Cette application qui supporte plusieurs niveaux de débogage utilise pourtant toujours le même appel pour afficher l'information de débogage : ``(debug_print[debug_level])(...);``. Cet appel profite des pointeurs vers les fonctions. Le tableau ``debug_print`` est un tableau de pointeurs vers des fonctions qui chacune prend comme argument un ``char *``. La variable globale ``debug_level`` est initialisée sur base de l'argument passé au programme. .. literalinclude:: /C/S5-src/fctptr.c :encoding: utf-8 @@ -55,11 +55,10 @@ Le premier est un pointeur vers le début de la zone mémoire à trier. Le secon :start-after: ///AAA :end-before: ///BBB -Il est utile d'analyser en détails les arguments de la fonction de comparaison utilisée par `qsort(3)`_. Celle-ci prend deux arguments de type ``const void *``. L'utilisation de pointeurs ``void *`` est nécessaire car la fonction doit être générique et pouvoir traiter n'importe quel type de pointeurs. ``void *`` est un pointeur vers une zone quelconque de mémoire qui peut être casté vers n'importe quel type de pointeur par la fonction de comparaison. Le qualificatif ``const`` indique que la fonction n'a pas le droit de modifier la donnée référencée par ce pointeur, même si elle reçoit un pointeur vers cette donnée. On retrouvera régulièrement cette utilisation de ``const`` dans les signatures des fonctions de la librairie pour spécifier des contraintes sur les arguments passés à une fonction [#frestrict]_. +Il est utile d'analyser en détails les arguments de la fonction de comparaison utilisée par `qsort(3)`_. Celle-ci prend deux arguments de type ``const void *``. L'utilisation de pointeurs ``void *`` est nécessaire car la fonction doit être générique et pouvoir traiter n'importe quel type de pointeurs. ``void *`` est un pointeur vers une zone quelconque de mémoire qui peut être casté vers n'importe quel type de pointeur par la fonction de comparaison. ``const`` indique que la fonction n'a pas le droit de modifier la donnée référencée par ce pointeur, même si elle reçoit un pointeur vers cette donnée. On retrouvera régulièrement cette utilisation de ``const`` dans les signatures des fonctions de la librairie pour spécifier des contraintes sur les arguments passés à une fonction [#frestrict]_. -.. todo:: restrict -Le second type de pointeurs que nous n'avons pas encore abordé en détails sont les pointeurs vers des pointeurs. En fait, nous les avons utilisés sans vraiment le savoir dans la fonction ``main``. En effet, le second argument de cette fonction est un tableau de pointeurs qui pointent chacun vers des chaînes de caractères différentes. La notation ``char *argv[]`` est équivalente à la notation ``char **argv``. ``**argv`` est donc un pointeur vers une zone qui contient des pointeurs vers des chaînes de caractères. Ce pointeur vers un pointeur doit être utilisé avec précaution. ``argv[0]`` est un pointeur vers une chaîne de caractères. La construction ``&(argv[0])`` permet donc d'obtenir un pointeur vers un pointeur vers une chaîne de caractères, ce qui correspond bien à la déclaration ``char **``. Ensuite, l'utilisation de ``*p`` pourrait surprendre. ``*p`` est un pointeur vers une chaîne de caractères. Il peut donc être comparé à ``NULL`` qui est aussi un pointeur, incrémenté et la chaîne de caractères qu'il référencie peut être affichée par `printf(3)`_. +Le second type de pointeurs que nous n'avons pas encore abordé en détails sont les pointeurs vers des pointeurs. En fait, nous les avons utilisés sans vraiment le savoir dans la fonction ``main``. En effet, le second argument de cette fonction est un tableau de pointeurs qui pointent chacun vers des chaînes de caractères différentes. La notation ``char *argv[]`` est équivalente à la notation ``char **argv``. ``**argv`` est donc un pointeur vers une zone qui contient des pointeurs vers des chaînes de caractères. Ce pointeur vers un pointeur doit être utilisé avec précaution. ``argv[0]`` est un pointeur vers une chaîne de caractères. La construction ``&(argv[0])`` permet donc d'obtenir un pointeur vers un pointeur vers une chaîne de caractères, ce qui correspond bien à la déclaration ``char **``. Ensuite, l'utilisation de ``*p`` pourrait surprendre. ``*p`` est un pointeur vers une chaîne de caractères. Il peut donc être comparé à ``NULL`` qui est aussi un pointeur, incrémenté et la chaîne de caractères qu'il référence peut être affichée par `printf(3)`_. .. literalinclude:: /C/S5-src/ptrptr.c :encoding: utf-8 @@ -69,7 +68,6 @@ Le second type de pointeurs que nous n'avons pas encore abordé en détails sont En pratique, ces pointeurs vers des pointeurs se retrouveront lorsque l'on doit manipuler des structures multidimensionnelles, mais aussi lorsqu'il faut qu'une fonction puisse modifier une adresse qu'elle a reçue en argument. -.. C'est notamment le cas lorsqu'il faut mettre à jour une structure chaînée. Lorsque nous avons construit une structure chaînée permettant de manipuler une pile, les fonctions ``push`` et ``pop`` récupéraient le sommet de la pile dans une variable globale. Cet aspect sera couvert par un des exercices. Un autre exemple d'utilisation de pointeurs vers des pointeurs est la fonction `strtol(3)`_ de la librairie standard. Cette fonction est une généralisation des fonctions comme `atoi(3)`_. Elle permet de convertir une chaîne de caractères en un nombre. La fonction `strtol(3)`_ prend trois arguments et retourne un ``long``. Le premier argument est un pointeur vers la chaîne de caractères à convertir. Le troisième argument est la base utilisée pour cette conversion. @@ -109,11 +107,7 @@ Cette partie de code utilise la fonction `isdigit(3)`_ pour vérifier si les car Il existe d'autres fonctions de la librairie standard qui utilisent des pointeurs vers des pointeurs comme arguments dont notamment `strsep(3)`_ et `strtok_r(3)`_. -.. .. note: Le qualificateur ``restrict`` -.. Explication de ``restrict`` - -.. todo:: mettre plus de détails De grands programmes en C ------------------------- @@ -148,7 +142,7 @@ Un module d'un programme C est en général décomposé en deux parties. Tout d' Un programmeur C peut utiliser deux types de fichiers header. Il y a tout d'abord les fichiers headers standards qui sont fournis avec le système. Ce sont ceux que nous avons utilisés jusque maintenant. Ces headers standards se reconnaissent car ils sont entourés des caractères ``<`` et ``>`` dans la directive ``#include``. Ceux-ci se trouvent dans des répertoires connus par le compilateur, normalement ``/usr/include``. Les fichiers headers qui accompagnent un module se trouvent eux généralement dans le même répertoire que le module. Dans l'exemple ci-dessus, le header ``min.h`` est inclus via la directive ``#include "min.h"``. Lorsque le préprocesseur rencontre une telle directive, il cherche le fichier dans le répertoire courant. Il est possible de spécifier des répertoires qui contiennent des fichiers headers via l'argument ``-I`` de `gcc(1)`_ ou en utilisant les variables d'environnement ``GCC_INCLUDE_DIR`` ou ``CPATH``. -Lorsque l'on doit compiler un programme qui fait appel à plusieurs modules, quelle que soit sa taille, il est préférable d'utiliser `make(1)`_ pour automatiser sa compilation. Le fichier ci-dessous est un exemple minimaliste de :term:`Makefile` utilisable pour un tel projet. +Lorsque l'on doit compiler un programme qui fait appel à plusieurs modules, quelle que soit sa taille, il est préférable d'utiliser `make(1)`_ pour automatiser sa compilation. Le fichier ci-dessous est un petit exemple de :term:`Makefile` utilisable pour un tel projet. .. literalinclude:: /C/S5-src/Makefile2 :encoding: utf-8 @@ -171,14 +165,14 @@ Lorsque plusieurs modules, potentiellement développés par des programmeurs qui Tout d'abord, les variables locales sont locales au bloc dans lequel elles sont définies. Ce principe permet d'utiliser le même nom de variable dans plusieurs blocs d'un même fichier. Il s'étend naturellement à l'utilisation de variables locales dans des fichiers différents. -Pour les variables globales, la situation est différente. Si une variable est définie en dehors d'un bloc dans un fichier, cette variable est considérée comme étant globale. Par défaut, elle est donc accessible depuis tous les modules qui composent le programme. Cela peut en pratique poser des difficultés si le même nom de variable est utilisé dans deux modules différents. Pour contourner ce problème, le langage C utilise le qualificateur ``static``. Lorsque ce qualificateur est placé devant une déclaration de variable en dehors d'un bloc dans un module, il indique que la variable doit être accessible à toutes les fonctions du module mais pas en dehors du module. Lorsqu'un module utilise des variables qui sont communes à plusieurs fonctions mais ne doivent pas être visibles en dehors du module, il est important de les déclarer comme étant ``static``. Le deuxième qualificateur relatif aux variables globales est ``extern``. Lorsqu'une déclaration de variable globale est préfixée par ``extern``, cela indique au compilateur que la variable est définie dans un autre module qui sera linké ultérieurement. Le compilateur réserve une place pour cette variable dans la table des symboles du fichier objet, mais cette place ne pourra être liée à la zone mémoire qui correspond à cette variable que lorsque l'éditeur de liens combinera les différents fichiers objet entre eux. +Pour les variables globales, la situation est différente. Si une variable est définie en dehors d'un bloc dans un fichier, cette variable est considérée comme étant globale. Par défaut, elle est donc accessible depuis tous les modules qui composent le programme. Cela peut en pratique poser des difficultés si le même nom de variable est utilisé dans deux modules différents. Pour contourner ce problème, le langage C utilise ``static``. Lorsque ``static`` est placé devant une déclaration de variable en dehors d'un bloc dans un module, il indique que la variable doit être accessible à toutes les fonctions du module mais pas en dehors du module. Lorsqu'un module utilise des variables qui sont communes à plusieurs fonctions mais ne doivent pas être visibles en dehors du module, il est important de les déclarer comme étant ``static``. Lorsqu'une déclaration de variable globale est préfixée par ``extern``, cela indique au compilateur que la variable est définie dans un autre module qui sera lié ultérieurement. Le compilateur réserve une place pour cette variable dans la table des symboles du fichier objet, mais cette place ne pourra être liée à la zone mémoire qui correspond à cette variable que lorsque l'éditeur de liens combinera les différents fichiers objet entre eux. .. note:: Les deux utilisations de ``static`` pour des variables - La qualificateur ``static`` peut être utilisé à la fois pour des variables qui sont définies en dehors d'un bloc et dans un bloc. Lorsqu'une variable est définie comme étant ``static`` hors d'un bloc dans un module, elle n'est accessible qu'aux fonctions de ce module. Par contre, lorsqu'une variable est définie comme étant ``static`` à l'intérieur d'un bloc, par exemple dans une fonction, le qualificatif indique que cette variable doit toujours se trouver à la même localisation en mémoire, quel que soit le moment où elle est appelée. Ces variables ``static`` sont placées par le compilateur dans le bas de la mémoire, avec les variables globales. Contrairement aux variables locales traditionnelles, une variable locale ``static`` garde sa valeur d'une invocation de la fonction à l'autre. En pratique, les variables locales ``static`` doivent être utilisées avec précaution et bien documentées. Un de leurs intérêt est qu'elles ne sont initialisées qu'au lancement du programme et pas à chaque invocation de la fonction où elles sont définies. + ``static`` peut être utilisé à la fois pour des variables qui sont définies en dehors d'un bloc et dans un bloc. Lorsqu'une variable est définie comme étant ``static`` hors d'un bloc dans un module, elle n'est accessible qu'aux fonctions de ce module. Par contre, lorsqu'une variable est définie comme étant ``static`` à l'intérieur d'un bloc, par exemple dans une fonction, cela indique que cette variable doit toujours se trouver à la même localisation en mémoire, quel que soit le moment où elle est appelée. Ces variables ``static`` sont placées par le compilateur dans le bas de la mémoire, avec les variables globales. Contrairement aux variables locales traditionnelles, une variable locale ``static`` garde sa valeur d'une invocation de la fonction à l'autre. En pratique, les variables locales ``static`` doivent être utilisées avec précaution et bien documentées. Un de leurs intérêt est qu'elles ne sont initialisées qu'au lancement du programme et pas à chaque invocation de la fonction où elles sont définies. -La qualificateur ``static`` peut aussi précéder des déclarations de fonctions. Dans ce cas, il indique que la fonction ne doit pas être visible en dehors du module dans lequel elle est définie. Sans le qualificateur ``static``, une fonction déclarée dans un module est accessible depuis n'importe quel autre module. +Il faut noter que ``static`` peut aussi précéder des déclarations de fonctions. Dans ce cas, il indique que la fonction ne doit pas être visible en dehors du module dans lequel elle est définie. Sans ``static``, une fonction déclarée dans un module est accessible depuis n'importe quel autre module. Afin d'illustrer l'utilisation de ``static`` et ``extern``, considérons le programme ``prog.c`` ci-dessous qui inclut le module ``module.c`` et également le module ``min.c`` présenté plus haut. @@ -190,7 +184,7 @@ Afin d'illustrer l'utilisation de ``static`` et ``extern``, considérons le prog :encoding: utf-8 :language: c -Ce module contient deux fonctions, ``vmin`` et ``min``. ``vmin`` est déclarée sans qualificatif. Elle est donc accessible depuis n'importe quel module. Sa signature est reprise dans le :term:`fichier header` ``module.h``. La fonction ``min`` par contre est déclarée avec le qualificatif ``static``. Cela implique qu'elle n'est utilisable qu'à l'intérieur de ce module et invisible de tout autre module. La variable globale ``num1`` est accessible depuis n'importe quel module. La variable ``num2`` également, mais elle est initialisée dans un autre module. Enfin, la variable ``num3`` n'est accessible qu'à l'intérieur de ce module. +Ce module contient deux fonctions, ``vmin`` et ``min``. ``vmin`` est accessible depuis n'importe quel module. Sa signature est reprise dans le :term:`fichier header` ``module.h``. La fonction ``min`` par contre est déclarée comme étant ``static``. Cela implique qu'elle n'est utilisable qu'à l'intérieur de ce module et invisible de tout autre module. La variable globale ``num1`` est accessible depuis n'importe quel module. La variable ``num2`` également, mais elle est initialisée dans un autre module. Enfin, la variable ``num3`` n'est accessible qu'à l'intérieur de ce module. .. literalinclude:: /C/S5-src/prog.c :encoding: utf-8 @@ -205,7 +199,7 @@ La fonction ``f`` mérite que l'on s'y attarde un peu. Cette fonction contient l :encoding: utf-8 :language: console -Le dernier point à mentionner concernant cet exemple est relatif à la fonction ``min`` qui est utilisée dans la fonction ``main``. Le module ``prog.c`` étant linké avec ``module.c`` et ``min.c``, le linker associe à ce nom de fonction la déclaration qui se trouve dans le fichier ``min.c``. La déclaration de la fonction ``min`` qui se trouve dans ``module.c`` est ``static``, elle ne peut donc pas être utilisée en dehors de ce module. +Le dernier point à mentionner concernant cet exemple est relatif à la fonction ``min`` qui est utilisée dans la fonction ``main``. Le module ``prog.c`` étant lié avec ``module.c`` et ``min.c``, le linker associe à ce nom de fonction la déclaration qui se trouve dans le fichier ``min.c``. La déclaration de la fonction ``min`` qui se trouve dans ``module.c`` est ``static``, elle ne peut donc pas être utilisée en dehors de ce module. Traitement des erreurs @@ -242,12 +236,12 @@ A titre d'exemple, le programme ci-dessous utilise `strerror(3)`_ pour afficher fprintf(stderr,"Erreur : errno=%d %s\n",errno,strerror(errno)); } -.. linker : gcc -v pour voir ce qu'il se passe dans gcc, montrer cpp et les include + .. rubric:: Footnotes -.. [#frestrict] La qualificateur ``restrict`` est également parfois utilisé pour indiquer des contraintes sur les pointeurs passés en argument à une fonction [Walls2006]_. +.. [#frestrict] ``restrict`` est également parfois utilisé pour indiquer des contraintes sur les pointeurs passés en argument à une fonction [Walls2006]_. diff --git a/Theorie/C/malloc.rst b/Theorie/C/malloc.rst index 7ae1078b0b9e629a42f262dfcf96fa2a929499eb..4a3e263161ff3e78b4a96d8463fc6a99a014340b 100644 --- a/Theorie/C/malloc.rst +++ b/Theorie/C/malloc.rst @@ -1,5 +1,5 @@ .. -*- coding: utf-8 -*- -.. Copyright |copy| 2012 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal +.. Copyright |copy| 2012, 2019 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal .. Ce fichier est distribué sous une licence `creative commons <http://creativecommons.org/licenses/by-sa/3.0/>`_ @@ -8,7 +8,7 @@ Déclarations Durant les chapitres précédents, nous avons principalement utilisé des variables locales. Celles-ci sont déclarées à l'intérieur des fonctions où elles sont utilisées. La façon dont les variables sont déclarées est importante dans un programme écrit en langage C. Dans cette section nous nous concentrerons sur des programmes C qui sont écrits sous la forme d'un seul fichier source. Nous verrons plus tard comment découper un programme en plusieurs modules qui sont répartis dans des fichiers différents et comment les variables peuvent y être déclarées. -La première notion importante concernant la déclaration des variables est leur :term:`portée`. La portée d'une variable peut être définie comme étant la partie du programme où la variable est accesible et où sa valeur peut être modifiée. Le langage C définit deux types de portée à l'intérieur d'un fichier C. La première est la :term:`portée globale`. Une variable qui est définie en dehors de toute définition de fonction a une portée globale. Une telle variable est accessible dans toutes les fonctions présentes dans le fichier. La variable ``g`` dans l'exemple ci-dessous a une portée globale. +La première notion importante concernant la déclaration des variables est leur :term:`portée`. La portée d'une variable peut être définie comme étant la partie du programme où la variable est accessible et où sa valeur peut être modifiée. Le langage C définit deux types de portée à l'intérieur d'un fichier C. La première est la :term:`portée globale`. Une variable qui est définie en dehors de toute définition de fonction a une portée globale. Une telle variable est accessible dans toutes les fonctions présentes dans le fichier. La variable ``g`` dans l'exemple ci-dessous a une portée globale. .. code-block:: c @@ -59,7 +59,7 @@ Les versions récentes de C [C99]_ permettent également de définir des variabl :end-before: ///BBB -Il y a deux façons de définir des constantes dans les versions récentes de C [C99]_. La première est via la macro ``#define`` du préprocesseur. Cette macro permet de remplacer une chaîne de caractères (par exemple ``M_PI`` qui provient de `math.h`_) par un nombre ou une autre chaîne de caractères. Ce remplacement s'effectue avant la compilation. Dans le cas de ``M_PI`` ci-dessus, le préprocesseur remplace toute les occurences de cette chaîne de caractères par la valeur numérique de :math:`\pi`. Lorsqu'une variable ``const`` est utilisée, la situation est un peu différente. Le préprocesseur n'intervient pas. Par contre, le compilateur réserve une zone mémoire pour la variable qui a été définie comme constante. Cela a deux avantages par rapport à l'utilisation de ``#define``. Premièrement, il est possible de définir comme constante n'importe quel type de données en C, y compris des structures ou des pointeurs alors qu'avec un ``#define`` on ne peut définir que des nombres ou des chaînes de caractères. Ensuite, comme une ``const`` est stockée en mémoire, il est possible d'obtenir son adresse et de l'examiner via un :term:`debugger`. +Il y a deux façons de définir des constantes dans les versions récentes de C [C99]_. La première est via la macro ``#define`` du préprocesseur. Cette macro permet de remplacer une chaîne de caractères (par exemple ``M_PI`` qui provient de `math.h`_) par un nombre ou une autre chaîne de caractères. Ce remplacement s'effectue avant la compilation. Dans le cas de ``M_PI`` ci-dessus, le préprocesseur remplace toute les occurrences de cette chaîne de caractères par la valeur numérique de :math:`\pi`. Lorsqu'une variable ``const`` est utilisée, la situation est un peu différente. Le préprocesseur n'intervient pas. Par contre, le compilateur réserve une zone mémoire pour la variable qui a été définie comme constante. Cela a deux avantages par rapport à l'utilisation de ``#define``. Premièrement, il est possible de définir comme constante n'importe quel type de données en C, y compris des structures ou des pointeurs alors qu'avec un ``#define`` on ne peut définir que des nombres ou des chaînes de caractères. Ensuite, comme une ``const`` est stockée en mémoire, il est possible d'obtenir son adresse et de l'examiner via un :term:`debugger`. Unions et énumérations @@ -109,7 +109,7 @@ Le compilateur C alloue la taille pour l'``union`` de façon à ce qu'elle puiss :start-after: ///EEE :end-before: ///FFF -Lors de son exécution, la zone mémoire correspondant à l'union ``u`` sera simplement interprétée comme contenant un ``char``, même si on vient d'y stocker un entier. En pratique, lorsqu'une ``union`` est vraiment nécessaire pour des raisons d'économie de mémoire, on l'encapsulera dans une ``struct`` en utilisant un type énuméré qui permet de spécifier le type de données qui est présent dans l'``union``. +Lors de son exécution, la zone mémoire correspondant à l'union ``u`` sera simplement interprétée comme contenant un ``char``, même si on vient d'y stocker un entier. En pratique, lorsqu'une ``union`` est vraiment nécessaire pour des raisons d'économie de mémoire, on préférera la placer dans une ``struct`` en utilisant un type énuméré qui permet de spécifier le type de données qui est présent dans l'``union``. .. literalinclude:: /C/S3-src/union.c :encoding: utf-8 @@ -197,8 +197,8 @@ La troisième zone est le :term:`segment des données non-initialisées`, réser -Le tas (ou heap) ----------------- +Le tas (ou `heap`) +------------------ La quatrième zone de la mémoire est le :term:`tas` (ou :term:`heap` en anglais). Vu l'importance pratique de la terminologie anglaise, c'est celle-ci que nous utiliserons dans le cadre de ce document. C'est une des deux zones dans laquelle un programme peut obtenir de la mémoire supplémentaire pour stocker de l'information. Un programme peut y réserver une zone permettant de stocker des données et y associer un pointeur. @@ -208,7 +208,11 @@ En C, la plupart des processus allouent et libèrent de la mémoire en utilisant La fonction `malloc(3)`_ prend comme argument la taille (en bytes) de la zone mémoire à allouer. La signature de la fonction `malloc(3)`_ demande que cette taille soit de type ``size_t``, c'est-à -dire le type retourné par l'expression ``sizeof``. Il est important de toujours utiliser ``sizeof`` lors du calcul de la taille d'une zone mémoire à allouer. `malloc(3)`_ retourne normalement un pointeur de type ``(void *)``. Ce type de pointeur est le type par défaut pour représenter dans un programme C une zone mémoire qui ne pointe pas vers un type de données particulier. En pratique, un programme va généralement utiliser `malloc(3)`_ pour allouer de la mémoire pour stocker différents types de données et le pointeur retourné par `malloc(3)`_ sera `casté` dans un pointeur du bon type. -.. note:: typecast en langage C +.. spelling:: + + typecast + +.. note:: ``typecast`` en langage C Comme le langage Java, le langage C supporte des conversions implicites et explicites entre les différents types de données. Ces conversions sont possibles entre les types primitifs et les pointeurs. Nous les rencontrerons régulièrement, par exemple lorsqu'il faut récupérer un pointeur alloué par `malloc(3)`_ ou le résultat de ``sizeof``. Contrairement au compilateur Java, le compilateur C n'émet pas toujours de message de :term:`warning` lors de l'utilisation de typecast qui risque d'engendrer une perte de précision. Ce problème est illustré par l'exemple suivant avec les nombres. @@ -229,7 +233,7 @@ Le programme ci-dessous illustre l'utilisation de `malloc(3)`_ et `free(3)`_. :start-after: ///AAA :end-before: ///BBB -Ce programme alloue trois zones mémoires. Le pointeur vers la première est sauvé dans le pointeur ``string``. Elle est destinée à contenir une chaîne de ``size`` caractères (avec un caractère supplémentaire pour stocker le caractère ``\0`` de fin de chaîne). Il y a deux points à remarquer concernant cette allocation. Tout d'abord, le pointeur retourné par `malloc(3)`_ est casté en un ``char *``. Cela indique au compilateur que ``string`` va bien contenir un pointeur vers une chaîne de caractères. Ce cast explicite rend le programme plus clair. Ensuite, la valeur de retour de `malloc(3)`_ est systématiquement testée. `malloc(3)`_ peut en effet retourner ``NULL`` lorsque la mémoire est remplie. Cela a peu de chance d'arriver dans un programme de test tel que celui-ci, mais tester les valeurs de retour des fonctions de la librairie est une bonne habitude à prendre lorsque l'on programme sous Unix. Le second pointeur, ``vector`` pointe vers une zone destiné à contenir un tableau d'entiers. Le dernier pointeur, ``fract_vect`` pointe vers une zone qui pourra stocker un tableau de ``Fraction``. Lors de son exécution, le programme affiche la sortie suivante. +Ce programme alloue trois zones mémoires. Le pointeur vers la première est sauvé dans le pointeur ``string``. Elle est destinée à contenir une chaîne de ``size`` caractères (avec un caractère supplémentaire pour stocker le caractère ``\0`` de fin de chaîne). Il y a deux points à remarquer concernant cette allocation. Tout d'abord, le pointeur retourné par `malloc(3)`_ est "casté" en un ``char *``. Cela indique au compilateur que ``string`` va bien contenir un pointeur vers une chaîne de caractères. Cette conversion explicite rend le programme plus clair. Ensuite, la valeur de retour de `malloc(3)`_ est systématiquement testée. `malloc(3)`_ peut en effet retourner ``NULL`` lorsque la mémoire est remplie. Cela a peu de chance d'arriver dans un programme de test tel que celui-ci, mais tester les valeurs de retour des fonctions de la librairie est une bonne habitude à prendre lorsque l'on programme sous Unix. Le second pointeur, ``vector`` pointe vers une zone destiné à contenir un tableau d'entiers. Le dernier pointeur, ``fract_vect`` pointe vers une zone qui pourra stocker un tableau de ``Fraction``. Lors de son exécution, le programme affiche la sortie suivante. .. literalinclude:: /C/S3-src/malloc.out :encoding: utf-8 @@ -246,7 +250,7 @@ Un autre exemple d'utilisation de `malloc(3)`_ est la fonction ``duplicate`` ci- :start-after: ///AAA :end-before: ///BBB -`malloc(3)`_ et `free(3)`_ sont fréquemment utilisés dans des programmes qui manipulent des structures de données dont la taille varie dans le temps. C'est le cas pour les différents sortes de listes chaînées, les piles, les queues, les arbres, ... L'exemple ci-dessous (:download:`/C/S3-src/stack.c`) illustre l'implémentation d'une pile simple en C. Le pointeur vers le sommet de la pile est défini comme une variable globale. Chaque élément de la pile est représenté comme un pointeur vers une structure qui contient un pointeur vers la donnée stockée (dans cet exemple des fractions) et l'élément suivant sur la pile. Les fonctions ``push`` et ``pop`` permettent respectivement d'ajouter un élément et de retirer un élément au sommet de la pile. La fonction ``push`` alloue la mémoire nécessaire avec `malloc(3)`_ tandis que la fonction ``pop`` utilise `free(3)`_ pour libérer la mémoire dès qu'un élément est retiré. +`malloc(3)`_ et `free(3)`_ sont fréquemment utilisés dans des programmes qui manipulent des structures de données dont la taille varie dans le temps. C'est le cas pour les différents sortes de listes chaînées, les piles, les queues, les arbres, ... L'exemple ci-dessous (:download:`/C/S3-src/stack.c`) illustre une implémentation d'une pile simple en C. Le pointeur vers le sommet de la pile est défini comme une variable globale. Chaque élément de la pile est représenté comme un pointeur vers une structure qui contient un pointeur vers la donnée stockée (dans cet exemple des fractions) et l'élément suivant sur la pile. Les fonctions ``push`` et ``pop`` permettent respectivement d'ajouter un élément et de retirer un élément au sommet de la pile. La fonction ``push`` alloue la mémoire nécessaire avec `malloc(3)`_ tandis que la fonction ``pop`` utilise `free(3)`_ pour libérer la mémoire dès qu'un élément est retiré. .. literalinclude:: /C/S3-src/stack.c @@ -286,7 +290,7 @@ Le tas (ou :term:`heap`) joue un rôle très important dans les programmes C. Le Un programmeur ne doit cependant `jamais` compter sur cet appel implicite à `free(3)`_. Ne pas libérer la mémoire lorsqu'elle n'est plus utilisée est un problème courant qui est généralement baptisé :term:`memory leak`. Ce problème est particulièrement gênant pour les processus tels que les serveurs Internet qui ne se terminent pas ou des processus qui s'exécutent longtemps. Une petite erreur de programmation peut causer un :term:`memory leak` qui peut après quelque temps consommer une grande partie de l'espace mémoire inutilement. Il est important d'être bien attentif à l'utilisation correcte de `malloc(3)`_ et de `free(3)`_ pour toutes les opérations d'allocation et de libération de la mémoire. -`malloc(3)` est la fonction d'allocation de mémoire la plus fréquemment utilisée [#fothermalloc]_. La librairie standard contient cependant d'autres fonctions permettant l'allocation et la réallocation de mémoire. `calloc(3)`_ est nettement moins utilisée que `malloc(3)`_. Elle a pourtant un avantage majeur par rapport à `malloc(3)`_ puisqu'elle initialise à zéro la zone de mémoire allouée. `malloc(3)`_ se contente d'allouer la zone de mémoire mais n'effectue aucune initialisation. Cela permet à `malloc(3)`_ d'être plus rapide, mais le programmeur ne doit jamais oublier qu'il ne peut pas utiliser `malloc(3)`_ sans initialiser la zone mémoire allouée. Cela peut s'observer en pratique avec le programme ci-dessous. Il alloue une zone mémoire pour ``v1``, l'initialise puis la libère. Ensuite, le programme alloue une nouvelle zone mémoire pour ``v2`` et y retrouve les valeurs qu'il avait stocké pour ``v1`` précédemment. En pratique, n'importe quelle valeur pourrait se trouver dans la zone retournée par `malloc(3)`. +`malloc(3)` est la fonction d'allocation de mémoire la plus fréquemment utilisée [#fothermalloc]_. La librairie standard contient cependant d'autres fonctions permettant d'allouer de la mémoire mais aussi de modifier des allocations antérieures. `calloc(3)`_ est nettement moins utilisée que `malloc(3)`_. Elle a pourtant un avantage majeur par rapport à `malloc(3)`_ puisqu'elle initialise à zéro la zone de mémoire allouée. `malloc(3)`_ se contente d'allouer la zone de mémoire mais n'effectue aucune initialisation. Cela permet à `malloc(3)`_ d'être plus rapide, mais le programmeur ne doit jamais oublier qu'il ne peut pas utiliser `malloc(3)`_ sans initialiser la zone mémoire allouée. Cela peut s'observer en pratique avec le programme ci-dessous. Il alloue une zone mémoire pour ``v1``, l'initialise puis la libère. Ensuite, le programme alloue une nouvelle zone mémoire pour ``v2`` et y retrouve les valeurs qu'il avait stocké pour ``v1`` précédemment. En pratique, n'importe quelle valeur pourrait se trouver dans la zone retournée par `malloc(3)`. .. literalinclude:: /C/S3-src/mallocinit.c :encoding: utf-8 @@ -378,6 +382,10 @@ Un étudiant pourrait vouloir éviter d'utiliser `malloc(3)`_ et écrire plutôt :start-after: ///BBB :end-before: ///CCC +.. spelling:: + + warning + Lors de la compilation, `gcc(1)`_ affiche le :term:`warning` ``In function ‘duplicate2’: warning: function returns address of local variable``. Ce warning indique que la ligne ``return str2;`` retourne l'adresse d'une variable locale qui n'est plus accessible à la fin de la fonction ``duplicate2``. En effet, l'utilisation de tableaux alloués dynamiquement sur la pile est équivalent à une utilisation implicite de `alloca(3)`_. La déclaration ``char str2[len];`` est équivalente à ``char *str2 =(char *)alloca(len*sizeof(char));`` et la zone mémoire allouée sur la pile pour ``str2`` est libérée lors de l'exécution de ``return str2;`` puisque toute mémoire allouée sur la pile est implicitement libérée à la fin de l'exécution de la fonction durant laquelle elle a été allouée. Donc, une fonction qui appelle ``duplicate2`` ne peut pas récupérer les données se trouvant dans la zone mémoire qui a été allouée par ``duplicate2``. @@ -386,11 +394,11 @@ Lors de la compilation, `gcc(1)`_ affiche le :term:`warning` ``In function ‘du .. [#fpossible] Pour des raisons de performance, le compilateur C ne génère pas de code permettant de vérifier automatiquement qu'un accès via un pointeur pointe vers une zone de mémoire qui est libre. Il est donc parfois possible d'accéder à une zone mémoire qui a été libérée, mais le programme n'a aucune garantie sur la valeur qu'il y trouvera. Ce genre d'accès à des zones mémoires libérées doit bien entendu être complètement proscrit. -.. [#ggetrlimit] Sur de nombreuses variantes de Unix, cette limite à la taille du stack dépend du matériel utilisé et est configurable par l'administrateur système. Un processus peut connaître la taille maximale de son stack en utilisant l'appel système `getrlimit(2)`_. L'administrateur système peut modifier ces limites via l'appel système `setrlimit(2)`_. La commande ``ulimit`` de `bash(1)`_ permet également de manipuler ces limites. +.. [#ggetrlimit] Sur de nombreuses variantes de Unix, cette limite à la taille du stack dépend du matériel utilisé et peut être configurée par l'administrateur système. Un processus peut connaître la taille maximale de son stack en utilisant l'appel système `getrlimit(2)`_. L'administrateur système peut modifier ces limites via l'appel système `setrlimit(2)`_. La commande ``ulimit`` de `bash(1)`_ permet également de manipuler ces limites. .. [#fetext] Dans de nombreuses variantes de Unix, il est possible de connaître le sommet du segment :term:`text` d'un processus grâce à la variable :term:`etext`. Cette variable, de type ``char`` est initialisée par le système au chargement du processus. Elle doit être déclarée comme variable de type ``extern char etext`` et son adresse (``&etext``) correspond au sommet du segment text. -.. [#fvmem] Nous verrons ultérieurement que grâce à l'utilisation de la mémoire virtuelle, il est possible pour un processus d'utiliser des zones de mémoire qui ne sont pas contigües. +.. [#fvmem] Nous verrons ultérieurement que grâce à l'utilisation de la mémoire virtuelle, il est possible pour un processus d'utiliser des zones de mémoire qui ne sont pas contiguës. .. [#fothermalloc] Il existe différentes alternatives à l'utilisation de `malloc(3)`_ pour l'allocation de mémoire comme `Hoard <http://www.hoard.org/>`_ ou `gperftools <http://code.google.com/p/gperftools/>`_ diff --git a/Theorie/Fichiers/fichiers.rst b/Theorie/Fichiers/fichiers.rst index 989372a8171ae46d8e41d21e5611a4fe8b16e96f..1544bec2c7a201f31465a1836aae94d3fc680f2e 100644 --- a/Theorie/Fichiers/fichiers.rst +++ b/Theorie/Fichiers/fichiers.rst @@ -1,5 +1,5 @@ .. -*- coding: utf-8 -*- -.. Copyright |copy| 2012 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal +.. Copyright |copy| 2012, 2019 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal .. Ce fichier est distribué sous une licence `creative commons <http://creativecommons.org/licenses/by-sa/3.0/>`_ .. _utilisateurs: @@ -8,6 +8,10 @@ Gestion des utilisateurs ======================== +.. spelling:: + + multi + Unix est un système d'exploitation multi-utilisateurs. Un tel système impose des contraintes de sécurité qui n'existent pas sur un système mono-utilisateur. Il est intéressant de passer en revue quelques unes de ces contraintes : - il doit être possible d'identifier et/ou d'authentifier les utilisateurs du système @@ -20,13 +24,16 @@ Aujourd'hui, la plupart des systèmes informatiques demandent une authentificati Les systèmes Unix supportent différents mécanismes d'authentification. Le plus simple et le plus utilisé est l'authentification par mot de passe. Chaque utilisateur est identifié par un nom d'utilisateur et il doit prouver son identité en tapant son mot de passe au démarrage de toute session sur le système. En pratique, une session peut s'établir localement sur l'ordinateur via son interface graphique par exemple ou à distance en faisant tourner un serveur tel que `sshd(8)`_ sur le système Unix et en permettant aux utilisateurs de s'y connecter via Internet en utilisant un client `ssh(1)`_. Dans les deux cas, le système d'exploitation lance un processus `login(1)`_ qui permet de vérifier le nom d'utilisateur et le mot de passe fourni par l'utilisateur. Si le mot de passe correspond à celui qui est stocké sur le système, l'utilisateur est authentifié et son shell peut démarrer. Sinon, l'accès au système est refusé. -.. todo:: note:: Les mots de passe sous Unix -.. Le mot de passe associé à un utilisateur est un :term:`secret partagé` entre l'utilisateur et le système Unix. Conceptuellement, ce mot de passe est stocké sur le système et lorsque l'utilisateur tape son mot de passe, le `pam(8)`_ +.. spelling:: + username + inode + inodes + Lorsqu'un utilisateur se connecte sur un système Unix, il fournit son nom d'utilisateur ou `username`. Ce nom d'utilisateur est une chaîne de caractères qui est facile à mémoriser par l'utilisateur. D'un point de vue implémentation, un système d'exploitation préfère manipuler des nombres plutôt que des chaînes de caractères. Unix associe à chaque utilisateur un identifiant qui est stocké sous la forme d'un nombre entier positif. La table de correspondance entre l'identifiant d'utilisateur et le nom d'utilisateur est le fichier `/etc/passwd`. Ce fichier texte, comme la grande majorité des fichiers de configuration d'un système Unix, comprend pour chaque utilisateur l'information suivante : - - nom d'utilisateur (username) + - nom d'utilisateur (`username`) - mot de passe (sur les anciennes versions de Unix) - identifiant de l'utilisateur (:term:`userid`) - identifiant du groupe principal auquel l'utilisateur appartient @@ -44,7 +51,7 @@ L'extrait ci-dessous présente un exemple de fichier ``/etc/passwd``. Des détai daemon:*:1:1:System Services:/var/root:/usr/bin/false slampion:*:1252:1252:Séraphin Lampion:/home/slampion:/bin/bash -Il y a en pratique trois types d'utilisateurs sur un système Unix. L'utilisateur :term:`root` est l'administrateur du système. C'est l'utilisateur qui a le droit de réaliser toutes les opérations sur le système. Il peut créer de nouveaux utilisateurs, mais aussi reformatter les disques, arrêter le système, interrompre des processus utilisateurs ou accéder à l'ensemble des fichiers sans restriction. Par convention, cet utilisateur a l'identifiant ``0``. Ensuite, il y a tous les utilisateurs `normaux` du système Unix. Ceux-ci ont le droit d'accéder à leurs fichiers, d'interagir avec leurs processus mais en général ne peuvent pas manipuler les fichiers d'autres utilisateurs ou interrompre leurs processus. L'utilisateur `slampion` dans l'exemple ci-dessus est un utilisateur `normal`. Enfin, pour faciliter l'administration du système, certains systèmes Unix utilisent des utilisateurs qui correspondent à un service particulier comme l'utilisateur `daemon` dans l'exemple ci-dessus. Une discussion de ce type d'utilisateur sort du cadre de ces notes. Le lecteur intéressé pourra consulter une référence sur l'administration des système Unix telle que [AdelsteinLubanovic2007]_ ou [Nemeth+2010]_. +Il y a en pratique trois types d'utilisateurs sur un système Unix. L'utilisateur :term:`root` est l'administrateur du système. C'est l'utilisateur qui a le droit de réaliser toutes les opérations sur le système. Il peut créer de nouveaux utilisateurs, mais aussi formatter les disques, arrêter le système, interrompre des processus utilisateurs ou accéder à l'ensemble des fichiers sans restriction. Par convention, cet utilisateur a l'identifiant ``0``. Ensuite, il y a tous les utilisateurs `normaux` du système Unix. Ceux-ci ont le droit d'accéder à leurs fichiers, d'interagir avec leurs processus mais en général ne peuvent pas manipuler les fichiers d'autres utilisateurs ou interrompre leurs processus. L'utilisateur `slampion` dans l'exemple ci-dessus est un utilisateur `normal`. Enfin, pour faciliter l'administration du système, certains systèmes Unix utilisent des utilisateurs qui correspondent à un service particulier comme l'utilisateur `daemon` dans l'exemple ci-dessus. Une discussion de ce type d'utilisateur sort du cadre de ces notes. Le lecteur intéressé pourra consulter une référence sur l'administration des système Unix telle que [AdelsteinLubanovic2007]_ ou [Nemeth+2010]_. Unix associe à chaque processus un identifiant d'utilisateur. Cet identifiant est stocké dans l'entrée du processus dans la table des processus. Un processus peut récupérer son identifiant d'utilisateur via l'appel système `getuid(2)`_. Outre cet appel système, il existe également l'appel système `setuid(2)`_ qui permet de modifier le :term:`userid` du processus en cours d'exécution. Pour des raisons évidentes de sécurité, seul un processus appartenant à l'administrateur système (:term:`root`) peut exécuter cet appel système. C'est le cas par exemple du processus `login(1)`_ qui appartient initialement à :term:`root` puis exécute `setuid(2)`_ afin d'appartenir à l'utilisateur authentifié puis exécute `execve(2)`_ pour lancer le premier shell appartenant à l'utilisateur. @@ -58,78 +65,6 @@ Systèmes de fichiers Outre un processeur et une mémoire, la plupart des ordinateurs actuels sont en général équipés d'un ou plusieurs dispositifs de stockage. Les dispositifs les plus courants sont le disque dur, le lecteur de CD/DVD, la clé USB, la carte mémoire, ... Ces dispositifs de stockage ont des caractéristiques techniques très différentes. Certains stockent l'information sous forme magnétique, d'autres sous forme électrique ou en creusant via un laser des trous dans un support physique. D'un point de vue logique, ils offrent tous une interface très similaire au système d'exploitation qui veut les utiliser. -En pratique, on peut modéliser la plupart des dispositifs de stockage comme étant une zone de stockage qui est décomposée en blocs de quelques centaines ou milliers d'octets qui sont appelés des secteurs. Sur de nombreux dispositifs de stockage, un :term:`secteur` peut stocker un bloc de 512 octets. Chaque secteur est identifié par une adresse et le dispositif de stockage offre au système d'exploitation une interface simple comprenant deux fonctions : - - - ``int err=device_read(addr_t addr, sector_t *buf)`` où ``addr`` est l'adresse du secteur dont la lecture est demandée et ``buf`` un buffer destiné à recevoir le contenu du secteur qui a été lu - - ``int err=device_write(addr_t addr, sector_t *buf)`` où ``addr`` est l'adresse d'un secteur et ``buf`` un buffer contenant le secteur à écrire - -Un dispositif de stockage permet donc essentiellement de lire et d'écrire des secteurs entiers. La plupart des implémentations sont optimisées pour pouvoir lire ou écrire plusieurs secteurs consécutifs, mais la plus petite unité de lecture ou d'écriture est le :term:`secteur`, c'est-à -dire un bloc de 512 octets consécutifs. La plupart des programmes ne sont pas prêts à manipuler directement de tels dispositifs de stockage et Unix comme d'autres systèmes d'exploitation contient une interface de plus haut niveau qui permet aux programmes applicatifs d'utiliser des fichiers et des répertoires. Ces fichiers et répertoires sont une abstraction qui est construite par le système d'exploitation au-dessus des secteurs qui sont stockés sur le disque. Cette abstraction est appelée un :term:`système de fichiers` ou :term:`filesystem` en anglais. Il existe des centaines de systèmes de fichiers et Linux supporte quelques dizaines de systèmes de fichiers différents. - -En simplifiant, un système de fichier Unix s'appuie toujours sur quelques principes de base assez simples. Le premier est qu'un :term:`fichier` est une suite ordonnée d'octets. Un nom est associé à cette suite d'octets et les programmes utilisent ce nom pour accéder au fichier. En pratique, cette suite ordonnée d'octets sera stockée dans un ou plusieurs secteurs. Comme rien ne garantit que la taille d'un fichier sera un multiple du nombre d'octets dans un secteur, le système d'exploitation devra être capable de gérer des secteurs qui sont partiellement remplis. En outre, même si c'est souvent efficace du point de vue des performances, rien ne garantit qu'un fichier sera stocké dans des secteurs consécutifs. Le système de fichiers doit donc pouvoir supporter des fichiers qui sont composés de données qui sont stockées dans des secteurs se trouvant n'importe où sur le dispositif de stockage. - -Sous Unix, le lien entre les différents secteurs qui composent un fichier est fait grâce à l'utilisation des inodes. Un :term:`inode` est une structure de données qui est stockée sur le disque et contient les méta-informations qui sont relatives à un fichier. -Un inode peut être représenté par une structure de données similaire à la structure ci-dessous [#fminixfs]_. Un :term:`inode` a une taille fixe et une partie du disque (en pratique un ensemble contigu de secteurs, souvent au début du disque) est réservée pour stocker `N` inodes. Connaissant la taille de cette zone, il est possible d'accéder facilement au :math:`i^{ème}` inode du système de fichiers. - -.. code-block:: c - - struct simple_inode { - uint16 mode; - uid_t uid; - gid_t gid; - uint32 size; - unit32 atime; - unit32 mtime; - unit32 ctime; - uint16 nlinks; - uint16 zone[10]; - }; - -L':term:`inode` contient les principales méta-données qui sont associées au fichier, à savoir : - - - le ``mode`` qui contient un ensemble de drapeaux binaires avec les permissions associées au fichier - - le ``userid`` du propriétaire du fichier - - le ``groupid`` qui identifie le groupe auquel le fichier appartient - - la taille du fichier en bytes - - l'instant de dernier accès au fichier (``atime``) - - l'instant de dernière modification du fichier (``mtime``) - - l'instant de dernier changement d'état du fichier (``ctime``) - - le nombre de liens vers ce fichier (``nlinks``) - - la liste ordonnée des secteurs qui contiennent le fichier (``zone``) [#finode]_ - - -Le lecteur attentif aura noté que parmi les méta-données qui sont associées via un :term:`inode` à un fichier on ne retrouve pas le nom du fichier. Sous Unix, le nom de fichier n'est pas directement associé au fichier lui-même comme sa taille ou ses permissions. Il est stocké dans les répertoires. Un :term:`répertoire` est une abstraction qui permet de regrouper ensemble plusieurs fichiers et/ou répertoires. En pratique, un :term:`répertoire` est un fichier qui a un format spécial. Il contient une suite d'entrées qui contiennent chacune un nom (de fichier ou de répertoire), une indication de type qui permet notamment de distinguer les fichiers des répertoires et le numéro de l':term:`inode` qui contient les méta-données du fichier ou répertoire. A titre d'exemple, l'extrait ci-dessous [#fext2fs]_ est la définition d'une entrée de répertoire dans le système de fichiers de type `ext2` sous Linux [Card+1994]_. Cette entrée contient également une indication de longueur. Un répertoire contiendra une entrée par fichier ou répertoire qui y a été placé. - -.. code-block:: c - - /* - * Structure of a directory entry - */ - #define EXT2_NAME_LEN 255 - - ext2_dir_entry_2 { - __le32 inode; /* Inode number */ - __le16 rec_len; /* Directory entry length */ - __u8 name_len; /* Name length */ - __u8 file_type; - char name[EXT2_NAME_LEN]; /* File name */ - }; - - /* - * Ext2 directory file types. Only the low 3 bits are used. The - * other bits are reserved for now. - */ - enum { - EXT2_FT_UNKNOWN = 0, - EXT2_FT_REG_FILE = 1, - EXT2_FT_DIR = 2, - EXT2_FT_CHRDEV = 3, - EXT2_FT_BLKDEV = 4, - EXT2_FT_FIFO = 5, - EXT2_FT_SOCK = 6, - EXT2_FT_SYMLINK = 7, - EXT2_FT_MAX = 8 - }; - Dans un système de fichiers Unix, l'ensemble des répertoires et fichiers est organisé sous la forme d'un arbre. La racine de cet arbre est le répertoire ``/``. Il est localisé sur un des dispositifs de stockage du système. Le système de fichiers Unix permet d'intégrer facilement des systèmes de fichiers qui se trouvent sur différents dispositifs de stockage. Cette opération est en général réalisée par l'administrateur système en utilisant la commande `mount(8)`_. A titre d'exemple, voici quelques répertoires qui sont montés sur un système Linux. .. code-block:: console @@ -204,40 +139,40 @@ Les valeurs de ces bits sont représentés pas les symboles ``rwx`` dans l'outpu Le :term:`nibble` de poids fort des bits de permission sert à encoder des permissions particulières relatives aux fichiers et répertoires. Par exemple, lorsque la permission ``S_ISUID (04000)`` est associée à un exécutable, elle indique que celui-ci doit s'exécuter avec les permissions du propriétaire de l'exécutable et pas les permissions de l'utilisateur. Cette permission spéciale est utilisée par des programmes comme `passwd(1)`_ qui doivent disposer des permissions de l'administrateur système pour s'exécuter correctement (`passwd(1)`_ doit modifier le fichier `passwd(5)`_ qui appartient à l'administrateur système). -Les exemples ci-dessous présentent le contenu partiel d'un répertoire avec en première colonne le numéro de l'inode associé à chaque fichier/répertoire. +Les exemples ci-dessous présentent le contenu partiel d'un répertoire. .. code-block:: console $ ls -lai /etinfo/users/obo total 1584396 - 24182823 drwx------ 78 obo stafinfo 4096 Mar 17 00:34 . - 2 drwxr-xr-x 93 root root 4096 Feb 22 11:37 .. - 24190937 -rwxr-xr-x 1 obo stafinfo 11490 Feb 28 00:43 a.out - 24183726 -rw------- 1 obo stafinfo 4055 Mar 22 15:13 .bash_history - 24183731 -rw-r--r-- 1 obo stafinfo 55 Sep 18 1995 .bash_profile - 24183732 -rw-r--r-- 1 obo stafinfo 101 Aug 28 2003 .bashrc - 24183523 drwxr-xr-x 2 obo stafinfo 4096 Nov 22 2004 bin - 24190938 -rw-r--r-- 1 obo stafinfo 346 Feb 13 15:37 hello.c - 48365569 drwxr-xr-x 3 obo stafinfo 4096 Mar 2 09:30 sinf1252 - 48791553 drwxr-xr-x 2 obo stafinfo 4096 May 17 2011 src + drwx------ 78 obo stafinfo 4096 Mar 17 00:34 . + drwxr-xr-x 93 root root 4096 Feb 22 11:37 .. + -rwxr-xr-x 1 obo stafinfo 11490 Feb 28 00:43 a.out + -rw------- 1 obo stafinfo 4055 Mar 22 15:13 .bash_history + -rw-r--r-- 1 obo stafinfo 55 Sep 18 1995 .bash_profile + -rw-r--r-- 1 obo stafinfo 101 Aug 28 2003 .bashrc + drwxr-xr-x 2 obo stafinfo 4096 Nov 22 2004 bin + -rw-r--r-- 1 obo stafinfo 346 Feb 13 15:37 hello.c + drwxr-xr-x 3 obo stafinfo 4096 Mar 2 09:30 sinf1252 + drwxr-xr-x 2 obo stafinfo 4096 May 17 2011 src -Dans un système Unix, que ce soit au niveau du shell ou dans n'importe quel processus écrit par exemple en langage C, les fichiers peuvent être spécifiés de deux façons. La première est d'indiquer le chemin complet depuis la racine qui permet d'accéder au fichier. Le chemin ``/etinfo/users/obo`` passé comme argument à la commande `ls(1)`_ ci-dessus en est un exemple. Le premier caractère ``/`` correspond à la racine du système de fichiers et ensuite ce caractère est utilisé comme séparateur entre les répertoires successifs. Ainsi, le fichier ``/etinfo/users/obo/hello.c`` est un fichier qui a comme nom ``hello.c`` qui se trouve dans un répertoire nommé ``obo`` qui lui-même se trouve dans le répertoire ``users`` qui est dans le répertoire baptisé ``etinfo`` dans le répertoire racine. La seconde façon de spécifier un nom de fichier est de préciser son nom relatif. Pour éviter de forcer l'utilisateur à spécifier chaque fois le nom complet des fichiers et répertoires auxquels il veut accéder, le kernel maintient dans sa table des processus le :term:`répertoire courant` de chaque processus. Par défaut, lorsqu'un processus est lancé, son répertoire courant est le répertoire à partir duquel le programme a été lancé. Ainsi, lorsque l'utilisateur tape une commande comme ``gcc hello.c`` depuis son shell, le processus `gcc(1)`_ peut directement accéder au fichier ``hello.c`` qui se situe dans le répertoire courant. Un processus peut modifier son répertoire courant en utilisant l'appel système `chdir(2)`_. +Dans un système Unix, que ce soit au niveau du shell ou dans n'importe quel processus écrit par exemple en langage C, les fichiers peuvent être spécifiés de deux façons. La première est d'indiquer le chemin complet depuis la racine qui permet d'accéder au fichier. Le chemin ``/etinfo/users/obo`` passé comme argument à la commande `ls(1)`_ ci-dessus en est un exemple. Le premier caractère ``/`` correspond à la racine du système de fichiers et ensuite ce caractère est utilisé comme séparateur entre les répertoires successifs. Ainsi, le fichier ``/etinfo/users/obo/hello.c`` est un fichier qui a comme nom ``hello.c`` qui se trouve dans un répertoire nommé ``obo`` qui lui-même se trouve dans le répertoire ``users`` qui est dans le répertoire baptisé ``etinfo`` dans le répertoire racine. La seconde façon de spécifier un nom de fichier est de préciser son nom relatif. Pour éviter de forcer l'utilisateur à spécifier chaque fois le nom complet des fichiers et répertoires auxquels il veut accéder, le noyau maintient dans sa table des processus le :term:`répertoire courant` de chaque processus. Par défaut, lorsqu'un processus est lancé, son répertoire courant est le répertoire à partir duquel le programme a été lancé. Ainsi, lorsque l'utilisateur tape une commande comme ``gcc hello.c`` depuis son shell, le processus `gcc(1)`_ peut directement accéder au fichier ``hello.c`` qui se situe dans le répertoire courant. Un processus peut modifier son répertoire courant en utilisant l'appel système `chdir(2)`_. .. code-block:: c #include <unistd.h> - int chdir(const char *path); + int chdir(const char *path); -Cet appel système prend comme argument une chaîne de caractères contenant le nom du nouveau répertoire courant. Ce nom peut être soit un nom complet (commençant par ``/``), ou un nom relatif au répertoire courant actuel. Dans ce cas, il est parfois utile de pouvoir référer au répertoire parent du répertoire courant. Cela se fait en utilisant l'alias ``..`` qui dans chaque répertoire correspond au répertoire parent. Ainsi, si le répertoire courant est ``/etinfo/users``, alors le répertoire ``../../bin`` est le répertoire ``bin`` se trouvant dans le répertoire racine. Depuis le shell, il est possible de modifier le répertoire courant avec la commande `cd(1posix)`_. La commande `pwd(1)`_ affiche le répertoire courant actuel. +Cet appel système prend comme argument une chaîne de caractères contenant le nom du nouveau répertoire courant. Ce nom peut être soit un nom complet (commençant par ``/``), ou un nom relatif au répertoire courant actuel. Dans ce cas, il est parfois utile de pouvoir référer au répertoire parent du répertoire courant. Cela se fait en utilisant ``..``. Dans chaque répertoire, cet alias correspond au répertoire parent. Ainsi, si le répertoire courant est ``/etinfo/users``, alors le répertoire ``../../bin`` est le répertoire ``bin`` se trouvant dans le répertoire racine. Depuis le shell, il est possible de modifier le répertoire courant avec la commande `cd(1posix)`_. La commande `pwd(1)`_ affiche le répertoire courant actuel. Il existe plusieurs appels systèmes et fonctions de la librairie standard qui permettent de parcourir le système de fichiers. Les principaux sont : - l'appel système `stat(2)`_ permet de récupérer les méta-données qui sont associées à un fichier ou un répertoire. La commande `stat(1)`_ fournit des fonctionnalités similaires depuis le shell. - les appels systèmes `chmod(2)`_ et `chown(2)`_ permettent de modifier respectivement le mode (i.e. les permissions), le propriétaire et le groupe associés à un fichier. Les commandes `chmod(1)`_, `chown(1)`_ et `chgrp(1)`_ permettent de faire de même depuis le shell. - - l'appel système `utime(2)`_ permet de modifier les timestamps associés à un fichier/répertoire. Cet appel système est utilisé par la commande `touch(1)`_ + - l'appel système `utime(2)`_ permet de modifier les dates de création/modification associées à un fichier/répertoire. Cet appel système est utilisé par la commande `touch(1)`_ - l'appel système `rename(2)`_ permet de changer le nom d'un fichier ou d'un répertoire. Il est utilisé notamment par la commande `rename(1)`_ - l'appel système `mkdir(2)`_ permet de créer un répertoire alors que l'appel système `rmdir(2)`_ permet d'en supprimer un - les fonctions de la librairie `opendir(3)`_, `closedir(3)`_, et `readdir(3)`_ permettent de consulter le contenu de répertoires. @@ -255,8 +190,13 @@ Les fonctions de manipulation des répertoires méritent que l'on s'y attarde un char d_name[256]; /* filename */ }; + +.. spelling:: -Cette structure comprend le numéro d'inode contenu dans ses deux premiers membres, la longueur de l'entrée, son type et le nom de l'entrée dans le répertoire. Chaque appel à `readdir(3)`_ retourne un pointeur vers une structure de ce type. + l'inode + métadonnée + +Cette structure comprend le numéro de l'inode, c'est-à -dire la métadonnée qui contient les informations relatives au fichier/répertoire, la position de l'entrée ``dirent`` qui suite, la longueur de l'entrée, son type et le nom de l'entrée dans le répertoire. Chaque appel à `readdir(3)`_ retourne un pointeur vers une structure de ce type. L'extrait de code ci-dessous permet de lister tous les fichiers présents dans le répertoire ``name``. @@ -269,9 +209,10 @@ L'extrait de code ci-dessous permet de lister tous les fichiers présents dans l La lecture d'un répertoire avec `readdir(3)`_ commence au début de ce répertoire. A chaque appel à `readdir(3)`_, le programme appelant récupère un pointeur vers une zone mémoire contenant une structure ``dirent`` avec l'entrée suivante du répertoire ou ``NULL`` lorsque la fin du répertoire est atteinte. Si une fonction doit relire à nouveau un répertoire, cela peut se faire en utilisant `seekdir(3)`_ ou `rewinddir(3)`_. + .. note:: `readdir(3)`_ et les threads - La fonction `readdir(3)`_ est un exemple de fonction non-réentrante qu'il faut éviter d'utiliser dans une application multithreadée dont plusieurs threads doivent pouvoir parcourir le même répertoire. Ce problème est causé par l'utilisation d'une zone de mémoire ``static`` afin de stocker la structure dont le pointeur est retourné par `readdir(3)`_. Dans une application utilisant plusieurs threads, il faut utiliser la fonction `readdir_r(3)`_ : + La fonction `readdir(3)`_ est un exemple de fonction non-réentrante qu'il faut éviter d'utiliser dans une application dont plusieurs threads doivent pouvoir parcourir le même répertoire. Ce problème est causé par l'utilisation d'une zone de mémoire ``static`` afin de stocker la structure dont le pointeur est retourné par `readdir(3)`_. Dans une application utilisant plusieurs threads, il faut utiliser la fonction `readdir_r(3)`_ : .. code-block:: c @@ -347,7 +288,7 @@ Du point de vue des appels systèmes de manipulation des fichiers, un fichier es int open(const char *pathname, int flags); int open(const char* pathname, int flags, mode_t mode); -Il existe deux variantes de l'appel système `open(2)`_. La première permet d'ouvrir des fichiers existants. Elle prend deux arguments. La deuxième permet de créer un nouveau fichier et d'ensuite l'ouvrir. Elle prend trois arguments. Le premier argument est le nom absolu ou relatif du fichier dont l'ouverture est demandée. Le deuxième argument est un entier qui contient un ensemble de drapeaux binaires qui précisent la façon dont le fichier doit être ouvert. Ces drapeaux sont divisés en deux groupes. Le premier groupe est relatif à l'accès en lecture et/ou en écriture du fichier. Lors de l'ouverture d'un fichier avec `open(2)`_, il est nécessaire de spécifier l'un des trois drapeaux d'accès suivants : +Il existe deux variantes de l'appel système `open(2)`_. La première permet d'ouvrir des fichiers existants. Elle prend deux arguments. La deuxième permet de créer un nouveau fichier et l'ouvre ensuite. Elle prend trois arguments. Le premier argument est le nom absolu ou relatif du fichier dont l'ouverture est demandée. Le deuxième argument est un entier qui contient un ensemble de drapeaux binaires qui précisent la façon dont le fichier doit être ouvert. Ces drapeaux sont divisés en deux groupes. Le premier groupe est relatif à l'accès en lecture et/ou en écriture du fichier. Lors de l'ouverture d'un fichier avec `open(2)`_, il est nécessaire de spécifier l'un des trois drapeaux d'accès suivants : - ``O_RDONLY`` : indique que le fichier est ouvert uniquement en lecture. Aucune opération d'écriture ne sera effectuée sur le fichier. - ``O_WRONLY`` : indique que le fichier est ouvert uniquement en écriture. Aucune opération de lecture ne sera effectuée sur le fichier. @@ -400,7 +341,7 @@ Les deux appels systèmes permettant de lire et d'écrire dans un fichier sont r Ces deux appels systèmes prennent trois arguments. Le premier est le `descripteur du fichier` sur lequel l'opération doit être effectuée. Le second est un pointeur ``void *`` vers la zone mémoire à lire ou écrire et le dernier est la quantité de données à lire/écrire. Si l'appel système réussit, il retourne le nombre d'octets qui ont été écrits/lus et sinon une valeur négative et la variable ``errno`` donne plus de précisions sur le type d'erreur. `read(2)`_ retourne ``0`` lorsque la fin du fichier a été atteinte. -Il est important de noter que `read(2)`_ et `write(2)`_ permettent de lire et d'écrire des séquences contigües d'octets. Lorsque l'on écrit ou lit des chaînes de caractères dans lesquels chaque caractère est représenté sous la forme d'un byte, il est possible d'utiliser `read(2)`_ et `write(2)`_ pour lire et écrire d'autres types de données que des octets comme le montre l'exemple ci-dessous. +Il est important de noter que `read(2)`_ et `write(2)`_ permettent de lire et d'écrire des séquences contiguës d'octets. Lorsque l'on écrit ou lit des chaînes de caractères dans lesquels chaque caractère est représenté sous la forme d'un byte, il est possible d'utiliser `read(2)`_ et `write(2)`_ pour lire et écrire d'autres types de données que des octets comme le montre l'exemple ci-dessous. .. literalinclude:: /Fichiers/src/read.c :encoding: utf-8 @@ -458,7 +399,7 @@ Cet appel système prend trois arguments. Le premier est le :term:`descripteur d .. note:: Fichiers temporaires - Il est parfois nécessaire dans un programme de créer des fichiers temporaires qui sont utilisés pour effectuer des opérations dans le processus sans pour autant être visible dans d'autres processus et sur le système de fichiers. Il est possible d'utiliser `open(2)`_ pour créer un tel fichier temporaire, mais il faut dans ce cas prévoir tous les cas d'erreur qui peuvent se produire lorsque par exemple plusieurs instances du même programme s'exécutent au même moment. Une meilleure solution est d'utiliser la fonction de la librairie `mkstemp(3)`_. Cette fonction prend comme argument un template de nom de fichier qui se termine par ``XXXXXX`` et génère un nom de fichier unique et retourne un descripteur de fichier associé à ce fichier. Elle s'utilise généralement comme suit : + Il est parfois nécessaire dans un programme de créer des fichiers temporaires qui sont utilisés pour effectuer des opérations dans le processus sans pour autant être visible dans d'autres processus et sur le système de fichiers. Il est possible d'utiliser `open(2)`_ pour créer un tel fichier temporaire, mais il faut dans ce cas prévoir tous les cas d'erreur qui peuvent se produire lorsque par exemple plusieurs instances du même programme s'exécutent au même moment. Une meilleure solution est d'utiliser la fonction de la librairie `mkstemp(3)`_. Cette fonction prend comme argument un modèle de nom de fichier qui se termine par ``XXXXXX`` et génère un nom de fichier unique et retourne un descripteur de fichier associé à ce fichier. Elle s'utilise généralement comme suit : .. code-block:: c @@ -485,44 +426,6 @@ Cet appel système prend trois arguments. Le premier est le :term:`descripteur d Dans certains cas il est utile de pouvoir dupliquer un descripteur de fichier. C'est possible avec les appels systèmes `dup(2)`_ et `dup2(2)`_. L'appel système `dup(2)`_ prend comme argument un descripteur de fichier et retourne le plus petit descripteur de fichier libre. Lorsqu'un descripteur de fichier a été dupliqué avec `dup(2)`_ les deux descripteurs de fichiers partagent le même :term:`offset pointer` et les mêmes modes d'accès au fichier. -.. _pipe: - -Les pipes ---------- - -Les appels systèmes de manipulation des fichiers permettent d'accéder à des données sur des dispositifs de stockage, mais ils peuvent aussi être utilisés pour échanger des informations entre processus. Les systèmes d'exploitation de la famille Unix supportent plusieurs mécanismes permettant à des processus de communiquer. Le plus simple est le :term:`pipe`. Un :term:`pipe` est un flux de bytes unidirectionnel qui relie deux processus qui ont un ancêtre commun. L'un des processus peut écrire sur le :term:`pipe` et le second peut lire sur le :term:`pipe`. Un :term:`pipe` est créé en utilisant l'appel système `pipe(2)`_. - - -.. todo:: dessin de pipe - - -.. code-block:: c - - #include <unistd.h> - - int pipe(int fd[2]); - - -Cet appel système prend comme argument un tableau permettant de stocker deux descripteurs de fichiers. Ces deux descripteurs de fichiers sont utilisés pour respectivement lire et écrire sur le :term:`pipe`. ``fd[0]`` est le descripteur de fichier sur lequel les opérations de lecture seront effectuées tandis que les opérations d'écriture se feront sur ``fd[1]``. Chaque fois qu'une donnée est écrite sur ``fd[1]`` avec l'appel système ``write(fd[1],...)``, elle devient disponible sur le descripteur de fichiers ``fd[0]`` et peut être lue avec ``read(fd[0],...)``. Même si il est possible de créer un :term:`pipe` dans un processus unique, `pipe(2)`_ s'utilise en général entre un père et son fils. Le programme ci-dessous illustre cette utilisation de pipes pour permettre à un processus père d'échanger de l'information avec son processus fils. - -.. literalinclude:: /Fichiers/src/fork-pipe.c - :encoding: utf-8 - :language: c - :start-after: ///AAA - :end-before: ///BBB - - -Dans cet exemple, le processus père crée d'abord un :term:`pipe`. Ensuite, il crée un fils avec `fork(2)`_. Comme l'exécution de `fork(2)`_ ne ferme pas les descripteurs de fichiers ouverts, le processus père et le processus fils ont accès au :term:`pipe` via ses deux descripteurs. Le développeur de ce programme a choisi que le processus père écrirait sur le :term:`pipe` tandis que le fils l'utiliserait uniquement en lecture. Le père (resp. fils) ferme donc le descripteur de fichier d'écriture (resp. lecture). C'est une règle de bonne pratique qui permet souvent d'éviter des bugs. Le processus père envoie ensuite des entiers sur ``fd[1]`` puis ferme ce descripteur de fichier. Le processus fils quant à lui lit les entiers reçus sur le :term:`pipe` via ``fd[0]``. Le processus fils peut détecter la fermeture du descripteur ``fd[1]`` du :term:`pipe` par le père en analysant la valeur de retour de `read(2)`_. En effet, `read(2)`_ retourne une valeur ``0`` lorsque la dernière donnée envoyée sur le :term:`pipe` a été lue. Le fils ferme proprement le descripteur de fichier ``fd[0]`` avant de se terminer. - -En pratique, les pipes sont utilisés notamment par le shell. En effet, lorsqu'une commande telle que ``cat test.txt | grep "student"`` est exécutée, le shell relie via un :term:`pipe` la sortie standard de ``cat`` avec l'entrée standard de ``grep``. - - -.. todo:: FILE et librairie standard - - - -.. todo:: exemple : faire un file copy complet avec une taille de buffer fixée et mesurer le temps qu'il faut -> exercice - .. rubric:: Footnotes diff --git a/Theorie/Threads/coordination.rst b/Theorie/Threads/coordination.rst index c9240111cbaaa74e1322d5cea20be19c80ed1ab4..df087173da4ab3e8ba638447559c583338231144 100644 --- a/Theorie/Threads/coordination.rst +++ b/Theorie/Threads/coordination.rst @@ -1,5 +1,5 @@ .. -*- coding: utf-8 -*- -.. Copyright |copy| 2012 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal +.. Copyright |copy| 2012, 2019 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal .. Ce fichier est distribué sous une licence `creative commons <http://creativecommons.org/licenses/by-sa/3.0/>`_ @@ -8,8 +8,6 @@ Les sémaphores ============== -.. todo:: Mieux introduire les sémaphores et leur intérêt en comparant avec les moniteurs, la synchronisation par messages, etc. - Le problème de la coordination entre threads est un problème majeur. Outre les :term:`mutex` que nous avons présenté, d'autres solutions à ce problème ont été développées. Historiquement, une des premières propositions de coordination sont les sémaphores [Dijkstra1965b]_. Un :term:`sémaphore` est une structure de données qui est maintenue par le système d'exploitation et contient : - un entier qui stocke la valeur, positive ou nulle, du sémaphore. @@ -17,6 +15,12 @@ Le problème de la coordination entre threads est un problème majeur. Outre les Tout comme pour les :term:`mutex`, la queue associée à un sémaphore permet de bloquer les threads qui sont en attente d'une modification de la valeur du sémaphore. +.. spelling:: + + décrémentée + s'implémenter + l'implémentation + Une implémentation des sémaphores se compose en général de quatre fonctions : - une fonction d'initialisation qui permet de créer le sémaphore et de lui attribuer une valeur initiale nulle ou positive. @@ -43,7 +47,7 @@ La librairie POSIX comprend une implémentation des sémaphores [#fSysV]_ qui ex int sem_wait(sem_t *sem); int sem_post(sem_t *sem); -Le fichier `semaphore.h`_ contient les différentes définitions de structures qui sont nécessaires au bon fonctionnement des sémaphores ainsi que les signatures des fonctions de l'API. Un sémaphore est représenté par une structure de données de type ``sem_t``. Toutes les fonctions de manipulation des sémaphores prennent comme argument un pointeur vers le sémaphore concerné. +Le fichier `semaphore.h`_ contient les différentes définitions de structures qui sont nécessaires au bon fonctionnement des sémaphores ainsi que les signatures des fonctions de cette API. Un sémaphore est représenté par une structure de données de type ``sem_t``. Toutes les fonctions de manipulation des sémaphores prennent comme argument un pointeur vers le sémaphore concerné. Pour pouvoir utiliser un sémaphore, il faut d'abord l'initialiser. Cela se fait en utilisant la fonction `sem_init(3)`_ qui prend comme argument un pointeur vers le sémaphore à initialiser. Nous n'utiliserons pas le second argument dans ce chapitre. Le troisième argument est la valeur initiale, positive ou nulle, du sémaphore. @@ -78,7 +82,7 @@ La fonction `sem_post(3)`_ quant à elle peut schématiquement s'implémenter co } } -Ces deux opérations sont bien entendu des opérations qui ne peuvent s'exécuter simultanément. Leur implémentation réelle comprend des sections critiques qui doivent être construites avec soin. Le pseudo-code ci-dessus ignore ces sections critiques. Des détails complémentaires sur l'implémentation des sémaphores peuvent être obtenus dans un textbook sur les systèmes d'exploitation [Stallings2011]_ [Tanenbaum+2009]_ . +Ces deux opérations sont bien entendu des opérations qui ne peuvent s'exécuter simultanément. Leur implémentation réelle comprend des sections critiques qui doivent être construites avec soin. Le pseudo-code ci-dessus ignore ces sections critiques. Des détails complémentaires sur l'implémentation des sémaphores peuvent être obtenus dans livre sur les systèmes d'exploitation [Stallings2011]_ [Tanenbaum+2009]_ . La meilleure façon de comprendre leur utilisation est d'analyser des problèmes classiques de coordination qui peuvent être résolus en utilisant des sémaphores. @@ -113,56 +117,8 @@ Les sémaphores peuvent être utilisés pour d'autres types de synchronisation. :end-before: ///BBB -Si conceptuellement un sémaphore initialisé à la valeur ``1`` est généralement utilisé comme un :term:`mutex`, il y a une différence importante entre les implémentations des sémaphores et des :term:`mutex`. Un sémaphore est conçu pour être manipulé par différents threads et il est fort possible qu'un thread exécute `sem_wait(3)`_ et qu'un autre exécute `sem_post(3)`_. Pour les mutex, certaines implémentations supposent que le même thread exécute `pthread_mutex_lock(3posix)`_ et `pthread_mutex_unlock(3posix)`_. Lorsque ces opérations doivent être effectuées dans des threads différents, il est préférable d'utiliser des sémaphores à la place de mutex. - - -Problème du rendez-vous ------------------------ - -Le problème du rendez-vous [Downey2008]_ est un problème assez courant dans les applications multithreadées. Considérons une application découpée en `N` threads. Chacun de ces threads travaille en deux phases. Durant la première phase, tous les threads sont indépendants et peuvent s'exécuter simultanément. Cependant, un thread ne peut démarrer sa seconde phase que si tous les `N` threads ont terminé leur première phase. L'organisation de chaque thread est donc : - -.. code-block:: c - - premiere_phase(); - // rendez-vous - seconde_phase(); - -Chaque thread doit pouvoir être bloqué à la fin de la première phase en attendant que tous les autres threads aient fini d'exécuter leur première phase. Cela peut s'implémenter en utilisant un mutex et un sémaphore. - - -.. code-block:: c - - sem_t rendezvous; - pthread_mutex_t mutex; - int count=0; - - sem_init(&rendezvous,0,0); - - -La variable ``count`` permet de compter le nombre de threads qui ont atteint le point de rendez-vous. Le ``mutex`` protège les accès à la variable ``count`` qui est partagée entre les différents threads. Le sémaphore ``rendezvous`` est initialisé à la valeur ``0``. Le rendez-vous se fera en bloquant les threads sur le sémaphore ``rendezvous`` tant que les ``N`` threads ne sont pas arrivés à cet endroit. - -.. code-block:: c - - premiere_phase(); - - // section critique - pthread_mutex_lock(&mutex); - count++; - if(count==N) { - // tous les threads sont arrivés - sem_post(&rendezvous); - } - pthread_mutex_unlock(&mutex); - // attente à la barrière - sem_wait(&rendezvous); - // libération d'un autre thread en attente - sem_post(&rendezvous); - - seconde_phase(); +Si un sémaphore initialisé à la valeur ``1`` est généralement utilisé comme un :term:`mutex`, il y a une différence importante entre les implémentations des sémaphores et des :term:`mutex`. Un sémaphore est conçu pour être manipulé par différents threads et il est fort possible qu'un thread exécute `sem_wait(3)`_ et qu'un autre exécute `sem_post(3)`_. Pour les mutex, certaines implémentations supposent que le même thread exécute `pthread_mutex_lock(3posix)`_ et `pthread_mutex_unlock(3posix)`_. Lorsque ces opérations doivent être effectuées dans des threads différents, il est préférable d'utiliser des sémaphores à la place de mutex. -Le pseudo-code ci-dessus présente une solution permettant de résoudre ce problème du rendez-vous. Le sémaphore étant initialisé à ``0``, le premier thread qui aura terminé la première phase sera bloqué sur ``sem_wait(&rendezvous);``. Les ``N-1`` premiers threads qui auront terminé leur première phase seront tous bloqués à cet endroit. Lorsque le dernier thread finira sa première phase, il incrémentera ``count`` puis exécutera ``sem_post(&rendezvous);`` ce qui libèrera un premier thread. Le dernier thread sera ensuite bloqué sur ``sem_wait(&rendezvous);`` mais il ne restera pas bloqué longtemps car chaque fois qu'un thread parvient à passer ``sem_wait(&rendezvous);``, il exécute immédiatement ``sem_post(&rendezvous);`` ce qui permet de libérer un autre thread en cascade. - -Cette solution permet de résoudre le problème du rendez-vous avec un nombre fixe de threads. Certaines implémentations de la librairie des threads POSIX comprennent une barrière qui peut s'utiliser de la même façon que la solution ci-dessus. Une barrière est une structure de données de type ``pthread_barrier_t``. Elle s'initialise en utilisant la fonction `pthread_barrier_init(3posix)`_ qui prend trois arguments : un pointeur vers une barrière, des attributs optionnels et le nombre de threads qui doivent avoir atteint la barrière pour que celle-ci s'ouvre. La fonction `pthread_barrier_destroy(3posix)`_ permet de détruire une barrière. Enfin, la fonction `pthread_barrier_wait(3posix)`_ qui prend comme argument un pointeur vers une barrière bloque le thread correspondant à celle-ci tant que le nombre de threads requis pour passer la barrière n'a pas été atteint. Problème des producteurs-consommateurs @@ -173,7 +129,7 @@ Le problème des producteurs-consommateurs est un problème extrêmement fréque - les `producteurs` : Ce sont des threads qui produisent des données et placent le résultat de leurs calculs dans une zone mémoire accessible aux consommateurs. - les `consommateurs` : Ce sont des threads qui utilisent les valeurs calculées par les producteurs. -Ces deux types de threads communiquent en utilisant un buffer qui a une capacité limitée à `N` slots comme illustré dans la figure ci-dessous. +Ces deux types de threads communiquent en utilisant un buffer qui a une capacité limitée à `N` places comme illustré dans la figure ci-dessous. .. figure:: /Threads/figures/figures-S7-001-c.png :align: center @@ -183,14 +139,14 @@ Ces deux types de threads communiquent en utilisant un buffer qui a une capacit La difficulté du problème est de trouver une solution qui permet aux producteurs et aux consommateurs d'avancer à leur rythme sans que les producteurs ne bloquent inutilement les consommateurs et inversement. Le nombre de producteurs et de consommateurs ne doit pas nécessairement être connu à l'avance et ne doit pas être fixe. Un producteur peut arrêter de produire à n'importe quel moment. -Le buffer étant partagé entre les producteurs et les consommateurs, il doit nécessairement être protégé par un :term:`mutex`. Les producteurs doivent pouvoir ajouter de l'information dans le buffer partagé tant qu'il y a au moins un slot de libre dans le buffer. Un producteur ne doit être bloqué que si tout le buffer est rempli. Inversement, les consommateurs doivent être bloqués uniquement si le buffer est entièrement vide. Dès qu'une donnée est ajoutée dans le buffer, un consommateur doit être réveillé pour traiter cette donnée. +Le buffer étant partagé entre les producteurs et les consommateurs, il doit nécessairement être protégé par un :term:`mutex`. Les producteurs doivent pouvoir ajouter de l'information dans le buffer partagé tant qu'il y a au moins une place de libre dans le buffer. Un producteur ne doit être bloqué que si tout le buffer est rempli. Inversement, les consommateurs doivent être bloqués uniquement si le buffer est entièrement vide. Dès qu'une donnée est ajoutée dans le buffer, un consommateur doit être réveillé pour traiter cette donnée. -Ce problème peut être résolu en utilisant deux sémaphores et un mutex. L'accès au buffer, que ce soit par les consommateurs ou les producteurs est une section critique. Cet accès doit donc être protégé par l'utilisation d'un mutex. Quant aux sémaphores, le premier, baptisé ``empty`` dans l'exemple ci-dessous, sert à compter le nombre de slots qui sont vides dans le buffer partagé. Ce sémaphore doit être initialisé à la taille du buffer puisqu'initialement celui-ci est vide. Le second sémaphore est baptisé ``full`` dans le pseudo-code ci-dessous. Sa valeur représente le nombre de slots du buffer qui sont occupés. Il doit être initialisé à la valeur ``0``. +Ce problème peut être résolu en utilisant deux sémaphores et un mutex. L'accès au buffer, que ce soit par les consommateurs ou les producteurs est une section critique. Cet accès doit donc être protégé par l'utilisation d'un mutex. Quant aux sémaphores, le premier, baptisé ``empty`` dans l'exemple ci-dessous, sert à compter le nombre de places qui sont vides dans le buffer partagé. Ce sémaphore doit être initialisé à la taille du buffer puisque celui-ci est initialement vide. Le second sémaphore est baptisé ``full`` dans le pseudo-code ci-dessous. Sa valeur représente le nombre de places du buffer qui sont occupées. Il doit être initialisé à la valeur ``0``. .. code-block:: c // Initialisation - #define N 10 // slots du buffer + #define N 10 // places dans le buffer pthread_mutex_t mutex; sem_t empty; sem_t full; @@ -199,7 +155,7 @@ Ce problème peut être résolu en utilisant deux sémaphores et un mutex. L'acc sem_init(&empty, 0 , N); // buffer vide sem_init(&full, 0 , 0); // buffer vide -Le fonctionnement général d'un producteur est le suivant. Tout d'abord, le producteur est mis en attente sur le sémaphore ``empty``. Il ne pourra passer que si il y a au moins un slot du buffer qui est vide. Lorsque la ligne ``sem_wait(&empty);`` réussit, le producteur s'approprie le ``mutex`` et modifie le buffer de façon à insérer l'élément produit (dans ce cas un entier). Il libère ensuite le ``mutex`` pour sortir de sa section critique. +Le fonctionnement général d'un producteur est le suivant. Tout d'abord, le producteur est mis en attente sur le sémaphore ``empty``. Il ne pourra passer que si il y a au moins une place du buffer qui est vide. Lorsque la ligne ``sem_wait(&empty);`` réussit, le producteur s'approprie le ``mutex`` et modifie le buffer de façon à insérer l'élément produit (dans ce cas un entier). Il libère ensuite le ``mutex`` pour sortir de sa section critique. .. code-block:: c @@ -210,17 +166,17 @@ Le fonctionnement général d'un producteur est le suivant. Tout d'abord, le pro while(true) { item=produce(item); - sem_wait(&empty); // attente d'un slot libre + sem_wait(&empty); // attente d'une place libre pthread_mutex_lock(&mutex); // section critique insert_item(); pthread_mutex_unlock(&mutex); - sem_post(&full); // il y a un slot rempli en plus + sem_post(&full); // il y a une place remplie en plus } } -Le consommateur quant à lui essaie d'abord de prendre le sémaphore ``full``. Si celui-ci est positif, cela indique la présence d'au moins un élément dans le buffer partagé. Ensuite, il entre dans la section critique protégée par le ``mutex`` et récupère la donnée se trouvant dans le buffer. Puis, il incrémente la valeur du sémaphore ``empty`` de façon à indiquer à un producteur qu'un nouveau slot est disponible dans le buffer. +Le consommateur quant à lui essaie d'abord de prendre le sémaphore ``full``. Si celui-ci est positif, cela indique la présence d'au moins un élément dans le buffer partagé. Ensuite, il entre dans la section critique protégée par le ``mutex`` et récupère la donnée se trouvant dans le buffer. Puis, il incrémente la valeur du sémaphore ``empty`` de façon à indiquer à un producteur qu'une nouvelle place est disponible dans le buffer. .. code-block:: c @@ -230,185 +186,20 @@ Le consommateur quant à lui essaie d'abord de prendre le sémaphore ``full``. S int item; while(true) { - sem_wait(&full); // attente d'un slot rempli + sem_wait(&full); // attente d'une place remplie pthread_mutex_lock(&mutex); // section critique item=remove(item); pthread_mutex_unlock(&mutex); - sem_post(&empty); // il y a un slot libre en plus + sem_post(&empty); // il y a une place libre en plus } } De nombreux programmes découpés en threads fonctionnent avec un ensemble de producteurs et un ensemble de consommateurs. +.. spelling:: -Problème des readers-writers ----------------------------- - -Le :term:`problème des readers-writers` est un peu différent du précédent. Il permet de modéliser un problème qui survient lorsque des threads doivent accéder à une base de données [Courtois+1971]_. Les threads sont généralement de deux types. - - - les lecteurs (`readers`) sont des threads qui lisent une structure de données (ou une base de données) mais ne la modifient pas. Comme ces threads se contentent de lire de l'information en mémoire, rien ne s'oppose à ce que plusieurs `readers` s'exécutent simultanément. - - les écrivains (`writers`). Ce sont des threads qui modifient une structure de données (ou une base de données). Pendant qu'un `writer` manipule la structure de données, il ne peut y avoir aucun autre `writer` ni de `reader` qui accède à cette structure de données. Sinon, la concurrence des opérations de lecture et d'écriture donnerait un résultat incorrect. - -Une première solution à ce problème est d'utiliser un mutex et un sémaphore [Courtois+1971]_. - -.. code-block:: c - - pthread_mutex_t mutex; - sem_t db; // accès à la db - int readcount=0; // nombre de readers - - sem_init(&db, NULL, 1). - -La solution utilise une variable partagée : ``readcount``. L'accès à cette variable est protégé par ``mutex``. Le sémaphore ``db`` sert à réguler l'accès des `writers` à la base de données. Le mutex est initialisé comme d'habitude par la fonction `pthread_mutex_init(3posix)`_. Le sémaphore ``db`` est initialisé à la valeur ``1``. Le `writer` est assez simple : - -.. code-block:: c - - void writer(void) - { - while(true) - { - prepare_data(); - sem_wait(&db); - // section critique, un seul writer à la fois - write_database(); - sem_post(&db); - } - } - } - - -Le sémaphore ``db`` sert à assurer l'exclusion mutuelle entre les `writers` pour l'accès à la base de données. Le fonctionnement des `readers` est plus intéressant. Pour éviter un conflit entre les `writers` et les `readers`, il est nécessaire d'empêcher aux `readers` d'accéder à la base de données pendant qu'un `writer` la modifie. Cela peut se faire en utilisant l'entier `readcount` qui permet de compter le nombre de `readers` qui manipulent la base de données. Cette variable est testée et modifiée par tous les `readers`, elle doit donc être protégée par un :term:`mutex`. Intuitivement, lorsque le premier `reader` veut accéder à la base de données (``readcount==0``), il essaye de décrémenter le sémaphore ``db``. Si ce sémaphore est libre, le `reader` accède à la base de données. Sinon, il bloque sur ``sem_wait(&db);`` et comme il possède ``mutex``, tous les autres `readers` sont bloqués sur ``pthread_mutex_lock(&mutex);``. Dès que le premier `reader` est débloqué, il autorise en cascade l'accès à tous les autres `readers` qui sont en attente en libérant ``pthread_mutex_unlock(&mutex);``. Lorsqu'un `reader` arrête d'utiliser la base de données, il vérifie s'il était le dernier `reader`. Si c'est le cas, il libère le sémaphore ``db`` de façon à permettre à un `writer` d'y accéder. Sinon, il décrémente simplement la variable ``readcount`` pour tenir à jour le nombre de `readers` qui sont actuellement en train d'accéder à la base de données. - - -.. code-block:: c - - void reader(void) - { - while(true) - { - pthread_mutex_lock(&mutex); - // section critique - readcount++; - if (readcount==1) - { // arrivée du premier reader - sem_wait(&db); - } - pthread_mutex_unlock(&mutex); - read_database(); - pthread_mutex_lock(&mutex); - // section critique - readcount--; - if(readcount==0) - { // départ du dernier reader - sem_post(&db); - } - pthread_mutex_unlock(&mutex); - process_data(); - } - } - -.. Cette solution est un exemple atypique de l'utilisation de :term:`mutex` puisque le :term:`mutex` ``db`` peut être réservé par un thread `reader` et libéré par un tout autre thread. D'habitude, c'est généralement le même thread qui exécute `pthread_mutex_lock(3)`_ et `pthread_mutex_unlock(3)`_ pour un mutex donné. - -Cette solution fonctionne et garantit qu'il n'y aura jamais qu'un seul `writer` qui accède à la base de données. Malheureusement, elle souffre d'un inconvénient majeur lorsqu'il y a de nombreux `readers`. Dans ce cas, il est tout à fait possible qu'il y ait en permanence des `readers` qui accèdent à la base de données et que les `writers` soient toujours empêchés d'y accéder. En effet, dès que le premier `reader` a effectué ``sem_wait(&db);``, aucun autre `reader` ne devra exécuter cette opération tant qu'il restera au moins un `reader` accédant à la base de données. Les `writers` par contre resteront bloqués sur l'exécution de ``sem_wait(&db);``. - -En utilisant des sémaphores à la place des :term:`mutex`, il est possible de contourner ce problème. Cependant, cela nécessite d'utiliser plusieurs sémaphores. Intuitivement, l'idée de la solution est de donner priorité aux `writers` par rapport aux `readers`. Dès qu'un `writer` est prêt à accéder à la base de données, il faut empêcher de nouveaux `readers` d'y accéder tout en permettant aux `readers` présents de terminer leur lecture. - -Cette solution utilise trois mutex, deux sémaphores et deux variables partagées : ``readcount`` et ``writecount``. Ces deux variables servent respectivement à compter le nombre de `readers` qui accèdent à la base de données et le nombre de `writers` qui veulent y accéder. Le sémaphore ``wsem`` est utilisé pour bloquer les `writers` tandis que le sémaphore ``rsem`` sert à bloquer les `readers`. Le mutex ``z`` a un rôle particulier qui sera plus clair lorsque le code des `readers` aura été présenté. Les deux sémaphores sont initialisés à la valeur ``1``. - -.. code-block:: c - - /* Initialisation */ - pthread_mutex_t mutex_readcount; // protège readcount - pthread_mutex_t mutex_writecount; // protège writecount - pthread_mutex_t z; // un seul reader en attente - sem_t wsem; // accès exclusif à la db - sem_t rsem; // pour bloquer des readers - int readcount=0; - int writecount=0; - - sem_init(&wsem, 0, 1); - sem_init(&rsem, 0, 1); - - -Un `writer` utilise la variable ``writecount`` pour compter le nombre de `writers` qui veulent accéder à la base de données. Cette variable est protégée par ``mutex_writecount``. Le sémaphore ``wsem`` est utilisé pour garantir qu'il n'y a qu'un seul `writer` qui peut accéder à un moment donné à la base de données. Cette utilisation est similaire à celle du sémaphore ``db`` dans la solution précédente. - -.. code-block:: c - - /* Writer */ - while(true) - { - think_up_data(); - - pthread_mutex_lock(&mutex_writecount); - // section critique - writecount - writecount=writecount+1; - if(writecount==1) { - // premier writer arrive - sem_wait(&rsem); - } - pthread_mutex_unlock(&mutex_writecount); - - sem_wait(&wsem); - // section critique, un seul writer à la fois - write_database(); - sem_post(&wsem); - - pthread_mutex_lock(&mutex_writecount); - // section critique - writecount - writecount=writecount-1; - if(writecount==0) { - // départ du dernier writer - sem_post(&rsem); - } - pthread_mutex_unlock(&mutex_writecount); - } - - -Pour comprendre le reste du fonctionnement des `writers`, il faut analyser en parallèle le fonctionnement des `readers` car les deux types de threads interagissent de façon importante. Un `reader` utilise la variable ``readcount`` protégée par le ``mutex_readcount`` pour compter le nombre de `readers` en attente. Un `reader` utilise deux sémaphores. Le premier est ``wsem`` qui joue un rôle similaire au sémaphore ``db`` de la solution précédente. Le premier `reader` qui veut accéder à la base de données (``readcount==1``) effectue ``sem_wait(&wsem)`` pour garantir qu'il n'y aura pas de `writer` qui accède à la base de données pendant qu'il s'y trouve. Lorsque le dernier `reader` n'a plus besoin d'accéder à la base de données (``readcount==0``), il libère les `writers` qui étaient potentiellement en attente en exécutant ``sem_post(&wsem)``. - -Le sémaphore ``rsem`` répond à un autre besoin. Il permet de bloquer les `readers` en attente lorsqu'un `writer` veut accéder à la base de données. En effet, le premier `writer` qui veut accéder à la base de données exécute ``sem_wait(&rsem)``. Cela a pour effet de bloquer les nouveaux `readers` qui voudraient accéder à la base de données sur ``sem_wait(&rsem)``. Ils ne seront débloqués que lorsque le dernier `writer` (``writecount==0``) quittera la base de données et exécutera ``sem_post(&rsem)``. Lorsqu'aucun writer n'accède à la base de données, les readers peuvent facilement exécuter ``sem_wait(&rsem)`` qui sera rapidement suivi de ``sem_post(&rsem)``. - - -.. code-block:: c - - /* Reader */ - while(true) - { - pthread_mutex_lock(&z); - // exclusion mutuelle, un seul reader en attente sur rsem - sem_wait(&rsem); - - pthread_mutex_lock(&mutex_readcount); - // exclusion mutuelle, readercount - readcount=readcount+1; - if (readcount==1) { - // arrivée du premier reader - sem_wait(&wsem); - } - pthread_mutex_unlock(&mutex_readcount); - sem_post(&rsem); // libération du prochain reader - pthread_mutex_unlock(&z); - - read_database(); - - pthread_mutex_lock(&mutex_readcount); - // exclusion mutuelle, readcount - readcount=readcount-1; - if(readcount==0) { - // départ du dernier reader - sem_post(&wsem); - } - pthread_mutex_unlock(&mutex_readcount); - use_data_read(); - } - -Pour comprendre l'utilité du mutex ``z``, il faut imaginer une solution dans laquelle il n'est pas utilisé. Dans ce cas, imaginons que plusieurs `readers` accèdent à la base de données et que deux `readers` et deux `writers` veulent y accéder. Le premier `reader` exécute ``sem_wait(&rsem);``. Le premier `writer` va exécuter ``sem_wait(&rsem);`` et sera bloqué en attendant que le premier `reader` exécute ``sem_post(&rsem);``. Le deuxième `writer` sera bloqué sur ``pthread_mutex_lock(&mutex_writecount);``. Lorsque le premier `reader` exécute ``pthread_mutex_unlock(&mutex_readcount);``, il permet au second `reader` de passer le mutex et d'exécuter ``sem_wait(&rsem);``. Lorsque le premier `reader` exécute finalement ``sem_post(&rsem);``, le système devra libérer un des threads en attente, c'est-à -dire le second `reader` ou le premier `writer`. Cette solution ne donne pas complètement la priorité aux `writers`. Le mutex ``z`` permet d'éviter ce problème en n'ayant qu'un seul `reader` à la fois qui peut exécuter la séquence ``sem_wait(&rsem); ... sem_post(&rsem);``. Avec le mutex ``z``, le second `reader` est nécessairement en attente sur le mutex ``z`` lorsque le premier `reader` exécute ``sem_post(&rsem);``. Si un `writer` est en attente à ce moment, il sera nécessairement débloqué. - - -.. note:: Read-Write locks - - Certaines implémentations de la librairie des threads POSIX contiennent des `Read-Write locks`. Ceux-ci constituent une API de plus haut niveau qui s'appuie sur des sémaphores pour résoudre le :term:`problème des readers-writers`. Les fonctions de création et de suppression de ces locks sont : `pthread_rwlock_init(3posix)`_, `pthread_rwlock_destroy(3posix)`_. Les fonctions `pthread_rwlock_rdlock(3posix)`_ et `pthread_rwlock_unlock(3posix)`_ sont réservées aux readers tandis que les fonctions `pthread_rwlock_wrlock(3posix)`_ et `pthread_rwlock_unlock(3posix)`_ sont utilisables par les writers. Des exemples d'utilisation de ces `Read-Write locks` peuvent être obtenus dans [Gove2011]_. + Solaris Compléments sur les threads POSIX @@ -416,7 +207,7 @@ Compléments sur les threads POSIX Il existe différentes implémentations des threads POSIX. Les mécanismes de coordination utilisables varient parfois d'une implémentation à l'autre. Dans les sections précédentes, nous nous sommes focalisés sur les fonctions principales qui sont en général bien implémentées. Une discussion plus détaillée des fonctions implémentées sous Linux peut se trouver dans [Kerrisk2010]_. [Gove2011]_ présente de façon détaillée les mécanismes de coordination utilisables sous Linux, Windows et Oracle Solaris. [StevensRago2008]_ comprend également une description des threads POSIX mais présente des exemples sur des versions plus anciennes de Linux, FreeBSD, Solaris et MacOS. -Il reste cependant quelques concepts qu'il est utile de connaître lorsque l'on développe des programmes multithreadés en langage C. +Il reste cependant quelques concepts qu'il est utile de connaître lorsque l'on développe des programmes découpés en threads en langage C. Variables ``volatile`` @@ -488,10 +279,14 @@ Lors de son exécution, ce programme affiche la sortie suivante sur :term:`stdou La seconde solution proposée par la librairie POSIX est plus complexe. Elle nécessite l'utilisation des fonctions `pthread_key_create(3posix)`_, `pthread_setspecific(3posix)`_, `pthread_getspecific(3posix)`_ et `pthread_key_delete(3posix)`_. Cette API est malheureusement plus difficile à utiliser que le qualificatif ``__thread``, mais elle illustre ce qu'il se passe en pratique lorsque ce qualificatif est utilisé. -Pour avoir une variable accessible depuis toutes les fonctions d'un thread, il faut tout d'abord créer une clé qui identifie cette variable. Cette clé est de type ``pthread_key_t`` et c'est l'adresse de cette structure en mémoire qui sert d'identifiant pour la variable spécifique à chaque thread. Cette clé ne doit être créée qu'une seule fois. Cela peut se faire dans le programme qui lance les threads ou alors dans le premier thread lancé en utilisant la fonction `pthread_once(3posix)`_. Une clé est créée grâce à la fonction `pthread_key_create(3posix)`_. Cette fonction prend deux arguments. Le premier est un pointeur vers une structure de type ``pthread_key_t``. Le second est la fonction optionnelle à appeler lorsque le thread utilisant la clé se termine. +Pour avoir une variable accessible depuis toutes les fonctions d'un thread, il faut tout d'abord créer une clé qui identifie cette variable. Cette clé est de type ``pthread_key_t`` et c'est l'adresse de cette structure en mémoire qui est utilisée comme identifiant pour la variable spécifique à chaque thread. Cette clé ne doit être créée qu'une seule fois. Cela peut se faire dans le programme qui lance les threads ou alors dans le premier thread lancé en utilisant la fonction `pthread_once(3posix)`_. Une clé est créée grâce à la fonction `pthread_key_create(3posix)`_. Cette fonction prend deux arguments. Le premier est un pointeur vers une structure de type ``pthread_key_t``. Le second est la fonction optionnelle à appeler lorsque le thread utilisant la clé se termine. Il faut noter que la fonction `pthread_key_create(3posix)`_ associe en pratique le pointeur ``NULL`` à la clé qui a été créée dans chaque thread. Le thread qui veut utiliser la variable correspondant à cette clé doit réserver la zone mémoire correspondante. Cela se fait en général en utilisant `malloc(3)`_ puis en appelant la fonction `pthread_setspecific(3posix)`_. Celle-ci prend deux arguments. Le premier est une clé de type ``pthread_key_t`` qui a été préalablement créée. Le second est un pointeur (de type ``void *``) vers la zone mémoire correspondant à la variable spécifique. Une fois que le lien entre la clé et le pointeur a été fait, la fonction `pthread_getspecific(3posix)`_ peut être utilisée pour récupérer le pointeur depuis n'importe quelle fonction du thread. L'implémentation des fonctions `pthread_setspecific(3posix)`_ et `pthread_getspecific(3posix)`_ garantit que chaque thread aura sa variable qui lui est propre. +.. spelling:: + + L'implémentation + L'exemple ci-dessous illustre l'utilisation de cette API. Elle est nettement plus lourde à utiliser que le qualificatif ``__thread``. Dans ce code, chaque thread démarre par la fonction ``f``. Celle-ci crée une variable spécifique de type ``int`` qui joue le même rôle que la variable ``__thread int count;`` dans l'exemple précédent. La fonction ``g`` qui est appelée sans argument peut accéder à la zone mémoire créée en appelant ``pthread_getspecific(count)``. Elle peut ensuite exécuter ses calculs en utilisant le pointeur ``count_ptr``. Avant de se terminer, la fonction ``f`` libère la zone mémoire qui avait été allouée par `malloc(3)`_. Une alternative à l'appel explicite à `free(3)`_ aurait été de passer ``free`` comme second argument à `pthread_key_create(3posix)`_ lors de la création de la clé ``count``. En effet, ce second argument est la fonction à appeler à la fin du thread pour libérer la mémoire correspondant à cette clé. .. literalinclude:: /Threads/S7-src/pthread-specific2.c @@ -500,11 +295,14 @@ L'exemple ci-dessous illustre l'utilisation de cette API. Elle est nettement plu :start-after: ///AAA :end-before: ///BBB -En pratique, on préfèrera évidemment d'utiliser le qualificatif ``__thread`` à la place de l'API explicite lorsque c'est possible. Cependant, il ne faut pas oublier que lorsque ce qualificatif est utilisé, le compilateur doit introduire dans le programme du code permettant de faire le même genre d'opérations que les fonctions explicites de la librairie. +En pratique, on préférera évidemment d'utiliser le qualificatif ``__thread`` plutôt que d'utiliser une API explicite lorsque c'est possible. Cependant, il ne faut pas oublier que lorsque ce qualificatif est utilisé, le compilateur doit introduire dans le programme du code permettant de faire le même genre d'opérations que les fonctions explicites de la librairie. + +.. spelling:: + thread-safe -Fonctions thread-safe ---------------------- +Fonctions ``thread-safe`` +------------------------- Dans un programme séquentiel, il n'y a qu'un thread d'exécution et de nombreux programmeurs, y compris ceux qui ont développé la librairie standard, utilisent cette hypothèse lors de l'écriture de fonctions. Lorsqu'un programme est découpé en threads, chaque fonction peut être appelée par plusieurs threads simultanément. Cette exécution simultanée d'une fonction peut poser des difficultés notamment lorsque la fonction utilise des variables globales ou des variables statiques. @@ -570,6 +368,10 @@ La fonction `strerror_r(3)`_ évite ce problème de tableau statique en utilisan Lorsque l'on intègre des fonctions provenant de la librairie standard ou d'une autre librairie dans un programme découpé en threads, il est important de vérifier que les fonctions utilisées sont bien :term:`thread-safe`. La page de manuel `pthreads(7)`_ liste les fonctions qui ne sont pas :term:`thread-safe` dans la librairie standard. +.. spelling:: + + Gene + Amdahl Loi de Amdahl ============= @@ -581,9 +383,9 @@ Un autre problème est de trier le contenu d'un tel tableau dans l'ordre croissa Dans les années 1960s, à l'époque des premières réflexions sur l'utilisation de plusieurs processeurs pour résoudre un problème, Gene Amdahl [Amdahl1967]_ a analysé quelles étaient les gains que l'on pouvait attendre de l'utilisation de plusieurs processeurs. Dans sa réflexion, il considère un programme ``P`` qui peut être découpé en deux parties : - une partie purement séquentielle. Il s'agit par exemple de l'initialisation de l'algorithme utilisé, de la collecte des résultats, ... - - une partie qui est parallélisable. Il s'agit en général du coeur de l'algorithme. + - une partie qui est peut être parallélisée. Il s'agit en général du coeur de l'algorithme. -Plus les opérations réalisées à l'intérieur d'un programme sont indépendantes entre elles, plus le programme est parallélisable et inversement. Pour Amdahl, si le temps d'exécution d'un programme séquentiel est `T` et qu'une fraction `f` de ce programme est parallélisable, alors le gain qui peut être obtenu de la parallélisation est :math:`\frac{T}{T \times( (1-f)+\frac{f}{N})}=\frac{1}{ (1-f)+\frac{f}{N}}` lorsque le programme est découpé en `N` threads. Cette formule, connue sous le nom de la :term:`loi de Amdahl` fixe une limite théorique sur le gain que l'on peut obtenir en parallélisant un programme. La figure ci-dessous [#famdahl]_ illustre le gain théorique que l'on peut obtenir en parallélisant un programme en fonction du nombre de processeur et pour différentes fractions parallélisables. +Plus les opérations réalisées à l'intérieur d'un programme sont indépendantes entre elles, plus le programme fonction en parallèle et inversement. Pour Amdahl, si le temps d'exécution d'un programme séquentiel est `T` et qu'une fraction `f` de ce programme peut être parallélisée, alors le gain qui peut être obtenu de la parallélisation est :math:`\frac{T}{T \times( (1-f)+\frac{f}{N})}=\frac{1}{ (1-f)+\frac{f}{N}}` lorsque le programme est découpé en `N` threads. Cette formule, connue sous le nom de la :term:`loi de Amdahl` fixe une limite théorique sur le gain que l'on peut obtenir en parallélisant un programme. La figure ci-dessous [#famdahl]_ illustre le gain théorique que l'on peut obtenir en parallélisant un programme en fonction du nombre de processeur et pour différentes fractions parallélisées. .. figure:: /Threads/figures/500px-AmdahlsLaw.png :align: center @@ -591,15 +393,19 @@ Plus les opérations réalisées à l'intérieur d'un programme sont indépendan Loi de Amdahl (source `wikipedia <http://en.wikipedia.org/wiki/Amdahl's_law>`_) -La loi de Amdahl doit être considérée comme un maximum théorique qui est difficile d'atteindre. Elle suppose que la parallélisation est parfaite, c'est-à -dire que la création et la terminaison de threads n'ont pas de coût en terme de performance. En pratique, c'est loin d'être le cas et il peut être difficile d'estimer a priori le gain qu'une parallélisation permettra d'obtenir. En pratique, avant de découper un programme séquentiel en threads, il est important de bien identifier la partie séquentielle et la partie parallélisable du programme. Si la partie séquentielle est trop importante, le gain dû à la parallélisation risque d'être faible. Si par contre la partie purement séquentielle est faible, il est possible d'obtenir théoriquement des gains élevés. Le tout sera de trouver des solutions efficaces qui permettront aux threads de fonctionner le plus indépendamment possible. +.. spelling:: + + profiling + +La loi de Amdahl doit être considérée comme un maximum théorique qui est difficile d'atteindre. Elle suppose que la parallélisation est parfaite, c'est-à -dire que la création et la terminaison de threads n'ont pas de coût en terme de performance. En pratique, c'est loin d'être le cas et il peut être difficile d'estimer a priori le gain qu'une parallélisation permettra d'obtenir. En pratique, avant de découper un programme séquentiel en threads, il est important de bien identifier la partie séquentielle et la partie du programme pouvant être parallélisée. Si la partie séquentielle est trop importante, le gain dû à la parallélisation risque d'être faible. Si par contre la partie purement séquentielle est faible, il est possible d'obtenir théoriquement des gains élevés. Le tout sera de trouver des solutions efficaces qui permettront aux threads de fonctionner le plus indépendamment possible. -En pratique, avant de s'attaquer à la découpe d'un programme séquentiel en threads, il est important de bien comprendre quelles sont les parties du programme qui sont les plus consommatrices de temps CPU. Ce seront souvent les boucles ou les grandes structures de données. Si le programme séquentiel existe, il est utile d'analyser son exécution avec des outils de profiling tels que `gprof(1)`_ [Graham+1982]_ ou `oprofile <http://oprofile.sourceforge.net/>`_. Un profiler est un logiciel qui permet d'analyser l'exécution d'un autre logiciel de façon à pouvoir déterminer notamment quelles sont les fonctions ou parties du programmes les plus exécutées. Ces parties de programme sont celles sur lesquelles l'effort de paraléllisation devra porter en pratique. +En pratique, avant de s'attaquer à la découpe d'un programme séquentiel en threads, il est important de bien comprendre quelles sont les parties du programme qui sont les plus consommatrices de temps CPU. Ce seront souvent les boucles ou les grandes structures de données. Si le programme séquentiel existe, il est utile d'analyser son exécution avec des outils de `profiling` tels que `gprof(1)`_ [Graham+1982]_ ou `oprofile <http://oprofile.sourceforge.net/>`_. Un profiler est un logiciel qui permet d'analyser l'exécution d'un autre logiciel de façon à pouvoir déterminer notamment quelles sont les fonctions ou parties du programmes les plus exécutées. Ces parties de programme sont celles sur lesquelles l'effort de parallélisation devra porter en pratique. Dans un programme découpé en threads, toute utilisation de fonctions de coordination comme des sémaphores ou des mutex, bien qu'elle soit nécessaire pour la correction du programme, risque d'avoir un impact négatif sur les performances. Pour s'en convaincre, il est intéressant de réfléchir au problème des producteurs-consommateurs. Il correspond à de nombreux programmes réels. Les performances d'une implémentation du problème des producteurs consommateurs dépendront fortement de la taille du buffer entre les producteurs et les consommateurs et de leur nombre et/ou vitesses relatives. Idéalement, il faudrait que le buffer soit en moyenne rempli à moitié. De cette façon, chaque producteur pourra déposer de l'information dans le buffer et chaque consommateur pourra en retirer. Si le buffer est souvent vide, cela indique que les consommateurs sont plus rapides que les producteurs. Ces consommateurs risquent d'être inutilement bloqués, ce qui affectera les performances. Il en va de même si le buffer était plein. Dans ce cas, les producteurs seraient souvent bloqués. .. rubric:: Footnotes -.. [#fSysV] Les systèmes Unix supportent également des sémaphores dits `System V` du nom de la version de Unix dans laquelle ils ont été introduits. Dans ces notes, nous nous focalisons sur les sémaphores POSIX qui ont une API un peu plus simple que l'API des sémaphores `System V`. Les principales fonctions pour les sémaphores `System V` sont `semget(3posix)`_, `semctl(3posix)`_ et `semop(3posix)`_. +.. [#fSysV] Les systèmes Unix supportent également des sémaphores dits `System V` du nom de la version de Unix dans laquelle ils ont été introduits. Dans ces notes, nous nous focalisons sur les sémaphores POSIX qui ont une API un peu plus simple que les es sémaphores `System V`. Les principales fonctions pour les sémaphores `System V` sont `semget(3posix)`_, `semctl(3posix)`_ et `semop(3posix)`_. .. [#fregister] Les premiers compilateurs C permettaient au programmeur de donner des indications au compilateur en faisant précéder les déclarations de certaines variables avec le qualificatif ``register`` [KernighanRitchie1998]_. Ce qualificatif indiquait que la variable était utilisée fréquemment et que le compilateur devrait en placer le contenu dans un registre. Les compilateurs actuels sont nettement plus performants et ils sont capables de détecter quelles sont les variables qu'il faut placer dans un registre. Il est inutile de chercher à influencer le compilateur en utilisant le qualificatif ``register``. Les compilateurs actuels, dont `gcc(1)`_ supportent de nombreuses `options <http://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html>`_ permettant d'optimiser les performances des programmes compilés. Certaines ont comme objectif d'accélérer l'exécution du programme, d'autres visent à réduire sa taille. Pour les programmes qui consomment beaucoup de temps CPU, il est utile d'activer l'optimisation du compilateur. diff --git a/Theorie/Threads/threads.rst b/Theorie/Threads/threads.rst index 142ae8f6afe13b0fbabd8503564a52b80e3fd5ed..044def23dc7f0af9cdd2e4f4eb3708d30071633b 100644 --- a/Theorie/Threads/threads.rst +++ b/Theorie/Threads/threads.rst @@ -7,7 +7,12 @@ Utilisation de plusieurs threads ================================ -Les performances des microprocesseurs se sont continuellement améliorées depuis les années 1960s. Cette amélioration a été possible grâce aux progrès constants de la microélectronique qui a permis d'assembler des microprocesseurs contenant de plus en plus de transistors sur une surface de plus en plus réduite. La figure [#ftransistors]_ ci-dessous illustre bien cette évolution puisqu'elle représente le nombre de transistors par microprocesseur en fonction du temps. +Les performances des microprocesseurs se sont continuellement améliorées depuis les années 1960s. Cette amélioration a été possible grâce aux progrès constants de la micro-électronique qui a permis d'assembler des microprocesseurs contenant de plus en plus de transistors sur une surface de plus en plus réduite. La figure [#ftransistors]_ ci-dessous illustre bien cette évolution puisqu'elle représente le nombre de transistors par microprocesseur en fonction du temps. + +.. spelling:: + + Moore + intel .. figure:: /Threads/figures/534px-Transistor_Count_and_Moore's_Law_-_2011.png @@ -16,9 +21,9 @@ Les performances des microprocesseurs se sont continuellement améliorées depui Evolution du nombre de transistors par microprocesseur -Cette évolution avait été prédite par Gordon Moore dans les années 1960s [Stokes2008]_. Il a formulé en 1965 une hypothèse qui prédisait que le nombre de composants par chip continuerait à doubler tous les douze mois pour la prochaine décennie. Cette prédiction s'est avérée tout à fait réaliste. Elle est maintenant connue sous le nom de :term:`Loi de Moore` et est fréquemment utilisée pour expliquer les améliorations de performance des ordinateurs. +Cette évolution avait été prédite par Gordon Moore dans les années 1960s [Stokes2008]_. Il a formulé en 1965 une hypothèse qui prédisait que le nombre de composants par puce continuerait à doubler tous les douze mois pour la prochaine décennie. Cette prédiction s'est avérée tout à fait réaliste. Elle est maintenant connue sous le nom de :term:`Loi de Moore` et est fréquemment utilisée pour expliquer les améliorations de performance des ordinateurs. -Le fonctionnement d'un microprocesseur est régulé par une horloge. Celle-ci rythme la plupart des opérations du processeur et notamment le chargement des instructions depuis la mémoire. Pendant de nombreuses années, les performances des microprocesseurs ont fortement dépendu de leur vitesse d'horloge. Les premiers microprocesseurs avaient des fréquences d'horloge de quelques centaines de :term:`kHz`. A titre d'exemple, le processeur intel 4004 avait une horloge à 740 kHz en 1971. Aujourd'hui, les processeurs rapides dépassent la fréquence de 3 :term:`GHz`. La figure ci-dessous présente l'évolution de la fréquence d'horloge des microprocesseurs depuis les années 1970s [#fperf]_. On remarque une évolution rapide jusqu'aux environs du milieu de la dernière décennie. La barrière des 10 MHz a été franchie à la fin des années 1970s. Les 100 :term:`MHz` ont étés atteints en 1994 et le Ghz aux environs de l'an 2000. +Le fonctionnement d'un microprocesseur est régulé par une horloge. Celle-ci rythme la plupart des opérations du processeur et notamment le chargement des instructions depuis la mémoire. Pendant de nombreuses années, les performances des microprocesseurs ont fortement dépendu de leur vitesse d'horloge. Les premiers microprocesseurs avaient des fréquences d'horloge de quelques centaines de :term:`kHz`. A titre d'exemple, le processeur intel 4004 avait une horloge à 740 kHz en 1971. Aujourd'hui, les processeurs rapides dépassent la fréquence de 3 :term:`GHz`. La figure ci-dessous présente l'évolution de la fréquence d'horloge des microprocesseurs depuis les années 1970s [#fperf]_. On remarque une évolution rapide jusqu'aux environs du milieu de la dernière décennie. La barrière des 10 MHz a été franchie à la fin des années 1970s. Les 100 :term:`MHz` ont étés atteints en 1994 et le GHz aux environs de l'an 2000. .. figure:: /Threads/figures/figures-001-c.png :align: center @@ -27,10 +32,14 @@ Le fonctionnement d'un microprocesseur est régulé par une horloge. Celle-ci ry Pendant près de quarante ans, l'évolution technologique a permis une amélioration continue des performances des microprocesseurs. Cette amélioration a directement profité aux applications informatiques car elles ont pu s'exécuter plus rapidement au fur et à mesure que la vitesse d'horloge des microprocesseurs augmentait. -Malheureusement, vers 2005 cette croissance continue s'est arrêtée. La barrière des 3 GHz s'est avérée être une barrière très couteuse à franchir d'un point de vue technologique. Aujourd'hui, les fabricants de microprocesseurs n'envisagent plus de chercher à continuer à augmenter les fréquences d'horloge des microprocesseurs. +Malheureusement, vers 2005 cette croissance continue s'est arrêtée. La barrière des 3 GHz s'est avérée être une barrière très difficile à franchir d'un point de vue technologique. Aujourd'hui, les fabricants de microprocesseurs n'envisagent plus de chercher à continuer à augmenter les fréquences d'horloge des microprocesseurs. Si pendant longtemps la fréquence d'horloge d'un microprocesseur a été une bonne heuristique pour prédire les performances du microprocesseur, ce n'est pas un indicateur parfait de performance. Certains processeurs exécutent une instruction durant chaque cycle d'horloge. D'autres processeurs prennent quelques cycles d'horloge pour exécuter chaque instruction et enfin certains processeurs sont capables d'exécuter plusieurs instructions durant chaque cycle d'horloge. +.. spelling:: + + d'Instructions + Une autre façon de mesurer les performances d'un microprocesseur est de comptabiliser le nombre d'instructions qu'il exécute par seconde. On parle en général de Millions d'Instructions par Seconde (ou :term:`MIPS`). Si les premiers microprocesseurs effectuaient moins de 100.000 instructions par seconde, la barrière du MIPS a été franchie en 1979. Mesurées en MIPS, les performances des microprocesseurs ont continué à augmenter durant les dernières années malgré la barrière des 3 GHz comme le montre la figure ci-dessous. @@ -39,16 +48,20 @@ Une autre façon de mesurer les performances d'un microprocesseur est de comptab Evolution des performances des microprocesseurs en MIPS +.. spelling:: + + Evaluation + benchmark + benchmarks + .. note:: Evaluation des performances de systèmes informatiques - La fréquence d'horloge d'un processeur et le nombre d'instructions qu'il est capable d'exécuter chaque seconde ne sont que quelques uns des paramètres qui influencent les performances d'un système informatique qui intègre ce processeur. Les performances globales d'un système informatique dépendent de nombreux autres facteurs comme la capacité de mémoire et ses performances, la vitesse des bus entre les différents composants, les performances des dispositifs de stockage ou des cartes réseaux. Les performances d'un système dépendront aussi fortement du type d'application utilisé. Un serveur web, un serveur de calcul scientifique et un serveur de bases de données n'auront pas les mêmes contraintes en termes de performance. L'évaluation complète des performances d'un système informatique se fait généralement en utilisant des benchmarks. Un :term:`benchmark` est un ensemble de logiciels qui reproduisent le comportement de certaines classes d'applications de façon à pouvoir tester les performances de systèmes informatiques de façon reproductibles. Différents organismes publient de tels benchmarks. Le plus connu est probablement `Standard Performance Evaluation Corporation <http://www.spec.org>`_ qui publie des benchmarks et des résultats de benchmarks pour différents types de systèmes informatiques et d'applications. + La fréquence d'horloge d'un processeur et le nombre d'instructions qu'il est capable d'exécuter chaque seconde ne sont que quelques uns des paramètres qui influencent les performances d'un système informatique qui intègre ce processeur. Les performances globales d'un système informatique dépendent de nombreux autres facteurs comme la capacité de mémoire et ses performances, la vitesse des bus entre les différents composants, les performances des dispositifs de stockage ou des cartes réseaux. Les performances d'un système dépendront aussi fortement du type d'application utilisé. Un serveur web, un serveur de calcul scientifique et un serveur de bases de données n'auront pas les mêmes contraintes en termes de performance. L'évaluation complète des performances d'un système informatique se fait généralement en utilisant des benchmarks. Un :term:`benchmark` est un ensemble de logiciels qui reproduisent le comportement de certaines classes d'applications de façon à pouvoir tester les performances de systèmes informatiques de façon reproductible. Différents organismes publient de tels benchmarks. Le plus connu est probablement `Standard Performance Evaluation Corporation <http://www.spec.org>`_ qui publie des benchmarks et des résultats de benchmarks pour différents types de systèmes informatiques et d'applications. Cette progression continue des performances en MIPS a été possible grâce à l'introduction de processeurs qui sont capables d'exécuter plusieurs threads d'exécution simultanément. On parle alors de processeur :term:`multi-coeurs` ou :term:`multi-threadé`. -.. .. note:: Vers des processeurs massivement multi-threadé -.. Aujourd'hui, les processeurs standards sont capables d'exécuter 4, 8 16 voire 32 threads d'exécution simultanément. La notion de thread d'exécution est très importante dans un système informatique. Elle permet non seulement de comprendre comme un ordinateur équipé d'un seul microprocesseur peut exécuter plusieurs programmes simultanément, mais aussi comment des programmes peuvent profiter des nouveaux processeurs capables d'exécuter plusieurs threads simultanément. Pour comprendre cette notion, il est intéressant de revenir à nouveau sur l'exécution d'une fonction en langage assembleur. Considérons la fonction ``f`` : @@ -113,13 +126,17 @@ Pour qu'un processeur puisse exécuter cette séquence d'instructions, il faut n - au registre ``%eip`` qui contient l'adresse de l'instruction en cours d'exécution - au registre ``eflags`` qui contient l'ensemble des drapeaux -Un processeur multithreadé a la capacité d'exécuter plusieurs programmes simultanément. En pratique, ce processeur disposera de plusieurs copies des registres. Chacun de ces blocs de registres pourra être utilisé pour exécuter ces programmes simultanément à raison d'un thread d'exécution par bloc de registres. Chaque thread d'exécution va correspondre à une séquence différente d'instructions qui va modifier son propre bloc de registres. C'est grâce à cette capacité d'exécuter plusieurs threads d'exécution simultanément que les performances en :term:`MIPS` des microprocesseurs ont pu continuer à croitre alors que leur fréquence d'horloge stagnait. +.. spelling:: + + multithreadé + +Un processeur `multithreadé` a la capacité d'exécuter plusieurs programmes simultanément. En pratique, ce processeur disposera de plusieurs copies des registres. Chacun de ces blocs de registres pourra être utilisé pour exécuter ces programmes simultanément à raison d'un thread d'exécution par bloc de registres. Chaque thread d'exécution va correspondre à une séquence différente d'instructions qui va modifier son propre bloc de registres. C'est grâce à cette capacité d'exécuter plusieurs threads d'exécution simultanément que les performances en :term:`MIPS` des microprocesseurs ont pu continuer à croître alors que leur fréquence d'horloge stagnait. Cette capacité d'exécuter plusieurs threads d'exécution simultanément n'est pas limitée à un thread d'exécution par programme. Sachant qu'un thread d'exécution n'est finalement qu'une séquence d'instructions qui utilisent un bloc de registres, il est tout à fait possible à plusieurs séquences d'exécution appartenant à un même programme de s'exécuter simultanément. Si on revient à la fonction assembleur ci-dessus, il est tout à fait possible que deux invocations de cette fonction s'exécutent simultanément sur un microprocesseur. Pour démarrer une telle instance, il suffit de pouvoir initialiser le bloc de registres nécessaire à la nouvelle instance et ensuite de démarrer l'exécution à la première instruction de la fonction. En pratique, cela nécessite la coopération du système d'exploitation. Différents mécanismes ont été proposés pour permettre à un programme de lancer différents threads d'exécution. Aujourd'hui, le plus courant est connu sous le nom de threads POSIX. C'est celui que nous allons étudier en détail, mais il en existe d'autres. .. note:: D'autres types de threads - A côté des threads POSIX, il existe d'autres types de threads. [Gove2011]_ présente l'implémentation des threads sur différents systèmes d'exploitation. Sous Linux, NTPL [DrepperMolnar2005]_ et LinuxThreads [Leroy]_ sont deux anciennes implémentations des threads POSIX. GNU PTH [GNUPTH]_ est une librairie qui implémente les threads sans interaction directe avec le système d'exploitation. Cela permet à la librairie d'être portable sur de nombreux systèmes d'exploitation. Malheureusement, tous les threads GNU PTH d'un programme doivent s'exécuter sur le même processeur. + A côté des threads POSIX, il existe d'autres types de threads. [Gove2011]_ présente comment implémenter des threads sur différents systèmes d'exploitation. Sous Linux, NTPL [DrepperMolnar2005]_ et LinuxThreads [Leroy]_ sont deux anciennes implémentations des threads POSIX. GNU PTH [GNUPTH]_ est une librairie qui implémente les threads sans interaction directe avec le système d'exploitation. Cela permet à la librairie d'être portable sur de nombreux systèmes d'exploitation. Malheureusement, tous les threads GNU PTH d'un programme doivent s'exécuter sur le même processeur. @@ -147,7 +164,7 @@ Le second argument permet de spécifier des attributs spécifiques au thread qui Le troisième argument contient l'adresse de la fonction par laquelle le nouveau thread va démarrer son exécution. Cette adresse est le point de départ de l'exécution du thread et peut être comparée à la fonction ``main`` qui est lancée par le système d'exploitation lorsqu'un programme est exécuté. Un thread doit toujours débuter son exécution par une fonction dont la signature est ``void * function(void *)``, c'est-à -dire une fonction qui prend comme argument un pointeur générique (de type ``void *``) et retourne un résultat du même type. -Le quatrième argument est l'argument qui est passé à la fonction qui débute le thread qui vient d'être créé. Cet argument est un pointeur générique de type ``void *``, mais la fonction peut bien entendu le caster dans un autre type. +Le quatrième argument est l'argument qui est passé à la fonction qui débute le thread qui vient d'être créé. Cet argument est un pointeur générique de type ``void *``, mais la fonction peut bien entendu le convertir dans un autre type. La fonction `pthread_create(3)`_ retourne un résultat entier. Une valeur de retour non-nulle indique une erreur et ``errno`` est mise à jour. diff --git a/Theorie/Threads/threads2.rst b/Theorie/Threads/threads2.rst index 1a08489a75b669d0eafbd00be2f361658ebed38b..c5538319b49347d91d99124369dd719b47f9a729 100644 --- a/Theorie/Threads/threads2.rst +++ b/Theorie/Threads/threads2.rst @@ -120,6 +120,10 @@ Coordination entre threads L'utilisation de plusieurs threads dans un programme fonctionnant sur un seul ou plusieurs processeurs nécessite l'utilisation de mécanismes de coordination entre ces threads. Ces mécanismes ont comme objectif d'éviter que deux threads ne puissent modifier ou tester de façon non coordonnée la même zone de la mémoire. +.. spelling:: + + Dijkstra + Exclusion mutuelle ------------------ @@ -127,7 +131,7 @@ Le premier problème important à résoudre lorsque l'on veut coordonner plusieu Considérons un programme décomposé en `N` threads d'exécution. Supposons également que chaque thread d'exécution est cyclique, c'est-à -dire qu'il exécute régulièrement le même ensemble d'instructions, sans que la durée de ce cycle ne soit fixée ni identique pour les `N` threads. Chacun de ces threads peut être décomposé en deux parties distinctes. La première est la partie non-critique. C'est un ensemble d'instructions qui peuvent être exécutées par le thread sans nécessiter la moindre coordination avec un autre thread. Plus précisément, tous les threads peuvent exécuter simultanément leur partie non-critique. La seconde partie du thread est appelée sa :term:`section critique`. Il s'agit d'un ensemble d'instructions qui ne peuvent être exécutées que par un seul et unique thread. Le problème de l':term:`exclusion mutuelle` est de trouver un algorithme qui permet de garantir qu'il n'y aura jamais deux threads qui simultanément exécuteront les instructions de leur section critique. -Cela revient à dire qu'il n'y aura pas de violation de la section critique. Une telle violation pourrait avoir des conséquences catastrophiques sur l'exécution du programme. Cette propriété est une propriété de :term:`sureté` (:term:`safety` en anglais). Dans un programme découpé en threads, une propriété de :term:`sureté` est une propriété qui doit être vérifiée à tout instant de l'exécution du programme. +Cela revient à dire qu'il n'y aura pas de violation de la section critique. Une telle violation pourrait avoir des conséquences catastrophiques sur l'exécution du programme. Cette propriété est une propriété de :term:`sûreté` (:term:`safety` en anglais). Dans un programme découpé en threads, une propriété de :term:`sûreté` est une propriété qui doit être vérifiée à tout instant de l'exécution du programme. En outre, une solution au problème de l':term:`exclusion mutuelle` doit satisfaire les contraintes suivantes [Dijkstra1965]_ : @@ -140,6 +144,11 @@ La troisième contrainte implique que la terminaison ou le crash d'un des thread La quatrième contrainte est un peu plus subtile mais tout aussi importante. Toute solution au problème de l'exclusion mutuelle contient nécessairement un mécanisme qui permet de bloquer l'exécution d'un thread pendant qu'un autre exécute sa section critique. Il est important qu'un thread puisse accéder à sa section critique si il le souhaite. C'est un exemple de propriété de :term:`vivacité` (:term:`liveness` en anglais). Une propriété de :term:`vivacité` est une propriété qui ne peut pas être éternellement invalidée. Dans notre exemple, un thread ne pourra jamais être empêché d'accéder à sa section critique. +.. spelling:: + + monoprocesseur + monoprocesseurs + Exclusion mutuelle sur monoprocesseurs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -169,11 +178,19 @@ Ces interactions entre les threads et le système d'exploitation sont importante Etats d'un thread d'exécution -Lorsqu'un thread est créé avec la fonction `pthread_create(3)`_, il est placé dans l'état `Ready`. Dans cet état, les instructions du thread ne s'exécutent sur aucun processeur mais il est prêt à être exécuté dès qu'un processeur se libèrera. Le deuxième état pour un thread est l'état `Running`. Dans cet état, le thread est exécuté sur un des processeurs du système. Le dernier état est l'état `Blocked`. Un thread est dans l'état `Blocked` lorsqu'il a exécuté un appel système bloquant et que le système d'exploitation attend l'information permettant de retourner le résultat de l'appel système. Pendant ce temps, les instructions du thread ne s'exécutent sur aucun processeur. +Lorsqu'un thread est créé avec la fonction `pthread_create(3)`_, il est placé dans l'état `Ready`. Dans cet état, les instructions du thread ne s'exécutent sur aucun processeur mais il est prêt à être exécuté dès qu'un processeur se libérera. Le deuxième état pour un thread est l'état `Running`. Dans cet état, le thread est exécuté sur un des processeurs du système. Le dernier état est l'état `Blocked`. Un thread est dans l'état `Blocked` lorsqu'il a exécuté un appel système bloquant et que le système d'exploitation attend l'information permettant de retourner le résultat de l'appel système. Pendant ce temps, les instructions du thread ne s'exécutent sur aucun processeur. + +.. spelling:: + ordonnanceur + scheduler + schedulers + l'ordonnanceur + l'implémentation + Les transitions entre les différents états d'un thread sont gérées par le système d'exploitation. Lorsque plusieurs threads d'exécution sont simultanément actifs, le système d'exploitation doit arbitrer les demandes d'utilisation du CPU de chaque thread. Cet arbitrage est réalisé par l'ordonnanceur (ou :term:`scheduler` en anglais). Le :term:`scheduler` est un ensemble d'algorithmes qui sont utilisés par le système d'exploitation pour sélectionner le ou les threads qui peuvent utiliser un processeur à un moment donné. Il y a souvent plus de threads qui sont dans l'état `Ready` que de processeurs disponibles et le scheduler doit déterminer quels sont les threads à exécuter. -Une description détaillée du fonctionnement d'un scheduler relève plutôt d'un cours sur les systèmes d'exploitation que d'un premier cours sur les systèmes informatiques, mais il est important de connaître les principes de base de fonctionnement de quelques schedulers. +Une description détaillée du fonctionnement d'un scheduler relève plutôt d'un cours sur les systèmes d'exploitation que d'un premier cours sur le langage C, mais il est important de connaître les principes de base de fonctionnement de quelques schedulers. Un premier scheduler simple est le :term:`round-robin`. Ce scheduler maintient en permanence une liste circulaire de l'ensemble des threads qui se trouvent dans l'état `Ready` et un pointeur vers l'élément courant de cette liste. Lorsqu'un processeur devient disponible, le scheduler sélectionne le thread référencé par ce pointeur. Ce thread passe dans l'état `Running`, est retiré de la liste et le pointeur est déplacé vers l'élément suivant dans la liste. Pour éviter qu'un thread ne puisse monopoliser éternellement un processeur, un scheduler :term:`round-robin` limite généralement le temps qu'un thread peut passer dans l'état `Running`. Lorsqu'un thread a utilisé un processeur pendant ce temps, le scheduler vérifie si il y a un thread en attente dans l'état `Ready`. Si c'est le cas, le scheduler force un changement de contexte, place le thread courant dans l'état `Ready` et le remet dans la liste circulaire tout en permettant à un nouveau thread de passer dans l'état `Running` pour s'exécuter. Lorsqu'un thread revient dans l'état `Ready`, soit parce qu'il vient d'être créé ou parce qu'il vient de quitter l'état `Blocked`, il est placé dans la liste afin de pouvoir être sélectionné par le scheduler. Un scheduler :term:`round-robin` est équitable. Avec un tel scheduler, si `N` threads sont actifs en permanence, chacun recevra :math:`\frac{1}{N}` de temps CPU disponible. @@ -184,7 +201,7 @@ Connaissant ces bases du fonctionnement des schedulers, il est utile d'analyser .. note:: Un thread peut demander de passer la main. - Dans la plupart de nos exemples, les threads cherchent en permanence à exécuter des instructions. Ce n'est pas nécessairement le cas de tous les threads d'un programme. Par exemple, une application de calcul scientifique pourrait être découpée en `N+1` threads. Les `N` premiers threads réalisent le calcul tandis que le dernier calcule des statistiques. Ce dernier thread ne doit pas consommer de ressources et être en compétition pour le processeur avec les autres threads. La librairie thread POSIX contient la fonction `pthread_yield(3)`_ qui peut être utilisée par un thread pour indiquer explicitement qu'il peut être remplacé par un autre thread. Si un thread ne doit s'exécuter qu'à intervalles réguliers, il est préférable d'utiliser des appels à `sleep(3)`_ ou `usleep(3)`_. Ces fonctions de la librarie permettent de demander au système d'exploitation de bloquer le thread pendant un temps au moins égal à l'argument de la fonction. + Dans la plupart de nos exemples, les threads cherchent en permanence à exécuter des instructions. Ce n'est pas nécessairement le cas de tous les threads d'un programme. Par exemple, une application de calcul scientifique pourrait être découpée en `N+1` threads. Les `N` premiers threads réalisent le calcul tandis que le dernier calcule des statistiques. Ce dernier thread ne doit pas consommer de ressources et être en compétition pour le processeur avec les autres threads. La librairie thread POSIX contient la fonction `pthread_yield(3)`_ qui peut être utilisée par un thread pour indiquer explicitement qu'il peut être remplacé par un autre thread. Si un thread ne doit s'exécuter qu'à intervalles réguliers, il est préférable d'utiliser des appels à `sleep(3)`_ ou `usleep(3)`_. Ces fonctions de la librairie permettent de demander au système d'exploitation de bloquer le thread pendant un temps au moins égal à l'argument de la fonction. Sur une machine monoprocesseur, tous les threads s'exécutent sur le même processeur. Une violation de section critique peut se produire lorsque le scheduler décide de réaliser un changement de contexte alors qu'un thread se trouve dans sa section critique. Si la section critique d'un thread ne contient ni d'appel système bloquant ni d'appel à `pthread_yield(3)`_, ce changement de contexte ne pourra se produire que si une interruption survient. Une solution pour résoudre le problème de l'exclusion mutuelle sur un ordinateur monoprocesseur pourrait donc être la suivante : @@ -199,189 +216,9 @@ Sur une machine monoprocesseur, tous les threads s'exécutent sur le même proce Cette solution est possible, mais elle souffre de plusieurs inconvénients majeurs. Tout d'abord, une désactivation des interruptions perturbe le fonctionnement du système puisque sans interruptions, la plupart des opérations d'entrées-sorties et l'horloge sont inutilisables. Une telle désactivation ne peut être que très courte, par exemple pour modifier une ou quelques variables en mémoire. Ensuite, la désactivation des interruptions, comme d'autres opérations relatives au fonctionnement du matériel, est une opération privilégiée sur un microprocesseur. Elle ne peut être réalisée que par le système d'exploitation. Il faudrait donc imaginer un appel système qui permettrait à un thread de demander au système d'exploitation de désactiver les interruptions. Si un tel appel système existait, le premier programme qui exécuterait ``disable_interrupts();`` sans le faire suivre de ``enable_interrupts();`` quelques instants après pourrait rendre la machine complètement inutilisable puisque sans interruption plus aucune opération d'entrée-sortie n'est possible et qu'en plus le scheduler ne peut plus être activé par l'interruption d'horloge. Pour toutes ces raisons, la désactivation des interruptions n'est pas un mécanisme utilisable par les threads pour résoudre le problème de l'exclusion mutuelle [#fdisable]_. - -Algorithme de Peterson -^^^^^^^^^^^^^^^^^^^^^^ - -.. todo:: Algorithme de Dijkstra, [Dijkstra1965]_ - -.. todo:: Algorithme de Dekker - -.. todo:: Lamport A New Solution of Dijkstra's Concurrent Programming Problem Communications of the ACM 17, 8 (August 1974), 453-455. (bakery algorithm) - -.. todo:: Autres algorithmes [Alagarsamy2003]_ - - -Le problème de l'exclusion mutuelle a intéressé de nombreux informaticiens depuis le début des années 1960s [Dijkstra1965]_ et différentes solutions à ce problème ont été proposées. Plusieurs d'entre elles sont analysées en détails dans [Alagarsamy2003]_. Dans cette section, nous nous concentrerons sur une de ces solutions proposées par G. Peterson en 1981 [Peterson1981]_. Cette solution permet à plusieurs threads de coordonner leur exécution de façon à éviter une violation de section critique en utilisant uniquement des variables accessibles à tous les threads. La solution proposée par Peterson permet de gérer `N` threads [Peterson1981]_ mais nous nous limiterons à sa version permettant de coordonner deux threads. - -Une première solution permettant de coordonner deux threads en utilisant des variables partagées pourrait être de s'appuyer sur une variable qui permet de déterminer quel est le thread qui peut entrer en section critique. Dans l'implémentation ci-dessous, la variable partagée ``turn`` est utilisée par les deux threads et permet de coordonner leur exécution. ``turn`` peut prendre les valeurs ``0`` ou ``1``. Le premier thread exécute la boucle ``while (turn != 0) { }``. Prise isolément, cette boucle pourrait apparaître comme une boucle inutile (``turn==0`` avant son exécution) ou une boucle infinie (``turn==1`` avant son exécution). Un tel raisonnement est incorrect lorsque la variable ``turn`` peut être modifiée par les deux threads. En effet, si ``turn`` vaut ``1`` au début de la boucle ``while (turn != 0) { }``, la valeur de cette variable peut être modifiée par un autre thread pendant l'exécution de la boucle et donc provoquer son arrêt. - -.. code-block:: c - - // thread 1 - while (turn!=0) - { /* loop */ } - section_critique(); - turn=1; - // ... - - // thread 2 - while (turn!=1) - { /* loop */ } - section_critique(); - turn=0; - -Il est intéressant d'analyser ces deux threads en détails pour déterminer si ils permettent d'éviter une violation de section critique et respectent les 4 contraintes précisées plus haut. Dans ces deux threads, pour qu'une violation de section critique puisse se produire, il faudrait que les deux threads passent en même temps la boucle ``while`` qui précède la section critique. Imaginons que le premier thread est entré dans sa section critique. Puisqu'il est sorti de sa boucle ``while``, cela implique que la variable ``turn`` a la valeur ``0``. Sinon, le premier thread serait toujours en train d'exécuter sa boucle ``while``. Examinons maintenant le fonctionnement du second thread. Pour entrer dans sa section critique, celui-ci va exécuter la boucle ``while (turn != 1){ }``. A ce moment, ``turn`` a la valeur ``0``. La boucle dans le second thread va donc s'exécuter en permanence. Elle ne s'arrêtera que si la valeur de ``turn`` change. Or, le premier thread ne pourra changer la valeur de ``turn`` que lorsqu'il aura quitté sa section critique. Cette solution évite donc toute violation de la section critique. Malheureusement, elle ne fonctionne que si il y a une alternance stricte entre les deux threads. Le second s'exécute après le premier qui lui même s'exécute après le second, ... Cette alternance n'est évidemment pas acceptable. - -Analysons une seconde solution. Celle-ci utilise un tableau ``flag`` contenant deux drapeaux, un par thread. Ces deux drapeaux sont initialisés à la valeur ``false``. Pour plus de facilité, nous nommons les threads en utilisant la lettre ``A`` pour le premier et ``B`` pour le second. Le drapeau ``flag[x]`` est modifié par le thread ``x`` et sa valeur est testée par l'autre thread. - -.. code-block:: c - - #define A 0 - #define B 1 - int flag[]; - flag[A]=false; - flag[B]=false; - - -Le premier thread peut s'écrire comme suit. Il comprend une boucle ``while`` qui teste le drapeau ``flag[B]`` du second thread. Avant d'entrer en section critique, il met son drapeau ``flag[A]`` à ``true`` et le remet à ``false`` dès qu'il en est sorti. - -.. code-block:: c - - // Thread A - while (flag[B]==true) - { /* loop */ } - flag[A]=true; - section_critique(); - flag[A]=false; - //... - -Le second thread est organisé d'une façon similaire. - -.. code-block:: c - - // Thread B - while (flag[A]==true) - { /* loop */ } - flag[B]=true; - section_critique(); - flag[B]=false; - // ... - -Analysons le fonctionnement de cette solution et vérifions si elle permet d'éviter toute violation de section critique. Pour qu'une violation de section critique se produise, il faudrait que les deux threads exécutent simultanément leur section critique. La boucle ``while`` qui précède dans chaque thread l'entrée en section critique parait éviter les problèmes puisque si le thread ``A`` est dans sa section critique, il a mis ``flag[A]`` à la valeur ``true`` et donc le thread ``B`` exécutera en permanence sa boucle ``while``. Malheureusement, la situation suivante est possible. Supposons que ``flag[A]`` et ``flag[B]`` ont la valeur ``false`` et que les deux threads souhaitent entrer dans leur section critique en même temps. Chaque thread va pouvoir traverser sa boucle ``while`` sans attente puis seulement mettre son drapeau à ``true``. A cet instant il est trop tard et une violation de section critique se produira. Cette violation a été illustrée sur une machine multiprocesseur qui exécute deux threads simultanément. Elle est possible également sur une machine monoprocesseur. Dans ce cas, il suffit d'imaginer que le thread ``A`` passe sa boucle ``while`` et est interrompu par le scheduler avant d'exécuter ``flag[A]=true;``. Le scheduler réalise un changement de contexte et permet au thread ``B`` de s'exécuter. Il peut passer sa boucle ``while`` puis entre en section critique alors que le thread ``A`` est également prêt à y entrer. - -Une alternative pour éviter le problème de violation de l'exclusion mutuelle pourrait être d'inverser la boucle ``while`` et l'assignation du drapeau. Pour le thread ``A``, cela donnerait le code ci-dessous : - - -.. code-block:: c - - // Thread A - flag[A]=true; - while (flag[B]==true) - { /* loop */ } - section_critique(); - flag[A]=false; - //... - -Le thread ``B`` peut s'implémenter de façon similaire. Analysons le fonctionnement de cette solution sur un ordinateur monoprocesseur. Un scénario possible est le suivant. Le thread ``A`` exécute la ligne permettant d'assigner son drapeau, ``flag[A]=true;``. Après cette assignation, le scheduler interrompt ce thread et démarre le thread ``B``. Celui-ci exécute ``flag[B]=true;`` puis démarre sa boucle ``while``. Vu le contenu du drapeau ``flag[A]``, celle-ci va s'exécuter en permanence. Après quelque temps, le scheduler repasse la main au thread ``A`` qui va lui aussi entamer sa boucle ``while``. Comme ``flag[B]`` a été mis à ``true`` par le thread ``B``, le thread ``A`` entame également sa boucle ``while``. A partir de cet instant, les deux threads vont exécuter leur boucle ``while`` qui protège l'accès à la section critique. Malheureusement, comme chaque thread exécute sa boucle ``while`` aucun des threads ne va modifier son drapeau de façon à permettre à l'autre thread de sortir de sa boucle. Cette situation perdurera indéfiniment. Dans la littérature, cette situation est baptisée un :term:`livelock`. Un :term:`livelock` est une situation dans laquelle plusieurs threads exécutent une séquence d'instructions (dans ce cas les instructions relatives aux boucles ``while``) sans qu'aucun thread ne puisse réaliser de progrès. Un :term:`livelock` est un problème extrêmement gênant puisque lorsqu'il survient les threads concernés continuent à utiliser le processeur mais n'exécutent aucune instruction utile. Il peut être très difficile à diagnostiquer et il est important de réfléchir à la structure du programme et aux techniques de coordination entre les threads qui sont utilisées afin de garantir qu'aucun :term:`livelock` ne pourra se produire. - -L'algorithme de Peterson [Peterson1981]_ combine les deux idées présentées plus tôt. Il utilise une variable ``turn`` qui est testée et modifiée par les deux threads comme dans la première solution et un tableau ``flag[]`` comme la seconde. Les drapeaux du tableau sont initialisés à ``false`` et la variable ``turn`` peut prendre la valeur ``A`` ou ``B``. - -.. code-block:: c - - #define A 0 - #define B 1 - int flag[]; - flag[A]=false; - flag[B]=false; - -Le thread ``A`` peut s'écrire comme suit. - -.. code-block:: c - - // thread A - flag[A]=true; - turn=B; - while((flag[B]==true)&&(turn==B)) - { /* loop */ } - section_critique(); - flag[A]=false; - // ... - -Le thread ``B`` s'implémente de façon similaire. - -.. code-block:: c - - // Thread B - flag[B]=true; - turn=A; - while((flag[A]==true)&&(turn==A)) - { /* loop */ } - section_critique(); - flag[B]=false; - // ... - -Pour vérifier si cette solution répond bien au problème de l'exclusion mutuelle, il nous faut d'abord vérifier qu'il ne peut y avoir de violation de la section critique. Pour qu'une violation de section critique soit possible, il faudrait que les deux threads soient sortis de leur boucle ``while``. Examinons le cas où le thread ``B`` se trouve en section critique. Dans ce cas, ``flag[B]`` a la valeur ``true``. Si le thread ``A`` veut entrer en section critique, il va d'abord devoir exécuter ``flag[A]=true;`` et ensuite ``turn=B;``. Comme le thread ``B`` ne modifie ni ``flag[A]`` ni ``turn`` dans sa section critique, thread ``A`` va devoir exécuter sa boucle ``while`` jusqu'à ce que le thread ``B`` sorte de sa section critique et exécute ``flag[B]=false;``. Il ne peut donc pas y avoir de violation de la section critique. - -Il nous faut également montrer que l'algorithme de Peterson ne peut pas causer de :term:`livelock`. Pour qu'un tel :term:`livelock` soit possible, il faudrait que les boucles ``while((flag[A]==true)&&(turn==A)) {};`` et ``while((flag[B]==true)&&(turn==B)) {};`` puissent s'exécuter en permanence en même temps. Comme la variable ``turn`` ne peut prendre que la valeur ``A`` ou la valeur ``B``, il est impossible que les deux conditions de boucle soient simultanément vraies. - -Enfin, considérons l'impact de l'arrêt d'un des deux threads. Si thread ``A`` s'arrête hors de sa section critique, ``flag[A]`` a la valeur ``false`` et le thread ``B`` pourra toujours accéder à sa section critique. - - -Utilisation d'instruction atomique -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Sur les ordinateurs actuels, il devient difficile d'utiliser l'algorithme de Peterson tel qu'il a été décrit et ce pour deux raisons. Tout d'abord, les compilateurs C sont capables d'optimiser le code qu'ils génèrent. Pour cela, ils analysent le programme à compiler et peuvent supprimer des instructions qui leur semblent être inutiles. Dans le cas de l'algorithme de Peterson, le compilateur pourrait très bien considérer que la boucle ``while`` est inutile puisque les variables ``turn`` et ``flag`` ont été initialisées juste avant d'entrer dans la boucle. - -La deuxième raison est que sur un ordinateur multiprocesseur, chaque processeur peut réordonner les accès à la mémoire automatiquement afin d'en optimiser les performances [McKenney2005]_. Cela a comme conséquence que certaines lectures et écritures en mémoires peuvent se faire dans un autre ordre que celui indiqué dans le programme sur certaines architectures de processeurs. Si dans l'algorithme de Peterson le thread ``A`` lit la valeur de ``flag[B]`` alors que l'écriture en mémoire pour ``flag[A]`` n'a pas encore été effectuée, une violation de la section critique est possible. En effet, dans ce cas les deux threads peuvent tous les deux passer leur boucle ``while`` avant que la mise à jour de leur drapeau n'aie été faite effectivement en mémoire. - -Pour résoudre ce problème, les architectes de microprocesseurs ont proposé l'utilisation d'opérations atomiques. Une :term:`opération atomique` est une opération qui lorsqu'elle est exécutée sur un processeur ne peut pas être interrompue par l'arrivée d'une interruption. Ces opérations permettent généralement de manipuler en même temps un registre et une adresse en mémoire. En plus de leur caractère interruptible, l'exécution de ces instructions atomiques par un ou plusieurs processeur implique une coordination des processeurs pour l'accès à la zone mémoire référencée dans l'instruction. Via un mécanisme qui sort du cadre de ces notes, tous les accès à la mémoire faits par ces instructions sont ordonnés par les processeurs de façon à ce qu'ils soient toujours réalisés séquentiellement. - -Plusieurs types d'instructions atomiques sont supportés par différentes architectures de processeurs. A titre d'exemple, considérons l'instruction atomique ``xchg`` qui est supportée par les processeurs [IA32]_. Cette instruction permet d'échanger, de façon atomique, le contenu d'un registre avec une zone de la mémoire. Elle prend deux arguments, un registre et une adresse en mémoire. Ainsi, l'instruction ``xchgl %eax,(var)`` est équivalente aux trois instructions suivantes, en supposant le registre ``%ebx`` initialement vide. La première sauvegarde dans ``%ebx`` le contenu de la mémoire à l'adresse ``var``. La deuxième copie le contenu du registre ``%eax`` à cette adresse mémoire et la dernière transfère le contenu de ``%ebx`` dans ``%eax`` de façon à terminer l'échange de valeurs. - -.. code-block:: nasm - - movl (var), %ebx - movl %eax, (var) - movl %ebx, %eax - -Avec cette instruction atomique, il est possible de résoudre le problème de l'exclusion mutuelle en utilisant une zone mémoire, baptisée ``lock`` dans l'exemple. Cette zone mémoire contiendra la valeur ``1`` ou ``0``. Cette zone mémoire est initialisée à ``0``. Lorsqu'un thread veut accéder à sa section critique, il exécute les instructions à partir de l'étiquette ``enter:``. Pour sortir de section critique, il suffit d'exécuter les instructions à partir de l'étiquette ``leave:``. - -.. code-block:: nasm - - lock: ; étiquette, variable - .long 0 ; initialisée à 0 - - enter: - movl $1, %eax ; %eax=1 - xchgl %eax, (lock) ; instruction atomique, échange (lock) et %eax - ; après exécution, %eax contient la donnée qui était - ; dans lock et lock la valeur 1 - testl %eax, %eax ; met le flag ZF à vrai si %eax contient 0 - jnz enter ; retour à enter: si ZF n'est pas vrai - ret - - leave: - mov $0, %eax ; %eax=0 - xchgl %eax, (lock) ; instruction atomique - ret - -Pour bien comprendre le fonctionnement de cette solution, il faut analyser les instructions qui composent chaque routine en assembleur. La routine ``leave`` est la plus simple. Elle place la valeur ``0`` à l'adresse ``lock``. Elle utilise une instruction atomique de façon à garantir que cet accès en mémoire se fait séquentiellement. Lorsque ``lock`` vaut ``0``, cela indique qu'aucun thread ne se trouve en section critique. Si ``lock`` contient la valeur ``1``, cela indique qu'un thread est actuellement dans sa section critique et qu'aucun autre thread ne peut y entrer. Pour entrer en section critique, un thread doit d'abord exécuter la routine ``enter``. Cette routine initialise d'abord le registre ``%eax`` à la valeur ``1``. Ensuite, l'instruction ``xchgl`` est utilisée pour échanger le contenu de ``%eax`` avec la zone mémoire ``lock``. Après l'exécution de cette instruction atomique, l'adresse ``lock`` contiendra nécessairement la valeur ``1``. Par contre, le registre ``%eax`` contiendra la valeur qui se trouvait à l'adresse ``lock`` avant l'exécution de ``xchgl``. C'est en testant cette valeur que le thread pourra déterminer si il peut entrer en section critique ou non. Deux cas sont possibles : - - a. ``%eax==0`` après exécution de l'instruction ``xchgl %eax, (lock)``. Dans ce cas, le thread peut accéder à sa section critique. En effet, cela indique qu'avant l'exécution de cette instruction l'adresse ``lock`` contenait la valeur ``0``. Cette valeur indique que la section critique était libre avant l'exécution de l'instruction ``xchgl %eax, (lock)``. En outre, cette instruction a placé la valeur ``1`` à l'adresse ``lock``, ce qui indique qu'un thread exécute actuellement sa section critique. Si un autre thread exécute l'instruction ``xchgl %eax, (lock)`` à cet instant, il récupèrera la valeur ``1`` dans ``%eax`` et ne pourra donc pas entre en section critique. Si deux threads exécutent simultanément et sur des processeurs différents l'instruction ``xchgl %eax, (lock)``, la coordination des accès mémoires entre les processeurs garantit que ces accès mémoires seront séquentiels. Le thread qui bénéficiera du premier accès à la mémoire sera celui qui récupèrera la valeur ``0`` dans ``%eax`` et pourra entrer dans sa section critique. Le ou les autres threads récupéreront la valeur ``1`` dans ``%eax`` et boucleront. - b. ``%eax==1`` après exécution de l'instruction ``xchgl %eax, (lock)``. Dans ce cas, le thread ne peut entrer en section critique et il entame une boucle active durant laquelle il va continuellement exécuter la boucle ``enter: movl ... jnz enter``. - - -.. todo:: inversion de priorité ? - -En pratique, rares sont les programmes qui coordonnent leurs threads en utilisant des instructions atomiques ou l'algorithme de Peterson. Ces programmes profitent généralement des fonctions de coordination qui sont implémentées dans des librairies du système d'exploitation. - - Coordination par Mutex ^^^^^^^^^^^^^^^^^^^^^^ -L'algorithme de Peterson et l'utilisation d'instructions atomiques sont des mécanismes de base permettant de résoudre le problème de l'exclusion mutuelle. Ils sont utilisés par des fonctions de la libraire POSIX threads. Il est préférable pour des raisons de portabilité et de prise en compte de spécificités matérielles de certains processeurs d'utiliser les fonctions de la librairie POSIX threads plutôt que de redévelopper soi-même ces primitives de coordination entre threads. - Le premier mécanisme de coordination entre threads dans la librairie POSIX sont les :term:`mutex`. Un :term:`mutex` (abréviation de `mutual exclusion`) est une structure de données qui permet de contrôler l'accès à une ressource. Un :term:`mutex` qui contrôle une ressource peut se trouver dans deux états : - `libre` (ou `unlocked` en anglais). Cet état indique que la ressource est libre et peut être utilisée sans risquer de provoquer une violation d'exclusion mutuelle. @@ -450,7 +287,6 @@ Lorsqu'un :term:`mutex` POSIX est initialisé, la ressource qui lui est associé `pthread_mutex_lock(3posix)`_ et `pthread_mutex_unlock(3posix)`_ sont toujours utilisés en couple. `pthread_mutex_lock(3posix)`_ doit toujours précéder l'accès à la ressource partagée et `pthread_mutex_unlock(3posix)`_ doit être appelé dès que l'accès exclusif à la ressource partagée n'est plus nécessaire. -.. todo:: L'identification de la section critique d'un programme est un des problèmes de design les plus importants L'utilisation des mutex permet de résoudre correctement le problème de l'exclusion mutuelle. Pour s'en convaincre, considérons le programme ci-dessus et les threads qui exécutent la fonction ``func``. Celle-ci peut être résumée par les trois lignes suivantes : @@ -460,121 +296,10 @@ L'utilisation des mutex permet de résoudre correctement le problème de l'exclu global=increment(global); pthread_mutex_unlock(&mutex_global); -Pour montrer que cette solution répond bien au problème de l'exclusion mutuelle, il faut montrer qu'elle respecte la propriété de sureté et la propriété de vivacité. Pour la propriété de sureté, c'est par construction des :term:`mutex` et parce que chaque thread exécute `pthread_mutex_lock(3posix)`_ avant d'entrer en section critique et `pthread_mutex_unlock(3posix)`_ dès qu'il en sort. Considérons le cas de deux threads qui sont en concurrence pour accéder à cette section critique. Le premier exécute `pthread_mutex_lock(3posix)`_. Il accède à sa section critique. A partir de cet instant, le second thread sera bloqué dès qu'il exécute l'appel à `pthread_mutex_lock(3posix)`_. Il restera bloqué dans l'exécution de cette fonction jusqu'à ce que le premier thread sorte de sa section critique et exécute `pthread_mutex_unlock(3posix)`_. A ce moment, le premier thread n'est plus dans sa section critique et le système peut laisser le second y entrer en terminant l'exécution de l'appel à `pthread_mutex_lock(3posix)`_. Si un troisième thread essaye à ce moment d'entrer dans la section critique, il sera bloqué sur son appel à `pthread_mutex_lock(3posix)`_. +Pour montrer que cette solution répond bien au problème de l'exclusion mutuelle, il faut montrer qu'elle respecte la propriété de sûreté et la propriété de vivacité. Pour la propriété de sûreté, c'est par construction des :term:`mutex` et parce que chaque thread exécute `pthread_mutex_lock(3posix)`_ avant d'entrer en section critique et `pthread_mutex_unlock(3posix)`_ dès qu'il en sort. Considérons le cas de deux threads qui sont en concurrence pour accéder à cette section critique. Le premier exécute `pthread_mutex_lock(3posix)`_. Il accède à sa section critique. A partir de cet instant, le second thread sera bloqué dès qu'il exécute l'appel à `pthread_mutex_lock(3posix)`_. Il restera bloqué dans l'exécution de cette fonction jusqu'à ce que le premier thread sorte de sa section critique et exécute `pthread_mutex_unlock(3posix)`_. A ce moment, le premier thread n'est plus dans sa section critique et le système peut laisser le second y entrer en terminant l'exécution de l'appel à `pthread_mutex_lock(3posix)`_. Si un troisième thread essaye à ce moment d'entrer dans la section critique, il sera bloqué sur son appel à `pthread_mutex_lock(3posix)`_. Pour montrer que la propriété de vivacité est bien respectée, il faut montrer qu'un thread ne sera pas empêché éternellement d'entrer dans sa section critique. Un thread peut être empêché d'entrer dans sa section critique en étant bloqué sur l'appel à `pthread_mutex_lock(3posix)`_. Comme chaque thread exécute `pthread_mutex_unlock(3posix)`_ dès qu'il sort de sa section critique, le thread en attente finira par être exécuté. Pour qu'un thread utilisant le code ci-dessus ne puisse jamais entrer en section critique, il faudrait qu'il y aie en permanence plusieurs threads en attente sur `pthread_mutex_unlock(3posix)`_ et que notre thread ne soit jamais sélectionné par le système lorsque le thread précédent termine sa section critique. -.. .. note:: spinlock versus mutex - -.. ??? Todo ??? - - -Le problème des philosophes ---------------------------- - -Le problème de l'exclusion mutuelle considère le cas de plusieurs threads qui se partagent une ressource commune qui doit être manipulée de façon exclusive. C'est un problème fréquent qui se pose lorsque l'on développe des programmes décomposés en différents threads d'exécution, mais ce n'est pas le seul. En pratique, il est souvent nécessaire de coordonner l'accès de plusieurs threads à plusieurs ressources, chacune de ces ressources devant être utilisée de façon exclusive. Cette utilisation de plusieurs ressources simultanément peut poser des difficultés. Un des problèmes classiques qui permet d'illustrer la difficulté de coordonner l'accès à plusieurs ressources est le `problème des philosophes`. Ce problème a été proposé par Dijkstra et illustre en termes simples la difficulté de coordonner l'accès à plusieurs ressources. - -Dans le :term:`problème des philosophes`, un ensemble de philosophes doivent se partager des baguettes pour manger. Tous les philosophes se trouvent dans une même pièce qui contient une table circulaire. Chaque philosophe dispose d'une place qui lui est réservée sur cette table. La table comprend autant de baguettes que de chaises et une baguette est placée entre chaque paire de chaises. Chaque philosophe est modélisé sous la forme d'un thread qui effectue deux types d'actions : `penser` et `manger`. Pour pouvoir manger, un philosophe doit obtenir la baguette qui se trouve à sa gauche et la baguette qui se trouve à sa droite. Lorsqu'il a fini de manger, il peut retourner à son activité philosophale. La figure ci-dessous illustre une table avec les assiettes de trois philosophes et les trois baguettes qui sont à leur disposition. - -.. figure:: /Threads/S6-fig/figures-002-c.png - :align: center - - Problème des philosophes - - -Ce problème de la vie courante peut se modéliser en utilisant un programme C avec les threads POSIX. Chaque baguette est une ressource partagée qui ne peut être utilisée que par un philosophe à la fois. Elle peut donc être modélisée par un :term:`mutex`. Chaque philosophe est modélisé par un thread qui pense puis ensuite appelle `pthread_mutex_lock(3posix)`_ pour obtenir chacune de ses baguettes. Le philosophe peut ensuite manger puis il libère ses baguettes en appelant `pthread_mutex_unlock(3posix)`_. L'extrait [#fphilo]_ ci-dessous comprend les fonctions utilisées par chacun des threads. - -.. literalinclude:: /Threads/S6-src/pthread-philo.c - :encoding: utf-8 - :language: c - :start-after: ///AAA - :end-before: ///BBB - -Malheureusement, cette solution ne permet pas de résoudre le problème des philosophes. En effet, la première exécution du programme (:download:`/Threads/S6-src/pthread-philo.c`) indique à l'écran que les philosophes se partagent les baguettes puis après une fraction de seconde le programme s'arrête en affichant une sortie qui se termine : - -.. code-block:: console - - Philosophe [0] a libéré baguette gauche [0] - Philosophe [0] a libéré baguette droite [1] - Philosophe [0] pense - Philosophe [0] possède baguette gauche [0] - Philosophe [2] possède baguette gauche [2] - Philosophe [1] possède baguette gauche [1] - -Ce blocage de programme est un autre problème majeur auquel il faut faire attention lorsque l'on découpe un programme en plusieurs threads d'exécution. Il porte le nom de :term:`deadlock` que l'on peut traduire en français pas étreinte fatale. Un programme est en situation de deadlock lorsque tous ses threads d'exécution sont bloqués et qu'aucun d'entre eux ne peux être débloqué sans exécuter d'instructions d'un des threads bloqués. Dans ce cas particulier, le programme est bloqué parce que le ``philosophe[0]`` a pu réserver la ``baguette[0]``. Malheureusement, en même temps le ``philosophe[2]`` a obtenu ``baguette[2]`` et ``philosophe[0]`` est donc bloqué sur la ligne ``pthread_mutex_lock(&baguette[right]);``. Entre temps, le ``philosophe[1]`` a pu exécuter la ligne ``pthread_mutex_lock(&baguette[left]);`` et a obtenu ``baguette[1]``. Dans cet état, tous les threads sont bloqués sur la ligne ``pthread_mutex_lock(&baguette[right]);`` et plus aucun progrès n'est possible. - -.. todo:: conditions deadlock .. comment:: A deadlock situation can arise only if all of the following conditions hold simultaneously in a system:[1] Mutual Exclusion: At least one resource must be non-shareable.[1] Only one process can use the resource at any given instant of time. Hold and Wait or Resource Holding: A process is currently holding at least one resource and requesting additional resources which are being held by other processes. No Preemption: The operating system must not de-allocate resources once they have been allocated; they must be released by the holding process voluntarily. Circular Wait: A process must be waiting for a resource which is being held by another process, which in turn is waiting for the first process to release the resource. In general, there is a set of waiting processes, P = {P1, P2, ..., PN}, such that P1 is waiting for a resource held by P2, P2 is waiting for a resource held by P3 and so on till PN is waiting for a resource held by P1.[1][7] These four conditions are known as the Coffman conditions from their first description in a 1971 article by Edward G. Coffman, Jr.[7] Unfulfillment of any of these conditions is enough to preclude a deadlock from occurring. - - -Ce problème de deadlock est un des problèmes les plus graves qui peuvent survenir dans un programme découpé en plusieurs threads. Si les threads entrent dans une situation de deadlock, le programme est complètement bloqué et la seule façon de sortir du deadlock est d'arrêter le programme et de le redémarrer. Ce n'est évidemment pas acceptable. Dans l'exemple des philosophes ci-dessus, le deadlock apparait assez rapidement car les threads passent la majorité de leur temps à exécuter les fonctions de la librairie threads POSIX. En pratique, un thread exécute de nombreuses lignes de code standard et utilise rarement la fonction `pthread_mutex_lock(3posix)`_. Dans de tels programmes, les tests ne permettent pas nécessairement de détecter toutes les situations qui peuvent causer un deadlock. Il est important que le programme soit conçu de façon à toujours éviter les deadlocks. Si c'est n'est pas le cas, le programme finira toujours bien à se retrouver en situation de deadlock après un temps plus ou moins long. - -Le problème des philosophes est une illustration d'un problème courant dans lequel plusieurs threads doivent utiliser deux ou plusieurs ressources partagées contrôlées par un mutex. Dans la situation de deadlock, chaque thread est bloqué à l'exécution de la ligne ``pthread_mutex_lock(&baguette[right]);``. On pourrait envisager de chercher à résoudre le problème en inversant les deux appels à `pthread_mutex_lock(3posix)`_ comme ci-dessous : - -.. code-block:: c - - pthread_mutex_lock(&baguette[right]); - pthread_mutex_lock(&baguette[left]); - -Malheureusement, cette solution ne résout pas le problème du deadlock. La seule différence par rapport au programme précédent est que les mutex qui sont alloués à chaque thread en deadlock ne sont pas les mêmes. Avec cette nouvelle version du programme, lorsque le deadlock survient, les mutex suivants sont alloués : - -.. code-block:: console - - Philosophe [2] possède baguette droite [1] - Philosophe [0] possède baguette droite [2] - Philosophe [1] possède baguette droite [0] - - -Pour comprendre l'origine du deadlock, il faut analyser plus en détails le fonctionnement du programme et l'ordre dans lequel les appels à `pthread_mutex_lock(3posix)`_ sont effectués. Trois mutex sont utilisés dans le programme des philosophes : ``baguette[0]``, ``baguette[1]`` et ``baguette[2]``. De façon imagée, chaque philosophe s'approprie d'abord la baguette se trouvant à sa gauche et ensuite la baguette se trouvant à sa droite. - -================== ================ =============== -Philosophe premier mutex second mutex -================== ================ =============== -``philosophe[0]`` ``baguette[2]`` ``baguette[0]`` -``philosophe[1]`` ``baguette[0]`` ``baguette[1]`` -``philosophe[2]`` ``baguette[1]`` ``baguette[2]`` -================== ================ =============== - -L'origine du deadlock dans cette solution au problème des philosophes est l'ordre dans lequel les différents philosophes s'approprient les mutex. Le ``philosophe[0]`` s'approprie d'abord le mutex ``baguette[2]`` et ensuite essaye de s'approprier le mutex ``baguette[0]``. Le ``philosophe[1]`` par contre peut lui directement s'approprier le mutex ``baguette[0]``. Au vu de l'ordre dans lequel les mutex sont alloués, il est possible que chaque thread se soit approprié son premier mutex mais soit bloqué sur la réservation du second. Dans ce cas, un deadlock se produit puisque chaque thread est en attente de la libération d'un mutex sans qu'il ne puisse libérer lui-même le mutex qu'il s'est déjà approprié. - -Il est cependant possible de résoudre le problème en forçant les threads à s'approprier les mutex qu'ils utilisent dans le même ordre. Considérons le tableau ci-dessous dans lequel ``philosophe[0]`` s'approprie d'abord le mutex ``baguette[0]`` et ensuite le mutex ``baguette[2]``. - -================== ================ =============== -Philosophe premier mutex second mutex -================== ================ =============== -``philosophe[0]`` ``baguette[0]`` ``baguette[2]`` -``philosophe[1]`` ``baguette[0]`` ``baguette[1]`` -``philosophe[2]`` ``baguette[1]`` ``baguette[2]`` -================== ================ =============== - -Avec cet ordre d'allocation des mutex, un deadlock n'est plus possible. Il y aura toujours au moins un philosophe qui pourra s'approprier les deux baguettes dont il a besoin pour manger. Pour s'en convaincre, analysons les différentes exécutions possibles. Un deadlock ne pourrait survenir que si tous les philosophes cherchent à manger simultanément. Si un des philosophes ne cherche pas à manger, ses deux baguettes sont nécessairement libres et au moins un de ses voisins philosophes pourra manger. Lorsque tous les philosophes cherchent à manger simultanément, le mutex ``baguette[0]`` ne pourra être attribué qu'à ``philosophe[0]`` ou ``philosophe[1]``. Considérons les deux cas possibles. - - 1. ``philosophe[0]`` s'approprie ``baguette[0]``. Dans ce cas, ``philosophe[1]`` est bloqué sur son premier appel à `pthread_mutex_lock(3posix)`_. Comme ``philosophe[1]`` n'a pas pu s'approprier le mutex ``baguette[0]``, il ne peut non plus s'approprier ``baguette[1]``. ``philosophe[2]`` pourra donc s'approprier le mutex ``baguette[1]``. ``philosophe[0]`` et ``philosophe[2]`` sont maintenant en compétition pour le mutex ``baguette[2]``. Deux cas sont à nouveau possibles. - - a. ``philosophe[0]`` s'approprie ``baguette[2]``. Comme c'est le second mutex nécessaire à ce philosophe, il peut manger. Pendant ce temps, ``philosophe[2]`` est bloqué en attente de ``baguette[2]``. Lorsque ``philosophe[0]`` aura terminé de manger, il libèrera les mutex ``baguette[2]`` et ``baguette[0]``, ce qui permettra à ``philosophe[2]`` ou ``philosophe[1]`` de manger. - b. ``philosophe[2]`` s'approprie ``baguette[2]``. Comme c'est le second mutex nécessaire à ce philosophe, il peut manger. Pendant ce temps, ``philosophe[0]`` est bloqué en attente de ``baguette[2]``. Lorsque ``philosophe[2]`` aura terminé de manger, il libèrera les mutex ``baguette[2]`` et ``baguette[1]``, ce qui permettra à ``philosophe[0]`` ou ``philosophe[1]`` de manger. - - 2. ``philosophe[1]`` s'approprie ``baguette[0]``. Le même raisonnement que ci-dessus peut être suivi pour montrer qu'il n'y a pas de deadlock non plus dans ce cas. - -La solution présentée empêche tout deadlock puisqu'à tout moment il n'y a au moins un philosophe qui peut manger. Malheureusement, il est possible avec cette solution qu'un philosophe mange alors que chacun des autres philosophes a pu s'approprier une baguette. Lorsque le nombre de philosophes est élevé (imaginez un congrès avec une centaine de philosophes), cela peut être une source importante d'inefficacité au niveau des performances. - -Une implémentation possible de l'ordre présenté dans la table ci-dessus est reprise dans le programme :download:`/Threads/S6-src/pthread-philo2.c` qui comprend la fonction ``philosophe`` suivante. - -.. literalinclude:: /Threads/S6-src/pthread-philo2.c - :encoding: utf-8 - :language: c - :start-after: ///AAA - :end-before: ///BBB - - -Ce problème des philosophes est à l'origine d'une règle de bonne pratique essentielle pour tout programme découpé en threads dans lequel certains threads doivent acquérir plusieurs mutex. Pour éviter les deadlocks, il est nécessaire d'ordonnancer tous les mutex utilisés par le programme dans un ordre strict. Lorsqu'un thread doit réserver plusieurs mutex en même temps, il doit *toujours* effectuer ses appels à `pthread_mutex_lock(3posix)`_ dans l'ordre choisi pour les mutex. Si cet ordre n'est pas respecté par un des threads, un deadlock peut se produire. - - -.. todo:: expliquer - - -.. Il est intéressant d'examiner ce qu'il s'est passé durant cette exécution. Les trois philosophes ont été lancés rapidement. Lorsque ``philosophe[0]`` mange, il utilise les mutex ``baguette[0]`` et ``baguette[2]``. Le ``philosophe[1]`` lui utilise les baguettes ``baguette[1]`` et ``baguette[0]``, ... Le premier cas intéressant est lorsque les trois philosophes pensent en même temps. Dès qu'ils se décident de manger, ils sont en compétition pour l'accès aux baguettes. Manifestement, c'est ``philosophe[0]`` qui parvient à exécuter ses deux appels ``pthread_mutex_lock(&baguette[left]);`` et ``pthread_mutex_lock(&baguette[right]);``. A ce moment, ``philosophe[2]`` avait déjà exécuté l'appel ` - - .. rubric:: Footnotes @@ -588,7 +313,7 @@ Ce problème des philosophes est à l'origine d'une règle de bonne pratique ess .. [#fman2] Les appels systèmes sont décrits dans la section ``2`` des pages de manuel tandis que la section ``3`` décrit les fonctions de la librairie. -.. [#fdisable] Certains systèmes d'exploitation utilisent une désactivation parfois partielle des interruptions pour résoudre des problèmes d'exclusion mutuelle qui portent sur quelques instructions à l'intérieur du système d'exploitation lui-même. Il faut cependant noter qu'une désactivation des interruptions peut être particulièrement couteuse en termes de performances dans un environnement multiprocesseurs. +.. [#fdisable] Certains systèmes d'exploitation utilisent une désactivation parfois partielle des interruptions pour résoudre des problèmes d'exclusion mutuelle qui portent sur quelques instructions à l'intérieur du système d'exploitation lui-même. Il faut cependant noter qu'une désactivation des interruptions peut être particulièrement coûteuse en termes de performances dans un environnement multiprocesseurs. .. [#fstaticinit] Linux supporte également la macro ``PTHREAD_MUTEX_INITIALIZER`` qui permet d'initialiser directement un ``pthread_mutex_t`` déclaré comme variable globale. Dans cet exemple, la déclaration aurait été : ``pthread_mutex_t global_mutex=PTHREAD_MUTEX_INITIALIZER;`` et l'appel à `pthread_mutex_init(3posix)`_ aurait été inutile. Comme il s'agit d'une extension spécifique à Linux, il est préférable de ne pas l'utiliser pour garantir la portabilité du code. diff --git a/Theorie/bib.rst b/Theorie/bib.rst index 67670a1ded9718f7005808ae584b7db9f3ebbb79..2dd91df4c41c46b45cb13d1ff967aee65998eb2c 100644 --- a/Theorie/bib.rst +++ b/Theorie/bib.rst @@ -10,23 +10,23 @@ Bibliographie .. [AdelsteinLubanovic2007] Adelstein, T., Lubanovic, B., `Linux System Administration`, OReilly, 2007, http://books.google.be/books?id=-jYe2k1p5tIC -.. [Alagarsamy2003] Alagarsamy, K., `Some myths about famous mutual exclusion algorithms`. SIGACT News 34, 3 (September 2003), 94-103. http://doi.acm.org/10.1145/945526.945527 +.. .. [Alagarsamy2003] Alagarsamy, K., `Some myths about famous mutual exclusion algorithms`. SIGACT News 34, 3 (September 2003), 94-103. http://doi.acm.org/10.1145/945526.945527 .. [Amdahl1967] Amdahl, G., `Validity of the Single-Processor Approach to Achieving Large-Scale Computing Capabilities`, Proc. Am. Federation of Information Processing Societies Conf., AFIPS Press, 1967, pp. 483-485, http://dx.doi.org/10.1145/1465482.1465560 -.. [Bashar1997] Bashar, N., `Ariane 5: Who Dunnit?`, IEEE Software 14(3): 15–16. May 1997. doi:10.1109/MS.1997.589224. +.. [Bashar1997] Bashar, N., `Ariane 5: Who Dunnit?`, IEEE Software 14(3): 15–16. May 1997. https://doi.ieeecomputersociety.org/10.1109/MS.1997.589224 -.. [BryantOHallaron2011] Bryant, R. and O'Hallaron, D., `Computer Systems : A programmer's perspective`, Second Edition, Pearson, 2011, http://www.amazon.com/Computer-Systems-Programmers-Perspective-2nd/dp/0136108040/ref=sr_1_1?s=books&ie=UTF8&qid=1329058781&sr=1-1 +.. .. [BryantOHallaron2011] Bryant, R. and O'Hallaron, D., `Computer Systems : A programmer's perspective`, Second Edition, Pearson, 2011, http://www.amazon.com/Computer-Systems-Programmers-Perspective-2nd/dp/0136108040/ref=sr_1_1?s=books&ie=UTF8&qid=1329058781&sr=1-1 .. [C99] http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf -.. [Card+1994] Card, R., Ts’o, T., Tweedie, S, `Design and implementation of the second extended filesystem`. Proceedings of the First Dutch International Symposium on Linux. ISBN 90-367-0385-9. http://web.mit.edu/tytso/www/linux/ext2intro.html +.. .. [Card+1994] Card, R., Ts’o, T., Tweedie, S, `Design and implementation of the second extended filesystem`. Proceedings of the First Dutch International Symposium on Linux. ISBN 90-367-0385-9. http://web.mit.edu/tytso/www/linux/ext2intro.html .. [Cooper2011] Cooper, M., `Advanced Bash-Scripting Guide`, http://tldp.org/LDP/abs/html/, 2011 -.. [Courtois+1971] Courtois, P., Heymans, F. and Parnas, D., `Concurrent control with “readers†and “writersâ€`. Commun. ACM 14, 10 (October 1971), 667-668. http://doi.acm.org/10.1145/362759.362813 +.. .. [Courtois+1971] Courtois, P., Heymans, F. and Parnas, D., `Concurrent control with “readers†and “writersâ€`. Commun. ACM 14, 10 (October 1971), 667-668. http://doi.acm.org/10.1145/362759.362813 .. [CPP] C preprocessor manual, http://gcc.gnu.org/onlinedocs/cpp/ @@ -35,9 +35,9 @@ Bibliographie .. [Dijkstra1965] Dijkstra, E., `Solution of a problem in concurrent programming control`. Commun. ACM 8, 9 (September 1965), 569 http://doi.acm.org/10.1145/365559.365617 -.. [Dijkstra1968] Dijkstra, E., `Go To Statement Considered Harmful`, Communications of the ACM, 11, March 1968, http://www.cs.utexas.edu/~EWD/transcriptions/EWD02xx/EWD215.html Voir aussi [Tribble2005]_ +.. .. [Dijkstra1968] Dijkstra, E., `Go To Statement Considered Harmful`, Communications of the ACM, 11, March 1968, http://www.cs.utexas.edu/~EWD/transcriptions/EWD02xx/EWD215.html Voir aussi [Tribble2005]_ -.. [Downey2008] Downey, A., `The Little Book of Semaphores`, Second Edition, Green Tea Press, 2008, +.. [Downey2008] Downey, A., `The Little Book of Semaphores`, Second Edition, Green Tea Press, 2008, https://greenteapress.com/wp/semaphores/ .. [Drepper2007] Drepper, U., `What every programmer should know about memory`, 2007, http://www.akkadia.org/drepper/cpumemory.pdf @@ -48,17 +48,17 @@ Bibliographie .. [Gove2011] Gove, D., `Multicore Application Programming for Windows, Linux and Oracle Solaris`, Addison-Wesley, 2011, http://books.google.be/books?id=NF-C2ZQZXekC -.. [GNUMake] http://www.gnu.org/software/make/manual/make.html +.. .. [GNUMake] http://www.gnu.org/software/make/manual/make.html .. [GNUPTH] Engelschall, R., `GNU Portable Threads`, http://www.gnu.org/software/pth/ .. [Graham+1982] Graham, S., Kessler, P. and Mckusick, M., `Gprof: A call graph execution profiler`. SIGPLAN Not. 17, 6 (June 1982), 120-126. http://doi.acm.org/10.1145/872726.806987 -.. [HennessyPatterson] Hennessy, J. and Patterson, D., `Computer Architecture: A Quantitative Approach`, Morgan Kauffmann, http://books.google.be/books?id=gQ-fSqbLfFoC +.. .. [HennessyPatterson] Hennessy, J. and Patterson, D., `Computer Architecture: A Quantitative Approach`, Morgan Kauffmann, http://books.google.be/books?id=gQ-fSqbLfFoC -.. [HP] HP, `Memory technology evolution: an overview of system memory technologies`, http://h20000.www2.hp.com/bc/docs/support/SupportManual/c00256987/c00256987.pdf +.. .. [HP] HP, `Memory technology evolution: an overview of system memory technologies`, http://h20000.www2.hp.com/bc/docs/support/SupportManual/c00256987/c00256987.pdf -.. [Hyde2010] Hyde, R., `The Art of Assembly Language`, 2nd edition, No Starch Press, http://webster.cs.ucr.edu/AoA/Linux/HTML/AoATOC.html +.. .. [Hyde2010] Hyde, R., `The Art of Assembly Language`, 2nd edition, No Starch Press, http://webster.cs.ucr.edu/AoA/Linux/HTML/AoATOC.html .. [IA32] intel, `Intel® 64 and IA-32 Architectures : Software Developer’s Manual`, Combined Volumes: 1, 2A, 2B, 2C, 3A, 3B and 3C, December 2011, http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-manual-325462.pdf @@ -78,14 +78,14 @@ Bibliographie .. [McKenney2005] McKenney, P., `Memory Ordering in Modern Microprocessors, Part I`, Linux Journal, August 2005, http://www.linuxjournal.com/article/8211 -.. [Mecklenburg+2004] Mechklenburg, R., Mecklenburg, R. W., Oram, A., `Managing projects with GNU make`, O'Reilly, 2004, http://books.google.be/books?id=rL4GthWj9kcC +.. .. [Mecklenburg+2004] Mechklenburg, R., Mecklenburg, R. W., Oram, A., `Managing projects with GNU make`, O'Reilly, 2004, http://books.google.be/books?id=rL4GthWj9kcC .. [Mitchell+2001] Mitchell, M., Oldham, J. and Samuel, A., `Advanced Linux Programming`, New Riders Publishing, ISBN 0-7357-1043-0, June 2001, http://www.advancedlinuxprogramming.com/ .. [Nemeth+2010] Nemeth, E., Hein, T., Snyder, G., Whaley, B., `Unix and Linux System Administration Handbook`, Prentice Hall, 2010, http://books.google.be/books?id=rgFIAnLjb1wC -.. [Peterson1981] Peterson, G., `Myths about the mutual exclusion problem`, Inform. Process. Lett. 12 (3) (1981) 115-116 +.. .. [Peterson1981] Peterson, G., `Myths about the mutual exclusion problem`, Inform. Process. Lett. 12 (3) (1981) 115-116 .. [Stallings2011] Stallings, W., `Operating Systems : Internals and Design Principles`, Prentice Hall, 2011, http://williamstallings.com/OperatingSystems/index.html @@ -93,10 +93,83 @@ Bibliographie .. [Stokes2008] Stokes, J., `Classic.Ars: Understanding Moore's Law`, http://arstechnica.com/hardware/news/2008/09/moore.ars -.. [Tanenbaum+2009] Tanenbaum, A., Woodhull, A., `Operating systems: design and implementation`, Prentice Hall, 2009 +.. [Tanenbaum+2009] Tanenbaum, A., Woodhull, A., `Operating systems: design and implementation`, Prentice Hall, 2009, https://www.pearson.com/us/higher-education/program/Tanenbaum-Operating-Systems-Design-and-Implementation-3rd-Edition/PGM228096.html -.. [Tribble2005] Tribble, D., `Go To Statement Considered Harmful: A Retrospective`, 2005, http://david.tribble.com/text/goto.html +.. .. [Tribble2005] Tribble, D., `Go To Statement Considered Harmful: A Retrospective`, 2005, http://david.tribble.com/text/goto.html .. [Walls2006] Walls, D., `How to Use the restrict Qualifier in C`. Sun Microsystems, 2006, http://developers.sun.com/solaris/articles/cc_restrict.html +.. spelling:: + + Cooper + Adelstein + Lubanovic + OReilly + Amdahl + Proc + Am + Federation + of + Processing + Societies + Press + pp + Bashar + May + doi + Cooper + preprocessor + manual + Dijkstra + September + Downey + Edition + Tea + Drepper + Molnar + Goldberg + Surv + March + Gove + Engelschall + Graham + Kessler + Mckusick + Not + intel + Combined + December + Kamp + July + Kerrisk + Starch + Press + Kernighan + Ritchie + Norton + company + Krakowiak + Leroy + August + Mitchell + Oldham + Samuel + New + Riders + Publishing + June + Nemeth + Snyder + Whaley + Stallings + Stevens + Rago + Addison + Wesley + Stokes + Tanenbaum + Woodhull + Prentice + Walls + Microsystems diff --git a/Theorie/conf.py b/Theorie/conf.py index 6ba43369f56c0372a3f778e8494452c246b37ad4..ddd3ce4384a51d841c87886b0176ddb94e791af6 100644 --- a/Theorie/conf.py +++ b/Theorie/conf.py @@ -56,17 +56,17 @@ source_encoding = 'utf-8' master_doc = 'index' # General information about the project. -project = u'SINF1252 : Systèmes informatiques' -copyright = u'2013, O. Bonaventure, G. Detal, C. Paasch' +project = u'LEPL1503 : Introduction au langage C' +copyright = u'2013, 2019, O. Bonaventure, G. Detal, C. Paasch' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '2014' +version = '2019' # The full version, including alpha/beta/rc tags. -release = '2014' +release = '2019' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -80,7 +80,7 @@ language = 'fr' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build/**', '.#*', '**/.#**', 'Exercices/QCM/**', "**.BASE.**", "**.REMOTE.**", "**.LOCAL.**", "**.BACKUP.**" ] +exclude_patterns = ['_build/**', '.#*', '**/.#**', 'Exercices/QCM/**', "**.BASE.**", "**.REMOTE.**", "**.LOCAL.**", "**.BACKUP.**", "MemoireVirtuelle/**", "Assembleur/**", "Fichiers/fichiers-signaux.rst", "Threads/processus.rst" ] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None @@ -128,7 +128,7 @@ html_theme = 'haiku' # The name for this set of Sphinx documents. If None, it defaults to # "<project> v<release> documentation". -html_title = u'Systèmes informatiques' +html_title = u'Langage C' # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None @@ -203,12 +203,16 @@ latex_elements = { # Additional stuff for the LaTeX preamble. #'preamble': '', + 'preamble': ''' + \\hypersetup{unicode=true} + ''' +# See https://tex.stackexchange.com/questions/120002/how-to-modify-the-default-latex-package-parameters-of-sphinx } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'SINF1252.tex', u'SINF1252 : Systèmes informatiques', + ('index', 'LEPL1503.tex', u'LEPL1503 : Introduction au langage C', u'O. Bonaventure, G. Detal, C. Paasch', 'manual'), ] @@ -270,10 +274,10 @@ texinfo_documents = [ # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. -epub_title = u'SINF1252 : Systèmes informatiques' +epub_title = u'LEPL1503 : Introduction au langage C' epub_author = u'O. Bonaventure, G. Detal, C. Paasch' epub_publisher = u'O. Bonaventure, G. Detal, C. Paasch' -epub_copyright = u'2013, O. Bonaventure, G. Detal, C. Paasch' +epub_copyright = u'2013, 2019, O. Bonaventure, G. Detal, C. Paasch' # The language of the text. It defaults to the language option # or en if the language is not set. diff --git a/Theorie/glossaire.rst b/Theorie/glossaire.rst index 89585efd5a3976299839bd5b7b42d354ee422c10..084fe246acbba37ca09acbd0a876f82efb9c80dc 100644 --- a/Theorie/glossaire.rst +++ b/Theorie/glossaire.rst @@ -1,5 +1,5 @@ .. -*- coding: utf-8 -*- -.. Copyright |copy| 2012 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal +.. Copyright |copy| 2012, 2019 by `Olivier Bonaventure <http://inl.info.ucl.ac.be/obo>`_, Christoph Paasch et Grégory Detal .. Ce fichier est distribué sous une licence `creative commons <http://creativecommons.org/licenses/by-sa/3.0/>`_ @@ -40,7 +40,7 @@ Glossaire microprocesseur processeur - à compléter + Unité centrale de l'ordinateur qui exécute les instructions en langage machine et interagit avec la mémoire. CPU Central Processing Unit. Voir :term:`microprocesseur` @@ -55,13 +55,10 @@ Glossaire Sortie d'erreur standard sur un système Unix (par défaut l'écran) X11 - à compléter + Interface graphique développée au MIT pour Unix. Voir https://en.wikipedia.org/wiki/X_Window_System Gnome - à compléter - - CDE - à compléter + Environnement graphique utilisé par de nombreuses distributions Linux. Voir https://en.wikipedia.org/wiki/GNOME shell Interpréteur de commandes sur un système Unix. `bash(1)`_ est l'interpréteur de commandes le plus utilisé de nos jours. @@ -86,7 +83,7 @@ Glossaire Variante de BSD Unix disponible depuis http://www.openbsd.org MacOS - Système d'exploitation développé par Apple Inc. comprenant de nombreux composantes provenant de :term:`FreeBSD` + Système d'exploitation développé par Apple Inc. comprenant de nombreux composantes provenant de :term:`FreeBSD`. Minix Famille de noyaux de systèmes d'exploitation inspiré de :term:`Unix` développée notamment par :term:`Andrew Tanenbaum`. Voir http://www.minix3.org pour la dernière version de Minix. @@ -115,7 +112,7 @@ Glossaire Linus Torvalds est le créateur et le mainteneur principal du noyau :term:`Linux`. Aqua - Aqua est une interface graphique spécifique à :term:`MacOS`. + Aqua est une interface graphique spécifique à :term:`MacOS`. Voir https://en.wikipedia.org/wiki/Aqua_(user_interface) pipe Mécanisme de redirection des entrées-sorties permettant de relier la sortie standard d'un programme à l'entrée standard d'un autre pour créer des pipelines de traitement. @@ -139,22 +136,22 @@ Glossaire Représentation de nombre réels en virgule flottante (type ``double`` en C). La norme `IEEE754 <http://ieeexplore.ieee.org/xpl/mostRecentIssue.jsp?punumber=4610933>`_ définit le format de ces nombres sur 64 bits. buffer overflow - Problème à compléter + Erreur dans laquelle un programme informatique cherche à stocker plus de données en mémoire que la capacité de la zone réservée en mémoire. Donne généralement lieu à des problèmes, parfois graves, de sécurité. https://en.wikipedia.org/wiki/Buffer_overflow garbage collector Algorithme permettant de libérer la mémoire qui n'est plus utilisée notamment dans des langages tels que Java pointeur - à compléter + Adresse d'une variable ou fonction en mémoire. adresse - à compléter + Position d'un donnée en mémoire. C99 Standard international définissant le langage C [C99]_ fichier header - à compléter + Fichier contenant des signatures de fonctions, des déclarations de types de données, des variables globales, permettant d'utiliser une librairie ou un API. segmentation fault Erreur à l'exécution à compléter @@ -181,44 +178,38 @@ Glossaire FSF Free Software Foundation, http://www.fsf.org - buffer overflow - à compléter - portée - à compléter + Zone d'un programme dans laquelle une variable est déclarée. portée locale - à compléter + Une variable ayant une portée locale est accessible uniquement dans le bloc dans lequelle elle est définie. portée globale - à compléter + Une variable ayant une portée globale est accessible dans tout le programme. debugger - à compléter + Logiciel text segment text - à compléter + Partie de la mémoire d'un programme contenant les instructions en langage machine à exécuter. segment des données initialisées - à compléter + Partie de la mémoire d'un programme contenant les données initialisées dans le code source du programme ainsi que les chaînes de caractères. segment des données non-initialisées - à compléter + Partie de la mémoire d'un programme contenant les données (tableaux notamment) qui sont déclarés mais pas explicitement initialisés dans le code source du programme. heap tas - à compléter - + Partie de la mémoire d'un programme gérée par `malloc(3)`_ et `free(3)`_. + stack pile - à compléter - - etext - à compléter + Partie de la mémoire d'un programme contenant les variables locales et adresses de retour des fonctions durant leur exécution. memory leak - à compléter + Fuite de mémoire. Erreur concernant un programme qui a alloué de la mémoire avec `malloc(3)`_ et ne l'utilise plus sans avoir fait appel à `free(3)`_ processus Ensemble cohérent d'instructions utilisant une partie de la mémoire, initié par le système d'exploitation et exécuté sur un des processeurs du système. Le système d'exploitation libère les ressources qui lui sont allouées à la fin de son exécution. @@ -240,7 +231,7 @@ Glossaire Un des inventaires des premiers ordinateurs. A défini l'architecture de base des premiers ordinateurs qui est maintenant connue comme le modèle de von Neumann [Krakowiak2011]_ mémoire - à compléter + Dispositif électronique permettant de stocker SRAM static RAM @@ -264,48 +255,12 @@ Glossaire mémoire cache Mémoire rapide de faible capacité. La mémoire cache peut stocker des données provenant de mémoires de plus grande capacité mais qui sont plus lentes, et exploite le :term:`principe de localité` en stockant de manière transparente les instructions et les données les plus récemment utilisées. Elle fait office d'interface entre le processeur et la mémoire principale et toutes les demandes d'accès à la mémoire principale passent par la mémoire cache, ce qui permet d'améliorer les performances de nombreux systèmes informatiques. - principe de localité - Voir :term:`localité spatiale` et :term:`localité temporelle`. - - localité spatiale - à compléter - - localité temporelle - à compléter - - lignes de cache - à compléter - - write through - Technique d'écriture dans les mémoires caches. Toute écriture est faite simultanément en mémoire cache et en mémoire principale. Cela garantit la cohérence entre les deux mémoires mais réduit les performances. - - write back - Technique d'écriture dans les mémoires caches. Toute écriture est faite en mémoire cache. La mémoire principale n'est mise à jour que lorsque la donnée modifiée doit être retirée de la cache. Cette technique permet d'avoir de meilleures performances que :term:`write through` mais il faut faire parfois attention aux problèmes qui pourraient survenir sachant que la mémoire cache et la mémoire principale ne contiennent pas toujours exactement la même information. - eip pc compteur de programme instruction pointer Registre spécial du processeur qui contient en permanence l'adresse de l'instruction en cours d'exécution. Le contenu de ce registre est incrémenté après chaque instruction et modifié par les instructions de saut. - mode d'adressage - à compléter - - accumulateur - Registre utilisé dans les premiers processeurs comme destination pour la plupart des opérations arithmétiques et logiques. Sur l'architecture [IA32]_, le registre ``%eax`` est le successeur de cet accumulateur. - - bus - à compléter - - ligne de cache - à compléter. Voir notamment [McKenney2005]_ et [Drepper2007]_ - - write-back - à compléter - - program counter - à compléter - makefile à compléter @@ -342,16 +297,16 @@ Glossaire multi-coeurs à compléter - multi-threadé - à compléter + multithreadé + Programme utilisant plusieurs threads. section critique - à compléter + Partie de programme ne pouvant pas être exécutée simultanément par deux threads différents. exclusion mutuelle à compléter - sureté + sûreté safety à compléter @@ -390,9 +345,6 @@ Glossaire mutex à compléter - problème des philosophes - à compléter - appel système à compléter @@ -402,18 +354,6 @@ Glossaire sémaphore à compléter - problèmes des readers-writers - à compléter - - inode - à compléter - - segment de données - à compléter - - problème des readers-writers - à compléter - thread-safe à compléter @@ -429,40 +369,12 @@ Glossaire librairie partagée à compléter - kernel - à compléter - - mode utilisateur - à compléter - - mode protégé - à compléter - - processus père - à compléter - - processus fils - à compléter - - processus orphelin - à compléter - - processus zombie - à compléter - - filesystem - système de fichiers - à compléter - descripteur de fichier à compléter répertoire à compléter - secteur - à compléter - répertoire courant à compléter @@ -478,118 +390,19 @@ Glossaire lien symbolique à compléter - lock - à compléter - - advisory lock - advisory locking - à compléter - - mandatory lock - mandatory locking - à compléter - - open file object - à compléter - - sémaphore nommé - à compléter - appel système lent à compléter - handler - à compléter - - signal synchrone - à compléter - - signal asynchrone - à compléter - - interpréteur - à compléter - - MMU - Memory Management Unit - à compléter - - adresse virtuelle - à compléter - - mémoire virtuelle - à compléter SSD Solid State Drive Système de stockage de données s'appuyant uniquement sur de la mémoire flash. - stratégie de remplacement de pages - à compléter - - page - à compléter - - table des pages - à compléter - - bit de validité - à compléter - - TLB - Translation Lookaside Buffer - à compléter - - Mémoire partagée - à compléter - - copy-on-write - à compléter - - adresse physique - à compléter - - page fault - défaut de page - à compléter - - file FIFO - De "First In, First Out". Le premier élement à entrer dans la file sera le premier à en sortir. (!= LIFO, "Last In First Out") - - dirty bit - bit de modification - à compléter - - reference bit - bit de référence - à compléter - - swapping - à compléter - - pagination - à compléter - - stdio - à compléter - - fifo - à compléter - - gnuth - à compléter - - partition de swap - à compléter - - stratégie de remplacement de pages - à compléter - - 2^64 - à compléter - root à compléter userid - à compléter \ No newline at end of file + à compléter + + Unicode + Norme d'encodage de caractères supportant l'ensemble des langues écrites, voir notamment https://en.wikipedia.org/wiki/Unicode diff --git a/Theorie/index.rst b/Theorie/index.rst index 6f1ace35810f5b4a8ab6a5e79195153a1f99de74..96b2f00c9d89ef5caa334c72e6f5b8f7237d4b1d 100644 --- a/Theorie/index.rst +++ b/Theorie/index.rst @@ -10,7 +10,7 @@ Systèmes informatiques .. only:: html - Ce site web contient la partie théorique du support du cours `SINF1252 <http://www.uclouvain.be/en-cours-2012-lsinf1252.html>`_ donné aux `étudiants en informatique <http://www.uclouvain.be/info.html>`_ à l'`Université catholique de Louvain <http://www.uclouvain.be>`_ (UCL). Les étudiants sont invités à ajouter leur commentaires en soumettant des patches via https://github.com/obonaventure/SystemesInformatiques . + Ce site web contient la partie théorique du support du cours `Projet P3 - LEPL1503 <https://uclouvain.be/cours-2019-lepl1503>`_ donné aux `étudiants ingénieurs et informaticiens <http://www.uclouvain.be/info.html>`_ à l'`Université catholique de Louvain <https://www.uclouvain.be>`_ (UCL). Les étudiants sont invités à ajouter leur commentaires en soumettant des patches via https://github.com/UCL-INGI/SyllabusC . La version HTML est la préférable car elle contient des liens hypertextes vers les pages de manuel Linux qui font partie de la matière. D'autres formats sont possibles pour ceux qui veulent lire le document hors ligne : @@ -18,6 +18,7 @@ Systèmes informatiques - `format pdf <http://sites.uclouvain.be/SystInfo/distrib/SINF1252-Theorie.pdf>`_ pour lecture via les logiciels Adobe ou pour impression + Introduction ************* @@ -37,12 +38,9 @@ Langage C C/malloc C/linker -Structure des ordinateurs -************************* -.. toctree:: - :maxdepth: 2 - Assembleur/memory + + Systèmes Multiprocesseurs ************************* @@ -52,27 +50,16 @@ Systèmes Multiprocesseurs Threads/threads Threads/threads2 Threads/coordination - Threads/processus +.. Threads/processus -Fichiers -******** - -.. toctree:: - :maxdepth: 2 - - Fichiers/fichiers - Fichiers/fichiers-signaux - - -Mémoire virtuelle -***************** - -.. toctree:: - :maxdepth: 2 - MemoireVirtuelle/vmem +Gestion des fichiers +******************** +.. toctree:: + :maxdepth: 2 + Fichiers/fichiers ******* Annexes diff --git a/Theorie/intro.rst b/Theorie/intro.rst index 85a71ac5724e8184ade201b32eb6d0fff42fa2aa..35447f806416385630297c75099f0e1639b6b6eb 100644 --- a/Theorie/intro.rst +++ b/Theorie/intro.rst @@ -1,17 +1,19 @@ .. -*- coding: utf-8 -*- -.. Copyright |copy| 2012 by `Olivier Bonaventure <http://perso.uclouvain.be/olivier.bonaventure>`_, Christoph Paasch et Grégory Detal +.. Copyright |copy| 2012, 2019 by `Olivier Bonaventure <http://perso.uclouvain.be/olivier.bonaventure>`_, Christoph Paasch et Grégory Detal .. Ce fichier est distribué sous une licence `creative commons <http://creativecommons.org/licenses/by-sa/3.0/>`_ Introduction ============ -Les systèmes informatiques jouent un rôle de plus en plus important dans notre société. En une septantaine d'années les ordinateurs se sont rapidement améliorés et démocratisés. Aujourd'hui, notre société est de plus en plus dépendante des systèmes informatiques. +Les systèmes informatiques jouent un rôle de plus en plus important dans notre société. Depuis les premiers calculateurs à la fin de la seconde guerre mondiale, les ordinateurs se sont rapidement améliorés et démocratisés. Aujourd'hui, notre société est de plus en plus dépendante des systèmes informatiques. +.. spelling:: -.. Expliquer le fonctionnement de base d'un ordinateur, modèle de Von Neumann - -.. http://en.wikipedia.org/wiki/Von_Neumann_architecture + Von Neumann + binary + digit + word Composition d'un système informatique ------------------------------------- @@ -21,10 +23,10 @@ Le système informatique le plus simple est composé d'un :term:`processeur` (:t - lire de l'information en mémoire - écrire de l'information en mémoire - réaliser des calculs - + L'architecture des ordinateurs est basée sur l'architecture dite de Von Neumann. Suivant cette architecture, un ordinateur est composé d'un processeur qui exécute un programme se trouvant en mémoire. La mémoire contient à la fois le programme à exécuter et les données qui sont manipulées par le programme. -L'élément de base pour stocker et représenter de l'information dans un système informatique est le :term:`bit`. Un bit (binary digit en anglais) peut prendre deux valeurs qui par convention sont représentées par : +L'élément de base pour stocker et représenter de l'information dans un système informatique est le :term:`bit`. Un bit (`binary digit` en anglais) peut prendre deux valeurs qui par convention sont représentées par : - ``1`` - ``0`` @@ -37,41 +39,47 @@ La composition de plusieurs bits donne lieu à des blocs de données qui peuvent être utiles dans différentes applications informatiques. Ainsi, un :term:`nibble` est un bloc de 4 bits consécutifs tandis qu'un :term:`octet` (ou :term:`byte` en anglais) -est un bloc de 8 bits consécutifs. On parlera de mots (word en +est un bloc de 8 bits consécutifs. On parlera de mots (`word` en anglais) pour des groupes comprenant généralement 32 bits et de long mot pour des groupes de 64 bits. - -.. Expliquer brièvement le rôle du hardware, les types de devices et le rôle du système d'exploitation (interface entre hardware et software, fourniture de services de base qui sont utilisables par tous les processus et évitent aux processus de devoir réinventer la roue - Le processeur et la mémoire ne sont pas les deux seuls composants d'un système informatique. Celui-ci doit également pouvoir interagir avec le monde extérieur, ne fut-ce que pour pouvoir charger le programme à exécuter et les données à analyser. Cette interaction se réalise grâce à un grand nombre de dispositifs d'entrées/sorties et de stockage. Parmi ceux-ci, on peut citer : - le clavier qui permet à l'utilisateur d'entrer des caractères - l'écran qui permet à l'utilisateur de visualiser le fonctionnement des programmes et les résultats qu'ils produisent - l'imprimante qui permet à l'ordinateur d'écrire sur papier les résultats de l'exécution de programmes - le disque-dur, les clés USB, les CDs et DVDs qui permettent de stocker les données sous la forme de fichiers et de répertoires - - la souris, le joystick ou la tablette graphique qui permettent à l'utilisateur de fournir à l'ordinateur des indications de positionnement + - la souris ou la tablette graphique qui permettent à l'utilisateur de fournir à l'ordinateur des indications de positionnement - le scanner qui permet à l'ordinateur de transformer un document en une image numérique - le haut-parleur avec lequel l'ordinateur peut diffuser différentes sortes de son - le microphone et la caméra qui permettent à l'ordinateur de capturer des informations sonores et visuelles pour les stocker ou les traiter - +.. spelling:: + + API + l'API + Bell + Laboratories + AT&T + Berkeley + Labs + Amsterdam + d'Amsterdam + Unix ---- -.. todo:: un peu d'histoire et plus de texte sur Unix - Unix est aujourd'hui un nom générique [#funix]_ correspondant à une famille de systèmes d'exploitation. La première version de Unix a été développée pour faciliter le traitement de documents sur mini-ordinateur. .. topic:: Quelques variantes de Unix De nombreuses variantes de Unix ont été produites durant les quarante dernières années. Il est impossible de les décrire toutes, mais en voici quelques unes. - - :term:`Unix`. Initialement développé aux Bell Laboratories d'AT&T, Unix a été ensuite développé par d'autres entreprises. C'est aujourd'hui une marque déposée par ``The Open group``, voir http://www.unix.org/ - - :term:`BSD Unix`. Les premières versions de Unix étaient librement distribuées par Bell Labs. Avec le temps, des variantes de Unix sont apparues. La variante développée par l'université de Berkeley en Californie a été historiquement importante car c'est dans cette variante que de nombreuses innovations ont été introduites dont notamment les implémentations des protocoles TCP/IP utilisés sur Internet. Aujourd'hui, :term:`FreeBSD` et :term:`OpenBSD` sont deux descendants de :term:`BSD Unix`. Ils sont largement utilisés dans de nombreux serveurs et systèmes embarqués. :term:`MacOS`, développé par Apple, s'appuie fortement sur un noyau et des utilitaires provenant de :term:`FreeBSD`. + - :term:`Unix`. Initialement développé aux AT&T Bell Laboratories, Unix a été ensuite développé par d'autres entreprises. C'est aujourd'hui une marque déposée par ``The Open group``, voir http://www.unix.org/ + - :term:`BSD Unix`. Les premières versions de Unix étaient librement distribuées par Bell Labs. Avec le temps, des variantes de Unix sont apparues. La variante développée par l'université de Berkeley en Californie a été historiquement importante car c'est dans cette variante que de nombreuses innovations ont été introduites dont notamment les piles de protocoles TCP/IP utilisés sur Internet. Aujourd'hui, :term:`FreeBSD` et :term:`OpenBSD` sont deux descendants de :term:`BSD Unix`. Ils sont largement utilisés dans de nombreux serveurs et systèmes embarqués. :term:`MacOS`, développé par Apple, s'appuie fortement sur un noyau et des utilitaires provenant de :term:`FreeBSD`. - :term:`Minix` est un système d'exploitation développé initialement par :term:`Andrew Tanenbaum` à l'université d'Amsterdam. :term:`Minix` est fréquemment utilisé pour l'apprentissage du fonctionnement des systèmes d'exploitation. - :term:`Linux` est un noyau de système d'exploitation largement inspiré de :term:`Unix` et `Minix`. Développé par :term:`Linus Torvalds` durant ses études d'informatique, il est devenu la variante de Unix la plus utilisée à travers le monde. Il est maintenant développé par des centaines de développeurs qui collaborent via Internet. - - :term:`Solaris` est le nom commercial de la variante Unix d'Oracle. + - :term:`Solaris` est le nom commercial de la variante Unix de Oracle. Dans le cadre de ce cours, nous nous focaliserons sur le système :term:`GNU/Linux`, c'est-à -dire un système qui intègre le noyau :term:`Linux` et les librairies et utilitaires développés par le projet :term:`GNU` de la :term:`FSF`. @@ -79,9 +87,17 @@ Un système Unix est composé de trois grands types de logiciels : - Le noyau du système d'exploitation qui est chargé automatiquement au démarrage de la machine et qui prend en charge toutes les interactions entre les logiciels et le matériel. - De nombreuses librairies qui facilitent l'écriture et le développement d'applications - - De nombreux programmes utilitaires précompilés qui peuvent résoudre un grand nombre de problèmes courants. Certains de ces utilitaires sont chargés automatiquement lors du démarrage de la machine. La plupart sont exécutés uniquement à la demande des utilisateurs. + - De nombreux programmes utilitaires simples qui permettent de résoudre un grand nombre de problèmes courants. Certains de ces utilitaires sont chargés automatiquement lors du démarrage de la machine. La plupart sont exécutés uniquement à la demande des utilisateurs. -Le rôle principal du noyau du système d'exploitation est de gérer les ressources matérielles (processeur, mémoire, dispositifs d'entrées/sorties et de stockage) de façon à ce qu'elles soient accessibles à toutes les applications qui s'exécutent sur le système. Gérer les ressources matérielles nécessite d'inclure dans le systèmes d'exploitation des interfaces programmatiques (Application Programming Interfaces - :term:`API`) qui facilitent leur utilisation par les applications. Les dispositifs de stockage sont une belle illustration de ce principe. Il existe de nombreux dispositifs de stockage (disque dur, clé USB, CD, DVD, mémoire flash, ...). Chacun de ces dispositifs a des caractéristiques électriques et mécaniques propres. Ils permettent en général la lecture et/ou l'écriture de blocs de données de quelques centaines d'octets. Nous reviendrons sur leur fonctionnement ultérieurement. Peu d'applications sont capables de piloter directement de tels dispositifs pour y lire ou y écrire de tels blocs de données. Par contre, la majorité des applications sont capables de les utiliser par l'intermédiaire du système de fichiers. Le système de fichiers (arborescence des fichiers) et l'API associée (`open(2)`_, `close(2)`_, `read(2)`_ `write(2)`_ ) sont un exemple des services fournis par le système d'exploitation aux applications. Le système de fichiers regroupe l'ensemble des fichiers qui sont accessibles depuis un système sous une arborescence unique, quel que soit le nombre de dispositifs de stockage utilisé. La racine de cette arborescence est le répertoire ``/`` par convention. Ce répertoire contient généralement une dizaine de sous répertoires dont les noms varient d'une variante de Unix à l'autre. Généralement, on retrouve dans la racine les sous-répertoires suivants : +.. spelling:: + + API + programmatiques + Application + Programming + Interface + +Le rôle principal du noyau du système d'exploitation est de gérer les ressources matérielles (processeur, mémoire, dispositifs d'entrées/sorties et de stockage) de façon à ce qu'elles soient accessibles à toutes les applications qui s'exécutent sur le système. Gérer les ressources matérielles nécessite d'inclure dans le systèmes d'exploitation des interfaces programmatiques (`Application Programming Interfaces` en anglais - :term:`API`) qui facilitent leur utilisation par les applications. Les dispositifs de stockage sont une belle illustration de ce principe. Il existe de nombreux dispositifs de stockage (disque dur, clé USB, CD, DVD, mémoire flash, ...). Chacun de ces dispositifs a des caractéristiques électriques et mécaniques propres. Ils permettent en général la lecture et/ou l'écriture de blocs de données de quelques centaines d'octets. Nous reviendrons sur leur fonctionnement ultérieurement. Peu d'applications sont capables de piloter directement de tels dispositifs pour y lire ou y écrire de tels blocs de données. Par contre, la majorité des applications sont capables de les utiliser par l'intermédiaire du système de fichiers. Le système de fichiers (arborescence des fichiers) et l'API associée (`open(2)`_, `close(2)`_, `read(2)`_ `write(2)`_ ) sont un exemple des services fournis par le système d'exploitation aux applications. Le système de fichiers regroupe l'ensemble des fichiers qui sont accessibles depuis un système sous une arborescence unique, quel que soit le nombre de dispositifs de stockage utilisé. La racine de cette arborescence est le répertoire ``/`` par convention. Ce répertoire contient généralement une dizaine de sous répertoires dont les noms varient d'une variante de Unix à l'autre. Généralement, on retrouve dans la racine les sous-répertoires suivants : - ``/usr`` : sous-répertoire contenant la plupart des utilitaires et librairies installées sur le système - ``/bin`` et ``/sbin`` : sous-répertoire contenant quelques utilitaires de base nécessaires à l'administrateur du système @@ -93,7 +109,7 @@ Le rôle principal du noyau du système d'exploitation est de gérer les ressour Un autre service est le partage de la mémoire et du processus. La plupart des systèmes d'exploitation supportent l'exécution simultanée de plusieurs applications. Pour ce faire, le système d'exploitation partage la mémoire disponible entre les différentes applications en cours d'exécution. Il est également responsable du partage du temps d'exécution sur le ou les processeurs de façon à ce que toutes les applications en cours puissent s'exécuter. -Unix s'appuye sur la notion de processus. Une application est composée de un ou plusieurs processus. Un processus peut être défini comme un ensemble cohérent d'instructions qui utilisent une partie de la mémoire et sont exécutées sur un des processeurs du système. L'exécution d'un processus est initiée par le système d'exploitation (généralement suite à une requête faite par un autre processus). Un processus peut s'exécuter pendant une fraction de secondes, quelques secondes ou des journées entières. Pendant son exécution, le processus peut potentiellement accéder aux différentes ressources (processeurs, mémoire, dispositifs d'entrées/sorties et de stockage) du système. A la fin de son exécution, le processus se termine [#ftermine]_ et libère les ressources qui lui ont été allouées par le système d'exploitation. Sous Unix, tout processus retourne au processus qui l'avait initié le résultat de son exécution qui est résumée en un nombre entier. Cette valeur de retour est utilisée en général pour déterminer si l'exécution d'un processus s'est déroulée correctement (zéro comme valeur de retour) ou non (valeur de retour différente de zéro). +Unix s'appuie sur la notion de processus. Une application est composée de un ou plusieurs processus. Un processus peut être défini comme un ensemble cohérent d'instructions qui utilisent une partie de la mémoire et sont exécutées sur un des processeurs du système. L'exécution d'un processus est initiée par le système d'exploitation (généralement suite à une requête faite par un autre processus). Un processus peut s'exécuter pendant une fraction de secondes, quelques secondes ou des journées entières. Pendant son exécution, le processus peut potentiellement accéder aux différentes ressources (processeurs, mémoire, dispositifs d'entrées/sorties et de stockage) du système. A la fin de son exécution, le processus se termine [#ftermine]_ et libère les ressources qui lui ont été allouées par le système d'exploitation. Sous Unix, tout processus retourne au processus qui l'avait initié le résultat de son exécution qui est résumée en un nombre entier. Cette valeur de retour est utilisée en général pour déterminer si l'exécution d'un processus s'est déroulée correctement (zéro comme valeur de retour) ou non (valeur de retour différente de zéro). Dans le cadre de ce cours, nous aurons l'occasion de voir en détails de nombreuses librairies d'un système Unix et verrons le fonctionnement d'appels systèmes qui permettent aux logiciels d'interagir directement avec le noyau. Le système Unix étant majoritairement écrit en langage C, ce langage est le langage de choix pour de nombreuses applications. Nous le verrons donc en détails. @@ -128,7 +144,7 @@ La plupart des utilitaires fournis avec un système Unix ont été conçus pour Shell ^^^^^ -Avant le développement des interfaces graphiques telles que :term:`X11`, :term:`Gnome`, :term:`CDE` ou :term:`Aqua`, l'utilisateur interagissait exclusivement avec l'ordinateur par l'intermédiaire d'un interpréteur de commandes. Dans le monde Unix, le terme anglais :term:`shell` est le plus souvent utilisé pour désigner cet interpréteur et nous ferons de même. Avec les interfaces graphiques actuelles, le shell est accessible par l'intermédiaire d'une application qui est généralement appelée ``terminal`` ou ``console``. +Avant le développement des interfaces graphiques telles que :term:`X11`, :term:`Gnome` ou :term:`Aqua`, l'utilisateur interagissait exclusivement avec l'ordinateur par l'intermédiaire d'un interpréteur de commandes. Dans le monde Unix, le terme anglais :term:`shell` est le plus souvent utilisé pour désigner cet interpréteur et nous ferons de même. Avec les interfaces graphiques actuelles, le shell est accessible par l'intermédiaire d'une application qui est généralement appelée ``terminal`` ou ``console``. Un :term:`shell` est un programme qui a été spécialement conçu pour faciliter l'utilisation d'un système Unix via le clavier. De nombreux shells Unix existent. Les plus simples permettent à l'utilisateur de taper une série de commandes à exécuter en les combinant. Les plus avancés sont des interpréteurs de commandes qui supportent un langage complet permettant le développement de scripts plus ou moins ambitieux. Dans le cadre de ce cours, nous utiliserons `bash(1)`_ qui est un des shells les plus populaires et les plus complets. La plupart des commandes `bash(1)`_ que nous utiliserons sont cependant compatibles avec de nombreux autres shells tels que `zsh <http://www.zsh.org>`_ ou `csh <http://www.tcsh.org/Home>`_. @@ -137,6 +153,9 @@ Lorsqu'un utilisateur se connecte à un système Unix, en direct ou à travers u .. literalinclude:: src/exemple.out :language: console +.. spelling:: + + La puissance du :term:`shell` ne vient pas de sa capacité d'exécuter des commandes individuelles telles que ci-dessus. Elle vient de la possibilité de combiner ces commandes en redirigeant les entrées et sorties standards. Les shells Unix supportent différentes formes de redirection. Tout d'abord, il est possible de forcer un programme à lire son entrée standard depuis un fichier plutôt que depuis le clavier. Cela se fait en ajoutant à la fin de la ligne de commande le caractère ``<`` suivi du nom du fichier à lire. Ensuite, il est possible de rediriger la sortie standard vers un fichier. Cela se fait en utilisant ``>`` ou ``>>``. Lorsqu'une commande est suivie de ``> file``, le fichier ``file`` est créé si il n'existait pas et remis à zéro si il existait et la sortie standard de cette commande est redirigée vers le fichier ``file``. Lorsqu'un commande est suivie de ``>> file``, la sortie standard est sauvegardée à la fin du fichier ``file`` (si ``file`` n'existait pas, il est créé). Des informations plus complètes sur les mécanismes de redirection de `bash(1)`_ peuvent être obtenues dans le `chapitre 20 <http://tldp.org/LDP/abs/html/io-redirection.html>`_ de [ABS]_. @@ -214,7 +233,7 @@ Un autre exemple d'utilisation des codes de retour est le script :download:`src/ Ce programme utilise le fichier spécial ``/dev/null``. Celui-ci est en pratique l'équivalent d'un trou noir. Il accepte toutes les données en écriture mais celles-ci ne peuvent jamais être relues. ``/dev/null`` est très utile lorsque l'on veut ignorer la sortie d'un programme et éviter qu'elle ne s'affiche sur le terminal. `bash(1)`_ supporte également ``/dev/stdin`` pour représenter l'entrée standard, ``/dev/stdout`` pour la sortie standard et ``/dev/stderr`` pour l'erreur standard. -.. Faire implémenter un programme qui prend deux arguments en entier et en fait la somme, le produit ou la différence en fonction des trois arguments qui sont passés + Une description complète de `bash(1)`_ sort du cadre de ce cours. De nombreuses références à ce sujet sont disponibles [Cooper2011]_. diff --git a/bib.rst b/bib.rst index 03cd9d82a698c76079c1ffd0a525c57f7bf2c301..e132a057f527233c383d5e4a02a1d12c9683b1ae 100644 --- a/bib.rst +++ b/bib.rst @@ -18,7 +18,7 @@ Bibliographie .. [BovetCesati2005] Bovet, D., Cesati, M, `Understanding the Linux Kernel, Third Edition`, O'Reilly, 2005, http://my.safaribooksonline.com/book/operating-systems-and-server-administration/linux/0596005652 -.. [BryantOHallaron2011] Bryant, R. and O'Hallaron, D., `Computer Systems : A programmer's perspective`, Second Edition, Pearson, 2011, http://www.amazon.com/Computer-Systems-Programmers-Perspective-2nd/dp/0136108040/ref=sr_1_1?s=books&ie=UTF8&qid=1329058781&sr=1-1 +.. .. [BryantOHallaron2011] Bryant, R. and O'Hallaron, D., `Computer Systems : A programmer's perspective`, Second Edition, Pearson, 2011, http://www.amazon.com/Computer-Systems-Programmers-Perspective-2nd/dp/0136108040/ref=sr_1_1?s=books&ie=UTF8&qid=1329058781&sr=1-1 .. [C99] http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf @@ -37,7 +37,7 @@ Bibliographie .. [DeveloppezMake] Introduction à Makefile, http://gl.developpez.com/tutoriel/outil/makefile/ -.. [Dijkstra1965b] Dijkstra, E., `Cooperating sequential processes`, 1965, http://www.cs.utexas.edu/users/EWD/transcriptions/EWD01xx/EWD123.html +.. .. [Dijkstra1965b] Dijkstra, E., `Cooperating sequential processes`, 1965, http://www.cs.utexas.edu/users/EWD/transcriptions/EWD01xx/EWD123.html .. [Dijkstra1965] Dijkstra, E., `Solution of a problem in concurrent programming control`. Commun. ACM 8, 9 (September 1965), 569 http://doi.acm.org/10.1145/365559.365617 @@ -62,13 +62,13 @@ Bibliographie .. [Graham+1982] Graham, S., Kessler, P. and Mckusick, M., `Gprof: A call graph execution profiler`. SIGPLAN Not. 17, 6 (June 1982), 120-126. http://doi.acm.org/10.1145/872726.806987 -.. [HennessyPatterson] Hennessy, J. and Patterson, D., `Computer Architecture: A Quantitative Approach`, Morgan Kauffmann, http://books.google.be/books?id=gQ-fSqbLfFoC +.. .. [HennessyPatterson] Hennessy, J. and Patterson, D., `Computer Architecture: A Quantitative Approach`, Morgan Kauffmann, http://books.google.be/books?id=gQ-fSqbLfFoC .. [Honeyford2006] Honeyford, M., `Speed your code with the GNU profiler`, http://www.ibm.com/developerworks/library/l-gnuprof.html -.. [HP] HP, `Memory technology evolution: an overview of system memory technologies`, http://h20000.www2.hp.com/bc/docs/support/SupportManual/c00256987/c00256987.pdf +.. .. [HP] HP, `Memory technology evolution: an overview of system memory technologies`, http://h20000.www2.hp.com/bc/docs/support/SupportManual/c00256987/c00256987.pdf -.. [Hyde2010] Hyde, R., `The Art of Assembly Language`, 2nd edition, No Starch Press, http://webster.cs.ucr.edu/AoA/Linux/HTML/AoATOC.html +.. .. [Hyde2010] Hyde, R., `The Art of Assembly Language`, 2nd edition, No Starch Press, http://webster.cs.ucr.edu/AoA/Linux/HTML/AoATOC.html .. [IA32] intel, `Intel® 64 and IA-32 Architectures : Software Developer’s Manual`, Combined Volumes: 1, 2A, 2B, 2C, 3A, 3B and 3C, December 2011, http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-manual-325462.pdf .. [Kamp2011] Kamp, P., `The Most Expensive One-byte Mistake`, ACM Queue, July 2011, http://queue.acm.org/detail.cfm?id=2010365 diff --git a/dict.txt b/dict.txt index 865ea5a45bcfb07265b7ac48672c6e524ac3cd75..facbe6daa81256488e5a15de5aba81e065bad9fc 100644 --- a/dict.txt +++ b/dict.txt @@ -1,2 +1,77 @@ Unix Linux +shell +shells +backup +shells +backups +rediriger +redirection +redirigeant +redirigé +redirigée +bash +web +implémentation +implémenter +qu +aujourd +Aujourd +hui +préprocesseur +macro +macros +portabilité +headers +header +tutoriel +octal +octale +octaux +plateforme +plateformes +pénalisant +pénalisantes +fractionnaire +resp +gcc +clang +Unicode +puisqu +encapsulant +timer +Wikipedia +identifiants +heap +stack +text +casté +linker +débogage +biométrique +descripteur +descripteurs +Minix +Linux +buffer +buffers +inode +thread +threads +byte +réentrante +décrémenter +décrémente +régulé +kHz +MHz +GHz +implémentations +reproductible +multitâche +désactivation +mutex +Mutex +Windows + + diff --git a/travis.sh b/travis.sh index 2fe57bd985f35e0ac481c6817f79ad39ff2ad27d..d43b6422c14e3fa589a95e1ad0dab569a9271b87 100755 --- a/travis.sh +++ b/travis.sh @@ -11,11 +11,11 @@ cd Theorie echo "**** Theorie ****" sphinx-build -nWNT --keep-going -b html . /tmp sphinx-build -b spelling . /tmp -cd ../Outils -echo "**** Outils ****" -sphinx-build -nWNT --keep-going -b html . /tmp -cd ../Exercices -echo "**** Exercices ****" +#cd ../Outils +#echo "**** Outils ****" +#sphinx-build -nWNT --keep-going -b html . /tmp +#cd ../Exercices +#echo "**** Exercices ****" #sphinx-build -nWNT --keep-going -b html . /tmp #cd QCM #echo "**** QCM ****"