Cours ‐ Processus - vbridonneau/CoursSysteme GitHub Wiki
Fonction | Description |
---|---|
fork |
Création d'un processus |
wait |
Attente d'un processus fils quelquonque |
waitpid |
Attente d'un processus fils en mentionnant son pid |
exit |
Permet de sortir d'un processus fils |
_exit |
Permet de sortir d'un processus fils sans appelé de fonctions atexit ou on_exit
|
atexit |
Exécuter une fonction lorsque exit est appelée |
on_exit |
Exécuter une fonction avec un paramètre lorsque exit est appelée |
getpid |
Obtention de l'identifiant d'un processus |
getppid |
Obtention de l'identifiant du parent d'un processus |
getuid |
Obtention de l'identifiant de l'utilisateur |
geteuid |
Obtention de l'identifiant effectif de l'utilisateur |
Pour créer un processus, il suffit d'utiliser l'appel système fork
.
Cet appel système va dupliquer le processus (copie des variables et de la pile notamment).
Il est défini comme suit :
int fork();
Lorsqu'un processus appelle fork
, un nouveau processus est créé.
Son fils commence son exécution à l'endroit où l'appel à fork a été réalisé.
Ce qui permet de différencier le père du fils est le résultat renvoyé par la fonction.
En effet, le père va recevoir le pid
(identifiant de processus) de son fils.
C'est une valeur strictement positive.
Le fils en revanche va recevoir la valeur 0
lui indiquant donc qu'il est le fils.
On peut donc savoir si un processus créé est un fils ou un père.
Cette information est importante, car un père doit toujours s'assurer que son fils termine sa tâche avant de pouvoir lui même terminer.
Un exemple d'utilisation de fork
est le suivant :
#include <sys/types.h>
#include <unistd.h>
int pidFils = fork();
if (!pidFils) {
/* On est le fils */
/* ... */
exit(EXIT_SUCCES);
} else {
/* On est le père */
/* ... */
waitpid(pidFils, NULL, 0); /* Attend que le fils ait terminé de s'exécuter */
exit(EXIT_SUCCESS);
}
Pour bien comprendre ce qu'il se passe, on peut illustrer le principe de la création de nouveaux processus avec le schéma suivant :
Une autre façon de comprendre ce qu'il se passe est de voir l'appel à fork
comme une duplication de tous les états d'un processus (code, variabales globales, pile, tas et fichiers ouverts).
Avant d'appeler fork, il n'y a qu'un processus.
Lorsque fork est appelée, un deuxième apparait, copie du premier, avec donc, sa propre pile, ses variables indépendantes (dont les valeurs sont les mêmes que celles du premier), etc. .
C'est d'ailleurs pour cela que la valeur renvoyée par fork
et contenue dans pid est différente entre le père et le fils : comme cette valeur sert à les différencier et que tout est copie, fork
peut renvoyer des valeurs différentes entre le père et le fils.
Lorsque le fils d'un processus termine, celui-ci envoit un signal à son père (souvent SIGCHLD
) pour lui indiquer qu'il a terminé de s'exécuter.
Un tel signal est émis quand un processus fils appelle _exit
ou exit
.
En ce qui concerne le père, il peut ou non continuer son exécution, mais il devra à un moment attendre que ses fils termine de s'exéuter pour pouvoir terminer.
S'il ne le fait pas, ces processus seront considérés comme des démons et rattaché au processus init
.
Pour attendre un processus, le parent peut appeler l'une des deux fonctions waitpid
ou 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 fonction waitpid
est plus précise car elle permet d'attendre un processus précis et peut être paramétrée via le champ option.
Voyons un exemple d'utilisation.
Mettons que l'on souhaite attendre un processus dont le pid est stocké dans une variable pidFils
, on peut exécuter le code suivant :
int main() {
int pidFils = fork();
if (pidFils > 0) {
/* Père */
/* ... */
waitpid(pidFils, NULL /* Si l'on a pas besoin du status */, 0);
} else {
/* Fils */
exit(EXIT_SUCCESS);
}
}
Ces appels sont par défaut bloquant, ce qui veut dire que ces fonctions attendent qu'un processus fils termine avant de rendre la main. Pour s'en rendre compte, on peut exécuter le code suivant :
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
int pidFils = fork();
if (pidFils > 0) {
/* Père */
waitpid(pidFils, NULL, 0);
printf("[Père] : Le fils à terminé\n");
} else {
/* Fils */
printf("[Fils] : Pause de 3 secondes.\n");
sleep(3);
printf("[Fils] : Je me termine.\n");
exit(EXIT_SUCCESS);
}
}
Il se peut que le fils que l'on souhaite attendre n'ait pas encore terminé de s'exécuter.
Si l'on souhaite reprendre l'exécution et attendre le fils plus tard, il est possible de renseigner l'option WNOHANG
ce qui veut dire que l'appel à waitpid
ne doit pas être bloquant.
On pourra alors écrire :
int main() {
int pidFils = fork();
if (pidFils > 0) {
/* Père */
/* ... */
int ret = waitpid(pidFils,
NULL /* Si l'on a pas besoin du status */,
WNOHANG /* Pour ne pas attendre si le fils n'a pas terminé */);
if (ret == 0) {
/* Le fils n'a pas terminé */
/* Fait des choses ... */
waitpid(pidFils, NULL, 0); /* Attend vraiment que le fils ait terminé */
}
} else {
/* Fils */
/* ... */
exit(EXIT_SUCCESS);
}
}
Lorsque l'on appelle wait
ou waitpid
, on remarque que l'un des paramètres est un pointeur vers un entier nommé wstatus
.
Ce paramètre contient la valeur passée en paramètre à exit
ou _exit
.
Il faut cependant faire attention.
En effet, si la variable se nomme wstatus
et pas status
, ce n'est pas pour rien.
wstatus
la valeur qui sera mise dans la variable passée en paramètre contient d'autres information que le status du processus ayant terminé.
On trouve entre autre d'autres information comme le fait de savoir si un programme s'est terminé correctement ou pas, le signal qui l'a fait se terminer, etc.
Pour récupérer ces informaitions, des macros on été définie afin d'y accéder.
Voici un tableau en donnant certaines :
Macro | Description |
---|---|
WEXITSTATUS |
Accède à la valeur passée à exit ou en retour de main. |
WIFEXITED |
Retourne vrai si le processus a terminé normalement (appel à exit ou retour de main). |
WIFSIGNALED |
Retourne vrai si le processus a terminé par reception d'un signal. |
WTERMSIG |
Retourne la valeur du signal ayant causée la fin du processus. |
Il existe des méthodes pour pouvoir accéder au pid du programme en cours d'exécution ou à celui de son parent.
Les fonctions associées sont respectivement getpid
et getppid
Nom | Description |
---|---|
getpid | Accède au pid du processus courrant |
getppid | Accède au pid du parent du processus courrant |
Leur prototype est le suivant :
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
Voici un exemple en C d'un programme qui crée trois processus : un père, son fils et un petit fils. Chacun affiche son pid et celui de son père. Cela donne :
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
int pidFils = fork();
if (pidFils > 0) {
/* Père */
waitpid(pidFils, NULL, 0);
printf("[Père] : %d -> (parent) %d\n", getpid(), getppid());
} else {
int pidPetitFils = fork();
if (pidPetitFils > 0) {
/* Fils */
waitpid(pidPetitFils, NULL, 0);
printf("[Fils] : %d -> (parent) %d\n", getpid(), getppid());
exit(EXIT_SUCCESS);
} else {
/* Petit fils */
printf("[Petit fils] : %d -> (parent) %d\n", getpid(), getppid());
exit(EXIT_SUCCESS);
}
/* Fils */
affichePidAndPere("Fils");
exit(EXIT_SUCCESS);
}
return 0;
}
Il existe d'ailleurs un moyen d'afficher l'arbre des processus via une ligne de commande :
ps axjf
Le rendu aura la forme suivante :
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 0 0 ? -1 Sl 0 0:00 /init
1 7 7 7 ? -1 Ss 0 0:00 /init
7 8 7 7 ? -1 S 0 0:00 \_ /init
8 9 9 9 pts/0 9 Ss+ 1000 0:00 \_ -bash
1 10 10 10 ? -1 Ss 0 0:00 /init
10 11 10 10 ? -1 S 0 0:00 \_ /init
11 12 12 12 pts/1 839 Ss 1000 0:00 \_ -bash
12 45 45 12 pts/1 839 S 1000 0:01 \_ zsh
45 839 839 12 pts/1 839 R+ 1000 0:00 \_ ps axjf
Lorsque l'on sort d'un programme, que ce soit par un simple return dans le main ou un appel à exit
, le programme ne se termine pas sans rien faire.
En effet, certaines procédures sont appelées afin d'effectuer certaines tâches (vider le buffer de stdout
par exemple).
Ces tâches sont spécifiées par des appels à deux fonctions : atexit
et on_exit
.
#include <stdlib.h>
int atexit(void (*function)(void));
int on_exit(void (*function)(int /* status */, void * /* argument */), void *arg);
Les arguments de atexit
et on_exit
sont ce que l'on appelle des pointeurs de fonctions.
De tels pointeurs servent à stocker l'adresse d'une fonction afin de pouvoir l'exécuter plus tard.
Pour pouvoir passer l'adresse d'une fonction, il suffit tout simplement de passer son nom et c'est tout.
Surtout il ne faut pas mettre les parenthèses, car cela revient à appeler la fonction et pas à passer son adresse.
Voici un exemple d'utilisation de atexit
afin d'illustrer l'usage des pointeurs de fonctions :
#include <stdlib.h> // atexit
#include <stdio.h>
void foo() {
printf("Appel a la fonction \"foo\"\n");
}
int main() {
atexit(foo);
printf("Ce message sera affiché en premier\n");
return EXIT_SUCCESS;
}
Si l'on exécute ce code, on aurra en sortie dans le terminal :
Ce message sera affiché en premier
Appel a la fonction "foo"
L'appel à la fonction foo
sera effectué après le retour de main
, c'est-à-dire lorsque le programme sortira.
Un autre chose importante à noté est que lors d'un appel à exit
ou lorsque le programme sort de main
, le flux standard stdout
est vidé et son contenu est affiché dans le terminal (ou redirigé si vous l'avez redirigé).
Il existe une autre façon de sortir d'un programme lors de son exécution.
Si on fait quelques recherches, on réalise que la fonction exit
possède une cousine : la fonction _exit
.
Cette fonction est en réalité un appel système et à la différence de exit
, aucune procédure enregistrée avec atexit
ou on_exit
n'est appelée et le flux de sortie standard stdout
est vidé et affiché dans le terminal.
Il est possible de lancer des scripts shell ou des programmes directement depuis un code C via l'appel à la fonction execve
ou une de ces cousines.
Cette fonction est un appel système et son prototype est le suivant :
#include <unistd.h>
int execve(const char *pathname, char *const argv[],
char *const envp[]);
Comme on peut le voir, cette fonction prend trois paramètres :
-
pathname
: le chemin (relatif ou absolue) vers un exécutable. -
argv
: liste des paramètres. Le premier doit être le nom de l'exécutable et doit terminer parNULL
. -
env
: les variables d'environnement. On y accède via une variable externe nomméeenviron
. Il est à noter que le nom de l'exécutable ne peut pas être seulement une commande accessible depuis la variable d'environnementPATH
. Il faut renseigner le nom complet du chemin. Pour pallier ce problème on peut passer par la fonctionexecvpe
qui elle prend en compte les chemins quePATH
contient. Cette fonction est une surcouche deexecve
. Il en existe d'autres dont voici les prototypes :
#include <unistd.h>
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[]);
#define _GNU_SOURCE
int execvpe(const char *file, char *const argv[],
char *const envp[]);
Pour communiquer entre eux, les processus (programmes en cours d'exécution sur une machine) s'envoient des signaux. Ces signaux correspondent souvent à une requête d'un processus à un autre. Parmis les signaux existant, certains sont utiles pour :
Signal | Description |
---|---|
TERM |
Terminer (tuer) un processus |
STOP |
Mettre en pause un processus |
CONT |
Reprendre l'exécution d'un processus |
L'utilisation de la commande kill
n'est pas le seul moyen d'envoyer des signaux à des processus, il est également possible de le faire en utilisant des raccourcis clavier.
Plus précisément, l'utilisation de la touche Ctrl
(abrégé en ^
) plus une lettre permet d'effectuer des actions dans le terminal.
Voici une liste de celles les plus utilisées :
Raccourci | Description |
---|---|
^C |
Envoie le signal SIGTERM à un processus |
^D |
Ferme l'entrée standard |
^L |
Equivaut à clear |
^R |
Fait une recherche dans le terminal des commandes précédentes |
L'appel système clone
permet également la création de processus comme le fait fork mais avec plus de précision sur la manière de créer le processus.
Son prototype est le suivant :
#include <sched.h>
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
Les différences avec fork
se résume au points suivants:
- Au lieu de revenir à l'appel de clone, le fils appel directement la fonction
fn
. - On doit préciser ou s'exécutera le fils. Cela se fait en précisant l'argument
stack
qui indique ou sera la pile pour le processus créé.
Pour cet appel système, il faut allouer soit même de la mémoire dans le tas afin qu'une fonction y soit exécutée (On doit tout faire soit même).
Il est à noté que la mémoire à allouer pour renseigner la pile peut être obtenue avec l'appel système mmap
en donnant l'option MAP_STACK
.
Attention à bien considérer qu'une pile décroit (les addresses grandissent en descendant).
Ainsi, l'adresse à donner correspond à la dernière adresse du tableau allouer et non la première.
Même si fork
et clone
sont différents dans la manière de fonctionner, on peut toutefois utiliser clone
pour créer un nouveau processus.
Comme fork
, le retour de clone
donne le pid
du fils.
Ceci permet au parent de pouvoir attendre son fils avec l'appel système waitpid
.