Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Pds Vol2 2023 01 06

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 26

Programmation des systemes – vol 21

Giuseppe Lipari

January 6, 2023

1
version 0.1
2
Contents

1 Multiprocessing 5
1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2 Processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 Création des processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.4 Terminaison d’un processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.5 Chargement en mémoire et exécution d’un processus externe . . . . . . . . . . . . . . . . 10
1.6 La primitive dup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.7 Questions récapitulatives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.7.1 Question : wait . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.7.2 Exercice : Fork . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.7.3 Exercice : spawn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

2 Les signaux POSIX 17


2.1 Un mécanisme d’interruptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2 Gestionnaire de signaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3 TODO Comportement des primitives bloquantes . . . . . . . . . . . . . . . . . . . . . . . 19
2.4 TODO Masquer les signaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.5 TODO Le signal SIG_CHILD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

3 Mécanismes de communications Inter-processus 21


3.1 TODO Les tubes anonymes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.2 TODO Les tubes FIFO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.3 TODO Les sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.4 TODO Cas d’étude : un serveur multi-processus . . . . . . . . . . . . . . . . . . . . . . . 21

4 Les threads – Part 2 23


4.1 TODO Le mécanisme de moniteur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.2 TODO Le mutexes et les variables condition . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.3 TODO Threads et Synchronisation en Java . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.4 TODO Threads et Synchronisation en Python . . . . . . . . . . . . . . . . . . . . . . . . 23
4.5 TODO Problèmes classiques de synchronisation . . . . . . . . . . . . . . . . . . . . . . . . 23
4.5.1 TODO Lecteurs / Écrivans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.5.2 TODO Thread Pool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.6 TODO Cas d’étude : un serveur multi-threadé . . . . . . . . . . . . . . . . . . . . . . . . 23

3
4 CONTENTS

A Solutions aux exercices 25


A.1 Exercices sur le Multiprocessing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
A.1.1 Reponse à la question 1.7.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
A.1.2 Solution de l’exercice 1.7.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
A.1.3 Solution de l’exercice 1.7.3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Chapter 1

Multiprocessing

1.1 Introduction
Dans ce deuxième volume, nous abordons la programmation multiprocessus dans les systèmes Unix.
Dans le premier chapitre, nous verrons comment créer et terminer un processus, et comment exécuter des
processus externes. Dans le deuxième chapitre, on aborde un mécanisme basilaire pour communiquer entre
processus, les signaux. Dans le troisième chapitre, on aborde d’autres mécanismes de communication
plus sophistiqués (IPC : interprocess communication en anglais) : les tubes (pipes en anglais) et les
sockets.
Dans le chapitre 4 on revient sur les threads, et étudie d’autres mécanismes de synchronisation avec
des exemples pratiques, et aussi dans d’autres langages de programmations.
Dans le chapitre 5 on étudie le problème de programmer une application client / serveur en utilisant
les processus ou les threads.

1.2 Processus
Un processus est caractérisé par :

un identifiant de processus c’est un entier positif qui identifie uniquement le processus dans le sys-
tème d’exploitation, aussi appelé PID (Process ID en anglais) ;

du code qui exécute dans l’ordinateur ;

un espace de mémoire dedié à son tour composé par des segments :

données initialisées toutes les variables globales initialisées ;


données non initialisées toutes les variables globales non-initialisées (parfois indiqué comme
BSS) ;
une pile utilisées pour les appels fonctions (stack en anglais) ;
un tas utilisées pour les données allouées dynamiquement (heap en anglais), c’est-à-dire avec un
malloc() en C ou avec un new en C++ ;

5
6 CHAPTER 1. MULTIPROCESSING

les descripteurs de fichiers un tableau contenant tous les descripteurs des fichiers ouverts par le pro-
cessus ;

l’état d’exécution c’est l’état d’exécution du processus, gardé dans les structures de données internes
au noyau.

Dans la suite, beaucoup de primitives repose sur la connaissance du PID d’un processus pour pouvoir
interagir. Un processus peut connaître son propre PID avec la primitive getpid() :
# include < sys / types .h >
# include < unistd .h >

pid_t getpid ( void ) ;

Cette primitive ne peut jamais retourner une erreur, sa valeur est toujours un entier positif. Le premier
processus qui est créé au démarrage du système d’exploitation est le processus INIT qui a PID égale à 1.
Une fois démarré, un processus ne change pas son PID. En revanche, quand un processus se termine son
PID est disponible pour être affecté à un nouveau processus.

1.3 Création des processus


La seule manière de créer un processus dans les systèmes POSIX est d’utiliser la primitive fork() :
# include < sys / types .h >
# include < unistd .h >

pid_t fork ( void ) ;

Cette syscall crée un nouveau processus quasi identique au processus qui appelle la fonction, sauf
quelques petites différences. Après l’exécution de la fork() il y a deux processus qui s’exécutent :

• le processus père est le processus qui a appelé la fork() ;

• le processus fils est le processus qui a été créé.

Le processus fils est un clone (une copie quasi identique) du processus père : toutes les variables
globales et locales du processus père sont copiées dans l’espace mémoire du processus fils avec la même
valeur. Le processus père et le processus fils ne partagent pas leur mémoire : chaque processus à sa
copie des variables en mémoire. Les deux processus continuent leurs exécutions à partir de l’instruction
suivante la fork().
Une première différence est, bien sûr, le PID : chaque processus a un PID unique. Une deuxième
différence est la valeur de retour de la fork(). En effet, la primitive est appelée par le processus père,
mais elle retourne deux fois : dans le processus père et dans le processus fils. Dans le père, la valeur de
retour est égale au PID du fils qui vient d’être créé ; dans le fils, la valeur de retour de la fork() est
zéro. Si la valeur de retour est négative (-1), la fonction fork() a échoué.
Considérez l’exemple suivant.
Example 1.3.1
1.4. TERMINAISON D’UN PROCESSUS 7

1 int main ()
2 {
3 pid_t child ;
4 int a = 0;
5

6 printf ( " Parent (1) : a =% d \ n " , a ) ;


7

8 child = fork () ;
9 if ( child <0) {
10 printf ( " Could not create the process \ n " ) ;
11 exit ( -1) ;
12 } else if ( child >0) {
13 sleep (2) ; /* Code ex é cut é seulement */
14 printf ( " Parent (2) : a =% d \ n " , a ) ; /* par le processus p è re */
15 }
16 else {
17 a ++; /* Code ex é cut é seulement */
18 printf ( " Child (1) : a =% d \ n " , a ) ; /* par le processus fils */
19 }
20 printf ( " Fin \ n " ) ; /* Code ex é cut é par les deux processus */
21 }

la primitive fork() retourne le PID du processus fils dans le processus père, et zéro dans le processus
fils. Par consequent, même si les deux processus père et fils continuent leur exécution après le retour de
la primitive fork(), le code entre les lignes 12 et 15 est exécuté seulement par le processus père, et le
code entre les lignes 16 et 19 est exécuté seulement par le processus fils. Finalement, l’instruction à la
ligne 20 est exécutée par les deux processus.
La fonction sleep(2) à la ligne 13 suspends l’exécution du processus pendant 2 seconds. Dans la
plupart des exécutions1 , l’ordre d’affichage du programme précédent est :
Parent (1) : a=0
Child (1) : a=1
Fin
Parent (2) : a=0
Fin
Notez que, après l’exécution de la fork(), il y a deux variables nommées a : une de ces variables
réside dans l’espace mémoire du processus père, l’autre dans l’espace mémoire du fils. Quand le fils
incrément sa variable a, il n’a aucun effet sur la variable a du père. Notez que la chaîne de caractères
Fin est imprimée 2 fois : par le processus père et par le processus fils. ■

1.4 Terminaison d’un processus


Nous avons vu precedemment comment un processus se termine : soit en appellant les fonctions _exit()
ou exit(), soit implicitement quand l’exécution de la fonction main() se termine par un return. Dans
1 Veuillez noter que l’ordre d’exécution des processus et des threads est décidé par l’ordonnanceur qui ne fourni pas de

garanties. Dans ce cas, il n’y a pas de garantie que le processus fils se terminera avant le processus père ; dans certaines
conditions de surcharge, il est en effet possible que le père se termine avant le fils.
8 CHAPTER 1. MULTIPROCESSING

le deux cas, le processus retourne une valeur : soit comme argument des fonctions exit(retval) ou
_exit(retval) ; soit avec l’instruction return retval.
Cette valeur doit être un nombre entier avec signe sur 8 bits, donc entre -128 et 127. Si on donne un
nombre plus élévé, seulement le 8 bits de poids faible sont rétenus.
Le processus père peut attendre la terminaison d’un processus fils et obtenir sa valeur de retour avec
les primitives de la famille wait :
# include < sys / types .h >
# include < sys / wait .h >

pid_t wait ( int * wstatus ) ;

pid_t waitpid ( pid_t pid , int * wstatus , int options ) ;

La primitive wait() attende que un des fils du processus termine. Elle est donc une primitive blo-
quante. Elle a le fonctionnement suivant :

• si le processus appellant la wait n’a pas de fils, la syscall échoue, en retournant un code d’erreur ;

• si le processus appellant a créé des processus enfants avec fork(), mais qu’aucun n’est encore
terminé, la syscall bloque le processus ;

• si le processus appelant a des fils qui sont déjà terminés, le PID est renvoyé comme valeur de retour,
et la valeur de sortie est encodée dans la variable pointé par le paramètre status.

Voici un exemple d’utilisation de la wait() :


1 # include < unistd .h >
2 # include < stdlib .h >
3 # include < stdio .h >
4 # include < sys / types .h >
5 # include < sys / wait .h >
6 # include < assert .h >
7

8 int main ()
9 {
10 pid_t ch ;
11

12 assert (( ch = fork () ) >=0) ;


13 if ( ch == 0) {
14 printf ( " Child : parent pid = % d ; my pid is % d \ n " , getppid () , getpid () ) ;
15 return 42;
16 }
17

18 /* Code ex é cut é seulement par le processus p è re */


19 printf ( " Parent : my pid % d ; child pid % d \ n " , getpid () , ch ) ;
20 sleep (2) ;
21 int status ;
22 int ch_p = wait (& status ) ;
23 printf ( " Child ’s pid : % d ; status : % d \ n " , ch_p , status ) ;
24 if ( WIFEXITED ( status ) )
1.4. TERMINAISON D’UN PROCESSUS 9

25 printf ( " Child ’s returned value is % d \ n " , WEXITSTATUS ( status ) ) ;


26 else if ( WIFSIGNALED ( status ) )
27 printf ( " Child received a signal \ n " ) ;
28 else if ( WIFSTOPPED ( status ) )
29 printf ( " Child stopped \ n " ) ;
30 return 0;
31 }

Un exemple de sortie du programme est la suivante :

Parent : my pid 120447; child pid 120448


Child : parent pid = 120447; my pid is 120448
Child’s pid : 120448; status : 10752
Child’s returned value is 42

Le programme commence par créer un processus fils qui retourne la valeur 42 après avoir imprimé
son PID et le PID de son père obtenue avec la primitive getppid().
Le processus père attends la terminaison du fils et récupéres son PID dans la variable ch_p, sa valeur
est égale à la valeur de la variable ch. La valeur de retour est encodée dans l’octet de poids faible de
la variable status. La variable status contient aussi des informations sur l’état de terminaisons du
processus fils. On peut obtenir ces informations avec des macros comme WIFEXITED(status) qui vaut 1
si le processus s’est terminé normalement ; WIFSIGNALED(status) qui vaut 1 si le processus est terminé
après avoir reçu un signal (voir le chapitre suivant) ; et WIFSTOPPED() qui vaut 1 si le processus a été
suspendu (voir le chapitre suivant).
Dans le cas d’une terminaison normale, il est possible de récupérer la valeur de sortie avec la macro
WEXITSTATUS() qui retourne les 8 bits de poids faible de status.
À la ligne 20, le processus père appele la primitive sleep(2) qui suspend le processus pour 2 seconds.
Il est donc très probable qu’au moment de l’appel de la primitive wait() le processus fils aie déjà
terminé. Cependant, le noyau ne peut pas eliminer immediatement le processus fils ; il doit conserver
les informations qui seront plus tard récupérées par la wait() du processus père, et en particulier il doit
conserver son PID, sa valeur de sortie et les information concernant son état de terminaison.
Un processus qui a terminé, mais dont les informations n’ont pas encore été récupérees pas le processus
père, est dit un processus zombie : il ne peut plus exécuter, mais il ne peut pas encore être éliminé du
système. Il sera éliminé définitivement seulement au moment de la wait().
Si le processus père se termine sans avoir récupéré les informations avec un appel à wait(), les
processus fils sont hérités par le processus INIT. En effet, tous les processus descendent du processus
initial, INIT. Ce dernier est un processus spécial qui ne se termine jamais, et qui se charge d’appeler
la wait() en permanence pour éviter l’accumulation de processus zombie dans le système. Un fils peut
savoir si le processus père est terminé en comparant la valeur de retour de getppid() avec 1 (le PID de
INIT).
La primitive waitpid() est une version évoluée de la primitive wait(). Elle permet d’attendre un
processus fils en particulier, et de ne pas se bloquer dans le cas que le processus n’a pas encore terminé.

pid_t waitpid ( pid_t pid , int * wstatus , int options ) ;


10 CHAPTER 1. MULTIPROCESSING

• Le premier paramètre indique le pid du processus fils qu’on veut attendre ; trois possibilités :

<-1 attend un des fils qui appartient au process group spécifié par la valeur -pid (voir la section
sur les process groups ci-dessous) ;
-1 attend un des fils ;
0 attend un des fils qui appartient au même process group du processus qui appel la waitpid() ;
>0 attend le fils avec le pid spécifié.

• Le deuxième paramètre est l’OR arithmétique des constantes suivantes :

WNOHANG retourne immédiatement avec erreur si le fils n’a pas encore terminé (comportement non
bloquant) ;
WUNTRACED retourne aussi si le fils est stoppé (voir chapitre suivant sur les signaux) ;
WCONTINUED retourne aussi si le fils était stoppé et il a reçu un signal di SIGCONT (voir chapitre
suivant sur les signaux).

1.5 Chargement en mémoire et exécution d’un processus externe


Comme mentionné, la primitive fork() permet de créer un nouveau processus en copiant dans son espace
mémoire l’espace mémoire du processus père. Comme conséquence, les processus père et fils partagent le
même code. Cependant, il est nécessaire de pouvoir exécuter un programme écrit er compilé séparément.
Pour ce faire, Unix fournit la famille de fonctions exec(). Chacune des fonctions de la famille, bien
qu’avec des façons légèrement différents, s’occupe de charger un programme externe du disque dans la
mémoire du processus appellant.
# include < unistd .h >

extern char ** environ ;

int execl ( const char * pathname , const char * arg , ...


/* ( char *) NULL */ ) ;
int execlp ( const char * file , const char * arg , ...
/* ( char *) NULL */ ) ;
int execle ( const char * pathname , const char * arg , ...
/* , ( char *) NULL , char * const envp [] */ ) ;
int execv ( const char * pathname , char * const argv []) ;
int execvp ( const char * file , char * const argv []) ;
int execvpe ( const char * file , char * const argv [] ,
char * const envp []) ;

Pour simplicité, ici on décriera la fonction execv, on renvoie le lecteur au manuel de votre système
pour plus de détails sur les autres fonctions.
La fonction execv prends en paramètre :

• pathname : le chemin relatif ou absolu vers le fichier exécutable qui contient le code du programme
à charger ;
1.5. CHARGEMENT EN MÉMOIRE ET EXÉCUTION D’UN PROCESSUS EXTERNE 11

• argv[] : tableau de chaînes de caractères contenant les arguments du programme à exécuter. Le


premier élément de ce tableau, argv[0], contient le nom du programme exécutable ; le tableau doit
toujours se terminer par 0.
Par exemple, si on veut exécuter la commande ls -a -l, dont l’exécutable se trouve dans /usr/bin
on utilisera le code suivant :
char * arguments [] = { " ls " , " -a " , " -l " , 0};

execv ( " / usr / bin / ls " , arguments ) ;

Attention, il faut toujours terminer le tableau par 0 !


Il est important de souligner que execv() ne crée pas un nouveau processus, mais modifie simplement
l’espace mémoire du processus qui l’appelle. Plus précisement, en cas de succès, execv() remplace le code
et les données du processus appellant avec le code et les donnée du programme chargé en mémoire. Le
processus conserve le même PID ; comme on verra dans la suite, aussi les descripteur de fichiers ouverts
restent les mêmes.
Pour cette raison, en cas de bon fonctionnement, execv() ne retournera pas dans le code du
processus appellant, mais le nouveau programme commencera son exécution à partir de son main.
Il est donc clair que, si cette primitive retourne une valeur à l’appelant, cette valeur est nécessairement
inférieure à 0 et sert à signaler une erreur (par exemple, fichier introuvable, ou fichier non exécutable).
Example 1.5.1
Considerez le programme suivant.
1 # include < stdio .h >
2 # include < stdlib .h >
3 # include < unistd .h >
4 # include < assert .h >
5 # include < errno .h >
6 # include < sys / types .h >
7 # include < sys / wait .h >
8

9 int main ()
10 {
11 pid_t ch ;
12 int status ;
13

14 ch = fork () ;
15 assert ( ch >= 0) ;
16 if ( ch == 0) {
17 char * arguments [] = { " ls " , " -a " , " -l " , 0 };
18 int ret = execv ( " / usr / bin / lsa " , arguments ) ;
19 printf ( " Valeur de retour : %d , errno = % d \ n " , ret , errno ) ;
20 perror ( " Error in the execv " ) ;
21 exit ( EXIT_FAILURE ) ;
22 }
23 wait (& status ) ;
24 if ( WIFEXITED ( status ) ) {
25 printf ( " Child process exiting with code % d \ n " , WEXITSTATUS ( status ) ) ;
26 }
12 CHAPTER 1. MULTIPROCESSING

27 else {
28 printf ( " Child process exited abnormally \ n " ) ;
29 }
30 return EXIT_SUCCESS ;
31 }

Le programme crée un processus fils qui essaie d’exécuter le programme externe /usr/bin/lsa.
Comme ce dernier n’existe pas, la primitive execv échouera et retournera la valeur -1 qui sera ensuite
imprimé par le processus fils, ainsi que la valeur de la variable errno et le message d’erreur correspondant
(No such file or directory). Le processus père attends la terminaison du fils et imprime sa valeur de sortie.
Compilez et exécutez ce programme pour vérifier la sortie. Ensuite, modifié le premier argument de
execv() en /usr/bin/ls et notez qu’il n’y a pas d’erreur, et donc la printf() à la ligne 16 n’est jamais
exécuté ; à sa place, la sortie de la commande "ls", c’est-à-dire le contenu du répertoire courant, sera
imprimé sur le terminal. ■
Comme montré dans l’exemple, les primitives fork() et execv() sont souvent utilisées en combinaison
; le processus père crée un fils qui charge le nouveau programme en mémoire avec une exec(). Cette
opération est souvent appéllée spawn en anglais et dans d’autres système d’exploitation est faite par
une seule primitive. Par exemple, l’API Windows fourni la famille de primitives _spawn2 qui créent un
nouveau processus qui exécute le nouveau programme.
Example 1.5.2
Considerez le programme suivant, qu’on appellera prog-pid.
1 # include < stdio .h >
2 # include < unistd .h >
3

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


5 {
6 int i ;
7

8 printf ( " Extrn program . pid = \% d ppid = % d \ n " ,


9 getpid () , getppid () ) ;
10 for ( i =0; i < argc ; i ++)
11 printf ( " Argument % d = % s \ n " , i , argv [ i ]) ;
12

13 return 0;
14 }

Ce programme simplement imprime sur le terminal son pid, le pid de son père et la liste des arguments
sur la ligne de commande.
Le programme suivant crée un fils pour exécuter le programme externe prog-pid après avoir imprimé
son pid et le pid de son père.
1 # include < stdio .h >
2 # include < unistd .h >
3 # include < stdlib .h >
4 # include < sys / types .h >
5 # include < sys / wait .h >
2 https://learn.microsoft.com/en-us/cpp/c-runtime-library/spawn-wspawn-functions?view=msvc-170
1.6. LA PRIMITIVE DUP 13

7 int main ( void )


8 {
9 pid_t child ;
10 char * array [] = { " arg1 " , " arg2 " , " arg3 " , 0};
11

12 if (( child = fork () ) < 0) perror ( " Error in the fork () " ) ;


13 else if ( child == 0) {
14 printf ( " Child process . pid = % d ppid = \% d \ n " , getpid () , getppid () ) ;
15

16 if ( execv ( " prog - pid " , array ) < 0) {


17 perror ( " Error in the exec " ) ;
18 exit ( EXIT_FAILURE ) ;
19 }
20

21 }
22 else {
23 printf ( " Parent process . My pid_t = \% d \ n " , getpid () ) ;
24 wait (0) ;
25 }
26 return EXIT_SUCCESS ;
27 }

Un exemple de sortie de ce dernier programme sera :

Parent process. My pid_t = 32635


Child process. pid = 32636 ppid = 32635
Extrn program. pid = 32636 ppid = 32635
Argument 0 = arg1
Argument 1 = arg2
Argument 2 = arg3

Notez que les pids affichés par le fils et par le programme externe sont les mêmes : il s’agit du même
processus ! ■

1.6 La primitive dup


Chaque processus a son propre tableau de descripteurs de fichiers ouverts (voir la Section 3.4 du
Volume 1 de ce cours). À chaque fois que le processus appelle la primitive open() une nouvelle entrée
est ajoutée à ce tableau. Normalement, les premiers 3 descripteurs sont déjà présents dans le tableau : le
descripteur 0 indique l’entrée standard (stdin), le descripteur 1 la sortie standard (stdout), le descripteur
2 la sortie d’erreur (stderr).
Lors d’un appel à fork() ce tableau est recopié dans le processus fils. Un fichier ouvert dans le père
est aussi ouvert dans le fils et les deux processus peuvent l’utiliser.
14 CHAPTER 1. MULTIPROCESSING

Notes

Il faut faire attention que les entrées correspondantes dans la table de fichiers ouverts, qui se
trouve dans le noyau, ne sont pas dupliqués : les deux processus père et fils partagent donc les
informations mémorisées dans ces entrées, comme le file status flag, le current offset et le pointer
vers le v-node. Par conséquent, une lecture de fichier dans le père modifie l’offset aussi pour le fils
!

Lors d’un exec(), le tableau de descripteur de fichiers n’est pas remplacé. Après l’appel, le nouveau
programme peut reutiliser les mêmes fichiers ouverts avant l’appel à exec. Cette fonctionnalité peut être
utilisée pour faire une redirection.
Supposons de vouloir mémoriser la sortie du programme ls sur un fichier ls.log. Dans l’exemple
suivant, on utilisera la primitive dup2() avec fork() et exec() pour faire ça.
La primitive int dup2(int fd1, int fd2);
• ferme le descripteur fd2 (s’il était ouvert)
• copie le descripteur fd1 sur fd2.

Example 1.6.1
Dans le code suivant, on crée un processus fils, qui ouvre le fichier ls.log en écriture et duplique
son descripteur sur le descripteur 1 (stdout). À partir de maintenant, toute sortie sur le descripteur 1
(la sortie standard) sera écrite sur le fichier ls.log. Ensuite, le fils lance l’exécution du programme ls.
Comme la execv ne change pas le tableau de fichiers ouverts, la sortie du programme sera sur le fichier
ls.log.
1 # include < stdio .h >
2 # include < stdlib .h >
3 # include < unistd .h >
4 # include < assert .h >
5 # include < errno .h >
6 # include < sys / types .h >
7 # include < sys / wait .h >
8 # include < sys / stat .h >
9 # include < fcntl .h >
10

11 int main ()
12 {
13 pid_t ch ;
14 int status ;
15

16 ch = fork () ;
17 assert ( ch >= 0) ;
18 if ( ch == 0) {
19 int f = open ( " ls . log " , O_WRONLY | O_CREAT ) ;
20 if (f <0) {
21 perror ( " Cannot open ls . log file " ) ;
22 exit ( EXIT_FAILURE ) ;
1.7. QUESTIONS RÉCAPITULATIVES 15

23 }
24 dup2 (f , 1) ;
25 char * arguments [] = { " ls " , " -a " , " -l " , 0 };
26 int ret = execv ( " / usr / bin / ls " , arguments ) ;
27 fprintf ( stderr , " Valeur de retour : %d , errno = % d \ n " , ret , errno ) ;
28 perror ( " Error in the execv " ) ;
29 exit ( EXIT_FAILURE ) ;
30 }
31 wait (& status ) ;
32 if ( WIFEXITED ( status ) ) {
33 printf ( " Child process exiting with code % d \ n " , WEXITSTATUS ( status ) ) ;
34 }
35 else {
36 printf ( " Child process exited abnormally \ n " ) ;
37 }
38 return EXIT_SUCCESS ;
39 }

1.7 Questions récapitulatives


1.7.1 Question : wait
La primitive wait() sert à attendre la terminaison d’un processus fils et à récuperer sa valeur de sortie.
S’il y a plusieurs fils, la wait() récupères la sortie d’un des fils, typiquement le premier à terminer.
Comment obtenir le PID du fils terminé ?

1.7.2 Exercice : Fork


Considerez le programme suivant:
# include < stdio .h >
# include < stdlib .h >
# include < sys / types .h >
# include < sys / wait .h >
# include < unistd .h >

int main () {
pid_t child ;
int i ;

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


child = fork () ;
if ( child == 0) {
printf ( " %d , " , i ) ;
return 0;
} else if ( child < 0)
exit ( EXIT_FAILURE ) ;
16 CHAPTER 1. MULTIPROCESSING

}
wait ( NULL ) ;
wait ( NULL ) ;
printf ( " % d \ n " , i ) ;
return EXIT_SUCCESS ;
}

Quel sont les affichages possibles sur la sortie standard ?

1.7.3 Exercice : spawn


Écrire une version POSIX de la primitive spawn() décrite ci-dessous :
int spawnv ( char * cmd , char * argv []) ;

La fonction crée un nouveau processus fils qui lance la execv() sur la commande cmd avec arguments
argv[] et attends sa terminaison. La fonction renvoie la valeur de sortie du programme cmd, ou -1 en
cas d’erreur.
Testez la nouvelle fonction en re-écrivant les exemples 1 et 2 avec spawnv.
Chapter 2

Les signaux POSIX

Nous avons vu comment créer et terminer des processus, comment charger le code et les données d’un
processus à partir d’une image sur disque et comment synchroniser un processus avec la fin de ses enfants
en récupérant les valeurs qui leur sont retournées. En général, les processus qui composent un programme
concurrent doivent cependant se synchroniser et communiquer entre eux de manière non triviale. De
plus, le noyau doit parfois informer un processus qu’une certaine condition (exceptionnelle ou non) s’est
produite.
Dans ce chapitre, nous traiterons des signaux, c’est-à-dire des communications asynchrones qui sig-
nalent une certaine condition. Les signaux peuvent être utilisés soit pour communiquer des conditions
d’erreur ou des exceptions, soit pour synchroniser deux processus entre eux.

2.1 Un mécanisme d’interruptions


Les signaux sont le mécanisme utilisé par les systèmes Unix pour rendre possible la communication
asynchrone de certaines conditions. En pratique, un signal est un événement asynchrone (parfois désigné
incorrectement sous le nom d’/interruption logicielle/), qu’un processus peut envoyer à un autre processus
(ou à un groupe de processus). Le mécanisme est en fait très général, car même le noyau est capable
de générer des signaux à envoyer aux différents processus pour signaler des conditions d’erreur ou des
exceptions. Le destinataire peut réagir différemment à l’arrivée de cet événement:

• ignorer le signal,

• exécuter un signal handler spécifié par le processus,

• sortir avec une condition d’erreur.

Un signal qui n’a pas encore généré l’une des trois actions ci-dessus est dit suspendu.
Dans les sections suivantes, quelques primitives de noyau seront introduites pour permettre à un
processus de :

• spécifier l’action à entreprendre en réponse à un signal (installer un signal handler );

• bloquer un signal (le masquer ),

17
18 CHAPTER 2. LES SIGNAUX POSIX

• vérifier s’il y a des signaux en attente;


• envoyer des signaux à un autre processus.

2.2 Gestionnaire de signaux


Une table des signaux est assignée à chaque processus, qui fait partie de son état privé et spécifie l’action
que le processus doit effectuer en réponse à un signal. Lors de la création du processus, chaque signal
se voit attribuer un comportement par défaut, qui peut être modifié via un système d’appel spécifique.
Historiquement, les systèmes Unix ont la fonction signal(), qui permet d’assigner un signal handler (typ-
iquement, une fonction du type void f(int s)) au signal spécifié et renvoie le signal handler précédem-
ment assigné au signal, toujours sous la forme d’un pointeur vers une fonction.
# include < signal .h >

void (* signal ( int signum , void (* handler ) ( int ) ) ) ( int ) ;

signum Identifiant du signal pour lequel le handler doit être installé.


handler pointeur vers la fonction qui contient le code du handler, ou l’une des deux macros par défaut :
• SIG_IGN, ce qui signifie ignorer le signal spécifié;
• SIG_DFL, qui installe le handler par défaut du signal spécifié.

La fonction retourne soit le pointer vers l’ancien signal handler, soit SIG_ERR en cas d’erreur.
Voici un petit exemple de configuration d’un nouveau handler pour le signal d’identification SIGUSR1.
# include < signal .h >
# include " myutils . h "

static void handler ( signo )


{
printf ( " J ’ ai re ç u le signal SIGUSR1 \ n " ) ;
}

int main ( void )


{
/* Installation du gestionnaire de signaux */
if ( signal ( SIGUSR1 , handler ) == SIG_ERR )
perror ( " Erreur dans l ’ installation de l ’ handler " ) ;
/* ... */
}

Le comportement de ce syscall n’est cependant pas standard, car dans certains systèmes, le signal
handler est réinitialisé à la valeur par défaut lorsqu’un signal est déclenché, tandis que sur d’autres il
reste réglé à la valeur spécifiée par signal même après le déclenchement du signal.
Pour éviter ce problème, la norme POSIX définit un nouvel appel système, sigaction, qui est standard
et donc un programme écrit en utilisant cet appel est certainement plus portable. Voici le prototype de
sigaction :
2.3. TODO COMPORTEMENT DES PRIMITIVES BLOQUANTES 19

# include < signal .h >

int sigaction ( int signum , const struct sigaction * act ,


struct sigaction * oldct ) ;

Les arguments sont :

signum Identifiant du signal dont le handler doit être installé.


act pointeur vers la structure contenant les paramètres qui spécifient le comportement du nouveau
gestionnaire.

oldact si différente de NULL, pointeur vers la structure dans laquelle les informations concernant l’ancien
gestionnaire sont enregistrées.

Comme d’habitude, la valeur de retour est 0 en cas de succès, et -1 en cas d’erreur.


La structure sigaction contient les champs suivants :
# include < signal .h >

struct sigaction {
void (* sa_handler ) () ;
sigset_t sa_mask ;
int sa_flags ;
};

Voici les significats des champs :

sa_handler Pointeur vers le code du nouveau gestionnaire, ou SIG_DFL si nous voulons le comportement
standard, ou SIG_IGN si nous voulons ignorer le signal.
sa_mask Masque les signaux qui doivent être bloqués lors de l’exécution du handler.
sa_flags Flags pour spécifier le comportement du système lors de l’exécution du gestionnaire.

2.3 TODO Comportement des primitives bloquantes


2.4 TODO Masquer les signaux
2.5 TODO Le signal SIG_CHILD
20 CHAPTER 2. LES SIGNAUX POSIX
Chapter 3

Mécanismes de communications
Inter-processus

3.1 TODO Les tubes anonymes


3.2 TODO Les tubes FIFO
3.3 TODO Les sockets
3.4 TODO Cas d’étude : un serveur multi-processus

21
22 CHAPTER 3. MÉCANISMES DE COMMUNICATIONS INTER-PROCESSUS
Chapter 4

Les threads – Part 2

4.1 TODO Le mécanisme de moniteur


4.2 TODO Le mutexes et les variables condition
4.3 TODO Threads et Synchronisation en Java
4.4 TODO Threads et Synchronisation en Python
4.5 TODO Problèmes classiques de synchronisation
4.5.1 TODO Lecteurs / Écrivans
4.5.2 TODO Thread Pool

4.6 TODO Cas d’étude : un serveur multi-threadé

23
24 CHAPTER 4. LES THREADS – PART 2
Appendix A

Solutions aux exercices

A.1 Exercices sur le Multiprocessing


A.1.1 Reponse à la question 1.7.1
La wait() retourne le PID du processus récupéré.

A.1.2 Solution de l’exercice 1.7.2


La sortie est 0, 1, 2 dans un ordre quelconque.

A.1.3 Solution de l’exercice 1.7.3


Voici une possible implementation :
int spawnv ( char * cmd , char * argv [])
{
int status ;
int ch = fork () ;
if ( ch < 0) {
perror ( " Error during fork () " ) ;
return -1;
}
if ( ch == 0) {
execv ( cmd , argv ) ;
perror ( " Error during execv " ) ;
exit ( EXIT_FAILURE ) ;
}
int ret = wait (& status ) ;
if ( ret < 0) {
perror ( " Error in the wait " ) ;
return -1;
}
if ( WIFEXITED ( status ) ) return WEXITSTATUS ( status ) ;
else {

25
26 APPENDIX A. SOLUTIONS AUX EXERCICES

fprintf ( stderr , " Child exited abnormally \ n " ) ;


return -1;
}
}

Voici l’example 1 avec spawn :


int main ()
{
pid_t ch ;
int status ;

char * arguments [] = { " ls " , " -a " , " -l " , 0 };


spawnv ( " / usr / bin / ls " , arguments ) ;
}

Vous aimerez peut-être aussi