Aller au contenu

Exam Rank 04 : Processus & Parsing

Télécharger les fichiers du sujet :

L’Exam Rank 04 teste votre capacité à travailler avec les appels système Unix. Ces compétences sont essentielles pour écrire des shells, des daemons et tout programme qui doit créer d’autres processus.

IMPORTANT : L’Exam Rank 04 est en C pur, pas en C++. Ces exercices testent les fondamentaux de la programmation système Unix : processus, pipes, descripteurs de fichiers et parsing.

L’examen a deux niveaux : Level 1 teste la gestion des processus, Level 2 teste le parsing. Vous obtiendrez un exercice de chaque niveau.

Structure de l’Examen :

  • Level 1 : picoshell, ft_popen ou sandbox (orienté processus)
  • Level 2 : argo ou vbc (orienté parsing)

Concepts Clés :

  • Création de processus avec fork()
  • Communication inter-processus avec pipe()
  • Manipulation de descripteurs de fichiers avec dup2()
  • Gestion des signaux avec sigaction()
  • Parsing par descente récursive
  • Évaluation d’expressions avec priorité des opérateurs

Cet examen couvre la programmation système Unix. Voici les opérateurs C que vous utiliserez :

pid_t pid = fork();
if (pid == -1) { /* erreur */ }
else if (pid == 0) { /* enfant */ }
else { /* parent - pid est l'ID de l'enfant */ }
  • fork() retourne des valeurs différentes dans le parent et l’enfant
  • -1 signifie erreur
  • 0 dans l’enfant signifie “je suis l’enfant”
  • Nombre positif dans le parent est l’ID de processus (PID) de l’enfant
  • C’est ainsi que vous différenciez les chemins de code parent et enfant
int pipefd[2];
pipe(pipefd);
// pipefd[0] = extrémité lecture
// pipefd[1] = extrémité écriture
  • pipefd[2] est un tableau de deux descripteurs de fichiers
  • pipefd[0] est l’extrémité lecture (0 ressemble à un O - “ouvrir pour lire”)
  • pipefd[1] est l’extrémité écriture (1 ressemble à un stylo - “écrire”)
  • Mnémonique : 0 = entrée, 1 = sortie
dup2(pipefd[1], STDOUT_FILENO); // Rediriger stdout vers le pipe
  • dup2(old, new) fait pointer le descripteur de fichier new vers le même fichier que old
  • Après cela, écrire dans new écrit en fait dans la destination de old
  • STDOUT_FILENO = 1 (sortie standard)
  • STDIN_FILENO = 0 (entrée standard)
  • STDERR_FILENO = 2 (erreur standard)
wait(NULL); // Attendre N'IMPORTE quel enfant
waitpid(pid, &status, 0); // Attendre un enfant SPÉCIFIQUE
  • wait(NULL) met le parent en pause jusqu’à ce qu’un processus enfant se termine
  • waitpid(pid, &status, 0) attend l’enfant spécifique avec le PID pid
  • &status stocke des informations sur comment l’enfant s’est terminé
  • Sans wait(), les processus enfants deviennent des “zombies” (terminés mais non nettoyés)
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
}
  • WIFEXITED(status) vérifie si l’enfant s’est terminé normalement (via exit() ou retour de main())
  • WEXITSTATUS(status) extrait le code de sortie (0-255)
  • WIFSIGNALED(status) vérifie si l’enfant a été tué par un signal
  • WTERMSIG(status) extrait quel signal a tué l’enfant
struct_ptr->member; // Équivalent à (*struct_ptr).member
  • -> combine le déréférencement de pointeur avec l’accès aux membres
  • Utilisé avec les pointeurs vers des structures
  • ptr->field est équivalent à (*ptr).field
  • Le * déréférence le pointeur, . accède au membre
char **argv; // Tableau de chaînes (tableau de pointeurs char)
  • ** signifie “pointeur vers pointeur”
  • char** est un tableau de chaînes (chaque chaîne est char*)
  • argv[0] est la première chaîne (nom du programme)
  • argv[1] est la deuxième chaîne (premier argument)
  • Commun dans main(int argc, char **argv)
*s++; // Avancer le pointeur dans le parsing par descente récursive
  • *s déréférence le pointeur pour obtenir le caractère actuel
  • (*s)++ obtient le caractère actuel puis avance le pointeur
  • Utilisé dans le parsing pour consommer les caractères un par un

fork() crée une copie exacte du processus actuel.

#include <unistd.h>
#include <sys/wait.h>
pid_t pid = fork();
if (pid == -1) {
// Erreur - fork a échoué
perror("fork");
exit(1);
}
else if (pid == 0) {
// Processus enfant
// pid == 0 signifie "je suis l'enfant"
printf("Je suis l'enfant (PID: %d)\n", getpid());
exit(0);
}
else {
// Processus parent
// pid == PID de l'enfant
printf("Je suis le parent, PID de l'enfant: %d\n", pid);
wait(NULL); // Attendre que l'enfant se termine
}
ParentEnfant
Retourne le PID de l’enfantRetourne 0
Continue l’exécutionDémarre au même point
A les descripteurs de fichiers originauxObtient des copies des descripteurs
Doit wait() ses enfantsDevrait exit() quand terminé

Un pipe crée un canal de données unidirectionnel :

  • pipefd[0] = extrémité lecture
  • pipefd[1] = extrémité écriture
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(1);
}
// pipefd[0] - lire depuis le pipe
// pipefd[1] - écrire dans le pipe

Pour connecter cmd1 | cmd2 :

  1. Créer le pipe
  2. Fork pour cmd1 : rediriger stdout vers l’extrémité écriture du pipe
  3. Fork pour cmd2 : rediriger stdin depuis l’extrémité lecture du pipe
  4. Fermer les extrémités de pipe inutilisées dans tous les processus
int pipefd[2];
pipe(pipefd);
pid_t pid1 = fork();
if (pid1 == 0) {
// Enfant 1 (cmd1) : écrit dans le pipe
close(pipefd[0]); // Fermer l'extrémité lecture
dup2(pipefd[1], STDOUT_FILENO); // Rediriger stdout vers le pipe
close(pipefd[1]); // Fermer l'extrémité écriture originale
execvp(cmd1[0], cmd1);
exit(1);
}
pid_t pid2 = fork();
if (pid2 == 0) {
// Enfant 2 (cmd2) : lit depuis le pipe
close(pipefd[1]); // Fermer l'extrémité écriture
dup2(pipefd[0], STDIN_FILENO); // Rediriger stdin depuis le pipe
close(pipefd[0]); // Fermer l'extrémité lecture originale
execvp(cmd2[0], cmd2);
exit(1);
}
// Parent : fermer les deux extrémités et attendre
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);

Règle Critique : Fermer les Extrémités de Pipe Inutilisées !

Section intitulée « Règle Critique : Fermer les Extrémités de Pipe Inutilisées ! »

Pourquoi ? L’extrémité lecture d’un pipe retourne EOF uniquement quand TOUTES les extrémités écriture sont fermées. Si vous oubliez de fermer l’extrémité écriture dans le processus lecteur, il attendra indéfiniment des données.

// FAUX - cmd2 va bloquer !
pid_t pid2 = fork();
if (pid2 == 0) {
dup2(pipefd[0], STDIN_FILENO);
// Oublié de fermer pipefd[1] !
execvp(cmd2[0], cmd2);
}
// CORRECT
pid_t pid2 = fork();
if (pid2 == 0) {
close(pipefd[1]); // DOIT fermer l'extrémité écriture !
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
execvp(cmd2[0], cmd2);
}

3. dup2() - Redirection de Descripteurs de Fichiers

Section intitulée « 3. dup2() - Redirection de Descripteurs de Fichiers »

dup2(oldfd, newfd) fait pointer newfd vers le même fichier que oldfd.

// Rediriger stdout vers un fichier
int fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
dup2(fd, STDOUT_FILENO); // Maintenant stdout écrit dans output.txt
close(fd); // Fermer le fd original (stdout fonctionne toujours)
printf("Ceci va dans output.txt\n");
// Rediriger stdout vers l'extrémité écriture du pipe
dup2(pipefd[1], STDOUT_FILENO);
// Rediriger stdin depuis l'extrémité lecture du pipe
dup2(pipefd[0], STDIN_FILENO);
// Rediriger stderr vers stdout
dup2(STDOUT_FILENO, STDERR_FILENO);

FonctionRésolution de CheminEnvironnement
execve(path, argv, envp)Doit être chemin completVous fournissez envp
execvp(file, argv)Recherche dans PATHUtilise l’environ actuel
// execvp - plus facile, recherche dans PATH
char *argv[] = {"ls", "-la", NULL};
execvp("ls", argv); // Trouvera /bin/ls automatiquement
// execve - plus de contrôle
char *envp[] = {"PATH=/bin", NULL};
execve("/bin/ls", argv, envp);

Important : execvp Ne Retourne Jamais en Cas de Succès !

Section intitulée « Important : execvp Ne Retourne Jamais en Cas de Succès ! »
execvp(cmd[0], cmd);
// Si nous arrivons ici, execvp a échoué !
perror("execvp");
exit(1); // L'enfant doit quitter en cas d'échec d'exec

#include <signal.h>
void handler(int sig) {
// Gérer le signal
write(1, "Signal capturé\n", 15);
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGALRM, &sa, NULL);
alarm(5); // Envoyer SIGALRM dans 5 secondes
pause(); // Attendre le signal
return 0;
}
alarm(timeout); // Planifier SIGALRM dans 'timeout' secondes
// ... faire le travail ...
alarm(0); // Annuler l'alarme
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
// Le processus s'est terminé normalement
int exit_code = WEXITSTATUS(status);
printf("Terminé avec le code %d\n", exit_code);
}
else if (WIFSIGNALED(status)) {
// Processus tué par un signal
int sig = WTERMSIG(status);
printf("Tué par le signal : %s\n", strsignal(sig));
}

Pour le parsing d’expressions, définir une grammaire :

expr = term (('+') term)*
term = factor (('*') factor)*
factor = '(' expr ')' | NUMBER
int parse_expr(const char **s);
int parse_term(const char **s);
int parse_factor(const char **s);
// Factor : nombre ou (expr)
int parse_factor(const char **s) {
if (**s == '(') {
(*s)++; // Sauter '('
int result = parse_expr(s);
if (**s != ')') error("')' attendu");
(*s)++; // Sauter ')'
return result;
}
if (isdigit(**s)) {
int num = **s - '0';
(*s)++;
return num;
}
error("Token inattendu");
return 0;
}
// Term : factor (* factor)*
int parse_term(const char **s) {
int result = parse_factor(s);
while (**s == '*') {
(*s)++;
result *= parse_factor(s);
}
return result;
}
// Expr : term (+ term)*
int parse_expr(const char **s) {
int result = parse_term(s);
while (**s == '+') {
(*s)++;
result += parse_term(s);
}
return result;
}

Pourquoi Ça Marche pour la Priorité des Opérateurs

Section intitulée « Pourquoi Ça Marche pour la Priorité des Opérateurs »
  • * a une priorité plus élevée que +
  • En parsant * dans term (appelé en premier), il lie plus fort
  • 3+4*5 est parsé comme 3+(4*5) = 23, pas (3+4)*5 = 35

value = string | number | object
object = '{' (pair (',' pair)*)? '}'
pair = string ':' value
string = '"' characters '"'
number = digits
int parse_value(FILE *f, json *dst) {
int c = getc(f);
if (c == '"') {
return parse_string(f, dst);
}
else if (isdigit(c)) {
ungetc(c, f);
return parse_number(f, dst);
}
else if (c == '{') {
return parse_object(f, dst);
}
else if (c == EOF) {
printf("Fin d'entrée inattendue\n");
return -1;
}
else {
printf("Token inattendu '%c'\n", c);
return -1;
}
}
int parse_string(FILE *f, json *dst) {
// " d'ouverture déjà consommé
char buffer[1024];
int i = 0;
int c;
while ((c = getc(f)) != EOF && c != '"') {
if (c == '\\') {
c = getc(f);
if (c == '"' || c == '\\') {
buffer[i++] = c;
} else {
// Échappement invalide
printf("Token inattendu '%c'\n", c);
return -1;
}
} else {
buffer[i++] = c;
}
}
if (c == EOF) {
printf("Fin d'entrée inattendue\n");
return -1;
}
buffer[i] = '\0';
// Stocker la chaîne dans dst...
return 1;
}

// FAUX - fuite de fds
int pipefd[2];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0) {
dup2(pipefd[1], 1);
execvp(cmd[0], cmd);
}
// Le parent a oublié de fermer pipefd[0] et pipefd[1] !
// CORRECT
if (pid == 0) {
close(pipefd[0]);
dup2(pipefd[1], 1);
close(pipefd[1]);
execvp(cmd[0], cmd);
exit(1);
}
close(pipefd[0]);
close(pipefd[1]);
// FAUX - crée des zombies
for (int i = 0; i < n; i++) {
if (fork() == 0) {
execvp(cmds[i][0], cmds[i]);
exit(1);
}
}
// Le parent quitte sans attendre !
// CORRECT
for (int i = 0; i < n; i++) {
pids[i] = fork();
if (pids[i] == 0) {
execvp(cmds[i][0], cmds[i]);
exit(1);
}
}
for (int i = 0; i < n; i++) {
waitpid(pids[i], NULL, 0);
}
// FAUX - l'enfant continue après un exec échoué
if (fork() == 0) {
execvp(cmd[0], cmd);
// Si execvp échoue, l'enfant continue à exécuter le code du parent !
}
// CORRECT
if (fork() == 0) {
execvp(cmd[0], cmd);
perror("execvp");
exit(1); // DOIT quitter !
}


Implémenter une fonction qui exécute un pipeline de commandes :

  • Entrée : tableau de commandes (chaque commande est un tableau de chaînes)
  • Connecter stdout de chaque commande à stdin de la suivante
  • Retourner 0 en cas de succès, 1 en cas d’erreur
  • Fermer tous les descripteurs de fichiers, attendre tous les enfants

Exemple de pipeline : ls | grep .c | wc -l

char *cmd1[] = {"ls", NULL};
char *cmd2[] = {"grep", ".c", NULL};
char *cmd3[] = {"wc", "-l", NULL};
char **cmds[] = {cmd1, cmd2, cmd3, NULL};
picoshell(cmds);

Réfléchir avant de coder :

  1. Qu’est-ce qu’un pipe ?

    • pipe(fd) crée deux descripteurs de fichiers
    • fd[0] = extrémité lecture, fd[1] = extrémité écriture
    • Les données écrites dans fd[1] peuvent être lues depuis fd[0]
  2. Pattern de pipeline :

    cmd1 stdout --> [pipe1] --> stdin cmd2 stdout --> [pipe2] --> stdin cmd3
  3. Pour chaque commande :

    • Fork un processus enfant
    • Dans l’enfant : rediriger stdin/stdout, exec la commande
    • Dans le parent : suivre les descripteurs de fichiers, attendre les enfants
  4. Règle critique : Fermer TOUS les descripteurs de fichiers inutilisés dans le parent ET l’enfant !

Étape 1 : Structure de base

picoshell.c
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int picoshell(char **cmds[])
{
int i = 0;
int prev_fd = -1; // Extrémité lecture du pipe précédent
int pipefd[2];
pid_t pid;
// Compter les commandes
while (cmds[i])
i++;
int cmd_count = i;
// Traiter chaque commande
for (i = 0; cmds[i]; i++)
{
// ... implémentation ici
}
// Attendre tous les enfants
for (i = 0; i < cmd_count; i++)
wait(NULL);
return 0;
}

Étape 2 : Créer les pipes et fork

picoshell.c - pipe et fork
for (i = 0; cmds[i]; i++)
{
// Créer le pipe si pas la dernière commande
if (cmds[i + 1] && pipe(pipefd) == -1)
{
if (prev_fd != -1)
close(prev_fd);
return 1;
}
pid = fork();
if (pid == -1)
{
if (prev_fd != -1)
close(prev_fd);
if (cmds[i + 1])
{
close(pipefd[0]);
close(pipefd[1]);
}
return 1;
}
// Gestion enfant ou parent...
}

Étape 3 : Processus enfant - rediriger et exec

picoshell.c - enfant
if (pid == 0)
{
// Processus enfant
// Rediriger stdin depuis le pipe précédent (si pas le premier)
if (prev_fd != -1)
{
dup2(prev_fd, STDIN_FILENO);
close(prev_fd);
}
// Rediriger stdout vers le pipe actuel (si pas le dernier)
if (cmds[i + 1])
{
close(pipefd[0]); // Fermer extrémité lecture (non utilisée)
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
}
execvp(cmds[i][0], cmds[i]);
exit(1); // exec échoué - DOIT quitter !
}

Étape 4 : Processus parent - gérer les fds

picoshell.c - parent
// Processus parent
if (prev_fd != -1)
close(prev_fd); // Terminé avec l'extrémité lecture précédente
if (cmds[i + 1])
{
close(pipefd[1]); // Fermer extrémité écriture (l'enfant l'utilise)
prev_fd = pipefd[0]; // Sauver extrémité lecture pour commande suivante
}

Comprendre dup2 :

AppelEffet
dup2(pipefd[1], STDOUT_FILENO)stdout écrit maintenant dans le pipe
dup2(prev_fd, STDIN_FILENO)stdin lit maintenant depuis le pipe précédent

Flux de données du pipe :

Avant dup2 :
stdin (0) --> clavier
stdout (1) --> terminal
Après dup2 dans la commande du milieu :
stdin (0) --> extrémité lecture pipe précédent
stdout (1) --> extrémité écriture pipe actuel

Pourquoi fermer après dup2 ?

dup2(pipefd[1], STDOUT_FILENO); // Crée une copie au fd 1
close(pipefd[1]); // L'original n'est plus nécessaire
// Maintenant : stdout (1) pointe vers l'écriture du pipe
// pipefd[1] est fermé (pas de fuite)

1. Oublier de fermer les extrémités de pipe :

// FAUX : Extrémité lecture du pipe jamais fermée
if (fork() == 0) {
dup2(pipefd[1], 1);
execvp(cmd, argv);
}
// CORRECT : Fermer toutes les extrémités inutilisées
if (fork() == 0) {
close(pipefd[0]); // Ne lit pas
dup2(pipefd[1], 1);
close(pipefd[1]); // Fermé après dup2
execvp(cmd, argv);
exit(1);
}

2. Ne pas quitter après un exec échoué :

// FAUX : L'enfant continue comme parent !
execvp(cmd, argv);
// Si exec échoue, continue ici...
// CORRECT : Quitter en cas d'échec
execvp(cmd, argv);
exit(1); // Atteint uniquement si exec échoue

3. Créer des processus zombies :

// FAUX : N'attend pas les enfants
for (i = 0; cmds[i]; i++) {
fork(); // Pas de wait !
}
// CORRECT : Attendre tous
for (i = 0; i < cmd_count; i++)
wait(NULL);

4. Le parent ne ferme pas l’extrémité écriture :

// FAUX : L'enfant bloque en lecture car le parent garde l'extrémité écriture
// Le parent garde pipefd[1] ouvert -> la lecture de l'enfant n'obtient jamais EOF
// CORRECT : Le parent ferme l'extrémité écriture
close(pipefd[1]);
prev_fd = pipefd[0];
Fenêtre de terminal
# Compiler
gcc -Wall -Wextra -Werror picoshell.c main.c -o picoshell
# Tester un pipeline simple
./picoshell echo hello "|" cat
# Tester plusieurs pipes
./picoshell ls "|" grep .c "|" wc -l
# Vérifier les fuites de fd
lsof -c picoshell
# Vérifier les zombies
ps aux | grep defunct
picoshell.c
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int picoshell(char **cmds[])
{
int i = 0;
int prev_fd = -1;
int pipefd[2];
pid_t pid;
while (cmds[i])
i++;
int cmd_count = i;
for (i = 0; cmds[i]; i++)
{
if (cmds[i + 1] && pipe(pipefd) == -1)
{
if (prev_fd != -1) close(prev_fd);
return 1;
}
pid = fork();
if (pid == -1)
{
if (prev_fd != -1) close(prev_fd);
if (cmds[i + 1]) { close(pipefd[0]); close(pipefd[1]); }
return 1;
}
if (pid == 0)
{
if (prev_fd != -1) { dup2(prev_fd, 0); close(prev_fd); }
if (cmds[i + 1]) { close(pipefd[0]); dup2(pipefd[1], 1); close(pipefd[1]); }
execvp(cmds[i][0], cmds[i]);
exit(1);
}
if (prev_fd != -1) close(prev_fd);
if (cmds[i + 1]) { close(pipefd[1]); prev_fd = pipefd[0]; }
}
for (i = 0; i < cmd_count; i++)
wait(NULL);
return 0;
}

Implémenter un popen() simplifié :

  • Prendre le chemin de la commande, les arguments et le mode (‘r’ ou ‘w’)
  • Retourner un descripteur de fichier connecté au stdin ou stdout de la commande
  • Mode ‘r’ : retourner fd pour lire la sortie de la commande
  • Mode ‘w’ : retourner fd pour écrire à l’entrée de la commande

Réfléchir avant de coder :

  1. Que signifie le mode ‘r’ ?

    • “Je veux LIRE la sortie de la commande”
    • L’enfant redirige stdout vers l’extrémité écriture du pipe
    • Le parent obtient l’extrémité lecture du pipe
  2. Que signifie le mode ‘w’ ?

    • “Je veux ÉCRIRE à l’entrée de la commande”
    • L’enfant redirige stdin depuis l’extrémité lecture du pipe
    • Le parent obtient l’extrémité écriture du pipe
  3. Visualisation :

    mode 'r' : parent <--lecture-- pipe <--écriture-- enfant stdout
    mode 'w' : parent --écriture--> pipe --lecture--> enfant stdin

Étape 1 : Configuration et validation

ft_popen.c
#include <unistd.h>
#include <stdlib.h>
int ft_popen(const char *file, char *const argv[], char type)
{
int pipefd[2];
pid_t pid;
// Valider le type
if (type != 'r' && type != 'w')
return -1;
// Créer le pipe
if (pipe(pipefd) == -1)
return -1;
pid = fork();
if (pid == -1)
{
close(pipefd[0]);
close(pipefd[1]);
return -1;
}
// ... gestion enfant/parent
}

Étape 2 : Processus enfant

ft_popen.c - enfant
if (pid == 0)
{
if (type == 'r')
{
// Le parent lit -> l'enfant écrit sur stdout
close(pipefd[0]); // L'enfant ne lira pas
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
}
else // type == 'w'
{
// Le parent écrit -> l'enfant lit depuis stdin
close(pipefd[1]); // L'enfant n'écrira pas
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
}
execvp(file, argv);
exit(1);
}

Étape 3 : Processus parent

ft_popen.c - parent
// Processus parent
if (type == 'r')
{
close(pipefd[1]); // Le parent n'écrira pas
return pipefd[0]; // Retourner extrémité lecture
}
else // type == 'w'
{
close(pipefd[0]); // Le parent ne lira pas
return pipefd[1]; // Retourner extrémité écriture
}

Table de direction des modes :

ModeParentEnfantParent retourne
’r’lit depuis le pipeécrit dans le pipe (stdout)pipefd[0]
’w’écrit dans le pipelit depuis le pipe (stdin)pipefd[1]

Astuce mémoire :

  • ‘r’ = “Je Lis” = le parent lit = l’enfant produit = stdout de l’enfant
  • ‘w’ = “J’Écris” = le parent écrit = l’enfant consomme = stdin de l’enfant

1. Direction inversée :

// FAUX : mode 'r' mais retourne extrémité écriture
if (type == 'r')
return pipefd[1]; // Faux !
// CORRECT : mode 'r' retourne extrémité lecture
if (type == 'r')
return pipefd[0]; // Correct

2. L’enfant redirige le mauvais fd :

// FAUX : mode 'r' mais l'enfant redirige stdin
if (type == 'r')
dup2(pipefd[0], STDIN_FILENO); // Faux !
// CORRECT : mode 'r', l'enfant redirige stdout
if (type == 'r')
dup2(pipefd[1], STDOUT_FILENO); // Correct
ft_popen.c
#include <unistd.h>
#include <stdlib.h>
int ft_popen(const char *file, char *const argv[], char type)
{
int pipefd[2];
pid_t pid;
if (type != 'r' && type != 'w')
return -1;
if (pipe(pipefd) == -1)
return -1;
pid = fork();
if (pid == -1)
{
close(pipefd[0]);
close(pipefd[1]);
return -1;
}
if (pid == 0)
{
if (type == 'r')
{
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
}
else
{
close(pipefd[1]);
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
}
execvp(file, argv);
exit(1);
}
if (type == 'r')
{
close(pipefd[1]);
return pipefd[0];
}
close(pipefd[0]);
return pipefd[1];
}

Tester si une fonction est “gentille” ou “méchante” :

  • Gentille : quitte avec le code 0, pas de signal, pas de timeout
  • Méchante : crash (signal), quitte avec non-zéro, ou timeout
  • Retourner 1 pour gentille, 0 pour méchante, -1 pour erreur de sandbox

Réfléchir avant de coder :

  1. Pourquoi fork ?

    • Exécuter la fonction non fiable dans un processus isolé
    • Si elle crash, le parent survit
  2. Comment détecter les problèmes ?

    • WIFEXITED(status) - s’est-il terminé normalement ?
    • WEXITSTATUS(status) - quel code de sortie ?
    • WIFSIGNALED(status) - tué par un signal ?
    • WTERMSIG(status) - quel signal ?
  3. Comment gérer le timeout ?

    • alarm(seconds) envoie SIGALRM après délai
    • Dans le handler, définir un flag
    • Après wait, tuer l’enfant si timeout
sandbox.c
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
static volatile sig_atomic_t g_timeout = 0;
static void alarm_handler(int sig) { (void)sig; g_timeout = 1; }
int sandbox(void (*f)(void), unsigned int timeout, bool verbose)
{
pid_t pid;
int status;
struct sigaction sa;
if (!f) return -1;
sa.sa_handler = alarm_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGALRM, &sa, NULL) == -1) return -1;
g_timeout = 0;
pid = fork();
if (pid == -1) return -1;
if (pid == 0) { f(); exit(0); }
if (timeout > 0) alarm(timeout);
if (waitpid(pid, &status, 0) == -1) { alarm(0); return -1; }
alarm(0);
if (g_timeout) {
kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
if (verbose) printf("Mauvaise fonction : timeout après %u secondes\n", timeout);
return 0;
}
if (WIFEXITED(status)) {
int code = WEXITSTATUS(status);
if (code == 0) { if (verbose) printf("Bonne fonction !\n"); return 1; }
if (verbose) printf("Mauvaise fonction : terminée avec le code %d\n", code);
return 0;
}
if (WIFSIGNALED(status)) {
if (verbose) printf("Mauvaise fonction : %s\n", strsignal(WTERMSIG(status)));
return 0;
}
return -1;
}


Évaluer des expressions mathématiques :

  • Opérateurs : + et *
  • Parenthèses : ( et )
  • Nombres : chiffres uniques 0-9
  • Priorité des opérateurs : * avant +

Exemples :

3+4*5 = 23 (pas 35 !)
(3+4)*5 = 35
1+2+3 = 6

Réfléchir avant de coder :

  1. Pourquoi * lie plus fort ?

    • 3+4*5 devrait être 3+(4*5) = 23
    • Pas (3+4)*5 = 35
  2. Grammaire pour la priorité :

    expr = term (('+') term)* // + est le plus bas
    term = factor (('*') factor)* // * est plus haut
    factor = '(' expr ')' | DIGIT // () et nombres les plus hauts
  3. Descente récursive :

    • parse_expr gère +
    • parse_term gère *
    • parse_factor gère () et les chiffres
    • Priorité plus haute = plus profond dans la pile d’appels
vbc.c
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
static int parse_expr(const char **s);
static void error_token(char c)
{
if (c == '\0')
printf("Fin d'entrée inattendue\n");
else
printf("Token inattendu '%c'\n", c);
exit(1);
}
static int parse_factor(const char **s)
{
if (**s == '(')
{
(*s)++;
int result = parse_expr(s);
if (**s != ')') error_token(**s);
(*s)++;
return result;
}
if (isdigit(**s))
{
int num = **s - '0';
(*s)++;
return num;
}
error_token(**s);
return 0;
}
static int parse_term(const char **s)
{
int result = parse_factor(s);
while (**s == '*')
{
(*s)++;
result *= parse_factor(s);
}
return result;
}
static int parse_expr(const char **s)
{
int result = parse_term(s);
while (**s == '+')
{
(*s)++;
result += parse_term(s);
}
return result;
}
int main(int argc, char **argv)
{
if (argc != 2) { printf("Usage: %s 'expression'\n", argv[0]); return 1; }
const char *s = argv[1];
int result = parse_expr(&s);
if (*s != '\0') error_token(*s);
printf("%d\n", result);
return 0;
}
Fenêtre de terminal
./vbc '3+4*5' # 23
./vbc '(3+4)*5' # 35
./vbc '1+2+3+4+5' # 15
./vbc '2*3*4' # 24
./vbc '((1+2))' # 3
./vbc '1+2)' # Erreur : Token inattendu ')'
./vbc '(1+2' # Erreur : Fin d'entrée inattendue
./vbc '1 + 2' # Erreur : Token inattendu ' '

Parser du JSON simplifié :

  • Types : nombres, chaînes, objets (pas de tableaux, booléens, null)
  • Pas de gestion des espaces (les espaces sont des erreurs)
  • Gérer les séquences d’échappement : \" et \\ seulement
  • Afficher les messages d’erreur exacts
Fenêtre de terminal
# Tester les nombres
echo -n '42' | ./argo /dev/stdin
# Tester les chaînes
echo -n '"hello"' | ./argo /dev/stdin
# Tester les objets
echo -n '{"a":1}' | ./argo /dev/stdin
echo -n '{"name":"test","value":42}' | ./argo /dev/stdin
# Tester imbriqué
echo -n '{"outer":{"inner":1}}' | ./argo /dev/stdin
# Tester les échappements
echo -n '"hello\"world"' | ./argo /dev/stdin
# Tester les erreurs
echo -n '{"a":}' | ./argo /dev/stdin # Valeur manquante
echo -n '{a:1}' | ./argo /dev/stdin # Clé pas une chaîne

FonctionObjectif
fork()Créer un processus enfant
wait(NULL)Attendre n’importe quel enfant
waitpid(pid, &status, 0)Attendre un enfant spécifique
exit(code)Terminer le processus
FonctionObjectif
pipe(int fd[2])Créer pipe (fd[0]=lecture, fd[1]=écriture)
dup2(old, new)Rediriger new vers old
close(fd)Fermer descripteur de fichier
FonctionObjectif
execvp(file, argv)Exécuter avec recherche PATH
execve(path, argv, envp)Exécuter avec chemin complet
FonctionObjectif
sigaction(sig, &sa, NULL)Définir gestionnaire de signal
alarm(seconds)Planifier SIGALRM
kill(pid, sig)Envoyer signal au processus
MacroObjectif
WIFEXITED(status)Vrai si terminé normalement
WEXITSTATUS(status)Obtenir code de sortie
WIFSIGNALED(status)Vrai si tué par signal
WTERMSIG(status)Obtenir numéro du signal

LevelExerciceConcept Clé
1picoshellChaînes de pipes, fork, exec, gestion fd
1ft_popenDirection du pipe selon le mode
1sandboxSignaux, timeout, statut de processus
2vbcDescente récursive, priorité des opérateurs
2argoParsing récursif, séquences d’échappement

Erreurs courantes à éviter :

  1. Ne pas fermer toutes les extrémités de pipe
  2. Ne pas quitter après un execvp échoué
  3. Mauvaise direction de pipe pour mode lecture/écriture
  4. Mauvaise priorité des opérateurs dans vbc
  5. Mauvais format de message d’erreur (doit être exact !)