Examen Réseau — Rattrapage
2 heures, 20 points. Questions de cours (wait/waitpid, IPC, sémaphores, threads vs processus), exercices fork avec récupération de code de retour, threads paramétriques, et socket TCP client/serveur.
i. Le sujet
1. Questions de cours (5 points)
Q1 — Différence entre wait et waitpid (1 pt)
pid_t wait(int* status)
Attend la fin d'n'importe lequel des fils du processus appelant. Bloquant. Renvoie le PID du fils terminé. Pratique pour synchroniser un père avec un seul fils.
pid_t waitpid(pid_t pid, int* status, int options)
Plus flexible :
pid > 0→ attend ce PID précis.pid = -1→ équivalent àwait.options = WNOHANG→ non bloquant, renvoie 0 si aucun fils prêt.
En résumé : waitpid = wait + ciblage + options. C'est la version moderne préférée.
Q2 — Trois mécanismes de communication inter-processus (1 pt)
- Tubes (pipes anonymes
pipe()ou nommésmkfifo()) — flux d'octets FIFO, unidirectionnel. - Signaux (
kill(),sigaction()) — notification asynchrone, sans payload. - Files de messages (
msgget/msgsnd/msgrcv) — messages typés, file FIFO partagée.
Autres alternatives possibles : mémoire partagée (shmget), sockets locales (AF_UNIX), sémaphores partagés.
Q3 — Définition d'un sémaphore + primitives (2 pts)
P(s) ou wait(s)
Décrémente le compteur. Si le résultat est négatif, le processus appelant est bloqué jusqu'à libération.
V(s) ou post(s)
Incrémente le compteur. Débloque un processus en attente (s'il y en a).
En POSIX : sem_init, sem_wait, sem_post, sem_destroy. Le sémaphore initialisé à 1 sert de mutex ; initialisé à n il limite l'accès à n consommateurs.
Q4 — Différence processus vs thread (1 pt)
Un processus a son propre espace d'adressage (pages mémoire indépendantes). Un thread partage l'espace mémoire du processus dont il fait partie.
Conséquences :
- Création de thread plus rapide (
pthread_create<<fork). - Communication entre threads triviale (mémoire partagée directe) mais demande des mutex pour la synchronisation.
- Crash d'un thread → tout le processus tombe. Crash d'un processus → les autres survivent.
- Threads partagent FDs, signaux, variables globales ; ils ne partagent pas leur pile ni leur ID de thread.
2. Exercice 1 — Fork avec wait/waitpid et code de retour (6 pts)
fork1.c — version avec wait
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == -1) { // vérification du fork
perror("fork");
exit(EXIT_FAILURE);
}
if (pid > 0) { // ─── PÈRE ───
printf("Père : PID=%d PPID=%d PID_fils=%d\n",
getpid(), getppid(), pid);
wait(NULL); // attente du fils
printf("Père : le fils est terminé.\n");
} else { // ─── FILS ───
printf("Fils : PID=%d PPID=%d\n", getpid(), getppid());
}
return EXIT_SUCCESS;
}
fork2.c — variante avec waitpid
if (pid > 0) { // PÈRE
printf("Père : PID=%d PPID=%d PID_fils=%d\n",
getpid(), getppid(), pid);
waitpid(pid, NULL, 0); // attend ce fils précis
printf("Père : le fils %d est terminé.\n", pid);
}
Q3 — Afficher le code de retour avec WIFEXITED / WEXITSTATUS
int status;
if (pid > 0) { // PÈRE
printf("Père : PID=%d PPID=%d PID_fils=%d\n",
getpid(), getppid(), pid);
waitpid(pid, &status, 0);
if (WIFEXITED(status)) { // le fils est sorti normalement
printf("Père : code de retour du fils = %d\n",
WEXITSTATUS(status));
} else {
printf("Père : le fils a été interrompu (signal, etc.)\n");
}
} else { // FILS
printf("Fils : PID=%d PPID=%d\n", getpid(), getppid());
exit(42); // code arbitraire pour démo
}
WIFEXITED(status)renvoie vrai si le fils s'est terminé normalement (viaexitoureturn).WEXITSTATUS(status)extrait le code de retour (les 8 bits de poids faible — un code de 0 à 255).- Si le fils est mort sur un signal, on peut tester
WIFSIGNALED(status)+WTERMSIG(status).
3. Exercice 2 — N threads paramétriques (4 pts)
Programme qui crée n threads (n passé en argument), chacun affiche un message, le main attend la terminaison.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void* worker(void* arg) {
int id = *(int*)arg;
printf("hello world ! (thread n°%d)\n", id);
return NULL;
}
int main(int argc, char** argv) {
if (argc != 2) {
fprintf(stderr, "Usage : %s <nombre_de_threads>\n", argv[0]);
exit(EXIT_FAILURE);
}
int n = atoi(argv[1]);
pthread_t* threads = malloc(n * sizeof(pthread_t));
int* ids = malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
ids[i] = i;
pthread_create(&threads[i], NULL, worker, &ids[i]);
}
for (int i = 0; i < n; i++) {
pthread_join(threads[i], NULL);
}
free(threads);
free(ids);
return EXIT_SUCCESS;
}
Compilation : gcc threads.c -lpthread -o threads. Exemple : ./threads 5.
pthread_create(&threads[i], NULL, worker, &i);, tous les threads recevraient la même adresse : la valeur lue dépendrait du moment d'exécution (race condition). En allouant un ids[i] distinct par thread, chaque worker reçoit une adresse stable et différente.
4. Exercice 3 — Client/serveur TCP (5 pts)
Q1 — Compléter le client TCP
Les zones manquantes, dans l'ordre :
// 1. socket de communication
sock = socket(AF_INET, SOCK_STREAM, 0)
// 2. famille
serveur.sin_family = AF_INET
// 3. connect
connect(sock, (struct sockaddr*)&serveur, sizeof(serveur)) < 0
// 4. write
ret = write(sock, buf_write, strlen(buf_write))
// 5. read
ret = read(sock, buf_read, sizeof(buf_read))
Q2 — Code du serveur TCP
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char** argv) {
int ecoute, dialogue;
struct sockaddr_in serveur, client;
socklen_t len = sizeof(client);
char buf[256];
if (argc != 2) {
fprintf(stderr, "Usage : %s <port>\n", argv[0]);
exit(1);
}
if ((ecoute = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket"); exit(1);
}
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);
printf("Serveur en écoute sur le port %s...\n", argv[1]);
while (1) {
dialogue = accept(ecoute, (struct sockaddr*)&client, &len);
printf("Client connecté : %s:%d\n",
inet_ntoa(client.sin_addr), ntohs(client.sin_port));
int n;
while ((n = read(dialogue, buf, 255)) > 0) {
buf[n] = '\0';
printf("Reçu : %s\n", buf);
write(dialogue, buf, n); // renvoie en écho
}
close(dialogue);
printf("Client déconnecté.\n");
}
close(ecoute);
return 0;
}
Pour gérer plusieurs clients simultanés : fork() juste après accept (cf. annales Session Normale).
socket()— créer le socket d'écoutebind()— l'attacher à une IP + portlisten()— passer en mode écoute (taille de la queue de connexions en attente)accept()— bloquer jusqu'à une connexion, renvoie un nouveau socket pour le dialogueread()/write()— échangeclose()— fermer le dialogue (et éventuellement l'écoute)
★ Notions ré-activées
- wait attend n'importe quel fils · waitpid = wait + ciblage + options
- WIFEXITED + WEXITSTATUS pour lire le code de retour du fils
- Sémaphore = compteur protégé · primitives P (wait) et V (post)
- pthread_create avec arguments distincts (sinon race condition)
- pthread_join = équivalent de wait pour les threads
- Compilation threads :
-lpthread - Serveur TCP : socket → bind → listen → accept → read/write
- INADDR_ANY = bind sur toutes les interfaces locales
- htons sur le port (network byte order)
- inet_ntoa = binaire → texte (affichage IP)