Linux System Programming — création de processus, exécution de programmes, clone(), attributs hérités et process account

Ce forum est dédié à apprendre le développement de programmes user mode sur Linux

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 48
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Linux System Programming — création de processus, exécution de programmes, clone(), attributs hérités et process account

Post by Hydraxx »

Linux System Programming — création de processus, exécution de programmes, clone(), attributs hérités et process accounting


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.
Le modèle classique d’un shell Linux est donc :

Code: Select all

fork();   // créer un enfant
exec();   // transformer l'enfant en programme demandé
wait();   // le parent attend l'enfant
Exemple mental : quand tu tapes :

Code: Select all

ls -l
Le shell ne devient pas directement ls. Il fait généralement :

Code: Select all

pid_t pid = fork();

if (pid == 0) {
    execlp("ls", "ls", "-l", NULL);
    _exit(127);
}

waitpid(pid, NULL, 0);
Le parent reste le shell. L’enfant devient ls.

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);
}
Si execv() réussit, le perror() ne sera jamais exécuté.

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.
Donc exec() ne crée pas un processus. Il remplace le programme dans un processus déjà existant.

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[]);
Paramètres :
  • pathname : chemin du programme à exécuter.
  • argv : tableau des arguments.
  • envp : tableau des variables d’environnement.
Exemple :

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 argv doit toujours finir par NULL.

Le tableau envp doit aussi finir par NULL.

4. argv : les arguments du programme

Quand tu exécutes :

Code: Select all

ls -l /home
Le programme reçoit approximativement :

Code: Select all

argv[0] = "ls";
argv[1] = "-l";
argv[2] = "/home";
argv[3] = NULL;
Convention importante :

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);
Le programme lancé est bien /bin/ls, mais son argv[0] vaut nom_bizarre.

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
Avec execve(), on peut choisir précisément l’environnement transmis.

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);
}
Le programme env affichera uniquement les variables fournies dans envp.

6. Les variantes de exec()

Il existe plusieurs fonctions de la famille exec :

Code: Select all

execl()
execle()
execlp()
execv()
execvp()
execve()
Les lettres ont une signification :
  • 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.
Exemple avec execl() :

Code: Select all

execl("/bin/ls", "ls", "-l", NULL);
Exemple avec execv() :

Code: Select all

char *argv[] = { "ls", "-l", NULL };
execv("/bin/ls", argv);
Exemple avec execlp() :

Code: Select all

execlp("ls", "ls", "-l", NULL);
Exemple avec execvp() :

Code: Select all

char *argv[] = { "ls", "-l", NULL };
execvp("ls", argv);
Ici, les versions avec p cherchent ls dans le PATH.

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
Résultat possible :

Code: Select all

/usr/local/bin:/usr/bin:/bin
Quand on utilise :

Code: Select all

execvp("ls", argv);
Linux cherche ls dans les dossiers du PATH.

Mais :

Code: Select all

execve("ls", argv, envp);
ne cherche pas dans le PATH. Il faut donner un chemin valide :

Code: Select all

execve("/bin/ls", argv, envp);
Résumé :
  • execve() demande un chemin précis.
  • execv() demande un chemin précis.
  • execvp() peut chercher dans PATH.
  • execlp() peut chercher dans PATH.
8. fork() + exec() : modèle classique

Si on fait seulement :

Code: Select all

execvp("ls", argv);
le programme courant devient ls.

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;
}
Pourquoi utiliser [_exit()] dans l’enfant si exec échoue ?

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
ou :

Code: Select all

#!/usr/bin/python3
Cette ligne est appelée shebang.

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"
Quand on lance ce script, le noyau exécute en réalité quelque chose du genre :

Code: Select all

/bin/bash ./script.sh
Autre exemple :

Code: Select all

#!/usr/bin/python3
print("hello")
Linux lance :

Code: Select all

/usr/bin/python3 ./script.py
10. File descriptors et exec()

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);
Ici, ls écrit dans out.txt, car il hérite de la sortie standard redirigée.

C’est comme ça que le shell implémente :

Code: Select all

ls > out.txt
Schéma :

Code: Select all

fork();
open("out.txt");
dup2(fd, STDOUT_FILENO);
execvp("ls", argv);
11. FD_CLOEXEC et O_CLOEXEC

Pour éviter qu’un descripteur soit hérité après exec, on utilise :

Code: Select all

FD_CLOEXEC
Exemple :

Code: Select all

int flags = fcntl(fd, F_GETFD);
flags |= FD_CLOEXEC;
fcntl(fd, F_SETFD, flags);
On peut aussi ouvrir directement avec :

Code: Select all

open("file.txt", O_RDONLY | O_CLOEXEC);
Intérêt :
  • é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.
12. system() : exécuter une commande shell

La fonction system() permet d’exécuter une commande via le shell.

Prototype :

Code: Select all

#include <stdlib.h>

int system(const char *command);
Exemple :

Code: Select all

system("ls -l");
Cela revient globalement à :

Code: Select all

/bin/sh -c "ls -l"
Implémentation simplifiée :

Code: Select all

pid_t pid = fork();

if (pid == 0) {
    execl("/bin/sh", "sh", "-c", command, NULL);
    _exit(127);
}

waitpid(pid, &status, 0);
13. system() vs exec()

Différence principale :
  • exec() remplace le programme courant.
  • system() lance un shell dans un enfant puis revient dans le programme appelant.
Exemple :

Code: Select all

printf("avant\n");
system("ls");
printf("après\n");
Ici, après s’affiche.

Avec :

Code: Select all

printf("avant\n");
execlp("ls", "ls", NULL);
printf("après\n");
après ne s’affiche pas si exec réussit.

14. Pourquoi system() peut être dangereux

Comme system() passe par le shell, il interprète les caractères spéciaux :

Code: Select all

;
&&
||
|
>
<
$()
`
*
Exemple dangereux :

Code: Select all

char cmd[256];
sprintf(cmd, "cat %s", filename);
system(cmd);
Si filename vient de l’utilisateur, il peut mettre :

Code: Select all

test.txt; rm -rf /tmp/test
La commande devient :

Code: Select all

cat test.txt; rm -rf /tmp/test
Donc dans un programme sérieux, surtout privilégié, il vaut mieux utiliser :

Code: Select all

fork();
execve();
avec des arguments séparés plutôt qu’une commande shell assemblée en string.

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.
Pourquoi bloquer SIGCHLD ?

Parce que si le programme a déjà un handler SIGCHLD qui fait :

Code: Select all

while (waitpid(-1, NULL, WNOHANG) > 0)
    ;
ce handler pourrait récupérer l’enfant créé par system() avant system() lui-même.

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.
L’appel système associé est :

Code: Select all

#include <unistd.h>

int acct(const char *filename);
Activer l’accounting :

Code: Select all

acct("/var/log/pacct");
Désactiver :

Code: Select all

acct(NULL);
Ce mécanisme est surtout utile pour l’administration, l’audit, la supervision et l’analyse d’activité système.

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
Le noyau écrit ces informations quand le processus se termine.

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é.
Résumé :

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
19. fork() : copie logique du parent

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.
Exemple :

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);
}
Le parent affichera toujours 10.

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();
ou :

Code: Select all

_exit();
Il ne faut pas modifier les variables du parent ou appeler n’importe quelles fonctions après un vfork().

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);
Contrairement à fork(), l’enfant ne reprend pas exactement à la même instruction. Il commence dans une fonction donnée.

Exemple :

Code: Select all

int childFunc(void *arg)
{
    printf("enfant clone\n");
    return 0;
}

clone(childFunc, stackTop, flags, arg);
Avec clone(), on peut choisir ce que parent et enfant partagent.

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;
Puis :

Code: Select all

clone(childFunc, stackTop, flags, arg);
Sur beaucoup d’architectures, la pile grandit vers les adresses basses. On passe donc généralement le haut de la zone allouée.

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;
}
Ici, le flag SIGCHLD indique que le parent recevra SIGCHLD quand l’enfant terminera, comme avec fork.

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
Chaque flag modifie fortement la relation parent/enfant.

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
Avec CLONE_VM :

Code: Select all

thread-like : mémoire commune
Exemple mental :

Code: Select all

int global = 0;

// enfant clone avec CLONE_VM
global = 42;

// parent voit global == 42
C’est une base du fonctionnement des threads.

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);
alors le parent voit aussi ce fd fermé.

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.
Donc si un enfant clone avec CLONE_FS fait :

Code: Select all

chdir("/tmp");
cela peut affecter le parent.

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
C’est pour ça que les threads Linux sont techniquement créés à partir de mécanismes proches de clone().

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.
Donc, conceptuellement :

Code: Select all

processus classique = isolation forte
thread = clone avec beaucoup de partage
31. clone() et les namespaces

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
Ces mécanismes sont importants pour comprendre les containers Linux.

Un container utilise l’isolation de ressources comme :
  • PID namespace ;
  • mount namespace ;
  • network namespace ;
  • user namespace ;
  • UTS namespace ;
  • cgroups pour limiter les ressources.
32. clone() et containers

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.
C’est une base technique derrière Docker, LXC et d’autres systèmes de containers.

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.
Mais il obtient :
  • 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.
34. Attributs conservés par exec()

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.
Mais exec() remplace :
  • 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.
35. Signaux et exec()

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.
Pourquoi les handlers personnalisés sont supprimés ?

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);
Après exec, mon_handler ne peut plus être utilisé.

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.
Avec fork() :

Code: Select all

ancien programme parent continue
nouveau processus enfant continue aussi
Avec exec() :

Code: Select all

ancien programme disparaît
nouveau programme prend sa place
PID inchangé
38. Comparatif fork(), vfork(), clone(), exec()

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
39. Exemple : mini-shell simple

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;
}
Ce mini-shell montre le modèle central :

Code: Select all

lire commande
fork
execvp dans l’enfant
waitpid dans le parent
40. Exemple : redirection stdout avant exec()

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;
}
Le programme ls hérite de la sortie standard redirigée.

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);
Préférer :

Code: Select all

char *argv[] = { "grep", user_input, "file.txt", NULL };
execvp("grep", argv);
Ici, user_input est transmis comme argument, pas interprété comme syntaxe shell.

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.
43. Résumé ultra court

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
44. Résumé final complet

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();
execve() est l’appel système central : il charge un nouveau programme avec ses arguments et son environnement.

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é.
Ces mécanismes sont à la base :
  • des shells Linux ;
  • des démons ;
  • des serveurs ;
  • des pipelines ;
  • des redirections ;
  • des threads Linux ;
  • des containers ;
  • de l’audit de processus.
Si tu comprends fork + exec + wait + FD hérités + clone flags, tu as déjà une très grosse base en programmation système Linux.

Who is online

Users browsing this forum: No registered users and 1 guest