Nous avons vu dans l’épisode 1 l’architecture générale de l’environnement du noyau Linux et la manière dont il gère les processus. L’épisode 2 nous a permis d’entrer dans le détail de la gestion de la mémoire. L’épisode 3 nous a permis de creuser comment le noyau Linux gère les périphériques. Dans l’épisode 4 nous avons vu comment le noyau Linux offre aux applications un service d’appels système.
Dans l’épisode 5 nous avons vu les mécanismes qu’offre le noyau Linux aux processus tournant sur le système pour communiquer entre eux, c’est à dire pour se synchroniser et/ou s’envoyer des données.
Il est temps de commencer les travaux pratiques. Dans cet épisode 6 nous allons nous amuser avec certains des appels système que le noyau met à disposition des processus.
Dans le programme prog.c ci-dessous, que nous allons utiliser pour ce TP, nous allons utiliser les appels système suivants :
getpid() qui permet à un processus de demander au noyau de lui communiquer son PID (process ID)
getppid() qui permet à un processus de demander au noyau de lui communiquer son PPID, c’est à dire le PID de son processus père (on rappelle que les processus sont rangés sous forme d’arbre, rangés sous le processus de PID 1 qui est soit init historiquement, soit systemd sur les systèmes Linux qui ont succombé à cette mode).
getuid() qui permet à un processus de demander au noyau de lui communiquer son UID, c’est à dire l’ID de l’utilisateur qui a lancé et à qui appartient le processus.
getgid() qui permet à un processus de demander au noyau de lui communiquer son GID, c’est à dire l’ID du groupe qui a lancé et à qui appartient le processus.
geteuid() qui permet à un processus de demander au noyau de lui communiquer son EUID, c’est à dire l’ID effectif de l’utilisateur qui a lancé et à qui appartient le processus. Si le programme lancé a le bit s positionné et appartient à root, alors le EUID vaut 0. Pareil si le programme a été lancé avec sudo.
getegid(), comme geteuid() mais pour le groupe effectif.
setuid() et setgid(), qui permettent au processus de demander au noyau de modifier l’UID et le GID du processus. Bien sûr le noyau ne s’exécute que si l’UID appelant est 0 c’est à dire root.
fork() qui permet de demander au noyau de créer un processus fils identique au processus appelant dans tous les détails, sauf sur le code de retour de l’appel système fork() qui vaut 0 dans le fils créé et le PID du fils créé dans le code de retour disponible pour le processus père. fork() est, avec exec, l’un des appels système les plus fondamentaux sous Unix.
execl() qui permet à un processus de demander au noyau de faire une commutation d’image, c’est à dire d’écraser le code en mémoire du processus appelant par celui d’un programme présent dans le système de fichiers. execl est l’un des nombreux appels système de la famille des exec.
exit() qui permet à un processus de demander au noyau de le tuer, et de transmettre un code de sortie jusqu’au processus père, pour l’informer par exemple du résultat de l’exécution du processus fils.
wait() qui permet à un processus de demander au noyau de le mettre en attente de la mort d’un processus fils et lui permettre de lire le code de retour envoyé par le processus fils à sa mort.
Notre programme utilise aussi la fonction sleep(), qui n’est pas à proprement parler un appel système car appartenant à la section 3 du manuel (man 3 sleep) et non à la section 2. Cette fonction est bien utile pour ralentir l’exécution pour qu’on voit ce qui se passe. sleep(n) permet au processus d’être mis en sommeil pendant n secondes. Il utilise aussi la fonction getenv() qui permet de lire le contenu d’une variable d’environnement (qu’on peut modifier par setenv() qui ne sera pas utilisé ici).
Voici le code source de notre programme de TP prog.c :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
int i,pid_fils,status;
// Séquence 1
printf("Séquence 1 :\n");
printf("Ici le processus père : mon PID est %d\n",getpid());
printf("Ici le processus père : mon PPID, c'est à dire le PID de mon père (bash) est %d\n",getppid());
printf("Ici le processus père : mon UID est %d et mon GID est %d\n",getuid(),getgid());
printf("Ici le processus père : mon EUID est %d et mon EGID est %d\n",geteuid(),getegid());
printf("Ici le processus père : ma variable d'environnement $HOME vaut %s\n",getenv("HOME"));
printf("Ici le processus père : je vais tenter de modifier mon UID et mon GID à 1000\n");
if(setgid(33)!=-1 && setuid(33)!=-1) printf("Ici le processus père : modification réussie, mon UID est %d et mon GID est %d\n",getuid(),getgid());
else printf("Ici le processus père : modification ratée, mon UID est %d et mon GID est %d\n",getuid(),getgid());
printf("Ici le processus père : je vais demander au noyau de me mettre en pause pendant 5 secondes\n");
sleep(5);
//Séquence 2
printf("\nSéquence 2 :\n");
printf("Ici le processus père : je vais créer un processus fils 1 et attendre sa mort\n");
if((pid_fils=fork())==0)
{
// Ici nous sommes dans le code du processus fils 1
sleep(1);
printf("Ici le processus fils 1 : mon PID est %d et mon PPID est %d\n",getpid(),getppid());
printf("Ici le processus fils 1 : Je vais compter tranquillement jusqu'à 10 et mourir pour faire patienter mon processus père\n");
for(i=1;i<=10;i++)
{
printf("%d\n",i);
sleep(1);
}
exit(4); // mort du processus fils 1
}
printf("Ici le processus père : je vais attendre la fin de mon processus fils 1 de PID %d\n",pid_fils);
wait(&status);
printf("Ici le processus père : mon fils de PID %d est mort et m'a renvoyé le code de sortie %d\n",pid_fils,WEXITSTATUS(status));
sleep(3);
//Séquence 3
printf("\nSéquence 3 :\n");
printf("Ici le processus père : je vais maintenant créer un processus fils 2 qui va effectuer une commutation d'image sur \"/usr/bin/ls / -lh\"\n");
if((pid_fils=fork())==0)
{
// Ici nous sommes dans le code du processus fils 2
sleep(2);
printf("Ici le processus fils 2 : mon PID est %d et je vais effectuer une commutation d'image sur \"/usr/bin / -lh\"\n",getpid());
execl("/usr/bin/ls","/usr/bin/ls","/","-lh",NULL);
printf("Ici le processus fils 2 : oups, la commutation d'image n'a pas fonctionné\n");
exit(12);
}
printf("Ici le processus père : je vais attendre la fin de mon processus fils 2 de PID %d\n",pid_fils);
wait(&status);
printf("Ici le processus père : mon fils de PID %d est mort et m'a renvoyé le code de sortie %d\n",pid_fils,status);
sleep(3);
//Séquence 4
printf("Séquence 4 :\n");
printf("Ici le processus père : je vais créer un processus fils 3 puis je vais mourir. On va voir quel processus va adopter mon processus fils\n");
if(fork()==0)
{
// Ici nous sommes dans le code du processus fils 3
sleep(3);
printf("Ici le processus fils 3 : mon PID est %d et mon PPID est %d\n",getpid(),getppid());
}
exit(0); // code de mort exécuté par le processus père et le processus fils 3. Code didactique mais inutile car la parenthèse fermante finale a le même effet
}
Comme on peut le voir, le programme est découpé en 4 séquences didactiques distinctes.
La séquence 1 montre comment un processus peut interroger le noyau pour en savoir plus sur lui.
La séquence 2 montre comment un processus peut demander au noyau de créer un processus fils et attendre la mort du processus fils
La séquence 3 montre comment un processus peut demander au noyau de créer un processus fils qui va exécuter la commande /usr/bin/ls / -lh
Et enfin la séquence 4 montre ce qui se passe si le processus père meurt avant l’un de ses fils. On peut voir ici qu’il est adopté par un des processus que lance systemd, car nous sommes sous Ubuntu. Sur un système non systemd (comme Slackware ou Devuan qui utilisent l’original Init, on verrait que les processus orphelins sont adoptés par Init, processus de PID 1).
Le programme se compile par un simple :
gcc ./prog.c -o prog
Pour en savoir plus sur la compilation C, voir ici.
Voici l’exécution de ce programme par un utilisateur lambda. Comme on peut le voir, le changement d’UID et de GID par setuid() et setgid() n’est pas autorisé.

Le même programme lancé avec sudo permet de faire réussir les changements d’UID et de GID :

Pour aller plus loin sur les appels système dédiés à la gestion des processus sous Linux :

