8.7 KiB
title | date | categories | tags | |||||
---|---|---|---|---|---|---|---|---|
Les processus | 2018-09-18 |
|
|
Il ne faut pas confondre processus et programme, un processus étant une instance chargée en mémoire et en cours d'exécution d'un programmme. Plusieurs instance d'un programme peuvent s'exécuter en même temps.
À chaque processus est assicié un identidiant (PID) et un espace de stockage en mémoire pou y stocker la pile, les données, le code etc. (espace d'adressage).
Un programme contient toutes les informations nécessaire à la création du processus :
- un format binaire ELF (historiquement a.out, Coff)
- le point d'entrée de la première instruction
- les données
- les information de débogage
- les information sur les librairies partagées
Un processus est une entité abstraite connue du noyau aui lui alloue des ressources pour son execution. Du point de vue du noyau un processus consiste en :
- En mode user
- une portion d'espace mémoire
- le code du programme
- les variables auquelles le programme a accès
- em mode kernel
- Une structure de données de l'état du processus
- la table des fichiers ouverts
- la table de la mémoire allouée
- le répertoire courant
- la priorité
- le gestionnaire d'évènements (signaux, fin de programmes ...)
- les limites du processus
le PID
Il est créé par le noyau et aussi utilisé dans l'espace utilisateur (kill
,
ps
, nice
)
#include <unistd.h>
pid_t getpid(void)
Renvoi le PID du processus appelant. Cette fonction est ouvent utilisée par des routines qui créées des fichiers temporaires uniques.
pid_t
est un type abstrait pouvant être transtypé en int
, sa valeur maximale
est définie dans /proc/sys/kernel/pid_max
Démarrage d'un programme en C
Un programme en C commence toujours par la fonction main()
:
int main (int argc, char **argv) {
}
argc
: entier, nombres d'arguments passés**argv
: tableau de pointeurs, arguments passés
L’invocateur du programme (ex: le shell) exécute l’appel système execvp et utilise les arguments passés au programme comme paramètres.
exemple
$ mycommand --color -l -s
argv = ["mycommand", "--color", "-l", "-s"]
argc = 4
on récupèrera les paramètres avec le code c suivant :
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv){
for (int i=0; i<argc; i++){
printf("Argument %i value %s",i, argv[i]);
}
}
La boucle aurait pu aussi être écrite comme suit :
for(int i=0; argv[i] != NULL; i++){}
Terminer un programme
Il y a deux façons de terminer un programme :
- nomale : par la fin du
main()
, unexit()
: nettoyage de toutes les ressources, appel declose()
sur tous les descripteurs de fichiers etr appel du getionnaire de sorties (exit handlers) - anormale : par la réception d'un signal (kill par exemple)
Par conventionm un programme qui termine correctement renvoi 0, dinon il renvoi un entier positif.
atexit
#include <unistd.h>
int atexit(void (*function)(void));
La fonction atexit()
enregistre la fonction donnée pour que celle-ci soit
automatiquement appelée lorsque le programme se termine normalement avec exit(3)
ou lors de la fin de la fonction main. Les fonctions ainsi enregistrées sont
invoquées dans l'ordre inverse de leur enregistrement ; aucun argument n'est
transmis atexit()
renvoie 0 en cas de succès et une valeur non nulle en cas
d'échec
Mémoire allouée
La mémoire allouée à chaque processus est divisée en segments :
- le segment texte, en lecture seule contient les instructions en langage machine (le binaire su programme au format ELF)
- le segment des données initialisées qui contient les variables globales et statiques explicitement initialisées. Les valeurs de ces variables sont lues depuis le fichiers exécutables lorsqu'il est chargé.
- le segment des données non-initialisées aui contient des variables globales et statiques non explicitement initialisées (BSS)
- la pile appelées aussi stack est un segment dynamique aui s'étend et se réduit. Il contient des blocs de pile appelés stack frame par fonction appelée. Une frame contient les variables locales et la/les valeur(s) de retour de la fonction.
- le tas aussi appelé heap est une zone où la mémoire peut être dynamiquement allouée pour les variables à l'exécution du programme.
L'espace mémoire d'un processus est virtuel car celui-ci n'adresse pas directement la mémoire. Les processus sont isolées du noyau et le noyau maintient un tableau de pages mémoires aui adresse les pages de la mémoire physique. Un processus peut adressser plus de mémoire que le système en a de disponible.
Sous Unix, un processus est définis sous forme arborescente : chacun d'eux a un processus parent (mis à pat le 1 - processus d'init) et chacun a aucun, un ou plusieurs fils.
Création de processus
#include <unistd.h>
pid_t fork(void);
Après le fork()
, le processus parent continue normalement son exécution. Le
processus fils commence son exécution juste après. On distingue le parent du
fils par la valeur de retour du fork()
0 pour le fils, le PID du fils pour
le père). Le fils est une copie du père cependant seule les zones modifiées sont
vraiment copiées. Le segment text est partagé entre les deux
Après le fork()
, la table des fichiers est partagées entre le parent et
l'enfant. Ainsi si l'offset est modifié dans un des processus, il le sera aussi
pour l'autre. Cela permet entres autres de faire collaborer les processus sur
les mêmes données.
exemple de fork()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main (int argc, char **argv) {
pid_t pid;
printf("Starting process with PID=%d\n", getpid());
pid = fork();
if (pid < 0) {
perror("Unable to fork");
}
else if (pid == 0) {
printf("Starting child with PID=%d (my parent PID=%d)\n", getpid
}
else {
printf("Still in process with PID=%d\n", getpid());
}
printf("Finishing process with PID=%d\n", getpid());
exit(EXIT_SUCCESS);
}
Terminaison de processus
Quelle que soit la façon dont se termine un processus (père comme fils), le noyau nettoie les ressources allouées. Il stocke cependant le status de terminaison du fils jusqu'au moment ou le père en prend connaissance.
Lorsque le père se termine, le processus fils devient enfant de 1 (PID 1)
Processus zombies
Lorsqu'un processus enfant est terminé mais que le parent n'a toujours pas pris connaissance de sa terminaison, il devient alors un processus zombie. Ce dernier continue de consommer des ressources noyau (code de terminaison, entrée dans la table des processus etc.)
Attendre la fin d'un processus enfant
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
pid_t wait(int *status); // waitpid(-1, &status, 0);
// en cas de réussite, l'identifiant du processus fils
// terminé (ou changé) est renvoyé ; en cas d'erreur,
// la valeur de retour est -1
L'appel système waitpid()
permet d'attendre la fin d'un processus. C'est un
appel bloquant (sauf avec certaines options). Il retourne une erreur s'il n'y a
pas de processus fils. Lorsque le processus fils se termine, la variable
status
contient son code de retour.
Création d'un nouveau processus
Sous Unix, la création d'un nouveau processus et l'exécution d'un programme sont deux choses distinctes. L'exécution d'un nouveau processus ne créé pas de nouveau processus, il remplace juste le programme appelant en allant chercher les données sur le système de fichiers.
À l'exécution, tous les segments son réinitialisés comme si le programme avait été exécuté normalement.
L'exécution d'un programme se fait grâce à l'appel execve()
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ..., (char *) NULL);
int execlp(const char *file, const char *arg, ..., (char *) NULL);
int execle(const char *path, const char *arg, ...,(char *) NULL, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
// Une sortie des fonctions exec() n'intervient que si une erreur s'est
// produite. La valeur de retour est -1, et errno contient le code
// d'erreur.
Le format ELF
ELF pour Executable and Linkable Format est utilisé pour l'exécution de code compilé. Dévellopé par Unix System Labortatories, il est utilisé par Linux, BSD, System V, IRIX, SPARC.