Processus

Une instance d’un programme en cours d’exécution est appelée un processus. Si vous avez deux fenêtres de terminal sur votre écran, vous exécutez probablement deux fois le même programme de terminal – vous avez deux processus de terminal. Chaque fenêtre de terminal exécute probablement un shell ; chaque shell en cours d’exécution est un processus indépendant. Lorsque vous invoquez un programme depuis le shell, le programme correspondant est exécuté dans un nouveau processus ; le processus shell reprend lorsque ce processus se termine.

Les programmeurs expérimentés utilisent souvent plusieurs processus coopérant entre eux au sein d’une même application afin de lui permettre de faire plus d’une chose à la fois, pour améliorer sa robustesse et utiliser des programmes déjà existants.

La plupart des fonctions de manipulation de processus décrites dans ce chapitre sont similaires à celles des autres systèmes UNIX. La majorité d’entre elles est déclarée au sein de l’entête <unistd.h> ; vérifiez la page de manuel de chaque fonction pour vous en assurer.

Aperçu des processus

Même lorsque tout ce que vous faites est être assis devant votre ordinateur, des processus sont en cours d’exécution. Chaque programme utilise un ou plusieurs processus. Commençons par observer les processus déjà présents sur votre ordinateur.

Identifiants de processus

Chaque processus d’un système Linux est identifié par son identifiant de processus unique, quelquefois appelé pid (process ID). Les identifiants de processus sont des nombres de 16 bits assignés de façon séquentielle par Linux aux processus nouvellement créés.

Chaque processus a également un processus parent (sauf le processus spécial init, décrit dans la Section 3.4.3, « Processus Zombies »). Vous pouvez donc vous représenter les processus d’un système Linux comme un arbre, le processus init étant la racine. L’identifiant de processus parent (parent process ID), ou ppid, est simplement l’identifiant du parent du processus.

Lorsque vous faites référence aux identifiants de processus au sein d’un programme C ou C++, utilisez toujours le typedef pid_t, défini dans <sys/types.h>. Un programme peut obtenir l’identifiant du processus au sein duquel il s’exécute en utilisant l’appel système getpid() et l’identifiant de son processus parent avec l’appel système getppid(). Par exemple, le programme du Listing !!printpid!! affiche son identifiant de processus ainsi que celui de son parent.

Listing (print-pid.c) Afficher l’identifiant de processus

#include <stdio.h>
#include <unistd.h>
 
int main ()
{
  printf ("L'identifiant du processus est %d\n", (int) getpid ());
  printf ("L'identifiant du processus parent est %d\n", (int) getppid ());
  return 0;
}

Notez que si vous invoquez ce programme plusieurs fois, un identifiant de processus différent est indiqué à chaque fois car chaque invocation crée un nouveau processus. Cependant, si vous l’invoquez toujours depuis le même shell, l’identifiant du processus parent (c’est-à-dire l’identifiant de processus du shell) est le même.

Voir les processus actifs

La commande ps affiche les processus en cours d’exécution sur votre système. La version GNU/Linux de ps dispose d’un grand nombre d’options car elle tente d’être compatible avec les versions de ps de plusieurs autres variantes d’UNIX. Ces options contrôlent les processus listés et les informations les concernant qui sont affichées.

Par défaut, invoquer ps affiche les processus contrôlés par le terminal ou la fenêtre de terminal où la commande est invoquée. Par exemple :

% ps
  PID TTY       TIME CMD
21693 pts/8 00:00:00 bash
21694 pts/8 00:00:00 ps

Cette invocation de ps montre deux processus. Le premier, bash, est le shell s’exécutant au sein du terminal. Le second est l’instance de ps en cours d’exécution. La première colonne, PID, indique l’identifiant de chacun des processus.

Pour un aperçu plus détaillé des programmes en cours d’exécution sur votre système GNU/Linux, invoquez cette commande :

% ps -e -o pid,ppid,command

L’option -e demande à ps d’afficher tous processus en cours d’exécution sur le système. L’option -o pid,ppid,command indique à ps les informations à afficher sur chaque processus – dans ce cas, l’identifiant de processus, l’identifiant du processus parent et la commande correspondant au processus.

Formats de sortie ps
Avec l’option -o de la commande ps, vous indiquez les informations sur les processus que vous voulez voir s’afficher sous forme d’une liste de valeurs séparées par des virgules. Par exemple, ps -o pid,user,start_time,command affiche l’identifiant de processus, le nom de l’utilisateur propriétaire du processus, l’heure de démarrage du processus et la commande s’exécutant au sein du processus. Consultez la page de manuel de ps pour une liste complète des codes. Vous pouvez utiliser les options -f (full listing, listing complet), -l (long listing) ou -j (jobs listing) pour obtenir trois formats de listing prédéfinis.

Voici les premières et dernières lignes de la sortie de cette commande sur mon système. Elles peuvent différer des vôtres, selon les programmes en cours d’exécution sur votre système :

% ps -e -o pid,ppid,command
  PID PPID COMMAND
    1     0 init [5]
    2     1 [kflushd]
    3     1 [kupdate]
...
21725 21693 xterm
21727 21725 bash
21728 21727 ps -e -o pid,ppid,command

Notez que l’identifiant du processus parent de la commande ps, 21727, est l’identifiant du processus de bash, le shell depuis lequel j’ai invoqué ps. L’identifiant du processus parent de bash est 21725, l’identifiant du processus du programme xterm dans lequel le shell s’exécute.

Tuer un processus

Vous pouvez tuer un processus en cours d’exécution grâce à la commande kill. Spécifiez simplement sur la ligne de commande l’identifiant du processus à tuer.

La commande kill envoie un signal1) SIGTERM, ou de terminaison au processus. Cela termine le processus, à moins que le programme en cours d’exécution ne gère ou ne masque le signal SIGTERM. Les signaux sont décrits dans la Section 3.3, « Signaux ».

Créer des processus

Deux techniques courantes sont utilisées pour créer de nouveaux processus. La première est relativement simple mais doit être utilisée avec modération car elle est peu performante et présente des risques de sécurité considérables. La seconde technique est plus complexe mais offre une flexibilité, une rapidité et une sécurité plus grandes.

Utiliser system

La fonction system de la bibliothèque standard propose une manière simple d’exécuter une commande depuis un programme, comme si la commande avait été tapée dans un shell. En fait, system crée un sous-processus dans lequel s’exécute le shell Bourne standard (/bin/sh) et passe la commande à ce shell pour qu’il l’exécute. Par exemple, le programme du Listing !!system!! invoque la commande ls pour afficher le contenu du répertoire racine, comme si vous aviez saisi ls -l / dans un shell.

Listing (system.c) Utiliser la fonction system

int main ()
{
  int return_value;
  return_value = system ("ls -l /");
  return return_value;
}

La fonction system renvoie le code de sortie de la commande shell. Si le shell lui-même ne peut pas être lancé, system renvoie 127 ; si une autre erreur survient, system renvoie -1.

Comme la fonction system utilise un shell pour invoquer votre commande, elle est soumise aux fonctionnalités, limitations et failles de sécurité du shell système. Vous ne pouvez pas vous reposer sur la disponibilité d’une version spécifique du shell Bourne. Sur beaucoup de systèmes UNIX, /bin/sh est en fait un lien symbolique vers un autre shell. Par exemple, sur la plupart des systèmes GNU/Linux, /bin/sh pointe vers bash (le Bourne-Again SHell) et des distributions différentes de GNU/Linux utilisent à priori des versions différentes de bash. Invoquer un programme avec les privilèges root via la fonction system peut donner des résultats différents selon le système GNU/Linux. Il est donc préférable d’utiliser la méthode de fork et exec pour créer des processus.

Utiliser fork et exec

Les API DOS et Windows proposent la famille de fonctions spawn. Ces fonctions prennent en argument le nom du programme à exécuter et créent une nouvelle instance de processus pour ce programme. Linux ne dispose pas de fonction effectuant tout cela en une seule fois. Au lieu de cela, Linux offre une fonction, fork, qui produit un processus fils qui est l’exacte copie de son processus parent. Linux fournit un autre jeu de fonctions, la famille exec, qui fait en sorte qu’un processus cesse d’être une instance d’un certain programme et devienne une instance d’un autre. Pour créer un nouveau processus, vous utilisez tout d’abord fork pour créer une copie du processus courant. Puis vous utilisez exec pour transformer un de ces processus en une instance du programme que vous voulez créer.

Appeler fork et exec

Lorsqu’un programme appelle fork, une copie du processus, appelée processus fils, est créée. Le processus parent continue d’exécuter le programme à partir de l’endroit où fork a été appelée. Le processus fils exécute lui aussi le même programme à partir du même endroit.

Qu’est-ce qui différencie les deux processus alors ? Tout d’abord, le processus fils est un nouveau processus et dispose donc d’un nouvel identifiant de processus, distinct de celui de l’identifiant de son processus parent. Un moyen pour un programme de savoir s’il fait partie du processus père ou du processus fils est d’appeler la méthode getpid(). Cependant, la fonction fork renvoie des valeurs différentes aux processus parent et enfant – un processus « rentre » dans le fork et deux en « ressortent » avec des valeurs de retour différentes. La valeur de retour dans le processus père est l’identifiant du processus fils. La valeur de retour dans le processus fils est zéro. Comme aucun processus n’a l’identifiant zéro, il est facile pour le programme de déterminer s’il s’exécute au sein du processus père ou du processus fils.

Le Listing !!fork!! est un exemple d’utilisation de fork pour dupliquer un processus. Notez que le premier bloc de la structure if n’est exécuté que dans le processus parent alors que la clause else est exécutée dans le processus fils.

Listing (fork.c) Utiliser fork pour dupliquer un processus

int main ()
{
  pid_t child_pid;
  printf ("ID de processus du programme principal : %d\n",(int)getpid ());
  child_pid = fork ();
  if (child_pid != 0) {
    printf ("je suis le processus parent, ID : %d\n", (int) getpid ());
    printf ("Identifiant du processus fils : %d\n", (int) child_pid);
  }
  else
    printf ("je suis le processus fils, ID : %d\n", (int) getpid ());
  return 0;
}

Utiliser la famille de exec

Les fonctions exec remplacent le programme en cours d’exécution dans un processus par un autre programme. Lorsqu’un programme appelle la fonction exec, le processus cesse immédiatement d’exécuter ce programme et commence l’exécution d’un autre depuis le début, en supposant que l’appel à exec se déroule correctement.

Au sein de la famille de exec existent plusieurs fonctions qui varient légèrement quant aux possibilités qu’elles proposent et à la façon de les appeler.

  • Les fonctions qui contiennent la lettre p dans leur nom (execvp et execlp) reçoivent un nom de programme qu’elles recherchent dans le path courant ; il est nécessaire de passer le chemin d’accès complet du programme aux fonctions qui ne contiennent pas de p.
  • Les fonctions contenant la lettre v dans leur nom (execv, execvp et execve) reçoivent une liste d’arguments à passer au nouveau programme sous forme d’un tableau de pointeurs vers des chaînes terminé par NULL. Les fonctions contenant la lettre l (execl, execlp et execle) reçoivent la liste d’arguments via le mécanisme du nombre d’arguments variables du langage C.
  • Les fonctions qui contiennent la lettre e dans leur nom (execve et execle) prennent un argument supplémentaire, un tableau de variables d’environnement. L’argument doit être un tableau de pointeurs vers des chaînes terminé par NULL. Chaque chaîne doit être de la forme “VARIABLE=valeur”.

Dans la mesure où exec remplace le programme appelant par un autre, on n’en sort jamais à moins que quelque chose ne se déroule mal.

Les arguments passés à un programme sont analogues aux arguments de ligne de commande que vous transmettez à un programme lorsque vous le lancez depuis un shell. Ils sont accessibles par le biais des paramètres argc et argv de main(). Souvenez-vous que lorsqu’un programme est invoqué depuis le shell, celui-ci place dans le premier élément de la liste d’arguments (argv[0]) le nom du programme, dans le second (argv[1]) le premier paramètre en ligne de commande, etc. Lorsque vous utilisez une fonction exec dans votre programme, vous devriez, vous aussi, passer le nom du programme comme premier élément de la liste d’arguments.

Utiliser fork et exec

Un idiome courant pour l’exécution de sous-programme au sein d’un programme est d’effectuer un fork puis d’appeler exec pour le sous-programme. Cela permet au programme appelant de continuer à s’exécuter au sein du processus parent alors qu’il est remplacé par le sous-programme dans le processus fils.

Le programme du Listing !!forkexec!!, comme celui du Listing !!system!!, liste le contenu du répertoire racine en utilisant la commande ls. Contrairement à l’exemple précédent, cependant, il utilise la commande ls directement, en lui passant les arguments -l et / plutôt que de l’invoquer depuis un shell.

Listing (fork-exec.c) Utiliser fork et exec

#include  <stdio.h>
#include  <stdlib.h>
#include  <sys/types.h>
#include  <unistd.h>
/* Crée un processus fils exécutant un nouveau programme. PROGRAM est le nom 
   du programme à exécuter ; le programme est recherché dans le path.
   ARG_LIST est une liste terminée par NULL de chaînes de caractères à passer 
   au programme comme liste d'arguments. Renvoie l'identifiant du processus
   nouvellement créé. */
int spawn (char* program, char** arg_list)
{
  pid_t child_pid;
  /* Duplique ce processus. */
  child_pid = fork ();
  if (child_pid != 0)
     /* Nous sommes dans le processus parent. */
     return child_pid;
  else {
     /* Exécute PROGRAM en le recherchant dans le path. */
     execvp (program, arg_list);
     /* On ne sort de la fonction execvp uniquement si une erreur survient. */
     fprintf (stderr, "une erreur est survenue au sein de execvp\n");
     abort ();
  }
}
int main ()
{
  /* Liste d'arguments à passer à la commande "ls". */
  char* arg_list[] = {
     "ls",     /* argv[0], le nom du programme. */
     "-l",
     "/",
     NULL      /* La liste d'arguments doit se terminer par NULL.  */
  };
  /* Crée un nouveau processus fils exécutant la commande "ls". Ignore
      l'identifiant du processus fils renvoyé. */
  spawn ("ls", arg_list);
  printf ("Fin du programme principal\n");
  return 0;
}

Ordonnancement de processus

Linux ordonnance le processus père indépendamment du processus fils ; il n’y a aucune garantie sur celui qui sera exécuté en premier ou sur la durée pendant laquelle le premier s’exécutera avant que Linux ne l’interrompe et ne passe la main à l’autre processus (ou à un processus quelconque s’exécutant sur le système). Dans notre cas, lorsque le processus parent se termine, la commande ls peut s’être exécutée entièrement, partiellement ou pas du tout2). Linux garantit que chaque processus finira par s’exécuter – aucun processus ne sera à cours de ressources.

Vous pouvez indiquer qu’un processus est moins important – et devrait avoir une priorité inférieure – en lui assignant une valeur de priorité d’ordonnancement3) plus grande. Par défaut, chaque processus a une priorité d’ordonnancement de zéro. Une valeur plus élevée signifie que le processus a une priorité d’exécution plus faible ; inversement un processus avec une priorité d’ordonnancement plus faible (c’est-à-dire, négative) obtient plus de temps d’exécution.

Pour lancer un programme avec une priorité d’ordonnancement différente de zéro, utilisez la commande nice, en spécifiant la valeur de la priorité d’ordonnancement avec l’option -n. Par exemple, voici comment vous pourriez invoquer la commande sort input.txt > output.txt, une opération de tri longue, avec une priorité réduite afin qu’elle ne ralentisse pas trop le système :

% nice -n 10 sort input.txt > output.txt

Vous pouvez utiliser la commande renice pour changer la priorité d’ordonnancement d’un processus en cours d’exécution depuis la ligne de commande.

Pour changer la priorité d’ordonnancement d’un processus en cours d’exécution par programmation, utilisez la fonction nice. Son argument est une valeur d’ajustement qui est ajoutée à la valeur de la priorité d’ordonnancement du processus qui l’appelle. Souvenez-vous qu’une valeur positive augmente la priorité d’ordonnancement du processus et donc réduit la priorité d’exécution du processus.

Notez que seuls les processus disposant des privilèges root peuvent lancer un processus avec une priorité d’ordonnancement négative ou réduire la valeur de la priorité d’ordonnancement d’un processus en cours d’exécution. Cela signifie que vous ne pouvez passer des valeurs négatives aux commandes nice et renice que lorsque vous êtes connecté en tant que root et seul un processus s’exécutant avec les privilèges root peut passer une valeur négative à la fonction nice. Cela évite que des utilisateurs ordinaires puissent s’approprier tout le temps d’exécution.

Signaux

Les signaux sont des mécanismes permettant de manipuler et de communiquer avec des processus sous Linux. Le sujet des signaux est vaste; nous traiterons ici quelques uns des signaux et techniques utilisés pour contrôler les processus.

Un signal est un message spécial envoyé à un processus. Les signaux sont asynchrones ; lorsqu’un processus reçoit un signal, il le traite immédiatement, sans même terminer la fonction ou la ligne de code en cours. Il y a plusieurs douzaines de signaux différents, chacun ayant une signification différente. Chaque type de signal est caractérisé par son numéro de signal, mais au sein des programmes, on y fait souvent référence par un nom. Sous Linux, ils sont définis dans /usr/include/bits/signum.h (vous ne devriez pas inclure ce fichier directement dans vos programmes, utilisez plutôt <signal.h>).

Lorsqu’un processus reçoit un signal, il peut agir de différentes façons, selon l’action enregistrée pour le signal. Pour chaque signal, il existe une action par défaut, qui détermine ce qui arrive au processus si le programme ne spécifie pas d’autre comportement. Pour la plupart des signaux, le programme peut indiquer un autre comportement – soit ignorer le signal, soit appeler un gestionnaire de signal, fonction chargée de traiter le signal. Si un gestionnaire de signal est utilisé, le programme en cours d’exécution est suspendu, le gestionnaire est exécuté, puis, une fois celui-ci terminé, le programme reprend.

Le système Linux envoie des signaux aux processus en réponse à des conditions spécifiques. Par exemple, SIGBUS (erreur de bus), SIGSEGV (erreur de segmentation) et SIGFPE (exception de virgule flottante) peuvent être envoyés à un programme essayant d’effectuer une action non autorisée. L’action par défaut pour ces trois signaux est de terminer le processus et de produire un ficher core.

Un processus peut également envoyer des signaux à un autre processus. Une utilisation courante de ce mécanisme est de terminer un autre processus en lui envoyant un signal SIGTERM ou SIGKILL4). Une autre utilisation courante est d’envoyer une commande à un programme en cours d’exécution. Deux signaux « définis par l’utilisateur » sont réservés à cet effet : SIGUSR1 et SIGUSR2. Le signal SIGHUP est également parfois utilisé dans ce but, habituellement pour réveiller un programme inactif ou provoquer une relecture du fichier de configuration.

La fonction sigaction peut être utilisée pour paramétrer l’action à effectuer en réponse à un signal. Le premier paramètre est le numéro du signal. Les deux suivants sont des pointeurs vers des structures sigaction ; la première contenant l’action à effectuer pour ce numéro de signal, alors que la seconde est renseignée avec l’action précédente. Le champ le plus important, que ce soit dans la première ou la seconde structure sigaction est sa_handler. Il peut prendre une des trois valeurs suivantes :

  • SIG_DFL, qui correspond à l’action par défaut pour le signal ;
  • SIG_IGN, qui indique que le signal doit être ignoré ;
  • Un pointeur vers une fonction de gestion de signal. La fonction doit prendre un paramètre, le numéro du signal et être de type void.

Comme les signaux sont asynchrones, le programme principal peut être dans un état très fragile lorsque le signal est traité et donc pendant l’exécution du gestionnaire de signal. C’est pourquoi vous devriez éviter d’effectuer des opérations d’entrées/sorties ou d’appeler la plupart des fonctions système ou de la bibliothèque C depuis un gestionnaire de signal.

Un gestionnaire de signal doit effectuer le minimum nécessaire au traitement du signal, puis repasser le contrôle au programme principal (ou terminer le programme). Dans la plupart des cas, cela consiste simplement à enregistrer que le signal est survenu. Le programme principal vérifie alors périodiquement si un signal a été reçu et réagit en conséquence.

Il est possible qu’un gestionnaire de signal soit interrompu par l’arrivée d’un autre signal. Bien que cela puisse sembler être un cas rare, si cela arrive, il peut être très difficile de diagnostiquer et résoudre le problème (c’est un exemple de conditions de concurrence critique, traité dans le Chapitre 4, « Threads », Section 4.4, « Synchronisation et Sections Critiques »). C’est pourquoi vous devez être très attentif à ce que votre programme fait dans un gestionnaire de signal.

Même l’affectation d’une valeur à une variable globale peut être dangereuse car elle peut en fait être effectuée en deux instructions machine ou plus, et un second signal peut survenir, laissant la variable dans un état corrompu. Si vous utilisez une variable globale pour indiquer la réception d’un signal depuis un gestionnaire de signal, elle doit être du type spécial sig_atomic_t. Linux garantit que les affectations de valeur à des variables de ce type sont effectuées en une seule instruction et ne peuvent donc pas être interrompues. Sous Linux, sig_atomic_t est un int ordinaire ; en fait, les affectations de valeur à des types de la taille d’un int ou plus petits ou à des pointeurs, sont atomiques. Néanmoins, si vous voulez écrire un programme portable vers n’importe quel système UNIX standard, utilisez le type sig_atomic_t.

Le squelette de programme du Listing !!sigusr1!!, par exemple, utilise une fonction de gestion de signal pour compter le nombre de fois où le programme reçoit le signal SIGUSR1, un des signaux utilisables par les applications.

Listing (sigusr1.c) Utiliser un gestionnaire de signal

#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
sig_atomic_t sigusr1_count = 0;
void handler (int signal_number)
{
  ++sigusr1_count;
}
int main ()
{
  struct sigaction sa;
  memset (&sa, 0, sizeof (sa));
  sa.sa_handler = &handler;
  sigaction (SIGUSR1, &sa, NULL);
  /* Faire quelque chose de long ici.  */
  /* ... */
  printf ("SIGUSR1 a été reçu %d fois\n", sigusr1_count);
  return 0;
}

Fin de processus

Dans des conditions normales, un processus peut se terminer de deux façons : soit le programme appelle la fonction exit(), soit la fonction main() du programme se termine. Chaque processus a un code de sortie : un nombre que le processus retourne à son parent. Le code de sortie est l’argument passé à la fonction exit() ou la valeur retournée depuis main().

Un processus peut également se terminer de façon anormale, en réponse à un signal. Par exemple, les signaux SIGBUS, SIGSEGV et SIGFPE évoqués précédemment provoquent la fin du processus. D’autres signaux sont utilisés pour terminer un processus explicitement. Le signal SIGINT est envoyé à un processus lorsque l’utilisateur tente d’y mettre fin en saisissant Ctrl+C dans son terminal. Le signal SIGTERM est envoyé par la commande kill. L’action par défaut pour ces deux signaux est de mettre fin au processus. En appelant la fonction abort, un processus s’envoie à lui-même le signal SIGABRT ce qui termine le processus et produit un fichier core. Le signal de terminaison le plus puissant est SIGKILL qui met fin à un processus immédiatement et ne peut pas être bloqué ou géré par un programme.

Chacun de ces signaux peut être envoyé en utilisant la commande kill en passant une option supplémentaire sur la ligne de commande ; par exemple, pour terminer un programme fonctionnant mal en lui envoyant un SIGKILL, invoquez la commande suivante, où pid est un identifiant de processus :

% kill -KILL pid

Pour envoyer un signal depuis un programme, utilisez la fonction kill. Le premier paramètre est l’identifiant du processus cible. Le second est le numéro du signal ; utilisez SIGTERM pour simuler le comportement par défaut de la commande kill. Par exemple, si child_pid contient l’identifiant du processus fils, vous pouvez utiliser la fonction kill pour terminer un processus fils depuis le père en l’appelant comme ceci :

kill (child_pid, SIGTERM);

Incluez les entêtes <sys/types.h> et <signal.h> si vous utilisez la fonction kill.

Par convention, le code de sortie est utilisé pour indiquer si le programme s’est exécuté correctement. Un code de sortie à zéro indique une exécution correcte, alors qu’un code différent de zéro indique qu’une erreur est survenue. Dans ce dernier cas, la valeur renvoyée peut donner une indication sur la nature de l’erreur. C’est une bonne idée d’adopter cette convention dans vos programmes car certains composants du système GNU/Linux reposent dessus. Par exemple, les shells supposent l’utilisation de cette convention lorsque vous connectez plusieurs programmes avec les opérateurs && (et logique) et || (ou logique). C’est pour cela que vous devriez retourner zéro explicitement depuis votre fonction main(), à moins qu’une erreur ne survienne.

Avec la plupart des shells, il est possible d’obtenir le code de sortie du dernier programme exécuté en utilisant la variable spéciale $?. Voici un exemple dans lequel la commande ls est invoquée deux fois et son code de sortie est affiché après chaque invocation. Dans le premier cas, ls se termine correctement et retourne le code de sortie 0. Dans le second cas, ls rencontre une erreur (car le fichier spécifié sur la ligne de commande n’existe pas) et renvoie donc un code de sortie différent de 0.

% ls /
bin   coda etc    lib         misc nfs proc
boot dev    home lost+found mnt     opt root
% echo $?
0
% ls fichierinexistant
ls: fichierinexistant: Aucun fichier ou répertoire de ce type
% echo $?
1

Notez que même si le paramètre de la fonction exit() est de type int et que la fonction main() renvoie un int, Linux ne conserve pas les 32 bits du code de sortie. En fait, vous ne devriez utiliser que des codes de sortie entre 0 et 127. Les codes de sortie au dessus de 128 ont une signification spéciale – lorsqu’un processus se termine à cause d’un signal, son code de sortie est zéro plus le numéro du signal.

Attendre la fin d'un processus

Si vous avez saisi et exécuté l’exemple de fork et exec du Listing !!forkexec!!, vous pouvez avoir remarqué que la sortie du programme ls apparaît souvent après la fin du programme principal. Cela est dû au fait que le processus fils, au sein duquel s’exécute ls, est ordonnancé indépendamment du processus père. Comme Linux est un système d’exploitation multitâche, les deux processus semblent s’exécuter simultanément et vous ne pouvez pas prédire si le programme ls va s’exécuter avant ou après le processus père.

Dans certaines situations, cependant, il est souhaitable que le processus père attende la fin d’un ou plusieurs processus fils. Pour cela, il est possible d’utiliser les appels systèmes de la famille de wait. Ces fonctions vous permettent d’attendre la fin d’un processus et permettent au processus parent d’obtenir des informations sur la façon dont s’est terminé son fils. Il y a quatre appels système différents dans la famille de wait ; vous pouvez choisir de récupérer peu ou beaucoup d’informations sur le processus qui s’est terminé et vous pouvez indiquer si vous voulez savoir quel processus fils s’est terminé.

Les appels système wait

La fonction la plus simple s’appelle simplement wait. Elle bloque le processus appelant jusqu’à ce qu’un de ses processus fils se termine (ou qu’une erreur survienne). Elle retourne un code de statut via un pointeur sur un entier, à partir duquel vous pouvez extraire des informations sur la façon dont s’est terminé le processus fils. Par exemple, la macro WEXITSTATUS extrait le code de sortie du processus fils.

Vous pouvez utiliser la macro WIFEXITED pour déterminer si un processus s’est terminé correctement à partir de son code de statut (via la fonction exit ou la sortie de main) ou est mort à cause d’un signal non intercepté. Dans ce dernier cas, utilisez la macro WTERMSIG pour extraire le numéro du signal ayant causé la mort du processus à partir du code de statut.

Voici une autre version de la fonction main de l’exemple de fork et exec. Cette fois, le processus parent appelle wait pour attendre que le processus fils, dans lequel s’exécute la commande ls, se termine.

int main ()
{
  int child_status;
  /* Liste d'arguments à passer à la commande "ls". */
  char* arg_list[] = {
     "ls",     /* argv[0], le nom du programme. */
     "-l",
     "/",
     NULL      /* La liste d'arguments doit se terminer par NULL.  */
  };
  /* Crée un nouveau processus fils exécutant la commande "ls". Ignore
      l'identifiant du processus fils renvoyé. */
  spawn ("ls", arg_list);
  /* Attend la fin du processus fils. */
  wait (&child_status);
  if (WIFEXITED (child_status))
    printf ("processus fils terminé normalement, le code de sortie %d\n",
             WEXITSTATUS (child_status));
  else
    printf ("processus fils terminé anormalement\n");
  return 0;
}

Plusieurs appels système similaires sont disponibles sous Linux, ils sont plus flexibles ou apportent plus d’informations sur le processus fils se terminant. La fonction waitpid peut être utilisée pour attendre la fin d’un processus fils spécifique au lieu d’attendre n’importe quel processus fils. La fonction wait3 renvoie des statistiques sur l’utilisation du processeur par le processus fils se terminant et la fonction wait4 vous permet de spécifier des options supplémentaires quant au processus dont on attend la fin.

Processus zombies

Si un processus fils se termine alors que son père appelle la fonction wait, le processus fils disparaît et son statut de terminaison est transmis à son père via l’appel à wait. Mais que se passe-t-il lorsqu’un processus se termine et que son père n’appelle pas wait ? Disparaît-il tout simplement ? Non, car dans ce cas, les informations concernant la façon dont il s’est terminé – comme le fait qu’il se soit terminé normalement ou son code de sortie – seraient perdues. Au lieu de cela, lorsqu’un processus fils se termine, il devient un processus zombie.

Un processus zombie est un processus qui s’est terminé mais dont les ressources n’ont pas encore été libérées. Il est de la responsabilité du processus père de libérer les ressources occupées par ses fils zombies. La fonction wait le fait, il n’est donc pas nécessaire de savoir si votre processus fils est toujours en cours d’exécution avant de l’attendre. Supposons, par exemple, qu’un programme crée un processus fils, fasse un certain nombre d’autres opérations puis appelle wait. Si le processus fils n’est pas encore terminé à ce moment, le processus parent sera bloqué dans l’appel de wait jusqu’à ce que le processus fils se termine. Si le processus fils se termine avant que le père n’appelle wait, il devient un processus zombie. Lorsque le processus parent appelle wait, le statut de sortie du processus fils est extrait, le processus fils est supprimé et l’appel de wait se termine immédiatement.

Que se passe-t-il si le père ne libère pas les ressources de ses fils ? Ils restent dans le système, sous la forme de processus zombies. Le programme du Listing !!zombie!! crée un processus fils qui se termine immédiatement, puis s’interrompt pendant une minute, sans jamais libérer les ressources du processus fils.

Listing (zombie.c) Créer un processus zombie

#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main ()
{
  pid_t child_pid;
  /* Crée un processus fils. */
  child_pid = fork ();
  if (child_pid > 0) {
    /* Nous sommes dans le processus parent. Attente d'une minute. */
    sleep (60);
  }
  else {
    /* Nous sommes dans le processus fils. Sortie immédiate. */
    exit (0);
  }
  return 0;
}

Compilez ce fichier en un exécutable appelé make-zombie. Exécutez-le, et pendant ce temps, listez les processus en cours d’exécution sur le système par le biais de la commande suivante dans une autre fenêtre :

% ps -e -o pid,ppid,stat,cmd

Elle dresse la liste des identifiants de processus, de processus pères, de leur statut et de la ligne de commande du processus. Observez qu’en plus du processus père make-zombie, un autre processus make-zombie est affiché. Il s’agit du processus fils ; notez que l’identifiant de son père est celui du processus make-zombie principal. Le processus fils est marqué comme <defunct> et son code de statut est Z pour zombie.

Que se passe-t-il lorsque le programme principal de make-zombie se termine sans appeler wait ? Le processus zombie est-il toujours présent ? Non – essayez de relancer ps et notez que les deux processus make-zombie ont disparu. Lorsqu’un programme se termine, un processus spécial hérite de ses fils, le programme init, qui s’exécute toujours avec un identifiant de processus valant 1 (il s’agit du premier processus lancé lorsque Linux démarre). Le processus init libère automatiquement les ressources de tout processus zombie dont il hérite.

Libérer les ressources des fils de façon asynchrone

Si vous utilisez un processus fils uniquement pour appeler exec, il est suffisant d’appeler wait immédiatement dans le processus parent, ce qui le bloquera jusqu’à ce que le processus fils se termine. Mais souvent, vous voudrez que le processus père continue de s’exécuter alors qu’un ou plusieurs fils s’exécutent en parallèle. Comment être sûr de libérer les ressources occupées par tous les processus fils qui se sont terminés afin de ne pas laisser de processus zombie dans la nature, ce qui consomme des ressources ?

Une approche envisageable serait que le processus père appelle périodiquement wait3 et wait4 pour libérer les ressources des fils zombies. Appeler wait de cette façon ne fonctionne pas de manière optimale car si aucun fils ne s’est terminé, l’appelant sera bloqué jusqu’à ce que ce soit le cas. Cependant, wait3 et wait4 prennent un paramètre d’option supplémentaire auquel vous pouvez passer le drapeau WNOHANG. Avec ce drapeau, la fonction s’exécute en mode non bloquant – elle libérera les ressources d’un processus fils terminé s’il y en a un ou se terminera s’il n’y en a pas. La valeur de retour de l’appel est l’identifiant du processus fils s’étant terminé dans le premier cas, zéro dans le second.

Une solution plus élégante est d’indiquer au processus père quand un processus fils se termine. Il y a plusieurs façons de le faire en utilisant les méthodes présentées dans le Chapitre 5, « Communication Interprocessus », mais heureusement, Linux le fait pour vous, en utilisant les signaux. Lorsqu’un processus fils se termine, Linux envoie au processus père le signal SIGCHLD. L’action par défaut pour ce signal est de ne rien faire, c’est la raison pour laquelle vous pouvez ne jamais l’avoir remarqué auparavant.

Donc, une façon élégante de libérer les ressources des processus fils est de gérer SIGCHLD. Bien sûr, lorsque vous libérez les ressources du processus fils, il est important de stocker son statut de sortie si cette information est nécessaire, car une fois que les ressources du processus fils ont été libérées par wait, cette information n’est plus disponible. Le Listing !!sigchld!! montre à quoi ressemble un programme utilisant un gestionnaire pour SIGCHLD afin de libérer les ressources de ses processus fils.

Listing (sigchld.c) Libérer les ressources des fils via SIGCHLD

#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
sig_atomic_t child_exit_status;
void clean_up_child_process (int signal_number)
{
  /* Nettoie le ou les processus fils. */
  int status;
  while(waitpid (-1, &status, WNOHANG));
  /* Stocke la statut de sortie du dernier dans une variable globale. */
  child_exit_status = status;
}
int main ()
{
  /* Gère SIGCHLD en appelent clean_up_child_process. */
  struct sigaction sigchld_action;
  memset (&sigchld_action, 0, sizeof (sigchld_action));
  sigchld_action.sa_handler = &clean_up_child_process;
  sigaction (SIGCHLD, &sigchld_action, NULL);
  /* Faire diverses choses, entre autres créer un processus fils. */
  /* ... */
  return 0;
}

Notez comment le gestionnaire de signal stocke le statut de sortie du processus fils dans une variable globale, à laquelle le programme principal peut accéder. Comme la variable reçoit une valeur dans un gestionnaire de signal, elle est de type sig_atomic_t.

1) Vous pouvez également utiliser la commande kill pour envoyer d’autres signaux à un processus. Reportez-vous à la Section 3.4, « Fin de Processus »
2) Une méthode visant à exécuter les deux processus de manière séquentielle est présentée dans la Section 3.4.1, « Attendre la Fin d’un Processus ».
3) NdT. le terme original est niceness (gentillesse), ce qui explique que plus la valeur est grande, plus le processus est « gentil » et donc moins il est prioritaire.
4) Quelle est la différence? Le signal SIGTERM demande au processus de se terminer ; le processus peut ignorer la requête en masquant ou ignorant le signal. Le signal SIGKILL tue toujours le processus immédiatement car il est impossible de masquer ou ignorer SIGKILL.
 
livre/chap3/processus.txt · Dernière modification: 2007/09/01 14:28 par david
 
Recent changes RSS feed Creative Commons License Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki Hosted by TuxFamily