Examen Réseau — Examen Général
2 heures, 4 exercices : procédure d'écriture sur fichier et pipeline père/fils avec remplacement de caractères, threads en parallèle puis en séquence, problème du coiffeur dormant (sémaphores + threads), serveur de fichiers TCP multi-clients.
i. Le sujet
ii. Notions mobilisées
| Exo | Sujet | Notion clé | Chapitre |
|---|---|---|---|
| 1 | Ecrire_fich + pipe entre fils | open avec droits, lecture stdin, fork + pipe | Cours 1 · Cours 3 |
| 2 | Threads 1-50 et 51-100 | pthread_create, pthread_join, synchronisation par mutex | Cours 4 |
| 3 | Coiffeur dormant | Sémaphores + threads, sleep/wake-up | Cours 2 · Cours 4 |
| 4 | Serveur de fichiers | Socket TCP, read fichier, fork multi-clients | Cours 5 |
1. Exercice 1 — Écriture sur fichier et pipeline (6 pts)
Q1 — Procédure Ecrire_fich
Crée un fichier avec droits rw pour user et groupe (mode 0664), lit stdin et écrit jusqu'à recevoir la chaîne « stop ».
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
void Ecrire_fich(const char* chemin_du_fichier) {
// O_CREAT pour créer, mode 0664 = rw-rw-r--
int fd = open(chemin_du_fichier,
O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
if (fd < 0) {
perror("open");
return;
}
char buf[256];
while (1) {
printf("> ");
if (scanf("%255s", buf) != 1) break;
if (strcmp(buf, "stop") == 0) break;
write(fd, buf, strlen(buf));
write(fd, " ", 1); // séparateur
}
close(fd);
}
Q2 — Procédure remplace_Q_to_A
void remplace_Q_to_A(char tab[], int taille) {
for (int i = 0; i < taille; i++) {
if (tab[i] == 'q') tab[i] = 'a';
}
}
Q3 — Pipeline père / 2 fils
Le père crée deux fils. Fils 1 lit fichier1 et écrit sur un pipe. Fils 2 lit le pipe, remplace q par a et écrit dans fichier2.
int main(int argc, char** argv) {
if (argc != 3) {
fprintf(stderr, "Usage : %s <fichier1> <fichier2>\n", argv[0]);
exit(1);
}
int p[2];
pipe(p);
pid_t f1 = fork();
if (f1 == 0) { // ─── FILS 1 : lecture fichier1 → pipe ───
close(p[0]); // pas besoin de lire
int fd = open(argv[1], O_RDONLY);
char buf[128];
int n;
while ((n = read(fd, buf, 128)) > 0) {
write(p[1], buf, n);
}
close(fd);
close(p[1]); // crucial : signale EOF au fils 2
exit(0);
}
pid_t f2 = fork();
if (f2 == 0) { // ─── FILS 2 : pipe → remplace → fichier2 ───
close(p[1]); // pas besoin d'écrire
int fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0664);
char buf[128];
int n;
while ((n = read(p[0], buf, 128)) > 0) {
remplace_Q_to_A(buf, n);
write(fd, buf, n);
}
close(fd);
close(p[0]);
exit(0);
}
// ─── PÈRE ───
close(p[0]); // le père n'utilise pas le pipe
close(p[1]);
wait(NULL); wait(NULL); // attend les deux fils
return 0;
}
p[0] et p[1], sinon le fils 2 ne détectera jamais EOF (puisque le père garde un descripteur écriture ouvert). Idem pour les fils : ils ferment l'extrémité inutile.
2. Exercice 2 — Threads (4 pts)
Q1 — Deux threads en parallèle
#include <stdio.h>
#include <pthread.h>
void* afficher_intervalle(void* arg) {
int* bornes = (int*)arg;
for (int i = bornes[0]; i <= bornes[1]; i++) {
printf("%d\n", i);
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
int args1[2] = {1, 50};
int args2[2] = {51, 100};
pthread_create(&t1, NULL, afficher_intervalle, args1);
pthread_create(&t2, NULL, afficher_intervalle, args2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
Sortie observée : les nombres apparaissent entremêlés (1, 51, 2, 52, … ou autre ordre selon l'ordonnanceur).
Q2 — Forcer l'ordre 1, 2, 3, …, 99, 100
Pour garantir t1 termine avant que t2 n'affiche, deux approches.
t1, pthread_join(t1), puis créer t2.
pthread_create(&t1, ..., args1);
pthread_join(t1, NULL);
pthread_create(&t2, ..., args2);
pthread_join(t2, NULL);
t2 en attente d'un sémaphore que t1 libère à la fin.
// t1 finit : sem_post(&sync)
// t2 commence par : sem_wait(&sync)
La solution simple est suffisante ici. Le sémaphore devient utile quand on veut une interaction plus fine (par exemple alterner deux threads à chaque pas).
3. Exercice 3 — Le coiffeur dormant (4 pts)
Les sémaphores nécessaires
| Sémaphore | Init | Rôle |
|---|---|---|
client_arrive | 0 | Le coiffeur attend (P) qu'un client le réveille (V). Sémaphore de signal. |
coupe_finie | 0 | Le client attend (P) que le coiffeur termine sa coupe (V). |
mutex | 1 | Protège le compteur nb_attente. |
Code
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define NBP 3 // nombre de places en salle d'attente
sem_t client_arrive, coupe_finie;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int nb_attente = 0;
void* coiffeur(void* arg) {
while (1) {
sem_wait(&client_arrive); // dort jusqu'à un client
pthread_mutex_lock(&mutex);
nb_attente--; // un client de moins dans la salle
pthread_mutex_unlock(&mutex);
printf("Coiffeur : coupe en cours...\n");
sleep(2); // simule la coupe
printf("Coiffeur : coupe terminée\n");
sem_post(&coupe_finie); // libère le client
}
return NULL;
}
void* client(void* arg) {
int id = *(int*)arg;
pthread_mutex_lock(&mutex);
if (nb_attente < NBP) {
nb_attente++;
printf("Client %d : entre, salle = %d/%d\n", id, nb_attente, NBP);
pthread_mutex_unlock(&mutex);
sem_post(&client_arrive); // réveille le coiffeur
sem_wait(&coupe_finie); // attend sa coupe
printf("Client %d : coiffé, je pars\n", id);
} else {
pthread_mutex_unlock(&mutex);
printf("Client %d : salle pleine, je repars\n", id);
}
return NULL;
}
int main(void) {
sem_init(&client_arrive, 0, 0);
sem_init(&coupe_finie, 0, 0);
pthread_t tcoif;
pthread_create(&tcoif, NULL, coiffeur, NULL);
pthread_t clients[10];
int ids[10];
for (int i = 0; i < 10; i++) {
ids[i] = i + 1;
pthread_create(&clients[i], NULL, client, &ids[i]);
usleep(500000); // 0.5s entre arrivées
}
for (int i = 0; i < 10; i++) pthread_join(clients[i], NULL);
return 0;
}
nb_attente ?Deux clients peuvent arriver simultanément et lire
nb_attente en même temps. Sans mutex, ils pourraient tous deux décider qu'il reste une place et faire nb_attente++, créant un dépassement. Le mutex linéarise les accès.
Compilation : gcc coiffeur.c -lpthread -o coiffeur.
4. Exercice 4 — Serveur de fichiers TCP (6 pts)
Client envoie un nom de fichier. Serveur cherche dans son répertoire Service/ et renvoie soit le contenu, soit un message d'erreur.
Q1 — Client TCP
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char** argv) {
if (argc != 4) {
fprintf(stderr, "Usage : %s <ip_serveur> <port> <nom_fichier>\n", argv[0]);
exit(1);
}
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveur;
serveur.sin_family = AF_INET;
serveur.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &serveur.sin_addr);
if (connect(sock, (struct sockaddr*)&serveur, sizeof(serveur)) < 0) {
perror("connect"); exit(1);
}
// Envoi du nom de fichier
write(sock, argv[3], strlen(argv[3]));
// Réception du contenu (ou message d'erreur)
char buf[1024];
int n;
while ((n = read(sock, buf, sizeof(buf) - 1)) > 0) {
buf[n] = '\0';
if (strstr(buf, "fichier introuvable") != NULL) {
printf("le fichier n'existe pas chez le serveur\n");
} else {
printf("%s", buf); // affiche le contenu
}
}
close(sock);
return 0;
}
Q2 — Serveur (un seul client puis multi-clients)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netinet/in.h>
void traiter_client(int dialogue) {
char nom[128];
int n = read(dialogue, nom, 127);
if (n <= 0) { close(dialogue); return; }
nom[n] = '\0';
char chemin[256];
snprintf(chemin, sizeof(chemin), "Service/%s", nom);
int fd = open(chemin, O_RDONLY);
if (fd < 0) {
char* msg = "fichier introuvable\n";
write(dialogue, msg, strlen(msg));
} else {
char buf[1024];
int m;
while ((m = read(fd, buf, sizeof(buf))) > 0) {
write(dialogue, buf, m);
}
close(fd);
}
close(dialogue);
}
int main(int argc, char** argv) {
if (argc != 2) {
fprintf(stderr, "Usage : %s <port>\n", argv[0]);
exit(1);
}
signal(SIGCHLD, SIG_IGN); // évite les zombies
int ecoute = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveur = {0}, client;
socklen_t len = sizeof(client);
serveur.sin_family = AF_INET;
serveur.sin_port = htons(atoi(argv[1]));
serveur.sin_addr.s_addr = INADDR_ANY;
bind(ecoute, (struct sockaddr*)&serveur, sizeof(serveur));
listen(ecoute, 10);
printf("Serveur fichiers en écoute sur %s...\n", argv[1]);
while (1) {
int dialogue = accept(ecoute, (struct sockaddr*)&client, &len);
if (dialogue < 0) continue;
if (fork() == 0) { // enfant : traite le client
close(ecoute);
traiter_client(dialogue);
exit(0);
} else { // parent : retourne accepter
close(dialogue);
}
}
return 0;
}
fork, on enlève le bloc fork() et on appelle directement traiter_client(dialogue);. Le serveur traite alors un seul client à la fois — les autres clients attendent dans la file de connexions (listen).
Q2 modification multi-clients — alternatives à fork
pthread_createpar client : plus léger qu'un fork, mais demande de gérer les mutex si les threads partagent des ressources.selectouepoll: serveur mono-thread qui multiplexe les I/O. Le plus performant à grande échelle (modèle réacteur).- Pool de threads : pré-créer N threads qui pickent dans une file de connexions. Évite le coût de création à chaque client.
★ Notions ré-activées
- open avec
O_CREATdemande un mode (0644,0664, etc.) - scanf("%s") s'arrête au premier espace
- strcmp = 0 pour égalité — toujours le piège oublié
- Pipeline 2 fils : père crée le pipe, ferme ses copies, attend les deux fils
- Threads séquentiels :
pthread_joinavant lepthread_createsuivant - Coiffeur dormant : 2 sémaphores signal (client_arrive, coupe_finie) + 1 mutex (nb_attente)
- Mutex obligatoire autour de tout compteur partagé entre threads
- signal(SIGCHLD, SIG_IGN) évite les zombies dans le serveur fork
- snprintf pour construire des chemins en sécurité (limite la taille)
- Multi-clients : fork (simple), threads (rapide), select (scalable)