Examen Réseau — Session Normale
2 heures, 4 exercices : questions de cours, pipes père/fils (transformation majuscules), trains A↔B avec sémaphores et threads, gestion des signaux SIGINT/SIGQUIT, et socket TCP client/serveur multi-clients.
i. Le sujet
ii. Notions mobilisées
| Exo | Sujet | Notion clé | Chapitre |
|---|---|---|---|
| 1 | Questions de cours | exec, processus vs threads, zombies, signaux, SIGINT | Cours 1 · Cours 2 · Cours 4 |
| 2.1 | Tubes père↔fils | pipe, fork, read/write, close | Cours 3 |
| 2.2 | Trains A↔B (voie unique) | Sémaphores POSIX, threads, exclusion mutuelle directionnelle | Cours 2 · Cours 4 |
| 3 | SIGINT confirm, SIGQUIT relai | sigaction, kill, gestion des signaux | Cours 2 |
| 4 | Socket TCP client/serveur | socket, bind, listen, accept, connect, fork pour multi-clients | Cours 5 |
1. Exercice 1 — Questions de cours (5 pts)
Q1 — Rôle de la fonction exec() en C
La famille exec* (execl, execv, execlp, execvp…) remplace l'image du processus courant par celle d'un nouveau programme. Le PID est conservé, mais le code, les données et la pile sont écrasés. Les descripteurs de fichiers ouverts sont préservés (sauf flag FD_CLOEXEC).
Combiné à fork, c'est le mécanisme standard pour lancer un programme depuis un autre : le père fork, le fils exec, et le père wait.
Q2 — Processus vs threads : 2 avantages, 2 inconvénients
- Isolation mémoire : chaque processus a son propre espace d'adressage. Un crash n'affecte pas les autres.
- Sécurité : pas de partage accidentel de données, communication explicite via IPC.
- Création coûteuse :
forkrecopie la table des pages, plus lent quepthread_create. - Communication lente : IPC (pipes, signaux, mémoire partagée…) demande des appels système, contrairement à la mémoire partagée directe entre threads.
Q3 — Processus zombie et gestion par l'OS
Un processus zombie est un processus terminé dont le parent n'a pas encore lu le code de sortie via wait() ou waitpid(). Il occupe une entrée dans la table des processus uniquement pour conserver son statut (PID, code de retour).
Gestion :
- Tant que le père n'appelle pas
wait, le zombie persiste. - Quand le père meurt sans avoir récupéré ses zombies, ces derniers sont adoptés par init (PID 1) qui les nettoie immédiatement.
- Pour éviter les zombies : appeler
wait, ou installer un gestionnaire pourSIGCHLDqui appellewaitpid, ou positionnersignal(SIGCHLD, SIG_IGN)pour que le système réolisse automatiquement.
Q4 — Rôle principal des signaux
Les signaux sont un mécanisme de notification asynchrone entre processus (ou du noyau vers un processus). Ils permettent de :
- Notifier un événement : interruption (
SIGINT), fin d'un fils (SIGCHLD), division par zéro (SIGFPE), accès mémoire invalide (SIGSEGV). - Contrôler l'exécution : arrêt (
SIGSTOP), reprise (SIGCONT), terminaison (SIGTERM,SIGKILL). - Communication minimale entre processus : signaux user-defined (
SIGUSR1,SIGUSR2).
Ils sont délivrés de façon asynchrone : le programme est interrompu, le handler s'exécute, puis le programme reprend où il en était.
Q5 — Comportement par défaut de SIGINT
SIGINT (signal n°2) est généré par le noyau quand l'utilisateur tape Ctrl+C dans le terminal. Le comportement par défaut est la terminaison immédiate du processus au premier plan, sans nettoyage spécifique (les destructeurs C++ par exemple ne sont pas appelés).
On peut intercepter SIGINT avec signal() ou sigaction() pour exécuter un nettoyage avant de sortir — ou pour demander confirmation comme dans l'exo 3.
2. Exercice 2 — Partie 1 : Pipes père/fils, transformation majuscules
Le père envoie une chaîne au fils via pipe 1. Le fils la met en majuscules et la renvoie au père via pipe 2. Le père affiche.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <ctype.h>
int main() {
int pere_vers_fils[2]; // pipe 1
int fils_vers_pere[2]; // pipe 2
char message[] = "bonjour le monde";
char buf[256];
pipe(pere_vers_fils);
pipe(fils_vers_pere);
if (fork() == 0) { // ────── FILS ──────
close(pere_vers_fils[1]); // fils ne PA→F que en lecture
close(fils_vers_pere[0]); // fils ne F→PA que en écriture
int n = read(pere_vers_fils[0], buf, 256);
buf[n] = '\0';
for (int i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
write(fils_vers_pere[1], buf, n);
close(pere_vers_fils[0]);
close(fils_vers_pere[1]);
exit(0);
} else { // ────── PÈRE ──────
close(pere_vers_fils[0]); // père écrit seulement vers fils
close(fils_vers_pere[1]); // père lit seulement depuis fils
write(pere_vers_fils[1], message, strlen(message));
close(pere_vers_fils[1]); // signaler EOF au fils
int n = read(fils_vers_pere[0], buf, 256);
buf[n] = '\0';
printf("%s\n", buf); // "BONJOUR LE MONDE"
close(fils_vers_pere[0]);
wait(NULL);
}
return 0;
}
3. Exercice 2 — Partie 2 : Trains A↔B avec sémaphores
Deux villes A et B, une voie unique. Les trains peuvent circuler dans le même sens en parallèle, mais jamais dans deux sens opposés simultanément. C'est le problème du pont à voie unique ou des lecteurs/rédacteurs avec deux groupes.
- Un compteur pour chaque sens (
nb_AB,nb_BA). - Un mutex pour protéger les compteurs.
- Un sémaphore
voiepour réserver l'accès au tronçon. - Le premier train d'un sens prend le sémaphore
voie; le dernier le libère.
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>
sem_t voie; // 1 si libre, 0 si occupée
pthread_mutex_t mtx_AB = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mtx_BA = PTHREAD_MUTEX_INITIALIZER;
int nb_AB = 0, nb_BA = 0;
void* train_AB(void* arg) {
pthread_mutex_lock(&mtx_AB);
nb_AB++;
if (nb_AB == 1) sem_wait(&voie); // premier de A→B : réserve la voie
pthread_mutex_unlock(&mtx_AB);
printf("Train A→B circule\n");
sleep(2); // traversée
printf("Train A→B sort par B\n");
pthread_mutex_lock(&mtx_AB);
nb_AB--;
if (nb_AB == 0) sem_post(&voie); // dernier de A→B : libère
pthread_mutex_unlock(&mtx_AB);
return NULL;
}
void* train_BA(void* arg) {
pthread_mutex_lock(&mtx_BA);
nb_BA++;
if (nb_BA == 1) sem_wait(&voie);
pthread_mutex_unlock(&mtx_BA);
printf("Train B→A circule\n");
sleep(2);
printf("Train B→A sort par A\n");
pthread_mutex_lock(&mtx_BA);
nb_BA--;
if (nb_BA == 0) sem_post(&voie);
pthread_mutex_unlock(&mtx_BA);
return NULL;
}
int main() {
sem_init(&voie, 0, 1); // 1 = voie libre
pthread_t t1, t2, t3;
pthread_create(&t1, NULL, train_AB, NULL);
pthread_create(&t2, NULL, train_AB, NULL); // 2 trains AB en // OK
pthread_create(&t3, NULL, train_BA, NULL); // devra attendre
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
sem_destroy(&voie);
return 0;
}
Compilation : gcc trains.c -lpthread -o trains.
Si des trains A→B arrivent en continu, les trains B→A peuvent attendre indéfiniment. Solution : ajouter un mécanisme d'équité (sémaphore
tourniquet, ou faire passer les trains par lots). Le sujet ne le demande pas, mais c'est un bon point à mentionner.
4. Exercice 3 — Signaux SIGINT et SIGQUIT
Q1 — Interrompre calculcomplexe avec Ctrl+C
On installe un gestionnaire pour SIGINT qui demande confirmation à l'utilisateur.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
int calculcomplexe(int i); // existe ailleurs
void handler_sigint(int sig) {
char rep;
printf("\nVoulez-vous arrêter ? (o/n) : ");
fflush(stdout);
scanf(" %c", &rep);
if (rep == 'o' || rep == 'O') {
printf("Arrêt définitif.\n");
exit(0);
}
printf("Calcul poursuivi.\n");
}
int main(void) {
int tab[100];
struct sigaction sa;
sa.sa_handler = handler_sigint;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // redémarre read/scanf si interrompu
sigaction(SIGINT, &sa, NULL);
for (int i = 0; i < 100; i++) {
tab[i] = calculcomplexe(i);
}
return EXIT_SUCCESS;
}
Q2 — Programme ping.c
Sur réception de SIGQUIT (Ctrl+\), afficher « ping ! » et relayer le signal à un autre PID. Le programme affiche « ping ! » indéfiniment.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
pid_t cible; // PID donné en argument
void handler_quit(int sig) {
printf("ping !\n");
kill(cible, SIGQUIT); // relaie le signal
}
int main(int argc, char** argv) {
if (argc != 2) {
fprintf(stderr, "Usage : %s <pid_cible>\n", argv[0]);
exit(1);
}
cible = atoi(argv[1]);
struct sigaction sa;
sa.sa_handler = handler_quit;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGQUIT, &sa, NULL);
while (1) {
printf("ping !\n");
sleep(1);
}
return 0;
}
sigaction plutôt que signal ?
signal() a un comportement variable selon les Unix (parfois le handler est réinitialisé après chaque réception). sigaction() est portable, plus précis (masque, drapeaux SA_RESTART, SA_SIGINFO) et toujours préféré dans le code de production.
5. Exercice 4 — Socket TCP client/serveur multi-clients
Q1 — Rôles de inet_aton et getsockname
inet_aton
Convertit une adresse IPv4 au format texte (« 192.168.1.10 ») en sa représentation binaire dans une structure in_addr.
int inet_aton(const char*, struct in_addr*);
Renvoie 1 si OK, 0 si erreur de format.
getsockname
Récupère l'adresse locale associée à une socket (utile quand bind a été fait avec port=0 ou IP automatique).
int getsockname(int sock, struct sockaddr*, socklen_t*);
Renseigne l'IP et le port effectivement utilisés côté local.
Q2 — Compléter le code client TCP
Les zones à compléter dans le sujet, dans l'ordre :
// création de la socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
fprintf(stderr, "%s: socket %s\n", argv[0], strerror(errno));
exit(1);
}
serveur.sin_family = AF_INET;
serveur.sin_port = htons(sport);
inet_aton(argv[2], &serveur.sin_addr);
// connexion au serveur
if (connect(sock, (struct sockaddr*)&serveur, sizeof(serveur)) < 0) {
fprintf(stderr, "%s: connect %s\n", argv[0], strerror(errno));
exit(1);
}
len = sizeof(client);
getsockname(sock, (struct sockaddr*)&client, &len);
while (1) {
char buf_read[256], buf_write[256];
printf("donner le message à envoyer: ");
scanf("%s", buf_write);
printf("le message à envoyer par le client : %s\n", buf_write);
ret = write(sock, buf_write, strlen(buf_write)); // ← première zone
if (ret <= strlen(buf_write)) { ... }
ret = read(sock, buf_read, sizeof(buf_read)); // ← deuxième zone
if (ret <= 0) { ... }
printf("le message reçu : %s\n", buf_read);
}
close(sock);
Q3 — Code du serveur multi-clients
Pour gérer plusieurs clients, on fork après chaque accept : le père retourne accepter le prochain client, le fils dialogue avec le client courant.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
int main(int argc, char** argv) {
int ecoute, dialogue;
struct sockaddr_in serveur, client;
socklen_t len = sizeof(client);
if (argc != 2) {
fprintf(stderr, "Usage : %s port\n", argv[0]);
exit(1);
}
// nettoyer les zombies automatiquement
signal(SIGCHLD, SIG_IGN);
ecoute = socket(AF_INET, SOCK_STREAM, 0);
serveur.sin_family = AF_INET;
serveur.sin_port = htons(atoi(argv[1]));
serveur.sin_addr.s_addr = INADDR_ANY;
if (bind(ecoute, (struct sockaddr*)&serveur, sizeof(serveur)) < 0) {
perror("bind");
exit(1);
}
listen(ecoute, 5);
while (1) {
dialogue = accept(ecoute, (struct sockaddr*)&client, &len);
if (dialogue < 0) continue;
if (fork() == 0) { // ─── enfant : dialogue dédié ───
close(ecoute); // l'enfant n'a pas besoin du socket écoute
char buf[256];
int n;
while ((n = read(dialogue, buf, 255)) > 0) {
buf[n] = '\0';
printf("reçu de %s : %s\n", inet_ntoa(client.sin_addr), buf);
// écho en majuscules par exemple
for (int i = 0; i < n; i++) buf[i] = toupper(buf[i]);
write(dialogue, buf, n);
}
close(dialogue);
exit(0);
} else { // ─── parent : retourne accepter ───
close(dialogue); // le parent n'utilise pas ce socket
}
}
return 0;
}
ecoute dans l'enfant et dialogue dans le parent ?
Les descripteurs sont hérités par fork : les deux processus ont les deux sockets ouverts. Fermer celui qu'on n'utilise pas libère la ressource côté noyau et permet la propagation de fin de connexion. Sinon le client ne détecte jamais l'EOF.
Alternatives à fork : pthread_create pour un dialogue par thread (plus léger, mais nécessite mutex pour les données partagées), ou select/epoll pour un serveur mono-thread multi-clients (plus complexe mais le plus performant).
★ Notions ré-activées
- exec remplace l'image du processus, conserve le PID
- Zombie = fils terminé sans
wait;signal(SIGCHLD, SIG_IGN)évite - SIGINT = Ctrl+C, terminaison par défaut ;
sigactionpour intercepter - SIGQUIT = Ctrl+\, fin avec core dump par défaut
- Pipes père↔fils = deux pipes pour bidirectionnel propre
- Voie unique trains = 1 sémaphore + 1 mutex par sens + compteurs
- inet_aton : texte → binaire IPv4
- getsockname : récupère l'adresse locale d'une socket
- Serveur multi-clients = boucle accept + fork ; père ferme dialogue, fils ferme écoute
- connect/accept : connect côté client, accept côté serveur
- htons sur le port (host to network short)