Le carnet On est
Annale 2022-2023 · Rattrapage · GIA

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.

2h · GIA 1 feuille recto-verso autorisée 3 exercices + cours

i. Le sujet

Le PDF ne s'affiche pas. Télécharger le sujet.

↗ Sujet en plein écran

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)
  1. Tubes (pipes anonymes pipe() ou nommés mkfifo()) — flux d'octets FIFO, unidirectionnel.
  2. Signaux (kill(), sigaction()) — notification asynchrone, sans payload.
  3. 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)
Définition Un sémaphore est un compteur protégé en mémoire système, manipulé exclusivement par des opérations atomiques. Inventé par Dijkstra (1965).
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 et WEXITSTATUS
  • WIFEXITED(status) renvoie vrai si le fils s'est terminé normalement (via exit ou return).
  • 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.

⚠ Piège classique — passage d'argument Si on écrivait 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).

Séquence canonique côté serveur TCP
  1. socket() — créer le socket d'écoute
  2. bind() — l'attacher à une IP + port
  3. listen() — passer en mode écoute (taille de la queue de connexions en attente)
  4. accept() — bloquer jusqu'à une connexion, renvoie un nouveau socket pour le dialogue
  5. read() / write() — échange
  6. close() — 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)