Poly Etudiant Fasc1 5

Télécharger au format pdf
Télécharger au format pdf
Vous êtes sur la page 1sur 68

version du 12 novembre 1999

Première partie

Rappels
version du 12 novembre 1999
9

Chapitre 1

Les outils Unix pour développer


en C

1.1 L’éditeur Emacs

Nous donnons ici les plus utilisées des commandes de l’éditeur Emacs. Dans C-x, le symbole C
signifie control. Ainsi pour exécuter la séquence C-x C-s qui sauve un fichier, il faut simultanément
appuyer sur ctrl et x , relâcher et enfin sans trop attendre, appuyer sur ctrl s .
Contrairement à la touche control qui s’utilise comme la touche shift ou ↑, la touche escape
s’utilise comme une touche de caractère ordinaire. Pour reculer d’un mot — séquence ESC b —
il faut appuyer d’abord sur la touche escape puis la lettre b. La table 1.1 donne les principales
commandes de l’éditeur Emacs. On peut l’obtenir sous Emacs par la commande ESC x describe-
bindings. Ne tapez pas tout ! utiliser les touches espace ou Tab pour complémenter les commandes,
comme sous tcsh.

1.2 Le compilateur gcc

gcc est un des compilateurs C les plus performants. La version actuelle 2.8.1 compile aussi bien
des programmes C que des programmes écrits en C++. L’extension naturelle d’un programme C est
.c, celle d’un programme écrit en C++ est .C. gcc effectue aussi l’édition de lien. Le programme
exécutable qui est produit s’appelle a.out par défaut.

1.2.1 Exemples d’utilisation


gcc prog.c compilation du fichier prog.c, édition de lien, l’exécutable s’appelle a.out ;
gcc prog.c -o prog compilation du fichier prog.c, édition de lien ; l’exécutable s’appelle
prog ;
10 Chapitre 1. Les outils Unix pour développer en C

gcc prog.c -o prog -lm idem mais l’édition de lien utilise la bibliothèque mathématique
(sin, cos, etc.) ;
gcc -c prog.c uniquement compilation du fichier prog.c, pas d’édition de lien ;
gcc prog1.c ssp.c -o prog compilation de deux fichiers C, édition de lien des deux fichiers,
l’exécutable s’appelle prog ;
gcc prog1.c prog.o -o prog compilation de prog1.c et édition de lien avec prog.o qui a
déjà été compilé.

1.2.2 Les options de compilation


-c supprime l’édition de lien. Le code objet compilé se trouve dans le fichier d’extension .o.
-ggdb ajoute les informations pour déboguer à l’aide de gdb ;
-Wall (Warning all ) ajoute tous les messages d’avertissements. Cette option ralentit la com-
pilation mais accélère la mise au point du programme ;
-O2 optimise le code en taille et vitesse. Cette option accélère la vitesse d’exécution.

1.3 Le débogueur gdb

Un débogueur permet un contrôle du programme pendant son exécution. On peut, en autre chose,
arrêter le programme en posant des points d’arrêt, visualiser le contenu des variables et connaı̂tre
l’instruction qui a provoqué une erreur fatale. Les étudiants n’aiment pas utiliser de débogueurs, cela
ne représente aucun intérêt pour eux car ils écrivent des programmes justes. Seuls les enseignants
utilisent les débogueurs pour retrouver les erreurs des programmes des étudiants. Signalons qu’il
existe des variantes de gdb telles que ddd ou xxgdb. Mais celles-ci nécessitent le système X.

1.3.1 Les principales commandes de gdb

Considérons un programme dont l’exécution se termine par le message segmentation fault,


core dumped. Voici dans l’ordre les commandes du débogueur qui permettent de localiser et com-
prendre l’erreur.

run lance l’exécution ;


where donne l’instruction qui a provoqué l’erreur fatale ;
up permet de remonter dans l’enchaı̂nement des appels de sous-programmes jusqu’au pro-
gramme utilisateur intéressant ;
list imprime les lignes C pour en connaı̂tre les numéros ;
break hnuméroi pose un point d’arrêt sur une ligne dont on donne le numéro ;
run relance le programme qui s’arrêtera sur le point d’arrêt qui vient d’être posé ;
print variable imprime le contenu d’une variable ;
next exécute une instruction (fastidieux) ;
version du 12 novembre 1999
1.4. Les makefiles 11

step exécute une instruction en entrant, le cas échéant, dans les sous-programmes (encore plus
fastidieux !).
continue permet de continuer jusqu’au prochain point d’arrêt.

1.4 Les makefiles


Ce paragraphe est rédigé à partir du cours de licence d’informatique de Nancy [4] et du livre de
Matt Welsh [3].

1.4.1 Définition

make est une commande Unix qui permet à partir d’un fichier de description d’exécuter une
séquence de commandes afin :

– de construire la version exécutable d’une application ;


– d’installer des logiciels ;
– de nettoyer des fichiers temporaires et de déplacer des fichiers ;
– de mettre à jour des bibliothèques ;
– ...

Afin de construire la version exécutable d’une application, le programmeur avisé a recours à la


compilation séparée qui permet :

– de faire le moins de travail possible ;


– de ne re-compiler que les fichiers qui ont été modifiés ou qui incluent des fichiers modifiés ;
– d’éviter d’écrire des commandes de compilation et d’édition inutiles, car les fichiers produits
seraient les mêmes qu’auparavant.

make est un programme qui (si le Makefile est bien fait) ne re-compile que ce qui a changé puis
qui réalise l’exécutable à partir de ce nouvel élément et des anciens objets toujours valides ; il suffit
de taper make, c’est tout.

1.4.2 Comment ça marche?

Le but de make est de vous permettre de construire un fichier par petites étapes. Afin d’y parvenir,
make enregistre quels fichiers sont nécessaires pour construire votre projet. Cette description est
contenue dans un fichier qui s’appelle en général Makefile ou makefile et qui se situe dans le même
répertoire que vos fichiers sources. Voici un Makefile très simple :

client : protocole.o ppclient.o


tabulation gcc -o client protocole.o ppclient.o
12 Chapitre 1. Les outils Unix pour développer en C

ppclient.o : ppclient.c protocole.h


tabulation gcc -c -Wall ppclient.c

protocole.o : protocole.c protocole.h


tabulation gcc -c -Wall protocole.c
# c’est fini

Grâce à la directive #include "protocole.h", les deux fichiers sources protocole.c et pp-
client.c incluent un fichier protocole.h qui se situe dans le même répertoire
Nous voyons qu’il y a trois entrées ou trois cibles. Chaque entrée débute par une ligne qui spécifie
les dépendances. La première ligne spécifie que l’exécutable client dépend d’un sous-programme
protocole.o et du programme principal ppclient.o. La deuxième ligne donne l’action à accomplir
pour construire la cible client. Il s’agit d’une édition de lien. Attention, comme toute ligne de
commande, celle-ci doit débuter par une tabulation. C’est la cause de beaucoup d’erreurs de syntaxe
dans make.
Les entrées suivantes spécifient les dépendances des fichiers protocole.c et ppclient.c. Ces
deux fichiers doivent être re-compilés à chaque modification et si le fichier d’en-tête protocole.h
venait à être modifié. Dans les deux cas, l’action à accomplir est l’appel au compilateur gcc avec
l’option -c.
La dernière ligne débutant par un # est une ligne de commentaire.
À chaque modification des programmes C ou du fichier inclus, il suffira de faire make client ou
plus simplement make pour que make ne fasse que le strict minimum.
Voici un autre Makefile un peu plus long. Il possède une cible appelée clean dont le rôle est de
nettoyer le répertoire des fichiers produits par gcc. C’est le cas quand il faut tout recompiler pour
passer d’une version compilée avec l’option -ggdb à une version finale optimisée et compilée avec
l’option -O2. On voit que make ne sert pas qu’à invoquer le compilateur. Toutes les commandes Unix
sont possibles ; mais attention à la tabulation en début de ligne !
Dans cet exemple, l’application est constituée de deux programmes appelés client et serveur.
Pour tout constuire il suffit de faire make all.

# pour effectuer le ménage


clean :
tabulation rm protocole.o ppclient.o client serveur ppserveur.o

#pour tout reconstruire


all : client serveur

# pour construire le client

client : protocole.o ppclient.o


tabulation gcc -o client protocole.o ppclient.o

ppclient.o : ppclient.c protocole.h


tabulation gcc -c -Wall ppclient.c
version du 12 novembre 1999
1.4. Les makefiles 13

protocole.o : protocole.c protocole.h


tabulation gcc -c -Wall protocole.c

# pour construire le serveur

serveur : protocole.o ppserveur.o


tabulation gcc -o serveur protocole.o ppserveur.o

ppserveur.o : protocole.h ppserveur.c


tabulation gcc -c -Wall ppseveur.o
# c’est fini

1.4.3 Quelques règles de syntaxe

Une ligne de commande débute par une tabulation. Une ligne trop longue peut être repliée en
tapant le caractère \ en fin de ligne. Vous pouvez placer un dièse (#) à n’importe quel endroit de la
ligne pour introduire un commentaire ; tout ce qui suit sera ignoré.
Il est possible d’introduire des macro-définitions. Il s’agit de chaı̂nes de caractères que make
expanse en d’autres. Dans l’exemple suivant, les macros OBJETS_CLI et OBJETS_SER permettent
d’économiser de la frappe au clavier et de reposer les petits doigts fatigués.

OBJETS_CLI = protocole.o ppclient.o


OBJETS_SER = protocole.o ppserveur.o
CFLAGS = -c -Wall

# pour effectuer le ménage


clean :
tabulation rm client serveur $(OBJETS_CLI) $(OBJETS_SER)

#pour tout reconstruire


all : client serveur

# pour construire le client

client : $(OBJETS_CLI)
tabulation gcc -o client $(OBJETS_CLI)

ppclient.o : ppclient.c protocole.h


tabulation gcc $(CFLAGS) ppclient.c

protocole.o : protocole.c protocole.h


tabulation gcc $(CFLAGS) protocole.c

# pour construire le serveur

serveur : $(OBJETS_SER)
14 Chapitre 1. Les outils Unix pour développer en C

tabulation gcc -o serveur $(OBJETS_SER)

ppserveur.o : protocole.h ppserveur.c


tabulation gcc $(CFLAGS) ppseveur.o
# c’est fini

Une macro non définie est remplacée par la chaı̂ne vide. On peut définir une macro à l’appel
de make. Par exemple, si on désire un comportement versatile du compilateur pour compiler tantôt
avec l’option -O2, tantôt avec l’option -ggdb, on écrira :
DEBUG=-ggdb
...
tabulation gcc $(CFLAGS) $(DEBUG) protocole.c
...

Si on veut une compilation optimisée, on invoquera la commande :


make DEBUG=-O2

Cette affectation supplantera celle faite dans le Makefile par la définition DEBUG=-ggdb qui
restera la définition par défaut.

1.4.4 Règles implicites

Pour des opération routinières comme la construction d’un fichier objet à partir d’un fichier
source, spécifier à chaque fois les dépendances est long et pénible. Sous Unix, un fichier se terminant
par .c sera toujours compilé en fichier se terminant par .o. La commande make permet de définir
des règles implicites pour traiter facilement ce type de cas. Voici une de ces règles pour traiter le cas
de la compilation des programmes C.

...
.c.o :
tabulation gcc $(CFLAGS) $(DEBUG) $<
...

Le signe .c.o : signifie « prendre un fichier .c en entrée et le transformer en fichier .o ». La


chaı̂ne $< est un code représentant le fichier d’entrée. Notre Makefile peut se réduire encore en
utilisant cette règle.

.c.o :
gcc $(CFLAGS) $(DEBUG) $<
OBJETS_CLI = protocole.o ppclient.o
OBJETS_SER = protocole.o ppserveur.o
CFLAGS = -c -Wall

# pour effectuer le ménage


clean :
tabulation rm client serveur $(OBJETS_CLI) $(OBJETS_SER)
version du 12 novembre 1999
1.4. Les makefiles 15

#pour tout reconstruire


all : client serveur

# pour construire le client

client : $(OBJETS_CLI)
tabulation gcc -o client $(OBJETS_CLI)

ppclient.o : ppclient.c protocole.h

protocole.o : protocole.c protocole.h

# pour construire le serveur

serveur : $(OBJETS_SER)
tabulation gcc -o serveur $(OBJETS_SER)

ppserveur.o : protocole.h ppserveur.c

# c’est fini
16 Chapitre 1. Les outils Unix pour développer en C

touche commande
C-a début de ligne
C-e fin de la ligne
C-b reculer d’un caractère (quand la flèche ne marche plus)
C-f avancer d’un caractère
C-n ligne suivante
C-p ligne précédente
C-d détruire un caractère
ESC a reculer d’une phrase (recherche le point précédent)
ESC f mot suivant
ESC b reculer d’un mot
ESC d détruire un mot
ESC g aller à la ligne de numéro donné
C-g fin de commande
C-k détruire le reste de la ligne ( = couper ) et le ranger dans le buffer de destruction
C-l repeindre l’écran
C-q quoted-insert.(pour insérer des caractères de contrôle)
C-r recherche arrière
C-s recherche avant
C-u argument universel
C-v scroll-up
C-w couper une région dans le buffer de destruction
C-y insère le buffer de destruction ( = coller)
C-x C-b liste les noms de buffers

C-x C-c sauver et sortir d’Emacs


C-x C-f trouve un fichier pour l’éditer
C-x C-r trouve un fichier en consultation (Read Only)
C-x C-s sauvegarder sans quitter
C-x C-w écrire un buffer sur fichier
C-x C-x échanger la position courante et la marque (cf. C-@)
C-x ( début de macro
C-x ) fin de macro
C-x E exécuter la macro

C-x 0 détruire une fenêtre


C-x b changer de buffer
C-x i insérer un fichier
C-x k détruire un buffer
C-x s sauver certains buffers
C-x ! shell-command
ESC C-r substitution à la demande
ESC (escape espace)place une marque

Tab. 1.1 – Aide mémoire de Micro-Emacs. On peut obtenir ce tableau à l’aide de la commande ESC
x describe-bindings
version du 12 novembre 1999
17

Chapitre 2

Compléments en C

2.1 La manipulation des chaı̂nes de bits

2.1.1 Définitions de constantes dans différentes bases

On peut définir des constantes dans diverses bases en C. Ainsi la constante notée 128 en base 10
s’écrira :

– 128 en base 10 ;
– 0200 en base 8 (octal) ;
– 0x80 en base 16 (hexa) ;

Les notations octales et hexadécimales servent généralement en système pour coder des constantes
de type int et char et leurs dérivés.

Exemple :
int eol1 = 0x0D0A ; /* CR et LF en ASCII */
char cr = 0x0D ; /* CR */

2.1.2 Les opérateurs logiques ET et OU

L’opérateur ET (noté &) agit entre deux bits et a pour résultat la multiplication ; l’opérateur
OU (noté |) agit entre deux bits et a pour résultat l’addition. Ces opérateurs peuvent agir sur deux
chaı̂nes de bits rangées dans des variables de type char (= 8 bits) ou int (= 16, 32 ou 64 bits suivant
l’architecture) en agissant bit à bit sur les composants des chaı̂nes.

Exemple :
int eol1 = 0x0D0A ;
int mask = 0x00FF ;
18 Chapitre 2. Compléments en C

int resEt = eol1 & mask ; /* le résultat sera 000A */


int resOu = eol1 | mask ; /* le résultat sera 0DFF */

2.1.3 Les opérateurs de décalage de bits

Il est possible de décaler vers la gauche (opérateur <<) une chaı̂ne de bits, il s’agit d’une multi-
plication par une puissance de deux ; les bits qui entrent à droite sont des zéros, les bits qui sortent
à gauche sont perdus. Le décalage à droite se note >> ; des zéros entrent à gauche et les bits qui
sortent à droite sont perdus.

Exemple :

int eol1 = 0x0D0A ;


int left8 = eol1 << 8 ; /* décal. à gauche de 8 bits, le résu. est 0x0A00 */
int right4 = eol1 >> 4 ; /* décal. à droite de 4 bits, ... : 0x00D0 */

2.1.4 Positionnement d’un bit à 1

Si on connaı̂t le rang du bit, il suffit d’utiliser OU avec une constante appelée masque.

Exemple : mettre à 1 le bit no. 3 de la variable mot : mot | 0x080 . Le bit le plus à droite a le
rang zéro par convention.
On veut placer le bit no i à 1 dans un mot. On agit par décalage et masque.

int i, mot ;
int res ;
... /* affectation de mot et i */
res = mot | (1 << i) ;

2.1.5 Positionnement d’un bit à 0

Si on connaı̂t le rang du bit, on effectue un masque avec un ET.

Exemple : mettre à 0 le bit no. 3 de la variable mot : mot & 0xF7 .


On veut placer le bit no. i à zéro dans un mot. On agit par décalage, complémentation et masque.

int i, mot ;
int res ;
... /* affectation de mot et i */
res = mot & ~(1 << i) ;
version du 12 novembre 1999
2.2. Définition d’un pointeur 19

2.1.6 Tester un bit

Encore une fois, on a deux possibilités suivant que l’on connaı̂t le rang du bit à la compilation –
on peut alors écrire le masque – ou non. On doit calculer le masque à l’exécution.

Exemple : tester le bit no. 3 de la variable mot


int mot ;
...
if (mot & 0x08 == 0x08)
/* le bit no. 3 vaut 1 */
else
/* devine ! */

2.2 Définition d’un pointeur

Un pointeur est une variable qui contient l’adresse d’une autre variable. Les pointeurs ont une
grande importance en C ; aussi consacrerons nous une section particulière sur leurs utilisations en
système d’exploitation et sur les sources d’erreurs dont ils sont la cause.
Pour déclarer un pointeur, il faut préciser quel sera le type de la variable dont il contiendra
l’adresse. Cela se réalise à l’aide du symbole *.

Exemple : int *pti ; déclare un pointeur appelé pti qui contiendra l’adresse d’une variable de
type int.

2.3 Utilisation des pointeurs

2.3.1 L’opérateur &

La première opération qu’on est amené à effectuer sur un pointeur est de préciser vers quelle
variable il pointe. Cela consiste à mettre dans le pointeur une adresse, le plus souvent celle d’une
variable déjà déclarée. On utilise l’opérateur & placé devant une variable pour en avoir l’adresse.
&var signifie l’adresse de la variable var.
void main()
{
int unObjetEntier ;
int *pe ;

pe = &unObjetEntier ; /* pe est affecté avec l’adresse de la


variable entière unObjetEntier */
...
}
20 Chapitre 2. Compléments en C

2.3.2 Propriétés de l’opérateur &

L’opérateur & ne s’applique qu’aux variables simples (même de type pointeur) et indicées ainsi
qu’aux structures. Sans précautions particulières, on ne peut pas écrire &tab si tab est un tableau ;
par définition tab est une constante qui représente l’adresse du premier élément du tableau.

2.3.3 L’opérateur d’accès indirect *

Ce type d’adressage souvent utilisé dans les langages d’assemblage, consiste à utiliser un pointeur
pour accéder à une autre variable. Il y a deux accès en mémoire : le premier pour lire la valeur du
pointeur et le deuxième pour accéder à cette adresse soit en lecture, soit en écriture. En C, ce type
d’adressage est réalisé grâce à l’opérateur * placé devant un pointeur.
*pe signifie une des trois assertions suivantes :

– accès indirect au travers de pe :


– l’objet dont l’adresse est dans pe ;
– l’objet pointé par pe.

Attention, cela ne signifie pas : contenu de pe.

Exemple :
void main()
{
int unObjetEntier ;
int *pe ;

pe = &unObjetEntier ; /* pe est affecté avec l’adresse de la


variable entière unObjetEntier */
*pe = 1515 ; /* on aurait pu aussi bien écrire :
unObjetEntier = 1515 ; */
}

Attention, avant d’utiliser un pointeur, il faut lui donner une valeur. Ceci est réalisé, soit par
affectation, soit dans un appel de fonction. Regardez ce programme qui n’a pas de sens :

void main()
{
char *buffer ; /* un pointeur vers une mémoire tampon */
FILE *pf ; /* un pointeur de fichier */
...
fread(buffer, 1, 80, pf) ; /* lecture de 80 caractères */

Ce programme effectue une lecture de 80 caractères depuis un fichier pf et les range dans une zone
dont l’adresse est dans buffer. Mais buffer ne contient rien de bon ; le programme écrira n’importe
version du 12 novembre 1999
2.4. Les tableaux de pointeurs 21

où ! Pour corriger cette erreur, on peut utiliser une déclaration de tableau char buffer[80] à la
place de la déclaration de pointeur char *buffer, ou bien encore faire un malloc.

2.3.4 L’opérateur d’accès indexé []

Cet opérateur qui se place derrière un identificateur de tableau, s’applique aussi aux pointeurs.
Il permet de se déplacer dans la zone mémoire qui débute à l’adresse contenue dans le pointeur.

Exemple :

void main()
{
char *buffer ; /* un pointeur vers une mémoire tampon */
buffer = (char *) malloc(80) ; /* qui pointe vers une cha^ıne de caractères */
...
/* initialisation avec 80 retour-chariot */
for (i = 0 ; i < 80 ; i++) buffer[i] = ’\n’ ;
...
free(buffer) ; /* ne pas oublier ! */

2.4 Les tableaux de pointeurs

On définit un tableau de pointeurs par la déclaration :


Type *identificateur[nElem] ;
L’opérateur [] est plus prioritaire sur l’opérateur *. Il s’agit d’un tableau de nElem pointeurs
vers des Type.

2.4.1 Exemple 1

Considérons un tableau de sept chaı̂nes contenant les jours de la semaine.

void main()
{
char *jours[7] ;
...
jours[0] = "Lundi" ;
jours[1] = "Mardi" ;
...
jours[6] = "Dimanche" ;

}
22 Chapitre 2. Compléments en C

On peut aussi user de la déclaration et de l’initialisation suivantes :


char *jours[7] = {
"Lundi",
"Mardi",
...
"Dimanche"} ;

2.4.2 Les arguments de la commande main

En C, il est possible de passer des paramètres à un programme principal pour en faire une
commande Unix qui accepte des paramètres.

Exemple : com arg1 arg2 arg3


Ces paramètres sont facilement récupérables ! Il suffit dans un premier temps de déclarer le main
comme une procédure qui accepte deux paramètres qui sont nommés conventionnellement argc et
argv.

void main(int argc, char *argv[])


{
...
}

argc est une variable entière qui contient à l’appel du programme principal le nombre de mots de
la ligne de commande. argv est un tableau de pointeurs vers des chaı̂nes de caractères, tout comme
le tableau jours. Ces chaı̂nes sont les différents mots (ou champs) de la ligne de commande. Si la
ligne de commande est com arg1 arg2 arg3, on aura au début du programme principal :

– argc qui contiendra 4 ;


– argv[0] qui pointera vers la chaı̂ne com ;
– argv[1] qui pointera vers la chaı̂ne arg1 ;
– argv[2] qui pointera vers la chaı̂ne arg2 ;
– argv[3] qui pointera vers la chaı̂ne arg3.

2.5 Exemple récapitulatif : la fonction Unix echo

La fonction Unix echo reproduit des arguments sur la sortie standard (l’écran en général). Une
version simplifiée peut se programmer ainsi :
/* fonction monecho.c
** version simplifiée de la fonction echo Unix
*/
#include <stdio.h>
version du 12 novembre 1999
2.5. Exemple récapitulatif : la fonction Unix echo 23

main(int argc, char *argv[])


{
int i ;

for (i = 1 ; i < argc ; i++) /* impression des arguments */


printf("%s ", argv[i]) ; /* un par un sur la m^ eme ligne */
printf("\n") ; /* passage à la ligne */
}

2.5.1 Exemple d’interaction avec le shell

Considérons la compilation et l’exécution de notre fonction echo. Au début, tout se passe bien.
[jfmari@localhost Programmes]$ gcc -Wall monecho.c -o monecho
[jfmari@localhost Programmes]$ monecho un programme qui marche
un programme qui marche
[jfmari@localhost Programmes]$

Essayons avec le paramètre * :


[jfmari@localhost Programmes]$ monecho *
#monecho.c# monecho monecho.c monecho.c~
[jfmari@localhost Programmes]$

Mais pourquoi ce résultat étrange? Je voulais simplement imprimer le caractère * et j’obtiens la liste
des fichiers du répertoire ! La réponse à cette question est aisée si on a bien compris le fonctionnement
de l’interpréteur de commandes : le shell. Quand celui-ci analyse la ligne de commande, il remplace le
caractère * par la liste de tous les fichiers. Ainsi la ligne de commande est considérablement allongée
avant la début du programme principal.
24 Chapitre 2. Compléments en C
version du 12 novembre 1999
25

Deuxième partie

Fichiers et processus
version du 12 novembre 1999
27

Chapitre 3

Le système de gestion de fichiers

3.1 Entrées/Sorties fichiers

3.1.1 Notion de table des i-nœuds

Cette table est placée au début de chaque région de disque contenant un système de fichiers
UNIX. Chaque nœud d’index de cette table — ou i-nœuds, ou encore inode — correspond à un
fichier et contient des informations essentielles sur les fichiers inscrits sur le disque :

– le type du fichier (détaillé plus bas) ;


– le nombre de liens (nombre de noms de fichier donnant accès au même fichier) ;
– le propriétaire et son groupe ;
– l’ensemble des droits d’accès associés au fichier pour le propriétaire du fichier, le groupe auquel
appartient le propriétaire, et enfin tous les autres usagers ;
– la taille en nombre d’octets ;
– les dates du dernier accès, de la dernière modification, et du dernier changement d’état (quand
le nœud d’index lui-même a été modifié) ;
– des pointeurs vers les blocs du disque contenant les données du fichier.

La structure stat correspondante est définie comme suit, dans le fichier <sys/stat.h> :

struct stat {
dev_t st_dev; /* identificateur du périphérique */
/* où se trouve le fichier */
ino_t st_ino; /* numéro du nœud d’index */
mode_t st_mode; /* droits d’accès du fichier */
nlink_t st_nlink; /* nombre de liens effectués sur le fichier */
uid_t st_uid; /* identificateur du propriétaire */
gid_t st_gid; /* identificateur du groupe du propriétaire */
dev_t st_rdev; /* type de périphérique */
28 Chapitre 3. Le système de gestion de fichiers

off_t st_size; /* taille en octets du fichier */


time_t st_atime; /* date du dernier accès au fichier */
time_t st_mtime; /* date de la dernière modification du fichier */
time_t st_ctime; /* date du dernier changement du nœud d’index */
} ;

Remarque
Cette table ne contient ni le nom du fichier, ni les données. Les types peuvent différer suivant
l’architecture. Par exemple, sous HP-UX, st_mode et st_nlink sont de type ushort.

3.1.2 Les types de fichiers

Il y a trois types de fichiers UNIX :

– les fichiers ordinaires : tableaux linéaires d’octets identifiés par leur numéro d’index ;
– les répertoires : ces fichiers permettent de repérer un fichier par un nom plutôt que par son
numéro d’index dans la table de nœud d’index ; le répertoire est donc constitué d’une table à
deux colonnes contenant d’un coté le nom que l’utilisateur donne au fichier, et de l’autre, le
numéro d’index donné par le système qui permet d’accéder à ce fichier. Cette paire est appelée
un lien ;
– les fichiers spéciaux, périphériques, tubes, sockets . . . que nous aborderons plus loin.

3.1.3 Descripteurs de fichier

Nous avons vu que le nœud d’index d’un fichier est la structure d’identification du fichier vis-à-vis
du système. Lorsqu’un processus veut manipuler un fichier, il va utiliser plus simplement un entier
appelé descripteur de fichier. L’association de ce descripteur au nœud d’index du fichier se fait lors
de l’appel à la primitive open() (voir en open cf. 3.1.5). Le descripteur devient alors le nom local du
fichier dans le processus. Chaque processus UNIX dispose de 20 descripteurs de fichier, numérotés
de 0 à 19. Par convention, les trois premiers sont toujours ouverts au début de la vie d’un processus :

– le descripteur de fichier 0 est l’entrée standard (généralement le clavier) ;


– le descripteur de fichier 1 est la sortie standard (généralement l’écran) ;
– le descripteur de fichier 2 est la sortie erreur standard (généralement l’écran) ;

Les 17 autres sont disponibles pour les fichiers et les fichiers spéciaux que le processus ouvre
lui-même. Cette notion de descripteur de fichier est utilisée par l’interface d’entrée/sortie de bas
niveau, principalement avec les primitives open(), write(), read(), close().

3.1.4 Pointeurs vers un fichier

En revanche, lorsqu’on utilise les primitives de la bibliothèque standard d’entrées/sorties fopen,


fread, fscanf, ..., les fichiers sont repérés par des pointeurs vers des objets de type FILE (type
version du 12 novembre 1999
3.1. Entrées/Sorties fichiers 29

défini dans le fichier <stdio.h>). Il y a trois pointeurs prédéfinis :

– stdin qui pointe vers le tampon du standard input (généralement le clavier) ;


– stdout qui pointe vers le tampon du standard output (généralement l’écran) ;
– stderr qui pointe vers le tampon du standard error output (généralement l’écran).

3.1.5 Description de quelques primitives et fonctions

Primitive creat()

int creat(const char *path, mode_t perm)


/* crée un fichier */
/* path = nom du fichier */
/* perm = droits d’accès */

Valeur retournée : descripteur de fichier ou −1 en cas d’erreur.

Cette primitive réalise la création d’un fichier, dont le nom est donné dans le paramètre path.
L’entier perm définit les droits d’accès. Si le fichier n’existait pas, il est ouvert en écriture. Ce n’est
pas une erreur de créer un fichier qui existait déjà. Reportez vous au manuel en ligne pour interpréter
correctement les droits d’accès. Pour créer un fichier de nom « essai_creat » avec les autorisations
lecture et écriture pour le propriétaire et le groupe, on écrira:

if ((fd = creat("essai_creat",0660)) == -1)


perror("Erreur creat()") ;

Primitive open()

La fonction open() a deux profils : avec 2 ou 3 paramètres.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);


int open(const char *pathname, int flags, mode_t mode);

Valeur retournée : descripteur de fichier ou −1 en cas d’erreur.

Cette primitive permet d’ouvrir (ou de créer) le fichier de nom pathname. L’entier flags déter-
mine le mode d’ouverture du fichier. Le paramètre optionnel mode n’est utilisé que lorsque open()
réalise la création du fichier. Dans ce cas, il indique les droits d’accès au fichier donnés à l’utilisateur,
à son groupe et au reste du monde. Le paramètre flags peut prendre une ou plusieurs des constantes
symboliques (qui sont dans ce cas séparées par des OU), définies dans le fichier d’inclusion fcntl.h.
30 Chapitre 3. Le système de gestion de fichiers

00000 O_RDONLY ouverture en lecture seule


00001 O_WRONLY ouverture en écriture seule
00002 O_RDWR ouverture en lecture et écriture
00010 O_APPEND ouverture en écriture à la fin du fichier
00400 O_CREAT création du fichier s’il n’existe pas (seul cas d’utilisation de l’argument perm)
01000 O_TRUNC troncature à la longueur zéro, si le fichier existe
02000 O_EXCL provoque un échec si le fichier existe déjà et si O_CREAT est positionné,
Exemple :
Pour effectuer la création et l’ouverture du fichier «essai_open» en écriture avec les autorisations
de lecture et écriture pour le propriétaire et le groupe, il faut écrire :

if ((fd = open("essai_open" , O_WRONLY | O_CREAT | O_TRUNC, 0660)) == -1)


perror("Erreur open()") ;

Remarque : l’inclusion du fichier <sys/types.h> est nécessaire, car des types utilisés dans <fcntl.h>
y sont définis.

Fonction fdopen()

Cette fonction permet de faire le pont entre les manipulations de fichiers de la librairie stan-
dard C, qui utilise des pointeurs vers des objets de type FILE (fclose(), fflush(), fprintf(),
fscanf()), et les primitives de bas niveau open(), write(), read() qui utilisent des descripteurs
de fichiers de type int.

#include <stdio.h>

FILE* fdopen(int fd,const char *mode)


/* convertit un descripteur de fichier en */
/* un pointeur sur un fichier */
/* fd : descripteur concerné par la conversion */
/* mode : mode d’ouverture désiré */

Valeur retournée : un pointeur sur le fichier associé au descripteur fd, ou la constante NULL
(prédéfinie dans <stdio.h>) en cas d’erreur.
Remarque :
Le fichier doit, au préalable, avoir été ouvert à l’aide de la primitive open(). D’autre part, le
paramètre mode choisi doit être compatible avec le mode utilisé lors de l’ouverture par open. Ce
paramètre peut prendre les valeurs suivantes :

– "r" : le fichier est ouvert en lecture.


– "w" : le fichier est créé et ouvert en écriture. S’il existait déjà, sa longueur est ramenée à zéro.
– "a" : ouverture en écriture à la fin du fichier (avec création préalable si le fichier n’existait pas).
version du 12 novembre 1999
3.1. Entrées/Sorties fichiers 31

Exemple :

/* ouverture préalable par open() par exemple en lecture */


if ((fd = open("mon_fichier", O_RDONLY, 0666)) == -1) {
perror("Erreur open()") ;
exit(-1) ;
}
/* association de fp (de type FILE*) à fd (de type int) */
if ((fp = fdopen(fd, "r")) == NULL) {
perror("Erreur fdopen()") ;
exit(-1) ;
}

Primitive close()

#include <unistd.h>
int close(int fd) /* fermeture de fichier */
/* fd est le descripteur de fichier */

Valeur retournée : 0 ou −1 en cas d’échec.


Cette primitive ne vide pas le tampon du noyau correspondant au processus, elle libère simple-
ment le descripteur de fichier pour une éventuelle réutilisation.

Primitive dup()

#include <unistd.h>
int dup(int fd) /* fd est le descripteur donné */

Cette primitive duplique un descripteur de fichier existant et fournit donc un descripteur ayant
exactement les mêmes caractéristiques que celui passé en argument. Elle garantit que la valeur
retournée est la plus petite possible parmi les valeurs de descripteurs disponibles.
Valeur retournée : nouveau descripteur de fichier ou −1 en cas d’erreur.

Primitives dup2()

#include <unistd.h>
int dup2(int fd1,int fd2) /* force fd2 comme synonyme de fd1 */
/* fd1 : descripteur à dupliquer */
/* fd2 : nouveau descripteur */

Valeur retournée : −1 en cas d’erreur.


Cette primitive oblige le descripteur fd2 à devenir synonyme du descripteur fd1. Notons que
dup2() ferme le descripteur fd2 si celui-ci était ouvert.
32 Chapitre 3. Le système de gestion de fichiers

Primitive write()
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes)
/* écriture dans un fichier */
/* fd : descripteur de fichier */
/* buf : adresse du tampon */
/* nbytes : nombre d’octets à écrire */

Valeur retournée : nombre d’octets écrits ou −1 en cas d’erreur.


Cette primitive écrit dans le fichier ouvert représenté par fd les nbytes octets sur lesquels pointe
buf. Il faut noter que l’écriture ne se fait pas directement dans le fichier, mais passe par un tampon
du noyau. Attention, ce n’est pas une erreur que d’écrire moins d’octets que souhaités.

Primitive read()
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes) /* lecture dans un fichier */
/* fd : descripteur de fichier */
/* buf : adresse du tampon */
/* nbytes : nombre d’octets à lire */

Valeur retournée : nombre d’octets lus, 0 si la fin de fichier a été atteinte, ou −1 en cas d’erreur.
La primitive lit les nbytes octets dans le fichier ouvert représenté par fd, et les place dans le
tampon sur lequel pointe buf.
Exemple 1 : Redirection standard.
Ce programme exécute la commande shell ps, puis redirige le résultat vers un fichier fic_sortie.
Ainsi l’exécution de ce programme ne devrait plus rien donner à l’écran. La primitive execl() exécute
la commande passée en argument, nous nous attarderons sur cette primitive dans le chapitre suivant,
concernant les processus.

/* fichier test_dup2.c */

#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>

#define STDOUT 1

main()
{
int fd ;
/* affecte au fichier fic_sortie le descripteur fd */
if ((fd = open("fic_sortie",O_CREAT | O_WRONLY | O_TRUNC, 0666)) == -1) {
perror("Erreur sur l’ouverture de fic_sortie") ;
version du 12 novembre 1999
3.2. La manipulation des répertoires 33

exit(1) ;
}

dup2(fd,STDOUT) ; /* duplique la sortie standard */


execl("/bin/ps","ps",NULL) ; /* exécute la commande */
}

Résultat de l’exécution :

bernard/> test_dup2
bernard/> more fic_sortie
PID TTY TIME COMMAND
3954 ttyp3 0:03 csh

Notons que les autres redirections suivent le même principe, et qu’il est possible de coupler les
redirections d’entrée et de sortie.
Exemple 2 (en exercice) : programme réalisant la copie d’un fichier vers un autre.

3.2 La manipulation des répertoires

Un répertoire est un fichier contenant un ensemble de couples (nom_de_fichier numéro_d’inode).


Il est possible d’ouvrir un répertoire pour le parcourir. C’est ce que fait la commande ls. Unix four-
nit plusieurs fonctions pour parcourir un répertoire. Rappelons que Unix hiérarchise l’ensemble des
répertoires comme un arbre dont la racine s’appelle « / ». Par convention, le répertoire courant
s’appelle « . » et son père dans la hiérarchie s’appelle « .. ».

3.2.1 Les fonctions opendir() et closedir()


#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *name);

La fonction opendir() ouvre le répertoire name et retourne un pointeur vers la structure DIR qui
joue le même rôle que FILE. La valeur NULL est renvoyée si l’ouverture n’est pas possible.

#include <sys/types.h>
#include <dirent.h>

int closedir(DIR *dir);

La fonction closedir() ferme un répertoire ouvert par opendir !


34 Chapitre 3. Le système de gestion de fichiers

3.2.2 La fonction readdir()


#include <sys/types.h>
#include <dirent.h>

struct dirent *readdir(DIR *dir);

La fonction readdir() retourne un pointeur vers une structure de type struct dirent représen-
tant le prochain couple (nom de fichier, inode) dans le répertoire. La structure dirent contient deux
champs d_name et d_ino qui représentent respectivement le nom du fichier et son numéro d’inode.
Les données retournées par readdir() sont écrasées à chaque appel. Quand la fin du répertoire est
atteinte, readdir() renvoie NULL.

3.2.3 Exemple : la lecture d’un répertoire

Écrire un programme qui ouvre le répertoire courant et imprime tous les fichiers ainsi que leurs
numéros d’inodes. Ce programme doit réaliser la même sortie que la commande ls -ai.

3.3 Les verrous POSIX : définitions et utilité


Les verrous servent à empécher que plusieurs processus accèdent simultanément aux mêmes
enregistrements.
Considérons le programme suivant qui écrit dans un fichier 10 fois son pid ainsi que l’heure
d’écriture.

/*
** fichier writel.c
*/
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <time.h>

void main(int argc, char **argv) /* argv[1] = fichier à écrire */


{
int desc ;
int i ;
char buf[1024] ;
int n ;
time_t secnow ;

if (argc < 2) {
version du 12 novembre 1999
3.3. Les verrous POSIX : définitions et utilité 35

fprintf(stderr,"Usage : %s filename \n", argv[0]) ;


exit(1) ;
}
if ((desc = open(argv[1], O_WRONLY | O_CREAT | O_APPEND, 0666)) == -1) {
perror("Ouverture impossible ");
exit(1);
}

#ifdef VERROU /* on verrouille tout le fichier */


if (lockf(desc, F_LOCK, 0) == -1) {
perror("lock") ;
exit(1) ;
}
else
printf("processus %ld : verrou posé \n", (long int)getpid()) ;
#endif

for (i =0 ; i< 10 ; i++) {


time(&secnow) ;
sprintf(buf,"%ld : écriture à %s ", (long int)getpid(),
ctime(&secnow)) ;
n = write (desc, buf, strlen(buf)) ;
sleep(1) ;
}

#ifdef VERROU /* levée du verrou */


lockf(desc, F_ULOCK, 0) ;
#endif

exit(0) ;
}

On compile le programme sans définir la constante VERROU, donc sans verrou implémenté par la
fonction lockf que nous verrons plus loin.

[jfmari@localhost ~/Systeme]\$ gcc -Wall writel.c


[jfmari@localhost ~/Systeme]\$ ./a.out essai & ./a.out essai & ./a.out essai & ./a.out es-
sai &
[1] 1959
[2] 1960
[3] 1961
[4] 1962
[jfmari@localhost ~/Systeme] \$ cat essai
1959 : écriture à Sat Nov 29 18:43:59 1997
1960 : écriture à Sat Nov 29 18:43:59 1997
1961 : écriture à Sat Nov 29 18:43:59 1997
1962 : écriture à Sat Nov 29 18:43:59 1997
1959 : écriture à Sat Nov 29 18:44:00 1997
36 Chapitre 3. Le système de gestion de fichiers

1960 : écriture à Sat Nov 29 18:44:00 1997


1961 : écriture à Sat Nov 29 18:44:00 1997
...
1962 : écriture à Sat Nov 29 18:44:08 1997

Toutes les écritures sont mélangées. Voyons comment sérialiser les accès.

3.4 La primitive lockf


#include <unistd.h>
int lockf(int desc, int op, off_t taille)

Le paramètre desc est un descripteur de fichier ouvert, soit en écriture soit en lecture/écriture.
Les différentes valeurs possibles pour le paramètre op sont définies dans les fichiers unistd.h ou
lockf.h.

F_ULOCK levée de verrou


F_LOCK verrouillage et accès exclusif, mode bloquant
F_TLOCK verrouillage et accès exclusif, mode non bloquant
F_TEST test d’existence de verrous.

Le paramètre taille permet de spécifier la portée du verrouillage. Cette portée s’exprime par
rapport à la position du pointeur de fichier ; on peut verrouiller une zone précédente en donnant une
valeur négative. Une valeur nulle (comme dans l’exemple) permet de verrouiller jusqu’à la fin du
fichier.
L’opération F_LOCK est bloquante ; le programme qui l’exécute attend derrière le verrou comme
derrière un sémaphore.
L’opération F_TLOCK n’est pas bloquante ; Si le verrou existe, lockf renvoie −1 et errno est mis
à EACCES si un verrou existe déjà.
L’opération F_ULOCK déverrouille une zone ; il est possible de ne déverrouiller qu’une partie d’une
zone verrouillée.

3.5 Exemple
On compile notre fichier writel.c en définissant la constante VERROU dans la ligne de commande.

[jfmari@localhost ~/Systeme]$ gcc -Wall -DVERROU writel.c


writel.c: In function ‘main’:
writel.c:17: warning: unused variable ‘timeptr’
writel.c:16: warning: unused variable ‘sizeR’
writel.c:12: warning: unused variable ‘pid’
version du 12 novembre 1999
3.5. Exemple 37

[jfmari@localhost ~/Systeme]$ ./a.out essai & ./a.out essai & ./a.out essai & ./a.out es-
sai &
[1] 2534
[2] 2535
[3] 2536
[4] 2537
[jfmari@localhost ~/Systeme]$ processus 2534 : verrou posé
processus 2536 : verrou posé
processus 2535 : verrou posé
processus 2537 : verrou posé

[4] Done ./a.out essai


[3] + Done ./a.out essai
[2] + Done ./a.out essai
[1] + Done ./a.out essai
[jfmari@localhost ~/Systeme]$

Et si on imprime essai, on voit que chaque processus a pu écrire sans être interrompu.

[jfmari@localhost ~/Systeme]$ cat essai


2534 : écriture à Sat Nov 29 19:23:39 1997
...
2534 : écriture à Sat Nov 29 19:23:48 1997
2536 : écriture à Sat Nov 29 19:23:49 1997
..
2536 : écriture à Sat Nov 29 19:23:58 1997
2535 : écriture à Sat Nov 29 19:23:59 1997
..
2535 : écriture à Sat Nov 29 19:24:08 1997
2537 : écriture à Sat Nov 29 19:24:09 1997
..
2537 : écriture à Sat Nov 29 19:24:19 1997
[jfmari@localhost ~/Systeme]$

3.5.1 Remarque

L’ordre d’appropriation du fichier par les processus est quelconque : le processus 2536 est passé
avant 2535.
38 Chapitre 3. Le système de gestion de fichiers
version du 12 novembre 1999
39

Chapitre 4

Les processus

4.1 Définition

Un processus est un programme qui s’exécute. Un processus possède un nom qui est un numéro :
le pid. Il s’agit d’un entier du type pid_t déclaré comme synonyme du type int ou unsigned
long int. Un processus incarne — exécute — un programme ; il appartient à un utilisateur ou au
noyau. Un processus possède une priorité et un mode d’exécution. Nous distinguerons deux modes :
le mode utilisateur (ou esclave) de basse priorité et le mode noyau (ou maı̂tre) de plus forte priorité.
Généralement un processus appartient à la personne qui a écrit le programme qui s’exécute, mais ce
n’est pas toujours le cas. Quand vous exécutez la commande ls vous exécutez le programme /bin/ls
qui appartient à root. Regardez le résultat de la commande ls -l !

% ls -l /bin/ls
-rwxr-xr-x 1 root root 29980 Apr 24 1998 /bin/ls
%

Un processus naı̂t, crée d’autres processus — ses fils, attend la fin d’exécution de ceux-ci ou entre
en compétition avec eux pour avoir des ressources du système et enfin meurt. Pendant sa période
de vie, il accède à des variables en mémoire suivant son mode d’exécution. En mode noyau, tout
l’espace adressable lui est ouvert ; en mode utilisateur, il ne peut accéder qu’à ses données privées.

4.2 Les identificateurs de processus

Chaque processus possède un identificateur unique nommé pid. Comme pour les utilisateurs, il
peut être lié à un groupe ; on utilisera alors l’identificateur pgrp. Citons les différentes primitives
permettant de connaı̂tre ces différents identificateurs:

pid_t getpid() /* retourne l’identificateur du processus */

pid_t getpgrp() /* retourne l’identificateur du groupe de processus */


40 Chapitre 4. Les processus

pid_t getppid() /* retourne l’identificateur du père du processus */

pid_t setpgrp() /* positionne l’identificateur du groupe de */


/* processus à la valeur de son pid: crée un */
/* nouveau groupe de processus */

Valeur retournée : nouvel identificateur du groupe de processus.


Exemple :

/* fichier test_idf.c */
#include <stdio.h>

main()
{
printf("je suis le processus %d de père %d et de groupe %d\n",getpid(),
getppid(),getpgrp()) ;
}

Résultat de l’exécution :

bernard> ps
PID TTY TIME COMMAND
6658 ttyp5 0:04 csh
bernard> test_idf
je suis le processus 8262 de père 6658 et de groupe 8262

Notons que le père du processus exécutant test_idf est csh.

4.3 L’utilisateur réel et l’utilisateur effectif


Vous pouvez utiliser le programme /usr/bin/passwd qui appartient à root et bénéficier des
mêmes droits que root afin de changer votre mot de passe en écrivant dans le fichier des mots
de passe. Unix distingue deux utilisateurs de processus : l’utilisateur réel et l’utilisateur effectif.
Quand vous modifiez votre mot de passe, vous restez l’utilisateur réel de la commande passwd, mais
l’utilisateur effectif est devenu root. Un bit particulier du fichier exécutable permet d’effectuer ce
changement temporaire d’identité ; c’est le bit s.

4.3.1 Les primitives setuid et seteuid

getuid et geteuid donnent l’identité de l’utilisateur.


#include <unistd.h>
uid_t getuid(void);
uid_t geteuid(void);
version du 12 novembre 1999
4.3. L’utilisateur réel et l’utilisateur effectif 41

getuid donne le numéro de l’usager réel à qui appartient processus effectuant l’appel ; geteuid
donne le numéro de l’usager effectif associé processus. Ce numéro peut être différent si le bit s du
fichier est positionné. Le type uid_t est généralement un int.

Remarque : la commande chmod u+s fichierExecutable positionne ce bit sur le fichier exécu-
table fichierExecutable.

4.3.2 La primitive getlogin


#include <unistd.h>

char * getlogin ( void );

Cette commande donne le nom – sous forme d’une suite de caractères – de l’utilisateur connecté
au système sur un terminal de contrôle. Chaque appel détruit l’ancien nom.

4.3.3 Les commandes getpwnam et getpwuid

Ces deux commandes explorent le fichier des mots de passe à la recherche de la ligne qui décrit
l’utilisateur passé en paramètre.

#include <pwd.h>
#include <sys/types.h>

struct passwd *getpwnam(const char * name);

struct passwd *getpwuid(uid_t uid);

La structure passwd comporte entre autres les champs suivants :

struct passwd {
char *pw_name; /* nom de login l’utilisateur */
char *pw_passwd; /* son mot de passe crypté */
uid_t pw_uid; /* numéro de l’utilisateur */
gid_t pw_gid; /* numéro du groupe */
char *pw_gecos; /* nom réel */
char *pw_dir; /* répertoire par défaut (home directory) */
char *pw_shell; /* interpréteur de commandes */
};

Pour obtenir les caractéristiques de l’utilisateur effectif, il faut utiliser la commande getpwuid(geteuid()).
42 Chapitre 4. Les processus

4.4 Les principales primitives de gestion de processus

4.4.1 Primitive fork()


#include <unistd.h>
#include <sys/types.h>

pid_t fork() /* création d’un fils */

Valeur retournée : 0 pour le processus fils, et l’identificateur du processus fils pour le père, −1
dans le cas d’épuisement de ressource.
Cette primitive est l’unique appel système permettant de créer un processus. Les processus père
et fils partagent le même code. Le segment de données de l’utilisateur du nouveau processus est
une copie exacte du segment correspondant de l’ancien processus, alors que la copie du segment
de données système diffère par certains attributs (par exemple le pid, le temps d’exécution). Le fils
hérite d’un double de tous les descripteurs de fichiers ouverts du père (si le fils en ferme un, la copie
du père n’est pas changée), par contre les pointeurs de fichier associés sont partagés (si le fils se
déplace dans le fichier, la prochaine manipulation du père se fera à la nouvelle adresse). Cette notion
est importante pour implémenter des tubes.

Exemple introductif
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
void main()
{
pid_t p1 ;
printf("Début de fork\n") ;
p1 = fork() ;
printf("Fin de fork %d\n", p1) ;
}

Résultat de l’exécution :

xures sources 174 % ./a.out


Début de fork
Fin de fork 16099
xures sources 175 % Fin de fork 0

xures sources 175 %

Dans cet exemple, on voit qu’un seul processus exécute l’écriture Début de fork, par contre, on
voit deux écritures Fin de fork suivies de la valeur de retour de la primitive fork(). Il y a bien
apparition d’un processus : le fils, qui ne débute pas son exécution au début du programme mais à
partir de la primitive fork.
version du 12 novembre 1999
4.5. Les primitives exec() 43

4.4.2 Primitive wait()


int wait(int *status) /* en attente de la mort d’un fils */
/* status : statut décrivant la mort du fils */

Valeur retournée : identificateur du processus mort ou −1 en cas d’erreur.


Le code retourné via status indique la raison de la mort du processus, qui est :

– soit l’appel de exit(), et dans ce cas, l’octet de droite de status vaut 0, et l’octet de gauche
est le paramètre passé à exit par le fils ;
– soit la réception d’un signal fatal, et dans ce cas, l’octet de droite est non nul. Les 7 premiers
bits de cet octet contiennent le numéro du signal qui a tué le fils. De plus, si le bit de gauche
de cet octet est 1, un fichier image a été produit (qui correspond au fichier core du répertoire
courant). Ce fichier image est une copie de l’espace mémoire du processus, et est utilisé lors
du débogage.

4.4.3 Primitive exit()


void exit(int status) /* terminaison du processus */
/* status : état de sortie */

Valeur retournée : c’est la seule primitive qui ne retourne jamais.


Tous les descripteurs de fichier ouverts sont fermés. Si le père meurt avant ses fils, le père du
processus fils devient le processus init de pid 1.
Par convention, un code de retour égal à zéro signifie que le processus s’est terminé normalement,
et un code non nul (généralement 1 ou −1) signifie qu’une erreur s’est produite.

4.5 Les primitives exec()

Il s’agit d’une famille de primitives permettant le lancement de l’exécution d’un programme


externe. Il n’y a pas création d’un nouveau processus, mais simplement changement de programme.
Il y a six primitives exec() que l’on peut répartir dans deux groupes : les execl(), pour lesquels le
nombre des arguments du programme lancé est connu, puis les execv() où il ne l’est pas. En outre
toutes ces primitives se distinguent par le type et le nombre de paramètres passés.
Premier groupe d’exec(). Les arguments sont passés sous forme de liste :

int execl(char *path, char *arg0, char *arg1,..., char *argn,NULL)


/* exécute un programme */
/* path : chemin du fichier programme */
/* arg0 : premier argument */
/* ... */
/* argn : (n+1)ième argument */
44 Chapitre 4. Les processus

int execle(char *path,char *arg0,char *arg1,...,char *argn,NULL,char *envp[])


/* envp : pointeur sur l’environnement */

int execlp(char *file,char *arg0,char *arg1,...,char *argn,NULL)

Second groupe d’exec(). Les arguments sont passés sous forme de tableau :

int execv(char *path,char *argv[])


/* argv : pointeur vers le tableau contenant les arguments */

int execve(char *path,char *argv[],char *envp[])

int execvp(char *file,char *argv[])

Il est important de noter que les caractères que le shell expanse (comme ~) ne sont pas interprétés
ici.

4.5.1 Recouvrement

Lors de l’appel d’une primitive exec(), il y a recouvrement du segment d’instructions du pro-


cessus appelant, ce qui implique qu’il n’y a pas de retour d’un exec() réussi (l’adresse de retour a
disparu). Le code du processus appelant est détruit.
/* fichier test_exec.c */

#include <stdio.h>

main()
{
execl("/bin/ls","ls",NULL) ;
printf ("je ne suis pas mort\n") ;
}

Résultat de l’exécution :

bernard> test_exec
fichier1
fichier2
test_exec
test_exec.c
bernard>

On note que la commande ls est réalisée, contrairement à l’appel à printf(), ce qui montre que
le processus ne retourne pas après exec(). D’où l’intérêt de l’utilisation de la primitive fork() :

/* fichier test_exec_fork.c */
version du 12 novembre 1999
4.5. Les primitives exec() 45

#include <stdio.h>

main() {

if ( fork()==0 ) execl( "/bin/ls","ls",NULL) ;


else {
sleep(2) ; /* attend la fin de ls pour exécuter printf() */
printf ("je suis le père et je peux continuer") ;
}
}

Résultat de l’exécution :

bernard> test_exec_fork
fichier1
fichier2
test_exec
test_exec.c
test_exec_fork
test_exec_fork.c
je suis le père et je peux continuer
bernard>

Dans ce cas, le fils meurt après l’exécution de ls, et le père continue à vivre et exécute printf().

4.5.2 Comportement vis-à-vis des fichiers ouverts

Les descripteurs de fichiers ouverts avant l’appel d’un exec() le restent, sauf demande contraire
(par la primitive fcntl()). L’un des effets du recouvrement est l’écrasement du tampon associé
au fichier dans la zone utilisateur, et donc la perte des informations qu’elle contenait. Il est donc
nécessaire de forcer avant l’appel à exec() le vidage de ce tampon au moyen de la fonction fflush().

4.5.3 Remarques

stdout est défini dans stdio.h et correspond à la sortie écran. Notons que les commandes
internes du shell ne seront pas exécutées par exec(). C’est le cas de la commande cd qui, si elle
était exécutée par un processus fils, serait sans effet parce qu’un attribut changé par un processus
fils (ici le répertoire courant) n’est pas remonté au père. Pour suivre un chemin relatif (commençant
sur le répertoire courant) le noyau doit savoir où commencer. Pour cela, il conserve tout simplement
pour chaque processus le numéro d’index du répertoire courant.
Ce programme utilise la primitive chdir(). Cette primitive est utilisée essentiellement dans l’im-
plémentation de la commande shell cd. Elle change le répertoire courant du processus qui l’exécute.

int chdir(char *path) /* change le répertoire courant */


46 Chapitre 4. Les processus

/* path : cha^
ıne spécifiant le nouveau répertoire */

/* fichier test_cd.c */

/* le changement de répertoire n’est valable */


/* que le temps de l’exécution du processus */

#include <stdio.h>

main()
{
chdir("..") ; /* on va au répertoire précédent */

/* on exécute un pwd qui va tuer le processus et */


/* renvoyer le répertoire dans lequel il se trouve */
if (execl("/bin/pwd","pwd",NULL)==-1) {
perror("impossible d’exécuter pwd") ;
exit(-1) ;
}
}

Résultat de l’exécution :
On utilise, une première fois, la commande shell pwd. On obtient :
/users/ens/bernard/Documentation/Exemples/Processus
On lance maintenant test_cd :
/users/ens/bernard/Documentation/Exemples
On relance pwd :
/users/ens/bernard/Documentation/Exemples/Processus
Comme le montre cet exemple, le processus créé par le shell pour l’exécution du programme
test_cd a hérité du répertoire courant. L’exécution de chdir("..") a permis effectivement de re-
monter dans l’arborescence. Cependant, ce changement n’est valable que pour le processus effectuant
chdir(). Aussi, à la mort du processus, on peut constater que le répertoire dans lequel on se trouve
est le même qu’au départ.
version du 12 novembre 1999
47

Troisième partie

Communication
version du 12 novembre 1999
49

Chapitre 5

Communication inter-processus

5.1 Les signaux

5.1.1 Introduction

Un signal est une interruption logicielle qui est envoyée aux processus par le système pour les
informer sur des événements anormaux se déroulant dans leur environnement (violation mémoire,
erreur dans les entrées/sorties). Il permet également aux processus de communiquer entre eux. À
une exception près (SIGKILL), un signal peut être traité de trois manières différentes :

– Il peut être ignoré. Par exemple, le programme peut ignorer les interruptions clavier générées
par l’utilisateur (c’est ce qui se passe lorsqu’un processus est lancé en arrière-plan).
– Il peut être pris en compte. Dans ce cas, à la réception d’un signal, l’exécution d’un processus est
détournée vers une procédure spécifiée par l’utilisateur, puis reprend où elle a été interrompue.
– Son comportement par défaut peut être restitué par un processus après réception de ce signal.

5.1.2 Types de signaux

Les signaux sont identifiés par le système par un nombre entier. Le fichier /usr/include/signal.h
contient la liste des signaux accessibles. Chaque signal est caractérisé par un mnémonique. La liste
des signaux usuels est donnée ci-dessous :

– SIGHUP (1) Coupure : signal émis aux processus associés à un terminal lorsque celui-ci se
déconnecte. Il est aussi émis à chaque processus d’un groupe dont le chef se termine.
– SIGINT (2) Interruption : signal émis aux processus du terminal lorsqu’on frappe la touche
d’interruption (INTR ou CTRL-C) de son clavier.
– SIGQUIT (3)* Abandon : idem avec la touche d’abandon (QUIT ou CTRL-D).
– SIGILL (4)* Instruction illégale : signal émis à la détection d’une instruction illégale, au niveau
matériel (exemple : lorsqu’un processus exécute une instruction flottante alors que l’ordinateur
ne possède pas d’instructions flottantes câblées).
50 Chapitre 5. Communication inter-processus

– SIGTRAP (5)* Piège de traçage : signal émis après chaque instruction en cas de traçage de
processus (utilisation de la primitive ptrace()).
– SIGIOT (6)* Piège d’instruction d’E/S : signal émis en cas de problème matériel.
– SIGEMT (7) Piège d’instruction émulateur : signal émis en cas d’erreur matérielle dépendant
de l’implémentation.
– SIGFPE (8)* signal émis en cas d’erreur de calcul flottant, comme un nombre en virgule
flottante de format illégal. Indique presque toujours une erreur de programmation.
– SIGKILL (9) Destruction : arme absolue pour tuer les processus. Ne peut être ni ignoré, ni
intercepté. (voir SIGTERM pour une mort plus douce).
– SIGBUS (10)* signal émis en cas d’erreur sur le bus.
– SIGSEGV (11)* signal émis en cas de violation de la segmentation : tentative d’accès à une
donnée en dehors du domaine d’adressage du processus actif.
– SIGSYS (12)* Argument incorrect d’un appel système.
– SIGPIPE (13) Écriture sur un conduit non ouvert en lecture.
– SIGALRM (14) Horloge : signal émis quand l’horloge d’un processus s’arrête. L’horloge est mise
en marche par la primitive alarm().
– SIGTERM (15) Terminaison logicielle : signal émis lors de la terminaison normale d’un proces-
sus. Il est également utilisé lors d’un arrêt du système pour mettre fin à tous les processus
actifs.
– SIGUSR1 (16) Premier signal à la disposition de l’utilisateur : utilisé pour la communication
inter-processus.
– SIGUSR2 (17) Deuxième signal à la disposition de l’utilisateur : idem SIGUSR1.
– SIGCLD (18) Mort d’un fils : signal envoyé au père à la terminaison d’un processus fils.
– SIGPWR (19) Réactivation sur panne d’alimentation.

Remarque :
Les signaux repérés par « * » génèrent un fichier core sur le disque, lorsqu’ils ne sont pas traités
correctement.
Pour plus de portabilité, on peut écrire des programmes utilisant des signaux en appliquant les
règles suivantes : éviter les signaux SIGIOT, SIGEMT, SIGBUS et SIGSEGV qui dépendent de l’implé-
mentation. Il est correct de les intercepter pour imprimer un message, mais il ne faut pas essayer de
leur attribuer une quelconque signification.

Signal SIGCLD : gestion des processus zombis

Le signal SIGCLD se comporte différemment des autres. S’il est ignoré, la terminaison d’un pro-
cessus fils, alors que le processus père n’est pas en attente, n’entraı̂nera pas la création de processus
zombi.
Exemple :
Le programme suivant génère un zombi, lorsque le père reçoit à la mort de son fils un signal
version du 12 novembre 1999
5.1. Les signaux 51

SIGCLD.

/* fichier test_sigcld.c */
#include <stdio.h>

main()
{
if (fork() != 0) {
while(1) ; /* boucle exécutée par le père */
}
}

Résultat de l’exécution :

bernard> test_sigcld &


bernard> ps -a
PID TTY TIME COMMAND
8507 0:00 <defunct>

Dans le programme qui suit, le père ignore le signal SIGCLD, et son fils ne devient plus un zombi.

/* fichier test_sigcld2.c */
#include <stdio.h>
#include <signal.h>

main()
{
signal(SIGCLD,SIG_IGN) ;/* ignore le signal SIGCLD */
if (fork() != 0) {
while(1) ;
}
}

Résultat de l’exécution :

bernard> test_sigcld2 &


bernard> ps -a
PID TTY TIME COMMAND

Remarque : la primitive signal() sera détaillée plus loin.


52 Chapitre 5. Communication inter-processus

Signal SIGHUP : gestion des applications longues

Ce signal peut être gênant lorsqu’on désire qu’un processus se poursuive après la fin de la session
de travail (application longue). En effet, si le processus ne traite pas ce signal, il sera interrompu par
le système au moment du délogage. Différentes solutions se présentent :

1. Utiliser la commande shell at, qui permet de lancer l’application à une certaine date, via
un processus du système, appelé démon. Dans ce cas, le processus n’étant attaché à aucun
terminal, le signal SIGHUP sera sans effet.
2. Inclure dans le code de l’application la réception du signal SIGHUP.
3. Lancer le processus en arrière-plan (en effet un processus lancé en arrière-plan traite automa-
tiquement le signal SIGHUP).
4. Lancer l’application sous le contrôle de la commande nohup, qui entraı̂ne un appel à trap, et
redirige la sortie standard sur nohup.out.

5.1.3 Traitement des signaux

Émission d’un signal

Primitive kill()
#include <signal.h>

int kill(pid_t pid,int sig) /* émission d’un signal */


/* pid : identificateur du processus ou du groupe destinataire */
/* sig : numéro du signal */

Valeur retournée : 0 si le signal a été envoyé, −1 sinon.


Cette primitive émet à destination du processus de numéro pid le signal de numéro sig. De plus,
si l’entier sig est nul, aucun signal n’est envoyé, et la valeur de retour permet de savoir si le nombre
pid est un numéro de processus ou non.
Utilisation du paramètre pid :

– Si pid > 0 : pid désigne alors le processus d’identificateur pid.


– Si pid = 0 : le signal est envoyé à tous les processus du même groupe que l’émetteur (cette
possibilité est souvent utilisée avec la commande shell

kill -9 0

pour tuer tous les processus en arrière-plan sans avoir à indiquer leurs identificateurs de pro-
cessus).
– Si pid = −1 :
si le processus appartient au super-utilisateur, le signal est envoyé à tous les processus, sauf
aux processus système et au processus qui envoie le signal. Sinon, le signal est envoyé à tous les
processus dont l’identificateur d’utilisateur réel est égal à l’identificateur d’utilisateur effectif du
version du 12 novembre 1999
5.1. Les signaux 53

processus qui envoie le signal (c’est un moyen de tuer tous les processus dont on est propriétaire,
indépendamment du groupe de processus).
– Si pid < −1 : le signal est envoyé à tous les processus dont l’identificateur de groupe de
processus (pgid) est égal à la valeur absolue de pid.

Notons que la primitive kill() est le plus souvent exécutée via la commande shell kill.

Primitive alarm()
#include <unistd.h>

unsigned alarm(unsigned secs) /* envoi d’un signal SIGALRM */


/* secs : nombre de secondes */

Valeur retournée : temps restant dans l’horloge.


Cette primitive envoie un signal SIGALRM au processus appelant après un laps de temps secs (en
secondes) passé en argument, puis réinitialise l’horloge d’alarme. À l’appel de la primitive, l’horloge
est initialisée à secs secondes, et est décrémentée jusqu’à 0. Si le paramètre secs est nul, toute
requête est annulée. Cette primitive peut être utilisée, par exemple, pour forcer la lecture au clavier
dans un délai donné. Le traitement du signal doit être prévu, sinon le processus est tué.
Exemple 1 : fonctionnement de alarm()
Exemple 2 : test avec deux alarm()

Réception des signaux : primitive signal()

#include <signal.h>

void (*signal (int sig, void (*fcn)(int)))(int) /* réception d’un signal */


/* sig : numéro du signal */
/* (*fcn) : action après réception */

Valeur retournée : adresse de la fonction spécifiant le comportement du processus vis-à-vis du


signal considéré, −1 sinon.
Cette primitive intercepte le signal de numéro sig. Le second argument est un pointeur sur une
fonction qui peut prendre une des trois valeurs suivantes :

1. SIGDFL : ceci choisit l’action par défaut pour le signal. La réception d’un signal par un processus
entraı̂ne alors la terminaison de ce processus, sauf pour SIGLD et SIGPWR, qui sont ignorés par
défaut. Dans le cas de certains signaux, il y a création d’un fichier image core sur disque.
2. SIGIGN : ceci indique que le signal doit être ignoré : le processus est immunisé. On rappelle que
le signal SIGKILL ne peut être ignoré.
3. Un pointeur sur une fonction (nom de la fonction) : ceci implique la capture du signal. La
fonction est appelée quand le signal arrive, et après son exécution, le traitement du processus
54 Chapitre 5. Communication inter-processus

reprend où il a été interrompu. On ne peut procéder à un déroutement sur la réception d’un
signal SIGKILL puisque ce signal n’est pas interceptable.

Nous voyons donc qu’il est possible de modifier le comportement d’un processus à l’arrivée d’un
signal donné. C’est ce qui se passe pour un certain nombre de processus standard : le shell, par
exemple, à la réception d’un signal SIGINT affiche le prompt (et n’est pas interrompu).

Primitive pause()
#include <unistd.h>
void pause(void) /* attente d’un signal quelconque */

Cette primitive correspond à de l’attente pure. Elle ne fait rien, et n’attend rien de particulier.
Cependant, puisque l’arrivée d’un signal interrompt toute primitive bloquée, on peut tout aussi bien
dire que pause() attend un signal. On observe alors le comportement de retour classique d’une
primitive bloquée, c’est à dire le positionnement de errno à EINTR. Notons que le plus souvent, le
signal que pause() attend est l’horloge d’alarm().
Exemple : test de la fonction pause()
Exemples
Héritage des signaux par fork()
Les processus fils recevant l’image mémoire du père, héritent de son comportement vis-à-vis des
signaux.

5.1.4 Communication entre processus

Il s’agit d’un exemple simple utilisant les primitives d’émission et de réception de signaux, afin
de faire communiquer deux processus entre eux. En outre, l’exécution de ce programme permet de
s’assurer que le processus exécutant la routine de déroutement est bien celui qui reçoit le signal.

/* fichier test_kill_signal.c */

/*
* communication simple entre deux processus
* au moyen des signaux
*/

#include <errno.h>
#include <signal.h>

void it_fils()
{
printf("- oui, et je le ferai moi m^
eme... ARGHH...") ;
kill (getpid(),SIGINT) ;
}
version du 12 novembre 1999
5.1. Les signaux 55

void fils()
{
signal(SIGUSR1,it_fils) ;
printf("- allo maman bobo, comment tu m’as fait j’suis pas beau!!!") ;
while(1) ;
}

main()
{
int ppid, pid ;

if ((pid=fork())==0) fils() ;
else {
sleep(3) ;
printf("- mon fils, veux-tu rejoindre le domaine des morts?") ;
kill (pid,SIGUSR1) ;
}
}

Résultat de l’exécution :

bernard> test_kill_signal
- allo maman bobo, comment tu m’as fait j’suis pas beau!!!
- mon fils, veux-tu rejoindre le domaine des morts?
- oui, et je le ferai moi m^
eme... ARGHH...

Histoire courte :
Un processus crée un fils qui ne semble pas heureux de vivre. Il lui envoie alors un signal SIGUSR1.
À la réception de ce dernier, le fils désespéré décide de s’envoyer un signal SIGINT, pour se suicider.

Contrôle de l’avancement d’une application

Tous ceux qui ont lancé des programmes de simulation ou de calcul numérique particulièrement
longs ont sûrement désiré connaı̂tre l’état d’avancement de l’application pendant son exécution. Ceci
est parfaitement réalisable grâce à la commande shell kill, en envoyant au processus concerné un
signal ; le processus peut alors à la réception d’un tel signal, afficher les données désirées. Voici un
exemple qui permet de résoudre le problème :

/* fichier surveillance.c */

#include <errno.h>
#include <signal.h>
#include <time.h>
56 Chapitre 5. Communication inter-processus

/* les variables à éditer doivent e


^tre globales */
long somme ;

void it_surveillance()
{
time_t t_date ;
signal(SIGUSR1, it_surveillance) ;/* réactive SIGUSR1 */
time(&t_date) ;
printf("date du test : %s\n",ctime(&t_date));
printf("valeur de la somme : %ld\n",somme);
}

main()
{
signal(SIGUSR1,it_surveillance) ;
printf ("Envoyez le signal\n");
while(1) somme++ ;
}

Exécution :
Si on lance le programme en tâche de fond, il suffira de taper sous le shell la commande :

kill -16 pid

pour obtenir l’affichage des variables de contrôle.

5.1.5 Conclusion

À l’exception de SIGCLD, les signaux qui arrivent ne sont pas mémorisés. Ils sont ignorés, ils
mettent fin aux processus, ou bien ils sont interceptés. C’est la raison principale qui rend les signaux
inadaptés pour la communication inter-processus : un message sous forme de signal peut être perdu
s’il est reçu à un moment où ce type de signal est temporairement ignoré. Lorsqu’un signal a été capté
par un processus, le processus réadopte son comportement par défaut vis-à-vis de ce signal. Donc, si
l’on veut pouvoir capter un même signal plusieurs fois, il convient de redéfinir le comportement du
processus par la primitive signal(). Généralement, on réarme l’interception du signal le plus tôt
possible (première instruction effectuée dans la procédure de traitement du déroutement).
Un autre problème est que les signaux sont plutôt brutaux : à leur arrivée, ils interrompent
le travail en cours. Par exemple, la réception d’un signal pendant que le processus est en attente
d’un événement (ce qui peut arriver lors de l’utilisation des primitives open(), read(), write(),
msgrcv(), pause(), wait()), lance l’exécution de la fonction de déroutement ; à son retour, la primi-
tive interrompue renvoie un message d’erreur sans s’être exécutée totalement (errno est positionné
à EINTR). Par exemple, lorsqu’un processus père qui intercepte les signaux d’interruption et d’aban-
don, est en cours d’attente de la terminaison d’un fils, il est possible qu’un signal d’interruption
ou d’abandon éjecte le père hors du wait() avant que le fils n’ait terminé (d’où la création d’un
version du 12 novembre 1999
5.2. Les pipes ou tubes de communication 57

<defunct>). Une méthode pour remédier à ce type de problème, est d’ignorer certains signaux avant
l’appel de telles primitives (ce qui pose alors d’autres problèmes, puisque ces signaux ne seront pas
traités).

5.2 Les pipes ou tubes de communication

5.2.1 Introduction

Les pipes (ou conduits, tubes) constituent un mécanisme fondamental de communication unidi-
rectionnelle entre processus. Ce sont des files de caractères (FIFO : First In First Out). Les informa-
tions y sont introduites à une extrémité et en sont extraites à l’autre. Les conduits sont implémentés
comme les fichiers (ils possèdent un i-node), même s’ils n’ont pas de nom dans le système. La tech-
nique du conduit est fréquemment mise en œuvre dans le shell pour rediriger la sortie standard d’une
commande sur l’entrée d’une autre (symbole « | »).

5.2.2 Particularités des tubes

Comme ils n’ont pas de nom, les tubes de communication sont temporaires, ils n’existent que
le temps d’exécution du processus qui les crée. De plus, leur création doit se faire à partir d’une
primitive spéciale : pipe(). Plusieurs processus peuvent écrire et lire sur un même tube, mais aucun
mécanisme ne permet de différencier les informations à la sortie. La capacité est limitée (en général
à 4096 octets). Si on continue à écrire dans le conduit alors qu’il est plein, on se place en situation
de blocage (deadlock ). Les processus communiquant au travers de tubes doivent avoir un lien de
parenté, et les tubes les reliant doivent avoir été ouverts avant la création des fils (voir le passage
des descripteurs de fichiers ouverts à l’exécution de fork(), en fork, cf. ??). Il est impossible de se
déplacer à l’intérieur d’un tube. Afin de faire dialoguer deux processus par tube, il est nécessaire
d’ouvrir deux conduits, et de les utiliser l’un dans le sens contraire de l’autre.

5.2.3 Création d’un conduit

Primitive pipe()

int pipe(int p_desc[2]) /* crée un tube */


/* p_desc[2] : descripteurs d’écriture et de lecture */

Valeur retournée : 0 si la création s’est bien passée, et −1 sinon.

– p_desc[0] contient le numéro du descripteur par lequel on peut lire dans le tube.
– p_desc[1] contient le numéro du descripteur par lequel on peut écrire dans le tube.

Ainsi, l’écriture dans p_desc[1] introduit les données dans le conduit, la lecture dans p_desc[0]
les en extrait.
58 Chapitre 5. Communication inter-processus

Sécurités apportées par le système

Dans le cas où tous les descripteurs associés aux processus susceptibles de lire dans un conduit
sont fermés, un processus qui tente d’y écrire reçoit le signal SIGPIPE, et donc est interrompu s’il ne
traite pas ce signal.
Si un tube est vide, ou si tous les descripteurs susceptibles d’y écrire sont fermés, la primitive
read() renvoie la valeur 0 (fin de fichier atteinte).
Exemple 1 : émission du signal SIGPIPE

/* fichier test_pipe_sig.c */

/*
* teste l’écriture dans un conduit fermé en lecture
*/

#include <errno.h>
#include <signal.h>

void it_sigpipe()
{
printf("Réception du signal SIGPIPE\n") ;
}

main()
{
int p_desc[2] ;
signal(SIGPIPE,it_sigpipe) ;
pipe(p_desc) ; /* création du tube */
close(p_desc[0]) ; /* fermeture du conduit en lecture */
if (write(p_desc[1],"0",1) == -1)
perror("Erreur write") ;
}

Résultat de l’exécution :

bernard> test_pipe_sig
Réception du signal SIGPIPE
Erreur write: Broken pipe

Dans cet exemple on essaie d’écrire dans le tube alors qu’on vient de le fermer en lecture ; le
signal SIGPIPE est émis, et le programme est dérouté. Au retour, la primitive write() renvoie −1
et perror affiche le message d’erreur.

/* fichier test_pipe_read.c */
version du 12 novembre 1999
5.2. Les pipes ou tubes de communication 59

/*
* teste la lecture dans un tube fermé en écriture
*/

#include <errno.h>
#include <signal.h>

main()
{
int i, ret, p_desc[2] ;
char c ;
pipe(p_desc) ; /* création du tube */
write(p_desc[1],"AB",2) ; /* écriture de deux lettres dans le tube */
close(p_desc[1]) ; /* fermeture du tube en écriture */

/* tentative de lecture dans le tube */


for (i=1; i<=3,i++) {
ret = read(p_desc[0],&c,1) ;
if (ret = 1)
printf("valeur lue: %c\n",c) ;
else
perror("impossible de lire dans le tube\n") ;
}
}

Résultat de l’exécution :

bernard> test_pipe_read
valeur lue:A
valeur lue:B
impossible de lire dans le tube: Not a typewriter

Cet exemple montre que la lecture dans le tube est possible même si celui-ci est fermé en écriture.
Bien sûr, lorsque le tube est vide, read() renvoie la valeur 0.

Application des primitives d’entrée/sortie aux tubes

Il est possible d’utiliser les fonctions de la bibliothèque standard sur un tube ayant été ouvert,
en lui associant, au moyen de la fonction fdopen(), un pointeur sur un objet de structure FILE.

write() : les données sont écrites dans le conduit dans leur ordre d’arrivée. Lorsque le conduit
est plein, write() se bloque en attendant qu’une place se libère. On peut éviter ce blocage en
positionnant le drapeau O_NDELAY.
read() : les données sont lues dans le conduit dans leur ordre d’arrivée. Une fois extraites, les
données ne peuvent pas être relues ou restituées au conduit.
60 Chapitre 5. Communication inter-processus

close() : cette fonction a plus d’importance sur un conduit que sur un fichier. Non seulement
elle libère le descripteur de fichier, mais lorsque le descripteur de fichier d’écriture est fermé,
elle agit comme une fin de fichier pour le lecteur.
dup() : cette primitive combinée avec la primitive pipe() permet d’implémenter des com-
mandes reliées par des tubes, en redirigeant la sortie standard d’une commande vers l’entrée
standard d’une autre.

Exemples globaux

Implémentation d’une commande pipée


Cet exemple permet d’observer comment combiner les primitives pipe() et dup() afin de réaliser
des commandes shell du type ls|wc|wc. Notons qu’il est nécessaire de fermer les descripteurs non
utilisés par les processus exécutant la routine.

Communication entre père et fils grâce à un tube

Exemple 1 : envoi d’un message à l’utilisateur


Exemple 2 : mise en évidence de l’héritage des descripteurs lors d’un fork()

5.2.4 Conclusion

Il est possible pour un processus d’utiliser lui-même un tube à la fois en lecture et en écriture ;
ce tube n’a plus alors son rôle de communication mais devient une implémentation de la structure
de file. Cela permet, sur certaines machines, de dépasser la limite de taille de zone de données.
Le mécanisme de communication par tubes présente un certain nombre d’inconvénients comme la
non-rémanence de l’information dans le système et la limitation de la classe de processus pouvant
s’échanger des informations.

5.3 Les messages

5.3.1 Introduction

La communication inter-processus (en anglais inter-process communication, IPC) par messages


s’effectue par échange de données, stockées dans le noyau, sous forme de files. Chaque processus peut
émettre des messages et en recevoir.

5.3.2 Principe

De même que pour les sémaphores et pour la mémoire partagée, une file de messages est associée à
une clé qui représente son nom numérique. Cette clé est utilisée pour définir et obtenir l’identificateur
version du 12 novembre 1999
5.3. Les messages 61

de la file de messages, noté msqid. Celui-ci est fourni par le système au processus qui donne la clé
associée.
Un processus qui désire envoyer un message doit obtenir l’identificateur de la file msqid, grâce à
la primitive msgget(). Il utilise alors la primitive msgsnd() pour obtenir le stockage de son message
(auquel est associé un type), dans une file.
De la même manière, un processus qui désire lire un message doit se procurer l’identificateur de
la file par l’intermédiaire de la primitive msgget(), avant de lire le message en utilisant la primitive
msgrcv().

Structure associée aux messages : msqid_ds

Comme nous l’avons vu, chaque file de messages est associée à un identificateur msqid. On lui
associe également la structure msqid_ds, définie dans le fichier sys/msg.h :

struct msqid_ds {
/* une pour chaque file dans le système */
struct ipc_perm msg_perm; /* opérations permises */
struct msg *msg_first; /* pointeur sur le premier message
* d’une file */
struct msg *msg_last; /* pointeur sur le dernier message
* d’une file */
ushort msg_cbytes; /* nombre courant d’octets de la file */
ushort msg_qnum; /* nombre de message dans la file */
ushort msg_qbytes; /* nombre maximal d’octets de la file */
ushort msg_lspid; /* pid du dernier processus écrivain */
ushort msg_lrpid; /* pid du dernier processus lecteur */
time_t msg_stime; /* date de la dernière écriture */
time_t msg_rtime; /* date de la dernière lecture */
time_t msg_ctime; /* date du dernier changement */
};

Primitive msgget()

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget ( key_t key, int msgflg )

Valeur retournée : l’identificateur msqid de la file, ou −1 en cas d’erreur.


Cette primitive est utilisée pour créer une nouvelle file de messages, ou pour obtenir l’identifi-
cateur de file msqid d’une file de messages existante. Elle prend deux arguments. Le premier (key)
est une clé indiquant le nom numérique de la file de messages. Le second (msgflg) est un drapeau
spécifiant les droits d’accès sur la file.
62 Chapitre 5. Communication inter-processus

Les valeurs possibles pour key

– IPC_PRIVATE (= 0) : la file de messages n’a alors pas de clé d’accès, et seul le processus
propriétaire, ou le créateur ont accès à cette file.
– la valeur désirée de la clé de la file de messages.

Les valeurs possibles pour msgflg On remarque que msgflg est semblable à semflg, utilisé
pour les sémaphores, et à shmflg, utilisé pour la mémoire partagée. Ce drapeau est la combinaison
de différentes constantes prédéfinies, permettant d’établir les droits d’accès, et les commandes de
contrôle (la combinaison est effectuée de manière classique à l’aide de « OU »).
Les constantes prédéfinies dans sys/msg.h, et sys/ipc.h sont :

#define MSG_R 400 /* permission en lecture pour l’utilisateur */


#define MSG_W 200 /* permission en écriture pour l’utilisateur */

#define IPC_CREAT 0001000 /* création d’une file de messages */


#define IPC_EXCL 0002000 /* associé au bit IPC_CREAT provoque */
/* un échec si le fichier existe déjà */

Comment créer une file de messages

La création d’une file de messages est semblable à la création d’un ensemble de sémaphores ou
d’un segment de mémoire partagée. Il faut pour cela respecter les points suivants :

– key doit contenir la valeur identifiant la file.


– msgflg doit contenir les droits d’accès désirés et la constante IPC_CREAT.
– si l’on désire tester l’existence d’une file correspondant à la clé désirée, il faut rajouter à msgflg
la constante IPC_EXCL. L’appel msgget() échouera dans le cas d’existence d’une telle file.

Notons que lors de la création d’une file de messages, un certain nombre de champs de l’objet de
structure msqid_ds sont initialisés (propriétaire, modes d’accès).

Exemple d’utilisation de msgget()

Ce programme crée une file de messages associée à la clé 123, et vérifie le contenu des structures
du système propres à cette file.

/* fichier test_msgget.c */

/*
* exemple d’utilisation de msgget()
*/

#include <errno.h>
version du 12 novembre 1999
5.3. Les messages 63

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define CLE 123

main()
{
int msqid ; /* identificateur de la file de messages */
char *path = "nom_de_fichier_existant" ;

/*
* création d’une file de messages en lecture et écriture
* si elle n’existe pas
*/
if (( msqid = msgget(ftok(path,(key_t)CLE),
IPC_CREAT|IPC_EXCL|MSG_R|MSG_W)) == -1) {
perror("Échec de msgget") ;
exit(1) ;
}
printf("identificateur de la file: %d\n",msqid);
printf("cette file est identifiée par la clé unique : %ld\n",
ftok(path,(key_t)CLE)) ;
}

Résultat de l’exécution :

identificateur de la file: 700


cette file est identifiée par la clé unique : 2064941749

Primitive msgctl()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl ( int msqid, int cmd, struct msqid_ds *buf )

Valeur retournée : 0 en cas de réussite, −1 sinon.


L’appel système msgctl() est utilisé pour examiner et modifier des attributs d’une file de mes-
sages existante. Il prend trois arguments : un identificateur de file de messages (msqid), un paramètre
de commande (cmd), et un pointeur vers une structure de type msqid_ds (buf).
Les différentes commandes possibles sont définies dans le fichier sys/ipc.h :

IPC_RMID (0) : la file de messages identifiée par msqid est détruite. Seul le super-utilisateur
ou un processus ayant pour numéro d’utilisateur msg_perm.uid (le propriétaire) peut détruire
une file. Toutes les opérations en cours sur cette file échoueront et les processus en attente de
lecture ou d’écriture sont réveillés.
64 Chapitre 5. Communication inter-processus

IPC_SET (1) : donne à l’identificateur de groupe, à l’identificateur d’utilisateur, aux droits


d’accès de la file de messages, et au nombre total de caractères des textes, les valeurs contenues
dans le champ msg_perm de la structure pointée par buf. On met également à jour l’heure de
modification.
IPC_STAT (2) : la structure associée à msqid est rangée à l’adresse pointée par buf.

Primitive msgsnd()

Le profil sous HP-UX est :

#include <sys/msg.h>
int msgsnd( int msqid, const void *msgp, size_t msgsz, int msgflg );

Sous linux, c’est :

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd ( int msqid, struct msgbuf *msgp, int msgsz, int msgflg );

Valeur retournée : 0 si le message est placé dans la file, −1 en cas d’erreur.


Cette primitive permet de placer un message dans une file.
Elle prend quatre arguments : l’identificateur de la file (msqid), un pointeur (msgp) vers la struc-
ture de type msgbuf qui contient le message, un entier (msgsz) indiquant la taille (en octets) de
la partie texte du message, et un drapeau (msgflg) agissant sur le mode d’exécution de l’envoi du
message.
La primitive msgsnd() met à jour la structure msqid_ds :

– incrémentation du nombre de messages de la file (msg_qnum)


– modification du numéro du dernier écrivain (msg_lspid)
– modification de la date de dernière écriture (msg_stime)

La structure msgbuf

La structure msgbuf décrit la structure du message proprement dit. Elle est définie dans le fichier
/usr/include/sys/msg.h de la façon suivante :

struct msgbuf {
long mtype ; /* type du message */
char mtext[1] ; /* texte du message (de longueur msgsz) */
}
version du 12 novembre 1999
5.3. Les messages 65

mtype est un entier positif. On peut s’en servir pour effectuer en lecture une sélection parmi les
éléments présents dans la file. Il est impératif que le champ mtype soit au début de la structure.
mtext est le message envoyé (tableau d’octets).
Il faut faire très attention, car la structure msgbuf est en fait une structure à taille variable. Une
structure de ce type là ne doit pas être passée en paramètre autrement qu’en passant un pointeur sur
la structure. Bien que mtext semble être un tableau d’un caractère (ce qui n’a en fait pas vraiment
d’utilité, car on aurait pu mettre un unique caractère à la place), il s’agit en réalité d’un pointeur
vers un tableau de msgsz caractères.
Cette déclaration d’un tableau d’un caractère est un hack du C, qui fera d’ailleurs partie, sous
une forme légèrement différente, du standard C9X. Nous citons en figure 5.1 un message du forum
comp.std.c expliquant le problème.
On définira en général une autre structure (que l’on utilisera à la place de msgbuf), de la façon
suivante :

#define MSG_SIZE_TEXT 256

struct msgtext {
long mtype ; /* type du message */
char mtexte[MSG_SIZE_TEXT] ; /* texte du message */
} ;

On pourra régler, suivant ses besoins, la taille maximum des messages échangés avec MSG_SIZE_TEXT.
Il faut noter que le champ mtype est bien placé au début de la structure.

Drapeau msgflg

Pour ce qui concerne le drapeau msgflg, il est utilisé comme suit : ce paramètre est mis à 0
pour provoquer le blocage de msgsnd() lorsque la file de messages est pleine. Il est positionné à
IPC_NOWAIT pour retourner immédiatement de msgsnd() avec une erreur lorsque la file est pleine.
Cet indicateur agit comme O_NDELAY sur les conduits nommés.

Primitive msgrcv()

Sous HP-UX, le profil est le suivant :

#include <sys/msg.h>
int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

Sous linux, le profil est :

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
66 Chapitre 5. Communication inter-processus

From David R Tribble <[email protected]>


Date Thu, 21 Jan 1999 18:08:55 -0600
Newsgroups comp.std.c

Norman Diamond wrote:


>...
> The structure hack I’m refering to, which was surely in use even
> before 6th edition, was to declare an array probably with just one
> element as the last member of a structure type, malloc whatever size
> was actually needed, and index the array within the bounds of the
> object actually allocated. The wording of C89 implies that this would
> still be possible, but the committee ruled differently in response to
> a defect report.

For example:

struct Hack
{
int size;
...
float f[1]; // 1..N elements
};

void foo(int n)
{
struct Hack * hp;
int i;
hp = malloc(sizeof(*hp) + (n-1)*sizeof(hp->f[0]));
// hp->fp[] has room for n elements
assert(hp != NULL);
hp->size = n;
for (i = 0; i < n; i++)
hp->f[i] = ...; // [A]
...
}

"Struct hack" was also the term used by the ISO committee for
this exact thing.

The trouble with this is that [A] invokes undefined behavior.


It may very well work (and does) on many implementations, but it
won’t work if the compiler does array bounds checking or somehow
optimizes away the array expression because it knows the size of
hp->f[]. These were some of the reasons why flexible arrays
were invented in C9X, so that the struct hack could be done in a
portable way blessed by the committee.

The new way is not very different from the old code:

struct Hack
{
int size;
...
float f[]; // 1..N elements
};

...the rest is the same as above...

Fig. 5.1 – Un hack du C


version du 12 novembre 1999
5.3. Les messages 67

int msgrcv ( int msqid, struct msgbuf *msgp, int msgsz,


long msgtyp, int msgflg )

Valeur retournée : nombre d’octets du message extrait, ou −1 en cas d’erreur.

Cette primitive permet de lire un message dans une file.

Elle prend cinq arguments : l’identificateur de la file (msqid), un pointeur (msgp) vers la structure
de type msgbuf qui contiendra le message, un entier (msgsz) indiquant la taille maximum du message
à recevoir, un entier (msgtyp) indiquant quel message on désire recevoir, et un drapeau (msgflg)
agissant sur le mode d’exécution de l’envoi du message.

La primitive range le message lu dans une structure pointée par msgp qui contient les éléments
suivants :

long mtype ; /* type du message */


char mtext[] ; /* texte du message */

La taille du champ mtext est fixée selon les besoins (voir msgsnd() pour plus de détail).

Pour ce qui concerne le paramètre msgflg :

– si IPC_NOWAIT est positionné, l’appel à msgrcv() retourne immédiatement avec une erreur,
lorsque la file ne contient pas de message de type désiré.
– si IPC_NOWAIT n’est pas positionné, il y a blocage du processus appelant msgrcv() jusqu’à ce
qu’il y ait un message du type msgtyp dans la file.
– si MSG_NOERROR est positionné, le message est tronqué à msgsz octets, la partie tronquée est
perdue, et aucune indication de la troncature n’est donnée au processus.
– si MSG_NOERROR n’est pas positionné, le message n’est pas lu, et msgrcv() renvoie un code
d’erreur.

Le paramètre msgtyp indique quel message on désire recevoir :

– Si msgtyp = 0, on reçoit le premier message de la file, c’est-à-dire le plus ancien.


– Si msgtyp > 0, on reçoit le premier message ayant pour type une valeur égale à msgtyp.
– Si msgtyp < 0, on reçoit le message dont le type a une valeur t qui vérifie :
– t est minimum
– t 6 |msgtyp|
68 Chapitre 5. Communication inter-processus

Par exemple, si l’on considère trois messages ayant pour types 100, 200, et 300, le tableau suivant
indique le type du message retourné pour différentes valeurs de msgtyp :

msgtyp type du message retourné


0 100
100 100
200 200
300 300
−100 100
−200 100
−300 100

5.4 Les sémaphores

5.4.1 Introduction

Les sémaphores sont des objets d’IPC utilisés pour synchroniser des processus entre eux. Ils
constituent aussi une solution pour résoudre le problème d’exclusion mutuelle, et permettent en
particulier de régler les conflits d’accès concurrents de processus. L’implémentation qui en est faite
ressemble à une synchronisation « sleep/wakeup ».

5.4.2 Principe

Tout comme pour les files de messages, pour créer un sémaphore, un utilisateur doit lui associer
une clé. Le système lui renvoie un identificateur de sémaphore auquel sont attachés n sémaphores
(ensemble de sémaphores), numérotés de 0 à n − 1. Pour spécifier un sémaphore, l’utilisateur devra
alors indiquer l’identificateur de sémaphore et le numéro de sémaphore. À chaque sémaphore est
associée une valeur, toujours positive, que l’utilisateur va pouvoir incrémenter ou décrémenter du
nombre qu’il souhaite. Soit N la valeur initiale, et n le nombre d’incrément de l’utilisateur :

– si n > 0 alors l’utilisateur augmente la valeur du sémaphore de n et continue en séquence.


– si n < 0

– si N + n > 0 alors l’utilisateur diminue la valeur du sémaphore de |n| et continue en


séquence.
– si N + n < 0 alors le processus de l’utilisateur se bloque, en attendant que N + n > 0.

– si n = 0
– si N = 0 alors l’utilisateur continue en séquence.
– si N 6= 0 alors le processus de l’utilisateur se bloque en attendant que N = 0.

Nous verrons que le blocage des processus est paramétrable, c’est-à-dire que l’on peut spécifier au
système de ne pas bloquer les processus mais de simplement renvoyer un code d’erreur et continuer
version du 12 novembre 1999
5.4. Les sémaphores 69

en séquence. D’autre part, à chaque identificateur de sémaphore sont associés des droits d’accès.
Ces droits d’accès sont nécessaires pour effectuer des opérations sur les valeurs des sémaphores. Ces
droits sont inopérants pour les deux manipulations suivantes :

– la destruction d’un identificateur de sémaphore


– la modification des droits d’accès.

Pour cela, il faudra être soit super-utilisateur, soit créateur, soit propriétaire du sémaphore.
Notons que le bon fonctionnement des mécanismes suppose que les opérations effectuées sur les
sémaphores sont indivisibles (non interruptibles).

Structures utiles : semid_ds, __sem, sembuf

Chaque ensemble de sémaphores du système est associé à plusieurs structures. La donnée de


ces structures n’est pas superflue, car elle permet de comprendre ce que provoquent les primitives
semget(), semctl(), semop(), au niveau du système. Ces structures sont contenues dans sys/sem.h.
Elles varient beaucoup d’une architecture à l’autre. Nous donnons ci-dessous celles sous HP-UX :

struct semid_ds { /* une par ensemble de sémaphores dans le système */


struct ipc_perm sem_perm; /* opérations permises */
struct __sem *sem_base; /* pointeur sur le premier */
/* sémaphore de l’ensemble */
time_t sem_otime; /* date de la dernière opération semop()*/
time_t sem_ctime; /* date de la dernière modification */
unsigned short sem_nsems; /* nombre de sémaphores dans l’ensemble */
...
};

struct __sem { /* une pour chaque sémaphore dans le système */


unsigned short int semval; /* valeur du sémaphore */
unsigned short int sempid; /* pid du dernier processus ayant effectué */
/* une opération sur le sémaphore */
unsigned short int semncnt; /* nombre de processus en attente que semval */
/* soit supérieur à la valeur courante */
unsigned short int semzcnt; /* nombre de processus en attente que */
/* semval devienne nulle */
};

struct sembuf {
unsigned short int sem_num; /* numéro du sémaphore */
short sem_op; /* opération à réaliser */
short sem_flg; /* indicateur d’opération (drapeaux) */
};
70 Chapitre 5. Communication inter-processus

Primitive semget()

Sous HP-UX :

#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

Sous linux :

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

Valeur retournée : l’identificateur de sémaphore semid, ou −1 en cas d’erreur.


L’appel système semget() est utilisé pour créer un nouvel ensemble de sémaphores, ou pour
obtenir l’identificateur de sémaphore d’un ensemble existant. Le premier argument key est une
clé indiquant le nom numérique de l’ensemble de sémaphores. Le second nsems indique le nombre
de sémaphores de l’ensemble. Le dernier semflg est un drapeau spécifiant les droits d’accès sur
l’ensemble de sémaphores.

Les valeurs possibles pour key

– IPC_PRIVATE (= 0) : l’ensemble n’a alors pas de clé d’accès, et seul le processus propriétaire,
ou le créateur a accès à l’ensemble de sémaphores.
– la valeur désirée de la clé de l’ensemble de sémaphores.

Les valeurs possibles pour l’option semflg Ce drapeau est en fait la combinaison de différentes
constantes prédéfinies, permettant d’établir les droits d’accès, et les commandes de contrôle (la
combinaison est effectuée de manière classique à l’aide de « OU »). Notons la similitude existant
entre les droits d’accès utilisés pour les sémaphores, et les droits d’accès aux fichiers UNIX : on
retrouve la notion d’autorisation en lecture ou écriture aux attributs utilisateur/groupe/autres. Le
nombre octal défini en octal pourra être utilisé (en forçant à 0 les bits de droite d’exécution 3, 6 et
9). Les constantes prédéfinies dans sys/sem.h, et sys/ipc.h sont :

#define SEM_A 0200 /* permission de modification (a pour alter)*/


#define SEM_R 0400 /* permission en lecture */
#define IPC_CREAT 0001000 /* création d’un ensemble de sémaphores */
#define IPC_EXCL 0002000 /* associé au bit IPC_CREAT provoque un */
/* échec si le fichier existe déjà */

Pour créer un ensemble de sémaphores, les points suivants doivent être respectés :

– key doit contenir la valeur identifiant l’ensemble (différent de IPC_PRIVATE= 0).


– semflg doit contenir les droits d’accès désirés, et la constante IPC_CREAT.
version du 12 novembre 1999
5.4. Les sémaphores 71

– si l’on désire tester l’existence d’un ensemble correspondant à la clé key désirée, il faut rajouter
à semflg la constante IPC_EXCL. L’appel à semget() échouera dans le cas d’existence d’un tel
ensemble.

Notons que lors de la création d’un ensemble de sémaphores, un certain nombre de champs de
l’objet de structure semid_ds sont initialisés (propriétaire, modes d’accès).
Exemple : ce programme crée un ensemble de quatre sémaphores associé à la clé 123.

/* fichier test_semget.c */

/* exemple d’utilisation de semget() */

#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define CLE 123

main()
{
int semid ; /* identificateur des sémaphores */
char *path = "nom_de_fichier_existant" ;

/*
* allocation de quatre sémaphores
*/
if (( semid = semget(ftok(path,(key_t)CLE), 4,
IPC_CREAT|IPC_EXCL|SEM_R|SEM_A)) == -1) {
perror("Échec de semget") ;
exit(1) ;
}
printf(" le semid de l’ensemble de sémaphore est : %d\n ",semid) ;
printf(" cet ensemble est identifié par la clé unique : %d\n ",
ftok(path,(key_t)CLE)) ;
}

Résultat de l’exécution :

bernard/> test_semget
le semid de l’ensemble de sémaphore est : 2
cet ensemble est identifié par la clé unique : 2073103658
bernard/> ipcs
IPC status from /dev/kmem as of Fri Nov 20 11:08:06 1998
T ID KEY MODE OWNER GROUP
Message Queues:
72 Chapitre 5. Communication inter-processus

Shared Memory:
m 100 0x0000004f --rw-rw-rw- root root
Sémaphores:
s 2 0x7b910d2a --ra------- bernard ens
bernard/> test_semget
Échec de semget: File exists

Primitive semctl()

Sous HP-UX :

#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);

La construction « , ... » représente un paramètre de taille variable. En l’occurrence, il peut


ici s’agir soit d’un entier, soit d’un pointeur sur une structure struct semid_ds, soit d’un tableau
d’entiers. On peut voir le quatrième paramètre comme une union :

union semun {
int val ;
struct semid_ds *buf ;
ushort array[] ; /* tableau de taille égale au nombre */
/* de sémaphores de l’ensemble */
} arg ;

La valeur retournée dépend de la valeur de cmd :

– si cmd = GETVAL : valeur de semval


– si cmd = GETPID : valeur de sem_pid
– si cmd = GETNCNT : valeur de sem_ncnt
– si cmd = GETZCNT : valeur de sem_zcnt

Pour les autres valeurs de cmd, la valeur retournée est 0 en cas de réussite, et −1 en cas d’erreur.
L’appel système semctl() est utilisé pour examiner et changer les valeurs de sémaphores d’un
ensemble de sémaphores. Elle a besoin de quatre arguments : un identificateur de l’ensemble de
sémaphores (semid), le numéro du sémaphore à examiner ou à changer (semnum), un paramètre de
commande (cmd), et une variable de taille variable analogue au type union (arg).
Les diverses commandes possibles pour semctl() sont décrites dans les fichiers sys/ipc.h et
sys/sem.h, les premières données ci-dessous sont communes à tous les IPC, les secondes sont spéci-
fiques.

IPC_RMID (0) : l’ensemble de sémaphores identifié par semid est détruit. Seul le super-utilisateur
ou un processus ayant pour numéro d’utilisateur sem_perm.uid peut détruire un ensemble.
Tous les processus en attente sur les sémaphores détruits sont débloqués et renvoient un code
d’erreur.
version du 12 novembre 1999
5.4. Les sémaphores 73

IPC_SET (1) : donne à l’identificateur de groupe, à l’identificateur d’utilisateur, et aux droits


d’accès de l’ensemble de sémaphores, les valeurs contenues dans le champ sem_perm de la
structure pointée par le quatrième paramètre de semctl qui doit être un pointeur sur struct
semid_ds. On met également à jour l’heure de modification.
IPC_STAT (2) : la structure associée à semid est rangée à l’adresse pointée par le quatrième
paramètre de semctl qui doit être un pointeur sur struct semid_ds.

Commandes spécifiques aux sémaphores

GETNCNT (3) : la fonction retourne la valeur de semncnt qui est le nombre de processus en
attente d’un incrément de la valeur d’un sémaphore particulier.
GETPID (4) : la fonction retourne le pid du processus qui a effectué la dernière opération sur
un sémaphore particulier.
GETVAL (5) : la fonction retourne la valeur semval du sémaphore de numéro semnum.
GETALL (6) : les valeurs semval de tous les sémaphores sont rangées dans le tableau dont
l’adresse est le tableau d’entier passé en quatrième paramètre.
GETZCNT (7) : la fonction retourne la valeur semzcnt qui est le nombre de processus en attente
d’un passage à zéro de la valeur d’un sémaphore particulier.
SETVAL (8) : cette action est l’initialisation de la valeur du sémaphore. La valeur semval du
sémaphore de numéro semnum est mise à la valeur donnée en quatrième paramètre de semctl.
SETALL (9) : les valeurs semval des semnum premiers sémaphores sont modifiées en concor-
dance avec les valeurs correspondantes du tableau dont l’adresse est le quatrième paramètre
de semctl vu comme un pointeur sur un tableau d’entiers.

Primitive semop()

Sous HP-UX :

#include <sys/sem.h>
int semop( int semid, struct sembuf *sops, unsigned int nsops );

Valeur retournée : la valeur semval du dernier sémaphore manipulé, ou −1 en cas d’erreur.


L’appel système semop() permet d’effectuer des opérations sur les sémaphores. Il utilise trois
arguments : un identificateur d’ensemble de sémaphores (semid), un pointeur vers un tableau de
structures de type struct sembuf (sops), et un entier donnant le nombre d’éléments de ce tableau
(nsops). Il sera donc possible de spécifier en une fois plusieurs opérations sur un sémaphore. La
structure sembuf spécifie le numéro du sémaphore qui sera traité, l’opération qui sera réalisée sur ce
sémaphore, et les drapeaux de contrôle de l’opération.
Le type d’opération dépend de la valeur de sem_op (champ de la structure sembuf) :

– Si sem op < 0 (demande de ressource)


– si semval > |sem op| alors semval = semval − |sem op| : décrément du sémaphore
74 Chapitre 5. Communication inter-processus

– si semval < |sem op| alors le processus se bloque jusqu’à ce que semval > |sem op|
– Si sem op = 0 (lecture)
– si semval = 0 alors l’appel retourne immédiatement
– si semval 6= 0 alors le processus se bloque jusqu’à ce que semval = 0
– Si sem op > 0 (restitution de ressource) : alors semval = semval + sem op

En effectuant des opérations semop() qui n’utilisent que des valeurs de sem_op égales à 1 ou −1,
on trouve le fonctionnement des sémaphores de Dijsktra.
System V fournit une gamme d’opérations plus étendue en jouant sur la valeur de semop. L’implé-
mentation est alors beaucoup plus complexe et la démonstration des garanties d’exclusion mutuelle
est extrêmement délicate.
Le positionnement de certains drapeaux (dans le champ sem_flg de la structure sembuf) entraı̂ne
une modification du résultat des opérations de type semop() :

– IPC_NOWAIT : évite le blocage du processus (dans le cas où l’on aurait dû avoir ce comportement)
et renvoie un code d’erreur.
– SEM_UNDO : les demandes et restitutions de ressource sont automatiquement équilibrées à la fin
du processus. Toutes les modifications faites sur les sémaphores par un processus sont défaites
à la mort de celui-ci. Pendant toute la vie d’un processus, les opérations effectuées avec le
drapeau SEM_UNDO sur tous les sémaphores de tous les identificateurs sont cumulées, et lors
de la mort de ce processus, le système refait ces opérations à l’envers. Cela permet de ne
pas bloquer indéfiniment des processus sur des sémaphores, après la mort accidentelle d’un
processus. Notons que ce procédé coûte cher, tant au point de vue temps CPU qu’au point de
vue réservation de place mémoire.

5.4.3 Sémaphores de Dijsktra

Principe : les sémaphores de Dijkstra (prononcé [DAÏKSTRA]) sont une solution simple au pro-
blème de l’exclusion mutuelle. On accède à ces sémaphores par les deux opérations P (acquisition)
et V (libération). Lorsqu’on réalise l’opération P sur un sémaphore, sa valeur s est décrémentée de
1 si s est différent de 0 ; sinon, le processus appelant est bloqué et est placé dans une file d’attente
liée au sémaphore. Lorsqu’on réalise l’opération V sur un sémaphore, on incrémente sa valeur s de
1 s’il n’y a pas de processus dans la file d’attente ; sinon, s reste inchangée, et on libère le premier
processus de la file.

5.4.4 Conclusion

Le mécanisme des sémaphores sous System V est complexe à mettre en œuvre. D’autre part,
dans un programme utilisant des sémaphores, il faut être capable de démontrer que l’accès aux
ressources partagées est exclusif, et qu’il n’y a ni interblocage, ni situation de famine (dans laquelle
on n’obtient jamais d’accès). Si cette analyse est assez difficile avec les sémaphores de Dijkstra, elle
devient extrêmement délicate avec les primitives de System V.

Vous aimerez peut-être aussi