Les processus sous Unix

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 04/08/2000 - Dernière mise à jour : 05/09/2001

Sommaire

Introduction
L'environnement d'un processus
Variables d'environnement
Environnement et fonction main()
Variable environ
Environnement et lancement d'un exécutable
Les fonctions système
Les fonctions système concernant directement les processus
Autres fonctions système liées à l'existence des processus
Autres fonctions système
Groupes de processus

Introduction

    L'un des rôles essentiels d'un système d'exploitation multitâches est de gérer les processus qui tournent en ("pseudo")-parallélisme. Cette gestion représente de très nombreuses activités. Par commodité, et parce que cela correspond à la réalité de certains systèmes d'exploitation, il est habituel de présenter le système d'exploitation comme une activité à laquelle un processus "client" ou "utilisateur" s'adresse pour obtenir de lui un service, en appelant une "fonction système". En fait, certains "services" du système sont rendus par le processus client lui-même : en appelant une fonction système, il bascule en mode noyau (kernel) et devient lui-même le système, ayant alors le droit d'effectuer certaines opérations (par exemple lire ou écrire dans un buffer de fichier). D'autres parties du système d'exploitation peuvent être elles-mêmes des processus indépendants, gérant les communications, les timers, les disques, etc. Cela n'a aucune importance pour l'utilisateur, qui dispose d'une interface de fonctions, sans savoir à quel traitement elles correspondent. On peut classer les activités du système d'exploitation en. différentes catégories :     Un processus occupe toujours un élément d'un tableau appelé "table des processus". Dès sa création et jusqu'à sa disparition complète lui est affecté un numéro de processus, nombre entier, généralement symbolisé par pid (process identificator) qui est unique et suffit à l'identifier (c'est une sorte de handle). Un élément de la table des processus contient toutes les informations concernant un processus[1]  : c'est le bloc de contrôle du processus ou PCB (Process Control Bloc). On y trouve en particulier les tables de description de la mémoire virtuelle du processus (voir le cours système), la table de description des traitants de signaux (voir TP précédent), celle des descripteurs de fichiers (idem), bref tout ce qui concerne le "contexte du processus". A un moment donné, un des processus est actif, c'est-à-dire en cours d'exécution, il suffit donc qu'existe un pointeur vers l'élément correspondant du tableau. Changer de processus actif correspond donc (en partie du moins) à modifier le pointeur.

    Sous Unix, un processus est toujours créé par un autre processus (sauf bien sûr le premier). Le mécanisme est le suivant : tout se passe comme si le processus qui demande la création était totalement dupliqué (virtuellement). Il y a alors deux processus, appelés le père et le fils, pratiquement identiques (à très peu de détails près) qui continuent à se dérouler en faisant exactement la même chose. Le père est le processus qui a gardé le pid initial, le fils quant à lui a reçu un nouveau pid correspondant à un nouveau bloc de contrôle de processus dans la table.

    La seconde étape consiste souvent à substituer au code de l'un des processus (en principe le fils) un nouveau code exécutable, afin que les deux processus soient différents. Ce code est chargé à partir de la mémoire secondaire (disque).

    Il n'y a aucun lien entre des processus "frères" ou "cousins". En revanche, un processus reste attaché à son père jusqu'à la disparition de l'un des deux. Un processus a donc toujours un père. Si son "vrai" père disparaît avant lui, il est automatiquement rattaché à un père adoptif (le processus de pid 0 ou 1 selon les systèmes Unix). Lorsqu'un processus meurt, son père reçoit, en standard, un signal SIGCHLD. Tout processus est donc averti, s'il le veut, de la disparition de ses processus fils. De même un processus peut se mettre en attente de la fin de l'un, quelconque ou particulier, de ses fils.

    Un processus peut avoir, sur les fichiers, les droits (user, group, others) soit de l'utilisateur qui l'a créé, soit du fichier qui contient le code exécutable.

L'environnement d'un processus

Variables d'environnement

    Outre les arguments de la commande, un processus reçoit à son lancement un ensemble d'indications appelées les variables d'environnement parmi lesquelles le répertoire principal (home directory),  le chemin du répertoire de travail courant (path working directory), l'ensemble des chemins d'accès qui doivent être explorés pour rechercher un exécutable, le user name, etc. Lorsqu'une session shell est ouverte, le programme shell lui-même dispose de cet ensemble de variables d'environnement, qui peuvent être consultées et/ou modifiées par la builtin-command setenv du c-shell.

    Elles apparaissent sous la forme d'une succession de lignes de la forme nom=valeur, par exemple :
 
HOSTNAME=duo.iut.univ-aix.fr
LOGNAME=mathieu
PATH=/usr/local/bin:/usr/local/jdk1.2.2/bin:/usr/ucb:/bin:/usr/bin:/usr/lib:/etc:/usr/sbin:/usr/bin/X11:.
HOME=/users/prof/mathieu
SHELL=/bin/csh
USER=mathieu

Environnement et fonction main()

    La plupart du temps, l'utilisateur ne les gère pas directement et utilise toutes les options par défaut.

    De nombreux compilateurs C/C++, en particulier sous Unix, proposent une "surcharge" (il n'y a pas de surcharge en C) particulière de la fonction main() :
 
int main (int argc, char * argv [], char * envp []);

dans laquelle envp est un tableau de pointeurs de chaînes de caractères C (NTCTS), le dernier pointeur devant être nul. Bien que non conforme strictement aux normes ISO de C et de C++, cette fonction permet à une application de récupérer l'ensemble des variables d'environnement, comme par exemple :
 
int main (int argc, char * argv [], char * envp [])
{
    for (int i = 0; envp [i]; ++i) cout << envp [i] << endl;

    // ...

} // main()

    Le mécanisme de passage des paramètres de la fonction main() et la localisation de ces informations dans la mémoire virtuelle sont décrits dans le cours consacré à la gestion mémoire sous Linux.

Variable environ

    Le C (et donc le C++) sous Unix permet à tout moment de récupérer dans n'importe quel programme l'ensemble des variables d'environnement, ou plus exactement l'ensemble des chaînes qui constituent l'environnement du processus. Il faut pour cela utiliser la variable globale environ, déclarée dans le fichier <stdlib.h> par :
 
extern char ** environ;

si la bibliothèque GNU C est utilisée (si la macro __USE_GNU est définie). Dans le cas contraire, il suffit d'ajouter la ligne ci-dessus au code, la définition de cette variable est de toutes façons incluse au moment de l'édition de liens :
 
extern char ** environ;

void fct (void)
{
    for (int i = 0; environ [i]; ++i) cout << environ [i] << endl;

    // ...

} // fct()

    De plus, la bibliothèque GNU C et la bibliothèque standard du C pour Linux (niveau 3) proposent la fonction getenv() qui permet de récupérer la "valeur" d'une variable d'environnement. La bibliothèque standard du C pour Linux propose aussi setenv(), unsetenv() et putenv()  qui permettent à un processus toutes les autres opérations sur les variables d'environnement (ajout, suppression, modification).

Environnement et lancement d'un exécutable

    La fonction système execve() permet de lancer un programme en lui passant comme troisième paramètre un vecteur de chaînes de caractères qui constituera l'ensemble des variables d'environnement du nouveau processus. Les autres fonctions execl(), execle(), execlp(), execv() et execvp() qui sont sous Linux des wrappers de execve() lui passent un environnement explicitement ou par défaut.

    Le tableau ci-dessous résume les caractéristiques des différentes fonctions de lancement d'un processus sous Unix :
 

Fonction Recherche dans PATH Ligne de commande Tableau d'environnement
execl() Non Liste Non
execle() Non Liste Oui
execlp() Oui Liste Non
execv() Non Tableau Non
execve() Non Tableau Oui
execvp() Oui Tableau Non

Remarque : le postfixe l signifie liste (d'arguments), le postfixe v signifie vecteur (d'arguments), le postfixe p signifie path, le postfixe e signifie environnement.

Les fonctions système

Les fonctions système concernant directement les processus

Autres fonctions système liées à l'existence des processus

    Bien que n'étant pas spécifiquement des fonctions de manipulation de processus, certaines fonctions système sont liées à leur existence et peuvent y être rattachées

Autres fonctions système

    Les fonctions dup() et dup2() n'ont aucun lien avec l'existence des processus. Cependant leur utilisation est indissociable des fonctions exec...() et pipe().

Groupes de processus

    La plupart des systèmes Unix utilisent la notion de groupes de processus (process group) pour représenter un ensemble de processus collaborant à une même activité. Un processus possède non seulement un numéro de processus (pid : process ident) personnel, mais aussi le numéro du groupe de processus auquel il appartient, et qui lui est attribué par le système.

    La notion de groupe de processus est utilisée dans deux cas :

    Par défaut, un processus reçoit comme numéro de groupe de processus celui de son père. Le processus dont le numéro de groupe de processus est égal à son propre pid est appelé le leader du groupe. Un groupe peut ne pas avoir de leader, par exemple si le leader meurt avant les autres processus.

    Il est possible de connaître le numéro de groupe de n'importe quel processus grâce à la fonction système getpgid(). De même tout processus peut changer son propre numéro de groupe de processus, ou celui d'un autre processus grâce à la fonction système setpgid().

    Lorsqu'un shell lance une commande, il crée un processus fils (fork()) qui hérite de son numéro de groupe, mais ce numéro de groupe est immédiatement remplacé par le pid du fils, qui devient leader d'un nouveau groupe. Ainsi, un signal SIGKILL envoyé au groupe d'un processus ne peut pas être propagé au shell qui l'a lancé.

    Lorsqu'un shell lance simultanément plusieurs commandes faisant partie d'une même activité (par exemple commandes enchaînées par un pipe (|), elles font toutes partie du même groupe.



[1] En fait il contient des pointeurs vers des struct en mémoire qui contiennent ces informations.

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique