Processus
Introduction aux processus
Un processus est un ensemble d'octets (en langage machine) en cours d'exécution ; en d'autres termes, c'est l'exécution d'un programme. Un processus UNIX se décompose en :
- un espace d'adressage (visible par l'utilisateur / programmeur) ;
- le bloc de contrôle du processus (BCP), lui-même décomposé en :
- une entrée dans la table des processus du noyau,
struct proc, définie dans<sys/proc.h>; - une structure
struct userappeléezone u, définie dans<sys/user.h>.
- une entrée dans la table des processus du noyau,
Les processus sous UNIX apportent :
- la multiplicité des exécutions : plusieurs processus peuvent être l'exécution d'un même programme ;
- la protection des exécutions : un processus ne peut exécuter que ses instructions propres, et ce de façon séquentielle ; il ne peut pas exécuter des instructions appartenant à un autre processus. Les processus sous UNIX communiquent entre eux et avec le reste du monde grâce aux appels système.
Création d'un processus - fork()
Sous UNIX, la création de processus est réalisée par l'appel système
pid_t fork(void); (déclaré dans <unistd.h>). Dans le modèle UNIX classique,
tous les processus, sauf le processus d'identifiant 0, sont créés par un appel à
fork. Le processus qui appelle fork est appelé processus père. Le nouveau
processus est appelé processus fils. Tout processus a un seul processus père.
Tout processus peut avoir zéro ou plusieurs processus fils. Chaque processus est
identifié par un numéro unique, son PID.
fork retourne deux fois : dans le père, la valeur de retour est le PID du fils ;
dans le fils, elle vaut 0. En cas d'échec, aucun fils n'est créé et fork
retourne -1 dans le père.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
/* processus fils */
printf("fils : PID %d, père %d\n", (int)getpid(), (int)getppid());
exit(EXIT_SUCCESS);
}
/* processus père : attente du fils pour éviter un zombie */
int status;
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
exit(EXIT_FAILURE);
}
if (WIFEXITED(status))
printf("fils terminé avec le code %d\n", WEXITSTATUS(status));
return EXIT_SUCCESS;
}
Un fils terminé dont le père n'a pas encore récupéré le code de retour (par
wait ou waitpid) reste dans la table des processus à l'état de zombie.
Le processus de PID 0 est créé « manuellement » au démarrage de la machine ; ce
processus a toujours un rôle spécial pour le système. De plus, fork renvoyant
0 dans le fils, le numéro 0 ne doit jamais être attribué à un autre processus.
Dans le schéma System V, le processus 0 crée, grâce à un appel de fork, le
processus init de PID 1 (sous Linux, le processus 1 est lancé directement par
le noyau).
Le processus de PID 1, init, est l'ancêtre de tous les autres processus
utilisateurs (le processus 0 ne réalisant plus de fork()). C'est lui qui
adopte tous les processus orphelins, afin de collecter leur code de retour à leur
mort et d'éviter l'accumulation de zombies.
Format d'un fichier exécutable
Les compilateurs nous permettent de créer des fichiers exécutables. Ces fichiers ont le format suivant, qui permet au noyau de les transformer en processus :
- un en-tête qui décrit l'ensemble du fichier, ses attributs et sa carte des sections ;
- la taille à allouer pour les variables non initialisées (BSS) ;
- une section TEXT qui contient le code (en langage machine) ;
- une section données (DATA) qui contient les données initialisées ;
- éventuellement d'autres sections : table des symboles pour le débogueur, images, icônes, table des chaînes, etc.
Chargement / changement d'un exécutable
L'appel système execve remplace l'exécutable du processus courant en chargeant
un nouvel exécutable. Les régions associées au processus sont préalablement
libérées. Le PID et les descripteurs ouverts (sauf ceux marqués
FD_CLOEXEC) sont conservés. En cas de succès, execve ne retourne pas ; il
retourne -1 en cas d'échec.
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
Les fonctions execl, execlp, execle, execv et execvp de la
bibliothèque C sont des interfaces à execve.
Pour chaque section de l'exécutable, une région en mémoire est allouée. Soit au moins les régions :
- du code ;
- des données initialisées ;
- des piles ;
- du tas.
La région de la pile est une pile de cadres d'appel (stack frames), qui sont
empilés et dépilés lors de l'appel ou du retour de fonction. Le pointeur de pile,
un des registres de l'unité centrale, indique la profondeur courante de la pile.
Un processus UNIX pouvant s'exécuter en deux modes (noyau, utilisateur), une pile
privée est utilisée dans chaque mode. La pile noyau est vide quand le
processus est en mode utilisateur. Le tas est une zone où est réalisée
l'allocation dynamique avec les fonctions malloc, calloc et realloc.
zone u et table des processus
Tous les processus sont associés à une entrée dans la table des processus, qui
est interne au noyau. De plus, le noyau alloue pour chaque processus une
structure appelée zone u, qui contient des données privées du processus,
uniquement manipulables par le noyau. La table des processus permet
d'accéder à la table des régions par processus, qui permet à son tour d'accéder
à la table des régions. Ce double niveau d'indirection permet de partager des
régions. Dans l'organisation avec une mémoire virtuelle, la table des régions
est matérialisée logiquement dans la table des pages. Les structures de la table
des régions contiennent des informations sur le type, les droits d'accès et la
localisation (adresses en mémoire ou adresses sur disque) de la région. Seule la
zone u du processus courant est manipulable par le noyau, les autres sont
inaccessibles. L'adresse de la zone u est placée dans le mot d'état du
processus.
fork et exec
Quand un processus réalise un fork, le contenu de son entrée de la table des
régions est dupliqué ; chaque région est ensuite, en fonction de son type,
partagée (code) ou copiée (données, pile). Quand un processus réalise un exec,
il y a libération des régions et allocation de nouvelles régions en fonction des
valeurs définies dans le nouvel exécutable.
Le contexte d'un processus
Le contexte d'un processus est l'ensemble des données qui permettent de reprendre l'exécution d'un processus qui a été interrompu. Le contexte d'un processus est l'ensemble de :
- son état ;
- son mot d'état ;
- les valeurs des variables globales, statiques ou dynamiques ;
- son entrée dans la table des processus ;
- sa zone u ;
- les piles utilisateur et système ;
- les zones de code et de données.
Le noyau et ses variables ne font partie du contexte d'aucun processus. L'exécution d'un processus se fait dans son contexte. Quand il y a un changement de processus courant, il y a réalisation d'une commutation de mot d'état et d'un changement de contexte. Le noyau s'exécute alors dans le nouveau contexte.
Commutation de mot d'état et interruptions
Ces fonctions de très bas niveau sont fondamentales pour pouvoir programmer un système d'exploitation. Pour être exécuté et donner naissance à un processus, un programme et ses données doivent être chargés en mémoire centrale. Les instructions du programme sont transférées une à une de la mémoire centrale vers l'unité centrale, où elles sont exécutées. L'unité centrale comprend des circuits logiques et arithmétiques qui effectuent les instructions, mais aussi des mémoires appelées registres. Certains de ces registres sont spécialisés directement par les constructeurs de l'unité centrale, d'autres le sont par les programmeurs du noyau. Quelques registres spécialisés :
- l'accumulateur, qui reçoit le résultat d'une instruction ; sur les machines à registres multiples, le jeu d'instructions permet souvent d'utiliser n'importe lequel des registres comme accumulateur ;
- le registre d'instruction ;
- le compteur ordinal : ce compteur change au cours de la réalisation d'une instruction pour pointer sur la prochaine instruction à exécuter. La majorité des instructions ne font qu'incrémenter ce compteur ; les instructions de branchement réalisent des opérations plus complexes sur ce compteur : affectation, incrémentation ou décrémentation plus importantes ;
- le registre d'adresse ;
- les registres de données, qui sont utilisés pour lire ou écrire une donnée à une adresse spécifiée en mémoire ;
- les registres d'état du processeur ;
- les registres d'état du processus : droits, adresses, priorités, etc.
Ces registres forment le contexte d'unité centrale d'un processus. À tout moment, un processus est caractérisé par deux contextes : le contexte d'unité centrale, qui est composé des mêmes données pour tous les processus, et le contexte qui dépend du code du programme exécuté. Pour pouvoir exécuter un nouveau processus, il faut pouvoir sauvegarder le contexte d'unité centrale du processus courant (mot d'état), puis charger le nouveau mot d'état du processus à exécuter. Cette opération délicate, réalisée de façon matérielle, est appelée commutation de mot d'état. Elle doit se faire de façon non interruptible. Cette « super instruction » utilise deux adresses, qui sont respectivement l'adresse de sauvegarde du mot d'état et l'adresse de lecture du nouveau mot d'état. Le compteur ordinal faisant partie du mot d'état, ce changement provoque l'exécution dans le nouveau processus. C'est le nouveau processus qui devra réaliser la sauvegarde du contexte global. En général, c'est le noyau qui réalise cette sauvegarde, le noyau n'ayant pas un contexte du même type. Le processus interrompu pourra ainsi reprendre exactement là où il s'était arrêté.
Les interruptions
Une interruption est une commutation de mot d'état provoquée par un signal produit par le matériel. Ce signal, conséquence d'un événement extérieur ou intérieur, modifie l'état d'un indicateur qui est régulièrement testé par l'unité centrale. Une fois que le signal est détecté, il faut déterminer la cause de l'interruption. Pour cela, on utilise un indicateur par cause possible ; on parle alors du vecteur d'interruptions. Trois grands types d'interruptions :
- externes (indépendantes du processus) : interventions de l'opérateur, pannes, fins d'entrées-sorties, horloge, etc. ;
- déroutements (traps) : erreur interne du processeur, débordement, division par zéro, défaut de page, etc. ;
- appels système : demande d'entrée-sortie, par exemple.
Suivant les machines et les systèmes, un nombre variable de niveaux d'interruption est utilisé. Ces différentes interruptions ne réalisent pas nécessairement un changement de contexte complet du processus courant. Il est possible que plusieurs niveaux d'interruption soient positionnés quand le système les consulte. C'est le niveau des différentes interruptions qui va permettre au système de sélectionner l'interruption à traiter en priorité. Dans l'UNIX System V classique, l'horloge fait partie des interruptions les plus prioritaires ; seules les erreurs machine la précèdent.
Le problème des cascades d'interruptions
Si pendant le traitement d'une interruption une autre interruption se produit, et que ceci se répète pendant le traitement de la nouvelle interruption, le système ne fait plus progresser ni les processus ni les interruptions en cours de traitement. Il est donc nécessaire de pouvoir retarder ou annuler la prise en compte d'un ou plusieurs signaux d'interruption. C'est le rôle des deux mécanismes de masquage et de désarmement d'un niveau d'interruption. Masquer, c'est ignorer temporairement un niveau d'interruption. Si ce masquage est fait dans le mot d'état d'un traitement d'interruption, à la nouvelle commutation d'état le masquage disparaît ; les interruptions peuvent de nouveau être prises en compte. Désarmer, c'est rendre le positionnement de l'interruption caduc.
Accès aux structures proc et user du processus courant
Les informations de la table des processus peuvent être lues grâce à la commande
shell ps, ou par des appels système. En revanche, les informations contenues
dans la zone u ne sont accessibles que par une requête du processus
lui-même (en programmation objet, on dirait que ce sont des variables d'instance
privées), d'où les appels système suivants : times, chroot, chdir,
fchdir, getuid, getgid, etc.