Poly Etudiant Fasc1 5
Poly Etudiant Fasc1 5
Poly Etudiant Fasc1 5
Première partie
Rappels
version du 12 novembre 1999
9
Chapitre 1
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.
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.
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é.
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.
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.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 :
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.
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 :
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.
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.
client : $(OBJETS_CLI)
tabulation gcc -o client $(OBJETS_CLI)
serveur : $(OBJETS_SER)
14 Chapitre 1. Les outils Unix pour développer en C
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
...
Cette affectation supplantera celle faite dans le Makefile par la définition DEBUG=-ggdb qui
restera la définition par défaut.
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) $<
...
.c.o :
gcc $(CFLAGS) $(DEBUG) $<
OBJETS_CLI = protocole.o ppclient.o
OBJETS_SER = protocole.o ppserveur.o
CFLAGS = -c -Wall
client : $(OBJETS_CLI)
tabulation gcc -o client $(OBJETS_CLI)
serveur : $(OBJETS_SER)
tabulation gcc -o serveur $(OBJETS_SER)
# 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
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
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 */
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
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 :
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) ;
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
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.
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.
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 ;
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.
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 :
Exemple :
void main()
{
int unObjetEntier ;
int *pe ;
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.
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.1 Exemple 1
void main()
{
char *jours[7] ;
...
jours[0] = "Lundi" ;
jours[1] = "Mardi" ;
...
jours[6] = "Dimanche" ;
}
22 Chapitre 2. Compléments en C
En C, il est possible de passer des paramètres à un programme principal pour en faire une
commande Unix qui accepte des paramètres.
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 :
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
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]$
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
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 :
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
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.
– 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.
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 :
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().
Primitive creat()
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:
Primitive open()
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
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
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>
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 :
Exemple :
Primitive close()
#include <unistd.h>
int close(int fd) /* fermeture de fichier */
/* fd est le descripteur de fichier */
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 */
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 */
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) ;
}
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.
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>
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.
É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.
/*
** 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>
if (argc < 2) {
version du 12 novembre 1999
3.3. Les verrous POSIX : définitions et utilité 35
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.
Toutes les écritures sont mélangées. Voyons comment sérialiser les accès.
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.
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]$ ./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é
Et si on imprime essai, on voit que chaque processus a pu écrire sans être interrompu.
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.
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:
/* 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
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.
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.
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 {
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
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 :
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
– 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.
Second groupe d’exec(). Les arguments sont passés sous forme de tableau :
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
#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() {
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().
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.
/* path : cha^
ıne spécifiant le nouveau répertoire */
/* fichier test_cd.c */
#include <stdio.h>
main()
{
chdir("..") ; /* on va au répertoire précédent */
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.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.
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.
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 :
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 :
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.
Primitive kill()
#include <signal.h>
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>
#include <signal.h>
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.
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.
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
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 :
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.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 « | »).
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.
Primitive pipe()
– 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
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 */
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.
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
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.1 Introduction
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().
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 )
– 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 :
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 :
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).
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>
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 :
Primitive msgctl()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl ( int msqid, int cmd, struct msqid_ds *buf )
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
Primitive msgsnd()
#include <sys/msg.h>
int msgsnd( int msqid, const void *msgp, size_t msgsz, int msgflg );
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd ( int msqid, struct msgbuf *msgp, int msgsz, int msgflg );
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 :
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()
#include <sys/msg.h>
int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
66 Chapitre 5. Communication inter-processus
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 new way is not very different from the old code:
struct Hack
{
int size;
...
float f[]; // 1..N elements
};
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 :
La taille du champ mtext est fixée selon les besoins (voir msgsnd() pour plus de détail).
– 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.
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 :
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
– 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 :
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).
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);
– 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 :
Pour créer un ensemble de sémaphores, les points suivants doivent être respectés :
– 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 */
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
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, ...);
union semun {
int val ;
struct semid_ds *buf ;
ushort array[] ; /* tableau de taille égale au nombre */
/* de sémaphores de l’ensemble */
} arg ;
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
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 );
– 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.
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.