1. Vision globale
Quand on parle d’exécution de programmes sous Linux, il faut bien séparer trois idées :
- fork() : crée un nouveau processus enfant à partir du processus courant.
- exec() : remplace le programme actuellement exécuté par un autre programme.
- wait()/waitpid() : permet au parent d’attendre et de récupérer le statut de fin de son enfant.
Code: Select all
fork(); // créer un enfant
exec(); // transformer l'enfant en programme demandé
wait(); // le parent attend l'enfant
Code: Select all
ls -l
Code: Select all
pid_t pid = fork();
if (pid == 0) {
execlp("ls", "ls", "-l", NULL);
_exit(127);
}
waitpid(pid, NULL, 0);
2. exec() : remplacer le programme courant
La famille exec sert à charger un nouveau programme dans le processus courant.
Le point le plus important :
Si exec réussit, il ne retourne jamais.
Pourquoi ? Parce que le programme appelant est remplacé par le nouveau programme.
Exemple :
Code: Select all
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *argv[] = { "ls", "-l", NULL };
printf("Avant exec\n");
execv("/bin/ls", argv);
perror("execv");
exit(EXIT_FAILURE);
}
Après un exec :
- le PID reste le même ;
- le programme change ;
- le code précédent disparaît ;
- la pile précédente disparaît ;
- le tas précédent disparaît ;
- les variables globales précédentes disparaissent ;
- les mappings mémoire précédents sont remplacés ;
- certains attributs du processus sont conservés.
3. execve() : le vrai appel système
L’appel système fondamental est :
Code: Select all
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
- pathname : chemin du programme à exécuter.
- argv : tableau des arguments.
- envp : tableau des variables d’environnement.
Code: Select all
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *argv[] = { "ls", "-l", "/tmp", NULL };
char *envp[] = { "PATH=/bin:/usr/bin", "LANG=C", NULL };
execve("/bin/ls", argv, envp);
perror("execve");
exit(EXIT_FAILURE);
}
Le tableau envp doit aussi finir par NULL.
4. argv : les arguments du programme
Quand tu exécutes :
Code: Select all
ls -l /home
Code: Select all
argv[0] = "ls";
argv[1] = "-l";
argv[2] = "/home";
argv[3] = NULL;
argv[0] contient généralement le nom du programme.
Mais ce n’est qu’une convention. On peut techniquement mettre autre chose.
Exemple :
Code: Select all
char *argv[] = { "nom_bizarre", "-l", NULL };
execv("/bin/ls", argv);
5. envp : l’environnement du programme
Un programme reçoit aussi des variables d’environnement :
Code: Select all
PATH=/usr/local/bin:/usr/bin:/bin
HOME=/home/user
USER=user
SHELL=/bin/bash
LANG=fr_FR.UTF-8
Exemple :
Code: Select all
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *argv[] = { "env", NULL };
char *envp[] = {
"USER=hydraxx",
"PATH=/bin:/usr/bin",
"CUSTOM=hello",
NULL
};
execve("/usr/bin/env", argv, envp);
perror("execve");
exit(EXIT_FAILURE);
}
6. Les variantes de exec()
Il existe plusieurs fonctions de la famille exec :
Code: Select all
execl()
execle()
execlp()
execv()
execvp()
execve()
- l = list : arguments donnés un par un.
- v = vector : arguments donnés sous forme de tableau.
- p = PATH : recherche du programme dans la variable PATH.
- e = environment : environnement fourni explicitement.
Code: Select all
execl("/bin/ls", "ls", "-l", NULL);
Code: Select all
char *argv[] = { "ls", "-l", NULL };
execv("/bin/ls", argv);
Code: Select all
execlp("ls", "ls", "-l", NULL);
Code: Select all
char *argv[] = { "ls", "-l", NULL };
execvp("ls", argv);
7. PATH et recherche du programme
La variable PATH contient une liste de dossiers où le shell et certaines fonctions exec cherchent les programmes.
Exemple :
Code: Select all
echo $PATH
Code: Select all
/usr/local/bin:/usr/bin:/bin
Code: Select all
execvp("ls", argv);
Mais :
Code: Select all
execve("ls", argv, envp);
Code: Select all
execve("/bin/ls", argv, envp);
- execve() demande un chemin précis.
- execv() demande un chemin précis.
- execvp() peut chercher dans PATH.
- execlp() peut chercher dans PATH.
Si on fait seulement :
Code: Select all
execvp("ls", argv);
Donc si on veut lancer ls tout en gardant le programme parent vivant, on utilise fork().
Exemple :
Code: Select all
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
char *argv[] = { "ls", "-l", NULL };
execvp("ls", argv);
perror("execvp");
_exit(127);
}
waitpid(pid, NULL, 0);
printf("Commande terminée\n");
return 0;
}
Parce qu’après un fork(), l’enfant hérite des buffers stdio du parent. exit() peut flusher des buffers hérités et provoquer des sorties dupliquées. [_exit()] termine directement le processus.
9. Les scripts et le shebang #!
Linux peut exécuter des scripts grâce à la première ligne :
Code: Select all
#!/bin/bash
Code: Select all
#!/usr/bin/python3
Quand on exécute un fichier script marqué exécutable, le noyau lit la première ligne. Si elle commence par #!, il utilise le programme indiqué comme interpréteur.
Exemple :
Code: Select all
#!/bin/bash
echo "hello"
Code: Select all
/bin/bash ./script.sh
Code: Select all
#!/usr/bin/python3
print("hello")
Code: Select all
/usr/bin/python3 ./script.py
Point très important :
Les file descriptors restent ouverts après exec(), sauf s’ils ont le flag FD_CLOEXEC.
Exemple :
Code: Select all
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
execvp("ls", argv);
C’est comme ça que le shell implémente :
Code: Select all
ls > out.txt
Code: Select all
fork();
open("out.txt");
dup2(fd, STDOUT_FILENO);
execvp("ls", argv);
Pour éviter qu’un descripteur soit hérité après exec, on utilise :
Code: Select all
FD_CLOEXEC
Code: Select all
int flags = fcntl(fd, F_GETFD);
flags |= FD_CLOEXEC;
fcntl(fd, F_SETFD, flags);
Code: Select all
open("file.txt", O_RDONLY | O_CLOEXEC);
- éviter les fuites de descripteurs ;
- éviter qu’un programme enfant ait accès à un fichier sensible ;
- éviter de garder involontairement des sockets ouvertes ;
- éviter certains bugs subtils dans les serveurs.
La fonction system() permet d’exécuter une commande via le shell.
Prototype :
Code: Select all
#include <stdlib.h>
int system(const char *command);
Code: Select all
system("ls -l");
Code: Select all
/bin/sh -c "ls -l"
Code: Select all
pid_t pid = fork();
if (pid == 0) {
execl("/bin/sh", "sh", "-c", command, NULL);
_exit(127);
}
waitpid(pid, &status, 0);
Différence principale :
- exec() remplace le programme courant.
- system() lance un shell dans un enfant puis revient dans le programme appelant.
Code: Select all
printf("avant\n");
system("ls");
printf("après\n");
Avec :
Code: Select all
printf("avant\n");
execlp("ls", "ls", NULL);
printf("après\n");
14. Pourquoi system() peut être dangereux
Comme system() passe par le shell, il interprète les caractères spéciaux :
Code: Select all
;
&&
||
|
>
<
$()
`
*
Code: Select all
char cmd[256];
sprintf(cmd, "cat %s", filename);
system(cmd);
Code: Select all
test.txt; rm -rf /tmp/test
Code: Select all
cat test.txt; rm -rf /tmp/test
Code: Select all
fork();
execve();
15. Les signaux dans system()
Une implémentation correcte de system() doit gérer les signaux.
Pendant que la commande tourne, le parent doit généralement :
- ignorer temporairement SIGINT ;
- ignorer temporairement SIGQUIT ;
- bloquer temporairement SIGCHLD ;
- attendre précisément l’enfant créé ;
- restaurer les anciens handlers ensuite.
Parce que si le programme a déjà un handler SIGCHLD qui fait :
Code: Select all
while (waitpid(-1, NULL, WNOHANG) > 0)
;
Résultat : system() ne pourrait plus attendre son enfant.
16. Process accounting : surveiller les processus terminés
Le process accounting est un mécanisme du noyau qui écrit un enregistrement quand un processus termine.
Il permet de savoir :
- quelle commande a été exécutée ;
- quel UID/GID l’a lancée ;
- quel PID/PPID elle avait ;
- combien de CPU elle a consommé ;
- combien de mémoire elle a utilisé ;
- quand elle a démarré ;
- comment elle s’est terminée.
Code: Select all
#include <unistd.h>
int acct(const char *filename);
Code: Select all
acct("/var/log/pacct");
Code: Select all
acct(NULL);
17. Les enregistrements d’accounting
Un record d’accounting peut contenir des champs comme :
Code: Select all
ac_comm // nom de la commande
ac_uid // utilisateur
ac_gid // groupe
ac_pid // PID
ac_ppid // PID parent
ac_btime // heure de démarrage
ac_etime // temps écoulé
ac_utime // temps CPU utilisateur
ac_stime // temps CPU noyau
ac_exitcode // statut de terminaison
Attention : ce n’est pas un mécanisme de tracing temps réel. C’est plutôt un historique écrit à la fin de vie du processus.
18. fork(), vfork() et clone()
Linux possède plusieurs mécanismes de création de processus :
- fork() : création classique d’un processus enfant.
- vfork() : variante optimisée/dangereuse historiquement utilisée avant exec.
- clone() : appel Linux avancé permettant de choisir ce qui est partagé.
Code: Select all
fork() = enfant séparé, copie logique du parent
vfork() = enfant temporairement lié au parent, surtout avant exec
clone() = création configurable, base des threads et containers
Avec fork(), l’enfant est presque une copie du parent.
Mais la mémoire n’est pas copiée physiquement immédiatement. Linux utilise souvent le copy-on-write.
Cela signifie :
- parent et enfant partagent d’abord les mêmes pages mémoire en lecture ;
- si l’un modifie une page, le noyau crée une copie privée ;
- cela rend fork beaucoup plus efficace.
Code: Select all
int x = 10;
pid_t pid = fork();
if (pid == 0) {
x = 20;
printf("enfant x=%d\n", x);
} else {
wait(NULL);
printf("parent x=%d\n", x);
}
20. vfork() : à connaître mais à éviter
vfork() a été conçu pour optimiser le cas où l’enfant fait immédiatement exec().
Avec vfork(), l’enfant partage temporairement l’espace mémoire du parent et le parent est suspendu jusqu’à ce que l’enfant fasse :
Code: Select all
exec();
Code: Select all
_exit();
Règle pratique :
En programmation normale, préfère fork() ou posix_spawn() plutôt que vfork().
21. clone() : l’appel Linux avancé
clone() est un appel système Linux permettant de créer un nouveau processus ou une entité proche d’un thread.
Prototype simplifié :
Code: Select all
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
Exemple :
Code: Select all
int childFunc(void *arg)
{
printf("enfant clone\n");
return 0;
}
clone(childFunc, stackTop, flags, arg);
22. clone() demande une pile enfant
Avec clone(), on doit fournir une stack pour l’enfant.
Exemple :
Code: Select all
#define STACK_SIZE 65536
char *stack = malloc(STACK_SIZE);
char *stackTop = stack + STACK_SIZE;
Code: Select all
clone(childFunc, stackTop, flags, arg);
23. Exemple simple avec clone()
Code: Select all
#define _GNU_SOURCE
#include <sched.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define STACK_SIZE 65536
static int childFunc(void *arg)
{
printf("Enfant clone: pid=%d\n", getpid());
return 0;
}
int main(void)
{
char *stack = malloc(STACK_SIZE);
if (stack == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
char *stackTop = stack + STACK_SIZE;
pid_t pid = clone(childFunc, stackTop, SIGCHLD, NULL);
if (pid == -1) {
perror("clone");
exit(EXIT_FAILURE);
}
waitpid(pid, NULL, 0);
free(stack);
return 0;
}
24. Les flags importants de clone()
Les flags de clone() permettent de définir ce qui est partagé.
Les plus importants :
Code: Select all
CLONE_VM
CLONE_FILES
CLONE_FS
CLONE_SIGHAND
CLONE_THREAD
CLONE_PARENT
CLONE_NEWNS
CLONE_NEWPID
CLONE_NEWNET
CLONE_NEWUTS
CLONE_NEWUSER
25. CLONE_VM : partager la mémoire
CLONE_VM signifie que parent et enfant partagent le même espace mémoire.
Sans CLONE_VM :
Code: Select all
fork-like : mémoire séparée via copy-on-write
Code: Select all
thread-like : mémoire commune
Code: Select all
int global = 0;
// enfant clone avec CLONE_VM
global = 42;
// parent voit global == 42
26. CLONE_FILES : partager les file descriptors
CLONE_FILES signifie que parent et enfant partagent la même table de file descriptors.
Conséquence : si l’enfant fait :
Code: Select all
close(fd);
Sans CLONE_FILES, parent et enfant ont deux tables séparées, même si elles pointent initialement vers les mêmes fichiers ouverts.
27. CLONE_FS : partager les infos filesystem
CLONE_FS partage des informations comme :
- le répertoire courant ;
- le répertoire racine ;
- l’umask.
Code: Select all
chdir("/tmp");
28. CLONE_SIGHAND : partager les handlers de signaux
CLONE_SIGHAND partage les dispositions de signaux.
Donc si un thread modifie le handler d’un signal, cela affecte les autres entités partageant les handlers.
En pratique, ce flag est utilisé avec CLONE_VM.
29. CLONE_THREAD : même groupe de threads
CLONE_THREAD place l’enfant dans le même groupe de threads.
Cela rapproche l’entité créée d’un vrai thread POSIX.
Un ensemble de threads Linux partage souvent :
Code: Select all
CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD
30. clone() et les threads Linux
Sous Linux, un thread est souvent vu comme une tâche créée avec clone() et partageant beaucoup d’attributs avec les autres threads du même processus.
Un processus multithreadé possède :
- un même espace mémoire ;
- plusieurs piles ;
- plusieurs contextes d’exécution ;
- des file descriptors communs ;
- des handlers de signaux communs ;
- un même groupe de threads.
Code: Select all
processus classique = isolation forte
thread = clone avec beaucoup de partage
Certains flags de clone créent des namespaces.
Exemples :
Code: Select all
CLONE_NEWNS // namespace de montage
CLONE_NEWPID // namespace PID
CLONE_NEWNET // namespace réseau
CLONE_NEWUTS // hostname/domainname séparés
CLONE_NEWUSER // namespace utilisateur
Un container utilise l’isolation de ressources comme :
- PID namespace ;
- mount namespace ;
- network namespace ;
- user namespace ;
- UTS namespace ;
- cgroups pour limiter les ressources.
Un container n’est pas une machine virtuelle complète.
C’est un ou plusieurs processus Linux isolés par des mécanismes du noyau.
Avec clone() et les namespaces, un processus peut avoir :
- sa propre vue des PID ;
- sa propre pile réseau ;
- ses propres montages ;
- son propre hostname ;
- sa propre correspondance utilisateur.
33. Attributs hérités par fork()
Après un fork(), l’enfant hérite de beaucoup de choses :
- variables d’environnement ;
- répertoire courant ;
- umask ;
- file descriptors ouverts ;
- mappings mémoire initiaux en copy-on-write ;
- handlers de signaux ;
- masque de signaux ;
- UID/GID réels/effectifs/sauvegardés ;
- groupes supplémentaires ;
- limites de ressources ;
- priorité/nice ;
- terminal de contrôle ;
- session et groupe de processus.
- un nouveau PID ;
- un PPID correspondant au parent ;
- ses propres statistiques CPU ;
- ses propres timers dans plusieurs cas ;
- ses propres verrous dans certains cas ;
- son propre statut de terminaison.
Après exec(), le processus garde notamment :
- le même PID ;
- le même PPID ;
- le même répertoire courant ;
- la même umask ;
- les mêmes file descriptors ouverts, sauf FD_CLOEXEC ;
- les mêmes UID/GID, sauf effets setuid/setgid ;
- les mêmes groupes supplémentaires ;
- les mêmes limites de ressources ;
- la même priorité ;
- le même terminal de contrôle ;
- certains paramètres de session/groupe de processus.
- le code exécutable ;
- les variables globales ;
- la pile ;
- le tas ;
- les mappings mémoire privés ;
- les handlers de signaux personnalisés ;
- les threads autres que le thread appelant.
Après exec() :
- les signaux ignorés peuvent rester ignorés ;
- les handlers personnalisés sont remis à leur action par défaut ;
- le masque de signaux est généralement conservé ;
- les signaux pendants peuvent être affectés selon les cas.
Parce que le code du handler appartenait à l’ancien programme. Après exec, ce code n’existe plus.
Exemple :
Code: Select all
signal(SIGINT, mon_handler);
execvp("ls", argv);
36. Threads et exec()
Dans un processus multithreadé, si un thread appelle exec(), le nouveau programme remplace tout le processus.
Les autres threads disparaissent.
C’est logique : exec() remplace l’image du programme entier.
Donc dans un programme multithreadé, appeler exec demande de la prudence.
37. Différence fondamentale entre fork() et exec()
Résumé clair :
Code: Select all
fork() crée un nouveau processus.
exec() ne crée pas de processus.
Code: Select all
ancien programme parent continue
nouveau processus enfant continue aussi
Code: Select all
ancien programme disparaît
nouveau programme prend sa place
PID inchangé
Code: Select all
fork()
- crée un enfant
- mémoire séparée par copy-on-write
- simple et sûr
- utilisé massivement
vfork()
- enfant partage temporairement l'espace du parent
- parent suspendu
- doit faire exec ou _exit rapidement
- dangereux si mal utilisé
clone()
- création configurable
- partage mémoire/fichiers/signaux selon flags
- base des threads et containers
- Linux spécifique
exec()
- ne crée pas d'enfant
- remplace le programme courant
- garde le même PID
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_LINE 256
#define MAX_ARGS 32
int main(void)
{
char line[MAX_LINE];
while (1) {
printf("mini-shell> ");
fflush(stdout);
if (fgets(line, sizeof(line), stdin) == NULL)
break;
line[strcspn(line, "\n")] = '\0';
if (strcmp(line, "exit") == 0)
break;
char *argv[MAX_ARGS];
int argc = 0;
char *tok = strtok(line, " ");
while (tok != NULL && argc < MAX_ARGS - 1) {
argv[argc++] = tok;
tok = strtok(NULL, " ");
}
argv[argc] = NULL;
if (argc == 0)
continue;
pid_t pid = fork();
if (pid == -1) {
perror("fork");
continue;
}
if (pid == 0) {
execvp(argv[0], argv);
perror("execvp");
_exit(127);
}
waitpid(pid, NULL, 0);
}
return 0;
}
Code: Select all
lire commande
fork
execvp dans l’enfant
waitpid dans le parent
Code: Select all
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
_exit(1);
}
dup2(fd, STDOUT_FILENO);
close(fd);
char *argv[] = { "ls", "-l", NULL };
execvp("ls", argv);
perror("execvp");
_exit(127);
}
waitpid(pid, NULL, 0);
return 0;
}
41. Exemple : system() sécurisé ?
Il vaut mieux éviter :
Code: Select all
char cmd[512];
snprintf(cmd, sizeof(cmd), "grep %s file.txt", user_input);
system(cmd);
Code: Select all
char *argv[] = { "grep", user_input, "file.txt", NULL };
execvp("grep", argv);
42. Pièges classiques
- Croire que exec crée un processus : faux.
- Oublier le NULL final dans argv ou envp.
- Utiliser exit() au lieu de _exit() après fork dans l’enfant quand exec échoue.
- Oublier que les fd restent ouverts après exec.
- Oublier FD_CLOEXEC sur des sockets sensibles.
- Utiliser system() avec une chaîne venant de l’utilisateur.
- Modifier trop de choses après vfork().
- Utiliser clone() sans bien comprendre les flags.
- Oublier d’allouer une stack correcte pour clone().
- Confondre CLONE_VM et fork classique.
Code: Select all
fork() = crée un enfant
exec() = remplace le programme courant
wait() = récupère la fin de l’enfant
system() = fork + /bin/sh -c + wait
acct() = journalise les processus terminés
clone() = création avancée configurable
Les chapitres 27 et 28 expliquent comment Linux lance et remplace les programmes, puis comment le noyau gère plus finement les attributs de processus.
Le modèle fondamental d’un shell est :
Code: Select all
fork();
exec();
wait();
Les fonctions execl(), execv(), execlp(), execvp() sont des wrappers pratiques autour de cette idée.
system() est pratique, car il permet d’exécuter une commande shell complète, mais il est plus dangereux car il passe par /bin/sh -c.
acct() sert à activer le process accounting, pour enregistrer des informations quand les processus se terminent.
clone() est l’appel Linux avancé qui permet de créer des tâches configurables. Selon ses flags, il peut ressembler à fork, à un thread, ou servir à créer des processus isolés par namespaces.
La grande idée finale :
Code: Select all
fork() copie un contexte.
exec() remplace le programme.
clone() choisit précisément ce qui est partagé.
- des shells Linux ;
- des démons ;
- des serveurs ;
- des pipelines ;
- des redirections ;
- des threads Linux ;
- des containers ;
- de l’audit de processus.
