Aller au contenu principal

Systèmes d'exploitation — fondations · L2 · Section 2/12

Processus

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#Processus

Un processus n'est pas seulement du code: c'est un ensemble vivant constitué d'un espace mémoire virtuel, d'un contexte processeur, de descripteurs de fichiers et d'un environnement qui lui est propre. Lorsque le noyau exécute ce processus, il charge ses registres, mappe ses pages, restaure sa pile et lui offre un accès exclusif au processeur pendant un court laps de temps. À l'extinction du quantum, l'état complet est sauvegardé pour que l'exécution puisse reprendre plus tard comme si rien ne s'était passé.

#1. Cycle de vie

Le diagramme classique « prêt → en cours → bloqué → terminé » n'est pas un simple schéma de manuel. Prenons un serveur web: il attend sur accept (état bloqué), reçoit une connexion et passe brièvement en exécution, puis se remet en attente. Si le serveur engendre des fils, chacun possède son propre PID, son espace utilisateur isolé et un code de retour qui devra être collecté par le parent via wait.

Deux états dégénérés méritent une attention particulière. Le zombie est un processus terminé dont le parent n'a pas encore collecté le code de sortie: il occupe une entrée dans la table des processus et libère le noyau de tout le reste. L'orphelin est un fils dont le parent est mort: il est adopté par init (PID 1 sur les systèmes traditionnels), qui fait le wait à sa place. La parade au zombie est donc toujours du côté du parent.

#Ce que le cours d'Unix appelle un processus

Un processus est un programme en cours d'exécution. La plupart des commandes exécutent un programme, et donc lancent un processus; le programme de démarrage correspond à un processus présent pendant toute la session. Des dizaines ou centaines de processus sont en fonctionnement à tout moment.

Un processus est identifié par un numéro entier et associé à un ensemble d'informations appelé son image:

  • le code du programme en cours d'exécution;
  • les données traitées par ce code;
  • l'identification des fichiers en cours de traitement et leur état;
  • le répertoire courant;
  • l'identité du propriétaire du processus;
  • le terminal associé.

Le cours distingue deux propriétaires, distinction qui n'existe pas dans le vocabulaire courant. Le propriétaire réel est l'utilisateur qui a lancé le processus; le propriétaire effectif est l'utilisateur dont les droits s'appliquent au processus. Ils coïncident en général, et diffèrent dès qu'un programme s'exécute avec des privilèges élevés — c'est le principe des programmes setuid, qui s'exécutent avec les droits de leur propriétaire et non de leur lanceur.

Les états sont au nombre de quatre: en attente d'un événement extérieur (une action de l'utilisateur), en attente d'exécution (une tranche de temps), en attente de l'arrivée d'une partie de la mémoire virtuelle, et en exécution — un seul à la fois. Le diagramme « prêt, en cours, bloqué, terminé » de la section précédente est donc la version fine de cette liste.

Tout processus est lancé par un processus père: c'est l'arbre généalogique des processus. Deux conséquences pratiques: un fils hérite du répertoire courant et des descripteurs de fichiers ouverts par son père — ce qui explique le fonctionnement des redirections du shell; et quand un processus se termine, tous ses processus fils se terminent aussi, ce qui explique pourquoi un xclock lancé en arrière-plan disparaît à la fermeture du terminal.

#2. Créer et remplacer un processus

Sous Unix, fork duplique le processus courant. Le parent et l'enfant se différencient uniquement par la valeur de retour: 0 pour l'enfant, PID du fils pour le parent, -1 en cas d'échec. Le noyau ne copie pas immédiatement toutes les pages; il marque simplement leurs entrées en copy-on-write. Dès qu'un des deux modifie une page, celle-ci est dupliquée. Pour exécuter un nouveau programme, l'enfant appelle execlp (ou toute variante exec) et remplace son image mémoire, en conservant ses descripteurs de fichiers. Ce couple fork/exec constitue la pierre angulaire des shells et des gestionnaires de processus.

cc

1#include <sys/types.h>2#include <sys/wait.h>3#include <unistd.h>4 5pid_t pid = fork();6if (pid == -1) { perror("fork"); return 1; }7if (pid == 0) {8    execlp("ls", "ls", "-l", NULL);9    perror("exec");              // atteint uniquement si exec échoue10    _exit(127);                  // convention: 127 = commande introuvable11}12int status = 0;13waitpid(pid, &status, 0);14if (WIFEXITED(status))

L'exemple illustre la règle d'or: toujours quitter explicitement si l'appel exec échoue, car l'enfant continuerait sinon dans le flot du parent. Les macros WIFEXITED et WEXITSTATUS décodent le statut brut: elles distinguent une fin volontaire (code 0 à 255) d'une mort par signal, exactement la convention 128+n que le shell vous affiche dans $?.

#2 bis. Le processus vu depuis le shell

Avant d'écrire du C, on observe les processus avec les outils du shell. Le cours en donne l'inventaire complet.

Commandes intrinsèques et externes. Une commande intrinsèque s'exécute dans le processus du shell; une commande externe provoque la création d'un processus. Le shell résout un nom dans cet ordre: fonction du shell, puis opération prédéfinie (intrinsèque), puis fichier exécutable trouvé dans PATH. Les intrinsèques citées par le cours: cd, pwd, fg, bg, exit, kill, echo, exec, . et son synonyme source, break, continue, shift, eval, whence. La commande exec mérite d'être isolée: elle remplace le processus du shell par celui de la commande, sans retour possible.

Observer. ps liste les processus; la forme du cours est celle du système BSD, sans tiret initial. ps x affiche tous les processus de l'utilisateur, ps aux tous les processus de l'ordinateur en détail — et comme la sortie est volumineuse, le cours précise qu'elle doit être mise dans un tube avec less ou grep. uptime donne la durée de fonctionnement, le nombre d'utilisateurs connectés et la charge moyenne des 1, 5 et 15 dernières minutes, qui est la longueur de la file d'attente pour exécution. top affiche les processus les plus actifs et se pilote au clavier: q pour quitter, k pour tuer, u pour filtrer sur un utilisateur.

bashbash

1ps x2ps aux | less3uptime4top

Piloter. C-c termine le processus de premier plan, C-z le suspend, fg le reprend au premier plan, bg en arrière-plan, et kill lui envoie un signal. Le cours qualifie les signaux de « plus ou moins forts »: -9 est la destruction inconditionnelle, -19 une suspension. kill accepte un numéro de processus ou un numéro de tâche précédé du signe pour cent.

Prolonger la vie d'un processus. Puisque les fils se terminent avec leur père, un processus d'arrière-plan meurt à la fermeture du terminal. nohup (no hang up) l'en détache, en redirigeant ses sorties vers ./nohup.out; le processus survit alors à la déconnexion. Pour choisir le moment du lancement, at lit un script sur son entrée standard et l'exécute à la date demandée. Pour un lancement régulier, crontab -e édite une table consultée par le démon cron, dont les cinq premiers champs sont minute, heure, jour du mois, mois et jour de la semaine.

bashbash

1nohup make install &2at now +1 hour3crontab -e

#3. Signaux et gestion asynchrone

Les signaux agissent comme des interruptions logicielles. SIGCHLD prévient un parent qu'un enfant a terminé, SIGINT transmet l'interruption clavier (Ctrl+C), SIGALRM déclenche une minuterie. Installer un gestionnaire avec sigaction permet de capturer l'événement et de décider quoi faire: nettoyer des fichiers temporaires, réarmer une minuterie, afficher une statistique.

cc

1#include <signal.h>2 3void on_int(int sig) {           // handler minimaliste4    /* ne faire que des opérations sûres: positionner un drapeau volatile */5}6 7struct sigaction sa = {0};8sa.sa_handler = on_int;9sigemptyset(&sa.sa_mask);10sa.sa_flags = SA_RESTART;        /* relancer les appels interrompus */11sigaction(SIGINT, &sa, NULL);

Deux règles de robustesse: dans un gestionnaire, n'utilisez que des opérations asynchrone-sûres (une variable volatile sig_atomic_t et les fonctions listées dans man 7 signal-safety); choisissez sigaction plutôt que signal, dont le comportement varie selon les systèmes. Certaines valeurs restent néanmoins réservées (SIGKILL, SIGSTOP) afin de garantir au noyau un moyen sûr d'arrêter un processus rétif.

#Atelier: un mini-shell

Écrivez un mini-shell qui:

  1. duplique le processus avec fork;
  2. applique execvp sur la ligne de commande saisie;
  3. collecte systématiquement le code de sortie via waitpid;
  4. traite SIGINT pour ne pas s'interrompre quand le programme enfant reçoit Ctrl+C.

L'objectif est de sentir la mécanique complète de la vie d'un processus. Exercice de vérification, exécutable en une commande: ./minishell puis tapez ls, false, sleep 10 suivi de Ctrl+C. Observations attendues: le listage s'affiche; false laisse votre prompt afficher un statut; Ctrl+C interrompt sleep mais votre shell survit et réaffiche son prompt. Si ces trois comportements sont réunis, votre waitpid et votre sigaction sont corrects.

#Exercices

1. Qu'est-ce qu'un processus? Qu'est-ce que son « image »? Citez les éléments qui la composent.

2. Quels sont les états d'un processus? Pourquoi le cours dit-il que « tout se passe comme s'ils s'exécutaient simultanément »?

3. Qu'appelle-t-on l'arbre généalogique des processus? Que distinguent le propriétaire réel et le propriétaire effectif?

4. Quelle différence entre une commande intrinsèque et une autre commande, du point de vue des processus créés? Que fait exec?

5. Qu'affichent ps x et ps aux? Pourquoi faut-il combiner la seconde avec less ou grep?

6. Que désigne la « charge moyenne » donnée par uptime? Que pilotent les touches q, k et u dans top?

7. Que se passe-t-il quand on ferme un terminal dans lequel un processus a été lancé en arrière-plan? Comment y remédier? Où vont alors les sorties du processus?

8. Que signifient les signaux -9 et -19 de kill? Quel code de retour le shell affiche-t-il pour un processus tué par le signal 15?

Corrections détaillées

1. Un processus est un programme en cours d'exécution. Son image regroupe le code du programme en cours d'exécution, les données traitées par ce code, l'identification des fichiers en cours de traitement et leur état, le répertoire courant, l'identité du propriétaire du processus et le terminal associé. Il est identifié par un numéro entier.

2. Quatre états: en attente d'un événement extérieur (action de l'utilisateur), en attente d'exécution (tranche de temps), en attente de l'arrivée d'une partie de la mémoire virtuelle, et en exécution — un seul à la fois. Le processeur est découpé en tranches très courtes réparties entre les processus: chacun progresse à son tour, si bien que l'observateur extérieur voit une exécution simultanée. C'est le temps partagé, qui suppose aussi le partage de la mémoire.

3. Tout processus est lancé par un processus père, ce qui dessine un arbre généalogique dont la racine est le processus d'initialisation. Le propriétaire réel est l'utilisateur qui a lancé le processus; le propriétaire effectif est l'utilisateur dont les droits s'appliquent au processus. Ils coïncident sauf lorsque le programme s'exécute avec des privilèges élevés, comme un binaire setuid.

4. Une commande intrinsèque est interne au shell: elle s'exécute dans le processus du shell et n'en crée aucun. Toute autre commande est un fichier exécutable que le shell fait exécuter par le noyau, ce qui crée un processus. C'est pour cette raison que cd doit être intrinsèque: exécuté dans un fils, le changement de répertoire serait perdu. exec commande remplace le processus du shell par celui de la commande; le shell ne reprend pas la main, ce que l'on constate en lançant exec gnome-terminal depuis un terminal — la fenêtre d'origine se ferme.

5. ps x affiche tous les processus de l'utilisateur; ps aux affiche de manière détaillée tous les processus de l'ordinateur. Les paramètres s'écrivent sans tiret initial, selon la forme BSD. La sortie de ps aux est volumineuse, d'où la recommandation du cours de la mettre dans un tube avec less ou grep.

6. La charge moyenne est la longueur de la file d'attente pour exécution: le nombre de processus en attente du processeur, moyenné sur la dernière minute, les 5 et les 15 dernières minutes. Dans top, q quitte, k permet de tuer un ou plusieurs processus, et u restreint l'affichage aux processus d'un utilisateur donné.

7. Le processus d'arrière-plan est un fils du shell: quand le shell se termine, ses fils se terminent aussi. Le shell signale les processus non terminés à la première commande exit, et les termine à la seconde. Pour l'éviter, on lance le processus avec nohup: il survit alors à la déconnexion. Sa sortie standard et sa sortie d'erreur sont redirigées vers le fichier ./nohup.out.

8. -9 correspond à la destruction inconditionnelle du processus; -19 à une suspension. Un processus tué par le signal 15 (SIGTERM) fait apparaître dans le shell le code de retour 143, selon la convention 128 + numéro du signal. Cette convention est la même que celle décodée côté C par WTERMSIG.