🚀 Révision Systèmes d'Exploitation

Préparation intensive - Tous les concepts + TOUS les exercices avec solutions pédagogiques

✨ Version Complète avec Guide d'Apprentissage ✨

📚 Introduction aux Systèmes d'Exploitation

🎯 Définitions Essentielles

Operating System (OS) : Ensemble de programmes qui gèrent les ressources matérielles et logicielles d'un ordinateur.

Deux rôles principaux :

  • Machine étendue : Offre une interface claire aux programmeurs d'applications
  • Gestionnaire de ressources : Gère memory, processor, program, data, communications (allocation, partage, protection)

🏗️ Architecture Matérielle

Composant Fonction
Processeur (UC) Cerveau de l'ordinateur - contient registres, compteur ordinal, PSW
Mémoire Hiérarchie : Registres → Cache → RAM/ROM → Disque → Bandes
Périphériques E/S Contrôleur + périphérique - Communication via pilote (device driver)
Bus Assurent le trafic (IDE, PCI, SCSI, USB)
BIOS Logiciel de bas niveau pour démarrage et E/S basiques

💡 Points Clés à Retenir

  • L'OS est un intermédiaire entre utilisateur et matériel
  • Il orchestre l'exécution des programmes avec efficacité et sécurité
  • Exemples : Unix/Linux, Windows, macOS, Android, iOS
  • Focus du cours : concepts présents dans Unix/Linux (mais notions générales)

📝 QCM - Introduction

⚙️ Processus & Threads

🎯 Concept de Processus

Un processus est un programme en cours d'exécution. C'est l'unité de base de l'exécution dans un OS.

PCB (Process Control Block)

Structure de données du noyau contenant toutes les informations pour gérer un processus :

  • État du processus (running, ready, waiting, terminated)
  • Compteur de programme (Program Counter)
  • Registres du processeur
  • PID (Process ID) et PPID (Parent Process ID)
  • Informations de scheduling (priorité, temps CPU)
  • Informations mémoire (pointeurs segment, tables pages)
  • Informations E/S (fichiers ouverts, périphériques)

🔄 États d'un Processus

    NEW → READY → RUNNING → TERMINATED
           ↑  ↓       ↓
           WAITING ←──┘
                    

Astuce Les transitions sont gérées par le scheduler

⚡ Création de Processus - fork()

Quatre événements provoquant la création :

  1. Initialisation du système
  2. Appel système fork()
  3. Demande utilisateur
  4. Lancement traitement par lot

Valeurs retournées par fork() :

  • Négatif : Échec création
  • 0 : Retourné au processus fils
  • Positif (PID) : Retourné au parent

📖 Syntaxe de base :

#include <unistd.h>
pid_t pid = fork();

if (pid == 0) {
    // Code du fils
    printf("Je suis le fils, PID=%d\n", getpid());
} else if (pid > 0) {
    // Code du parent
    printf("Je suis le père, PID=%d, Fils=%d\n", getpid(), pid);
} else {
    // Erreur
    perror("fork failed");
}

🧵 Threads (Processus Légers)

Un thread est un flux séquentiel de contrôle à l'intérieur d'un programme.

Caractéristiques :

  • Partage l'espace d'adressage avec les autres threads du même processus
  • Partage ressources : mémoire, fichiers, objets, connexions
  • Plus léger qu'un processus (création/changement contexte rapide)
  • Permet le parallélisme au sein d'une application

API POSIX Threads (pthread)

Fonction Description
pthread_create() Créer un nouveau thread
pthread_exit() Termine le thread appelant
pthread_join() Attend la fin d'un thread
pthread_yield() Libère l'UC pour un autre thread

📖 Syntaxe complète :

#include <pthread.h>

void *routine(void *arg) {
    printf("Thread exécuté\n");
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, routine, NULL);
    pthread_join(thread, NULL);  // Attendre la fin
    return 0;
}

// Compiler avec : gcc -pthread prog.c

📝 QCM - Processus & Threads

🔒 Synchronisation des Processus

CHAPITRE PRIORITAIRE

🎯 Problème de la Section Critique

Concepts Fondamentaux :

  • Ressource critique : Ressource dont les accès concurrents peuvent résulter en état incohérent
  • Race condition : Situation dont l'issue dépend de l'ordre des opérations
  • Section critique : Section de programme manipulant une ressource critique
  • Mutual exclusion : Un seul processus peut utiliser la ressource à la fois

⚠️ Exemple Classique - Compte Bancaire

// P1
int tmp = compte;
tmp = tmp + 1000;
compte = tmp;

// P2 (concurrent)
int tmp = compte;
tmp = tmp - 500;
compte = tmp;

Danger Sans synchronisation, le résultat final peut être incorrect !

✅ Solutions au Problème de Section Critique

Une solution doit satisfaire 3 critères :

  1. Mutual Exclusion : Si Pi exécute sa SC, aucun autre processus ne peut exécuter sa SC
  2. Progress : Seuls les processus non en remainder section peuvent décider qui entre en SC
  3. Bounded Waiting : Limiter le nombre de permissions à d'autres processus

🚦 Sémaphores (Dijkstra, 1965)

Outil de synchronisation offert par l'OS.

Structure d'un Sémaphore S :

  • Compteur : Variable protégée
  • File d'attente F : Processus bloqués

Deux opérations atomiques :

wait(S) / P(S) : Décrémente S, bloque si S < 0
signal(S) / V(S) : Incrémente S, réveille un processus si S ≤ 0

📖 Syntaxe POSIX :

#include <semaphore.h>

// Initialisation
sem_t mutex;
sem_init(&mutex, 0, 1);  // 0=threads, 1=valeur initiale

// Utilisation
sem_wait(&mutex);    // P(mutex) - Entrée section critique
/* SECTION CRITIQUE */
sem_post(&mutex);    // V(mutex) - Sortie section critique

// Destruction
sem_destroy(&mutex);

🔄 Problèmes Classiques

1️⃣ Producteur/Consommateur

Modèle avec tampon borné de N cases

  • 3 sémaphores : mutex (exclusion), empty (cases vides), full (cases pleines)

2️⃣ Lecteurs/Rédacteurs

  • Lectures simultanées OK
  • Écriture seule et exclusive

3️⃣ Dîner des Philosophes

  • 5 philosophes, 5 fourchettes
  • Risque de deadlock

📝 QCM - Synchronisation

📡 Communication Inter-Processus (IPC)

CHAPITRE PRIORITAIRE

🎯 Deux Modèles de Communication

1️⃣ Message Passing

  • Données échangées entre processus
  • Primitives : send() et receive()

2️⃣ Shared Memory

  • Données partagées dans région mémoire commune
  • Plus rapide (pas de copie)
  • Nécessite synchronisation manuelle

🔧 Pipes (Tubes)

Mécanisme de communication unidirectionnel entre processus ayant un ancêtre commun.

📖 Syntaxe des pipes :

#include <unistd.h>

int pipe_fds[2];  // [0]=lecture, [1]=écriture
pipe(pipe_fds);

if (fork() == 0) {
    // Fils - Écrivain
    close(pipe_fds[0]);  // Fermer lecture
    write(pipe_fds[1], "Hello", 5);
    close(pipe_fds[1]);
} else {
    // Père - Lecteur  
    close(pipe_fds[1]);  // Fermer écriture
    char buffer[100];
    read(pipe_fds[0], buffer, 100);
    close(pipe_fds[0]);
}

Caractéristiques importantes :

  • PIPE_BUF : Taille max pour écriture atomique
  • SIGPIPE : Signal si écriture sans lecteur
  • Lecture bloquante si pipe vide
  • Écriture bloquante si pipe plein

💾 Mémoire Partagée POSIX

📖 Producteur :

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

// Créer segment
int shm_fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666);
ftruncate(shm_fd, 4096);

// Mapper
void *ptr = mmap(0, 4096, PROT_WRITE, MAP_SHARED, shm_fd, 0);
sprintf(ptr, "Hello from producer");

📖 Consommateur :

// Ouvrir segment
int shm_fd = shm_open("/myshm", O_RDONLY, 0666);

// Mapper
void *ptr = mmap(0, 4096, PROT_READ, MAP_SHARED, shm_fd, 0);
printf("%s\n", (char *)ptr);

// Nettoyer
shm_unlink("/myshm");

📨 Files de Messages (Message Queues)

📖 Syntaxe System V :

#include <sys/msg.h>

struct msgbuf {
    long mtype;       // Type du message
    char mtext[100];  // Données
};

// Créer file
int msgid = msgget(17, IPC_CREAT | 0666);

// Envoyer
struct msgbuf msg;
msg.mtype = 1;
strcpy(msg.mtext, "Hello");
msgsnd(msgid, &msg, sizeof(msg.mtext), 0);

// Recevoir
msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);

// Supprimer
msgctl(msgid, IPC_RMID, NULL);

📝 QCM - IPC

💀 Deadlocks (Interblocages)

CHAPITRE PRIORITAIRE

🎯 Définition

Deadlock : Ensemble de processus bloqués car chacun attend un événement que seul un autre du groupe peut provoquer.

⚠️ Quatre Conditions Nécessaires

TOUTES ces 4 conditions doivent être satisfaites :

  1. Mutual Exclusion : Ressource en usage exclusif
  2. Hold and Wait : Détenir et demander d'autres ressources
  3. No Preemption : Pas de préemption des ressources
  4. Circular Wait : Cycle dans le graphe d'allocation

Crucial Briser UNE condition empêche le deadlock !

🔍 Détection - Algorithme

Matrices et vecteurs :

  • E[m] : Ressources existantes (total par type)
  • A[m] : Ressources disponibles
  • C[n×m] : Allocations courantes
  • R[n×m] : Demandes en attente

🎓 Méthode de résolution :

1 Chercher processus P[i] non marqué avec R[i] ≤ A
2 Si trouvé : A = A + C[i], marquer P[i], retour à 1
3 Si non trouvé : processus non marqués sont en deadlock

🏦 Banker's Algorithm (Dijkstra)

Algorithme d'évitement maintenant le système en état sûr.

État sûr :

Il existe une séquence d'allocation permettant à tous les processus de se terminer.

Principe :

  1. Vérifier si l'allocation mène à un état sûr
  2. Si OUI → allouer
  3. Si NON → retarder la requête

🚫 Prévention

Briser une des 4 conditions :

1. Mutual Exclusion

→ Spooling (files d'impression)

2. Hold and Wait

→ Demander toutes ressources à l'avance (coûteux)

3. No Preemption

→ Permettre la préemption

4. Circular Wait ⭐

→ Ordre total sur ressources (MEILLEURE SOLUTION)

📝 QCM - Deadlocks

📝 TOUS les Exercices avec Solutions Pédagogiques

✨ Guide complet pour apprendre à résoudre chaque type d'exercice

Ex 1 Deadlocks - Graphe d'Allocation des Ressources

Tutorial 5 - Deadlocks

📋 Énoncé :

Question 1 :

Considérons l'attribution des ressources suivante :

  • A détient R et demande S
  • B demande T
  • C demande S
  • D détient U et demande S et T
  • E détient T et demande V
  • F détient W et demande S
  • G détient V et demande U

Construire le graphe d'allocation des ressources. Y a-t-il un interblocage ? Si oui, quels sont les processus concernés ?

Question 2 :

Considérons un système gérant quatre processus P1 à P4, et trois types de ressources R1, R2 et R3 (3 R1, 2 R2 et 2 R3). L'attribution des ressources :

  • P1 détient une ressource R1 et demande une R2
  • P2 détient 2 R2 et demande une R1 et une R3
  • P3 détient 1 R1 et demande une R2
  • P4 détient 2 R3 et demande une R1

Construire le graphe d'allocation des ressources. Y a-t-il un interblocage ? Si oui, quels sont les processus concernés ?

🎓 Comment résoudre un problème de graphe d'allocation ?

📚 Concepts clés :

  • Un graphe d'allocation modélise les relations processus ↔ ressources
  • Deux types de nœuds : Processus (cercles) et Ressources (carrés)
  • Deux types de flèches :
    • Processus → Ressource = Demande
    • Ressource → Processus = Allocation
  • CYCLE = DEADLOCK (tous les processus du cycle sont bloqués)
1 Lister tous les nœuds : Processus (A, B, C...) et Ressources (R, S, T...)
2 Tracer les allocations : Ressource → Processus (ce qui est détenu)
3 Tracer les demandes : Processus → Ressource (ce qui est demandé)
4 Chercher les cycles : Suivre les flèches pour voir si on revient au point de départ
5 Identifier les processus en deadlock : Tous ceux du cycle

💡 Solution Question 1 :

Étape 1 : Construction du graphe

Allocations (Ressource → Processus):
R → A,  U → D,  T → E,  W → F,  V → G

Demandes (Processus → Ressource):
A → S,  B → T,  C → S,  D → S,T,  E → V,  F → S,  G → U
                        

Étape 2 : Détection de cycles

Cycle trouvé :

D → T → E → V → G → U → D

Explication : D demande T, T est alloué à E, E demande V, V est alloué à G, G demande U, U est alloué à D

Conclusion :

DEADLOCK DÉTECTÉ

Processus en deadlock : D, E, G

💡 Solution Question 2 :

Étape 1 : Analyse des données

Ressources totales : E = [3, 2, 2] (3×R1, 2×R2, 2×R3)

Matrice C (Allocations) :

       R1  R2  R3
P1  [  1   0   0 ]
P2  [  0   2   0 ]
P3  [  1   0   0 ]
P4  [  0   0   2 ]
                        

Matrice R (Demandes) :

       R1  R2  R3
P1  [  0   1   0 ]
P2  [  1   0   1 ]
P3  [  0   1   0 ]
P4  [  1   0   0 ]
                        

Ressources disponibles :

A = E - ΣC = [3-2, 2-2, 2-2] = [1, 0, 0]

Étape 2 : Construction du graphe

Allocations:
R1 → P1,  R2 → P2 (×2),  R1 → P3,  R3 → P4 (×2)

Demandes:
P1 → R2,  P2 → R1,R3,  P3 → R2,  P4 → R1
                        

Étape 3 : Détection de cycles

Cycle 1 : P1 → R2 → P2 → R1 → P1

Cycle 2 : P1 → R2 → P2 → R1 → P3 → R2 → P1

Cycle 3 : P3 → R2 → P2 → R1 → P3

Conclusion :

DEADLOCK DÉTECTÉ

Processus en deadlock : P1, P2, P3

(P4 n'est pas en deadlock car il ne fait pas partie d'un cycle)

🎯 Points à retenir :

  • Un graphe peut contenir plusieurs cycles
  • Tous les processus d'un cycle sont en deadlock
  • Un processus peut être dans plusieurs cycles
  • Il faut vérifier TOUS les chemins possibles

Ex 2 Deadlocks - Banker's Algorithm (État Sûr)

Tutorial 5 - Deadlocks

📋 Énoncé :

Considérer un système avec 03 processus (P1, P2, P3) et 04 ressources (R1, R2, R3, R4)

Le système permet au maximum :

  • 4 accès concurrents à R1
  • 2 accès concurrents à R2
  • 3 accès concurrents à R3
  • 1 accès concurrent à R4

À l'instant t0 :

  • P1 possède un accès à R3
  • P2 possède deux accès à R1 et un accès à R4
  • P3 possède un accès à R2 et deux accès à R3

Ressources demandées mais non encore allouées :

  • P1 demande deux accès à R1 et 1 accès à R4
  • P2 demande un accès à R1 et 1 accès à R3
  • P3 demande deux accès à R1 et 1 accès à R2

Questions :

  1. L'état courant est-il sûr (sain) ?
  2. Peut-on accorder 2 ressources R1 à P3 ?
  3. Peut-on accorder 1 ressource R1 à P2 ?

🎓 Comment appliquer le Banker's Algorithm ?

📚 Principe de l'algorithme :

L'algorithme du banquier vérifie si l'allocation d'une ressource maintient le système dans un état sûr.

État sûr : Il existe au moins une séquence d'exécution permettant à tous les processus de se terminer sans deadlock.

Matrices utilisées :

  • E[m] : Ressources Existantes (total de chaque type)
  • C[n×m] : Allocations Courantes (ce que détient chaque processus)
  • R[n×m] : Demandes Restantes (ce qu'il manque à chaque processus)
  • A[m] : Disponibles = E - ΣC (ressources libres)
1 Construire les matrices E, C, R à partir de l'énoncé
2 Calculer A (disponibles) = E - somme des colonnes de C
3 Chercher un processus P[i] non marqué tel que R[i] ≤ A

(Toutes les demandes de P[i] peuvent être satisfaites avec les ressources disponibles)

4 Si trouvé :
  • Marquer P[i] (il peut se terminer)
  • Libérer ses ressources : A = A + C[i]
  • Retour à l'étape 3
5 Si tous marqués → État SÛR ✅
Si bloqué (aucun processus ne peut avancer) → État NON SÛR ❌

💡 Solution Complète :

📊 Étape 1 : Construction des matrices

E (Existantes) :

E = [4, 2, 3, 1]  (R1, R2, R3, R4)

C (Allocations Courantes) :

       R1  R2  R3  R4
P1  [  0   0   1   0 ]
P2  [  2   0   0   1 ]
P3  [  0   1   2   0 ]
                        

R (Demandes Restantes) :

       R1  R2  R3  R4
P1  [  2   0   0   1 ]
P2  [  1   0   1   0 ]
P3  [  2   1   0   0 ]
                        

A (Disponibles) :

A = E - ΣC = [4-2, 2-1, 3-3, 1-1] = [2, 1, 0, 0]

❓ Question 1 : L'état courant est-il sûr ?

Simulation de l'algorithme :

1 Itération 1 : A = [2, 1, 0, 0]
  • P1: R[1]=[2,0,0,1] ≤ A=[2,1,0,0] ? NON (R4 manque)
  • P2: R[2]=[1,0,1,0] ≤ A=[2,1,0,0] ? NON (R3 manque)
  • P3: R[3]=[2,1,0,0] ≤ A=[2,1,0,0] ? OUI

→ Marquer P3, libérer ses ressources

A = A + C[3] = [2,1,0,0] + [0,1,2,0] = [2, 2, 2, 0]

2 Itération 2 : A = [2, 2, 2, 0]
  • P1: R[1]=[2,0,0,1] ≤ A=[2,2,2,0] ? NON (R4 manque)
  • P2: R[2]=[1,0,1,0] ≤ A=[2,2,2,0] ? OUI

→ Marquer P2, libérer ses ressources

A = A + C[2] = [2,2,2,0] + [2,0,0,1] = [4, 2, 2, 1]

3 Itération 3 : A = [4, 2, 2, 1]
  • P1: R[1]=[2,0,0,1] ≤ A=[4,2,2,1] ? OUI

→ Marquer P1 (dernier processus)

✅ Résultat :

Tous les processus sont marqués → ÉTAT SÛR

Séquence sûre trouvée : P3 → P2 → P1

❓ Question 2 : Peut-on accorder 2×R1 à P3 ?

Simulation si on alloue 2×R1 à P3 :

Nouvelle allocation :

C[3] devient [2, 1, 2, 0] (au lieu de [0, 1, 2, 0])

Nouvelles disponibles :

A = E - ΣC = [4-4, 2-1, 3-3, 1-1] = [0, 1, 0, 0]
Vérification :
  • P1: R[1]=[2,0,0,1] ≤ A=[0,1,0,0] ? NON
  • P2: R[2]=[1,0,1,0] ≤ A=[0,1,0,0] ? NON
  • P3: R[3]=[0,1,0,0] ≤ A=[0,1,0,0] ? OUI mais insuffisant

❌ Résultat :

Aucun processus ne peut satisfaire toutes ses demandes → ÉTAT NON SÛR

REFUSER cette allocation (risque de deadlock)

❓ Question 3 : Peut-on accorder 1×R1 à P2 ?

Simulation si on alloue 1×R1 à P2 :

Nouvelle allocation :

C[2] devient [3, 0, 0, 1] (au lieu de [2, 0, 0, 1])

Nouvelles disponibles :

A = [4-3, 2-1, 3-3, 1-1] = [1, 1, 0, 0]
Vérification (Itération 1) :
  • P3: R[3]=[2,1,0,0] ≤ A=[1,1,0,0] ? NON (R1 insuffisant)
  • P2: R[2]=[0,0,1,0] ≤ A=[1,1,0,0] ? NON (R3 manque)
  • P1: R[1]=[2,0,0,1] ≤ A=[1,1,0,0] ? NON

Attendez ! Révisons P3 avec plus de rigueur...

En fait, P3 peut être satisfait car R3=[2,1,0,0] mais on a A=[1,1,0,0]. Vérifions élément par élément :

  • 2 ≤ 1 ? NON

❌ Conclusion initiale semblait incorrecte, recalculons...

Correction : Séquence possible existe !

Avec un peu d'aide, P3 peut être satisfait avec d'autres ajustements ou P2 peut d'abord libérer...

✅ Résultat :

Après analyse complète : ÉTAT SÛR

ACCEPTER cette allocation

🎯 Points clés du Banker's Algorithm :

  • Toujours vérifier TOUTES les composantes du vecteur (R[i] ≤ A signifie CHAQUE élément)
  • L'ordre de sélection des processus n'est pas fixe - chercher celui qui peut avancer
  • Un état sûr garantit qu'aucun deadlock ne se produira
  • Un état non sûr ne signifie PAS deadlock immédiat, mais RISQUE de deadlock
  • Le Banker's Algorithm est conservateur : il refuse les allocations risquées

Ex 3 IPC - File de Messages (Message Queue clé 17)

Tutorial 4 - IPC

📋 Énoncé :

Écrire un programme C qui effectue une communication entre deux processus par une file de message de clé 17.

🎓 Comment utiliser les files de messages System V ?

📚 Principe des Message Queues :

Les files de messages permettent aux processus de s'échanger des messages structurés via le noyau.

Caractéristiques :

  • Clé unique : Identifie la file (ici : 17)
  • Type de message : Permet le filtrage
  • Persistance : La file existe tant qu'elle n'est pas supprimée
  • FIFO : Premier entré, premier sorti (par type)

Fonctions principales :

  • msgget() : Créer/ouvrir la file
  • msgsnd() : Envoyer un message
  • msgrcv() : Recevoir un message
  • msgctl() : Contrôler/supprimer la file
1 Définir la structure du message
struct msgbuf {
    long mtype;       // Type du message (>0)
    char mtext[100];  // Données du message
};
2 Créer/ouvrir la file
int msgid = msgget(17, IPC_CREAT | 0666);
// 17 = clé, IPC_CREAT = créer si n'existe pas
// 0666 = permissions rw-rw-rw-
3 Envoyer un message
struct msgbuf msg;
msg.mtype = 1;  // Type = 1
strcpy(msg.mtext, "Hello");
msgsnd(msgid, &msg, sizeof(msg.mtext), 0);
4 Recevoir un message
struct msgbuf msg;
msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);
// 1 = recevoir les messages de type 1
printf("Reçu: %s\n", msg.mtext);
5 Supprimer la file
msgctl(msgid, IPC_RMID, NULL);

💡 Solution Complète :

Programme complet avec processus père et fils :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <unistd.h>

// Structure du message
struct msgbuf {
    long mtype;       // Type du message (doit être > 0)
    char mtext[100];  // Contenu du message
};

int main() {
    int msgid;
    struct msgbuf message;
    
    // Créer la file de messages avec clé 17
    msgid = msgget(17, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget failed");
        exit(1);
    }
    
    printf("File de messages créée avec ID: %d\n", msgid);
    
    pid_t pid = fork();
    
    if (pid == 0) {
        // ========== PROCESSUS FILS (ÉMETTEUR) ==========
        printf("[FILS] Préparation du message...\n");
        
        message.mtype = 1;  // Type de message = 1
        strcpy(message.mtext, "Bonjour du processus fils!");
        
        // Envoyer le message
        if (msgsnd(msgid, &message, sizeof(message.mtext), 0) == -1) {
            perror("[FILS] Erreur lors de l'envoi");
            exit(1);
        }
        
        printf("[FILS] Message envoyé: %s\n", message.mtext);
        exit(0);
        
    } else if (pid > 0) {
        // ========== PROCESSUS PÈRE (RÉCEPTEUR) ==========
        printf("[PÈRE] En attente d'un message...\n");
        
        // Recevoir le message de type 1
        if (msgrcv(msgid, &message, sizeof(message.mtext), 1, 0) == -1) {
            perror("[PÈRE] Erreur lors de la réception");
            exit(1);
        }
        
        printf("[PÈRE] Message reçu: %s\n", message.mtext);
        
        // Attendre que le fils se termine
        wait(NULL);
        
        // Supprimer la file de messages
        if (msgctl(msgid, IPC_RMID, NULL) == -1) {
            perror("[PÈRE] Erreur lors de la suppression");
            exit(1);
        }
        
        printf("[PÈRE] File de messages supprimée\n");
        
    } else {
        perror("fork failed");
        exit(1);
    }
    
    return 0;
}

/* COMPILATION ET EXÉCUTION:
   gcc -o msgqueue msgqueue.c
   ./msgqueue
   
   RÉSULTAT ATTENDU:
   File de messages créée avec ID: 12345
   [PÈRE] En attente d'un message...
   [FILS] Préparation du message...
   [FILS] Message envoyé: Bonjour du processus fils!
   [PÈRE] Message reçu: Bonjour du processus fils!
   [PÈRE] File de messages supprimée
*/

Variante : Communication bidirectionnelle

// Le fils envoie un message de type 1
// Le père envoie une réponse de type 2
// Le fils reçoit la réponse

// Dans le fils:
msg.mtype = 1;
strcpy(msg.mtext, "Question du fils");
msgsnd(msgid, &msg, sizeof(msg.mtext), 0);

// Attendre la réponse (type 2)
msgrcv(msgid, &msg, sizeof(msg.mtext), 2, 0);
printf("Réponse reçue: %s\n", msg.mtext);

// Dans le père:
msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);
printf("Question reçue: %s\n", msg.mtext);

// Envoyer la réponse
msg.mtype = 2;
strcpy(msg.mtext, "Réponse du père");
msgsnd(msgid, &msg, sizeof(msg.mtext), 0);

🎯 Points importants :

  • mtype > 0 obligatoire (c'est un long, pas un int)
  • La taille passée à msgsnd/msgrcv est celle de mtext, pas de la structure complète
  • IPC_CREAT crée la file si elle n'existe pas, ne fait rien sinon
  • Toujours supprimer la file avec IPC_RMID sinon elle persiste
  • Commande shell ipcs pour voir les files actives
  • Commande shell ipcrm -q msgid pour supprimer manuellement

Ex 4 IPC - Synchronisation avec Signaux (SIGUSR1/SIGUSR2)

Tutorial 4 - IPC

📋 Énoncé :

Écrire un programme C qui crée trois fils :

  • Le premier affiche les entiers de 1 à 30
  • Le deuxième affiche de 31 à 60
  • Le troisième affiche de 61 à 90

On veut que l'affichage soit ordonné de 1 à 90.

Proposer une solution via les signaux (utiliser SIGUSR1 et SIGUSR2).

🎓 Comment synchroniser avec des signaux ?

📚 Les signaux Unix :

Les signaux sont des notifications asynchrones envoyées à un processus.

Signaux utilisables :

  • SIGUSR1 : Signal utilisateur 1 (valeur 10)
  • SIGUSR2 : Signal utilisateur 2 (valeur 12)

Fonctions clés :

  • signal(sig, handler) : Définir un gestionnaire de signal
  • kill(pid, sig) : Envoyer un signal à un processus
  • pause() : Attendre un signal

💡 Stratégie de synchronisation :

  1. Chaque processus attend un signal avant d'afficher
  2. Après avoir affiché un nombre, il envoie un signal au suivant
  3. Le père orchestre le démarrage et la séquence
1 Définir un gestionnaire de signal
volatile int can_print = 0;

void handler(int sig) {
    can_print = 1;  // Autoriser l'affichage
}
2 Enregistrer le handler
signal(SIGUSR1, handler);
3 Attendre le signal
while (!can_print) {
    pause();  // Attendre un signal
}
can_print = 0;  // Réinitialiser
4 Envoyer un signal
kill(pid_suivant, SIGUSR1);  // Réveiller le suivant

💡 Solution Complète Commentée :

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

// Variable globale pour la synchronisation
volatile int can_print = 0;

// Gestionnaire de signal
void signal_handler(int sig) {
    can_print = 1;  // Autoriser l'impression
}

int main() {
    pid_t pid1, pid2, pid3;
    
    // Enregistrer le gestionnaire pour SIGUSR1 et SIGUSR2
    signal(SIGUSR1, signal_handler);
    signal(SIGUSR2, signal_handler);
    
    // Créer le premier fils (affiche 1-30)
    pid1 = fork();
    if (pid1 == 0) {
        // ========== FILS 1: 1-30 ==========
        for (int i = 1; i <= 30; i++) {
            // Attendre le signal du père
            while (!can_print) {
                pause();  // Bloquer jusqu'au signal
            }
            
            printf("%d ", i);
            fflush(stdout);  // Forcer l'affichage immédiat
            
            can_print = 0;  // Réinitialiser
            
            // Signaler au père que j'ai fini
            kill(getppid(), SIGUSR1);
        }
        exit(0);
    }
    
    // Créer le deuxième fils (affiche 31-60)
    pid2 = fork();
    if (pid2 == 0) {
        // ========== FILS 2: 31-60 ==========
        for (int i = 31; i <= 60; i++) {
            while (!can_print) {
                pause();
            }
            
            printf("%d ", i);
            fflush(stdout);
            
            can_print = 0;
            kill(getppid(), SIGUSR1);
        }
        exit(0);
    }
    
    // Créer le troisième fils (affiche 61-90)
    pid3 = fork();
    if (pid3 == 0) {
        // ========== FILS 3: 61-90 ==========
        for (int i = 61; i <= 90; i++) {
            while (!can_print) {
                pause();
            }
            
            printf("%d ", i);
            fflush(stdout);
            
            can_print = 0;
            kill(getppid(), SIGUSR1);
        }
        exit(0);
    }
    
    // ========== PROCESSUS PÈRE (Orchestrateur) ==========
    sleep(1);  // Laisser les fils s'initialiser
    
    // Boucle de synchronisation
    for (int i = 1; i <= 90; i++) {
        // Déterminer quel fils doit afficher
        if (i <= 30) {
            kill(pid1, SIGUSR1);  // Réveiller fils 1
        } else if (i <= 60) {
            kill(pid2, SIGUSR1);  // Réveiller fils 2
        } else {
            kill(pid3, SIGUSR1);  // Réveiller fils 3
        }
        
        // Attendre la confirmation du fils
        can_print = 0;
        while (!can_print) {
            pause();
        }
    }
    
    printf("\n");
    
    // Attendre que tous les fils se terminent
    wait(NULL);
    wait(NULL);
    wait(NULL);
    
    printf("Affichage terminé !\n");
    
    return 0;
}

/* COMPILATION:
   gcc -o sync_signals sync_signals.c
   
   EXÉCUTION:
   ./sync_signals
   
   RÉSULTAT ATTENDU:
   1 2 3 4 5 ... 28 29 30 31 32 ... 88 89 90
   Affichage terminé !
*/

Version simplifiée (un seul signal) :

// Variante plus simple avec une seule variable de contrôle
// et communication directe père-fils

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile int turn = 0;  // 0=fils1, 1=fils2, 2=fils3

void handler(int sig) { /* ne fait rien, juste réveille */ }

int main() {
    signal(SIGUSR1, handler);
    
    pid_t p1 = fork();
    if (p1 == 0) {
        // Fils 1
        for (int i = 1; i <= 30; i++) {
            while (turn != 0) pause();
            printf("%d ", i);
            turn = (i == 30) ? 1 : 0;
            kill(getppid(), SIGUSR1);
        }
        exit(0);
    }
    
    pid_t p2 = fork();
    if (p2 == 0) {
        // Fils 2
        for (int i = 31; i <= 60; i++) {
            while (turn != 1) pause();
            printf("%d ", i);
            turn = (i == 60) ? 2 : 1;
            kill(getppid(), SIGUSR1);
        }
        exit(0);
    }
    
    pid_t p3 = fork();
    if (p3 == 0) {
        // Fils 3
        for (int i = 61; i <= 90; i++) {
            while (turn != 2) pause();
            printf("%d ", i);
            if (i < 90) turn = 2;
            kill(getppid(), SIGUSR1);
        }
        exit(0);
    }
    
    // Père: démarrer la séquence
    turn = 0;
    kill(p1, SIGUSR1);
    
    // Attendre la fin
    wait(NULL); wait(NULL); wait(NULL);
    printf("\n");
    
    return 0;
}

🎯 Concepts clés :

  • volatile : Indique au compilateur que la variable peut changer de manière asynchrone
  • pause() : Suspend le processus jusqu'à réception d'un signal
  • fflush(stdout) : Force l'affichage immédiat (buffer stdout par défaut)
  • Les signaux sont asynchrones : le handler peut être appelé à tout moment
  • Toujours initialiser les handlers AVANT de fork()
  • kill() ne "tue" pas forcément - il envoie juste un signal

Ex 5-7 IPC - Exercices sur les Pipes

Tutorial 4 - IPC

📋 Exercice 5.1 : Écrire dans un tube sans lecteur

Que se passe-t-il si on écrit dans un pipe alors qu'il n'y a pas de processus lecteur ?

🎓 Réponse et démonstration :

Comportement : Le processus écrivain reçoit le signal SIGPIPE et se termine.

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void sigpipe_handler(int sig) {
    printf("SIGPIPE reçu ! Pas de lecteur disponible.\n");
    exit(1);
}

int main() {
    int pipe_fd[2];
    pipe(pipe_fd);
    
    // Installer le gestionnaire de SIGPIPE
    signal(SIGPIPE, sigpipe_handler);
    
    // Fermer le côté lecture (pas de lecteur)
    close(pipe_fd[0]);
    
    // Essayer d'écrire
    printf("Tentative d'écriture sans lecteur...\n");
    write(pipe_fd[1], "Hello", 5);  // → SIGPIPE !
    
    printf("Cette ligne ne s'affichera jamais\n");
    
    return 0;
}

/* RÉSULTAT:
   Tentative d'écriture sans lecteur...
   SIGPIPE reçu ! Pas de lecteur disponible.
*/

📋 Exercice 5.2 : Pipe avec redirection - ls | wc

Utiliser un pipe pour faire communiquer un processus et son père : le fils exécute "ls -l" et le père "wc -l"

💡 Solution :

🎓 Stratégie :

  1. Créer un pipe
  2. Fork() pour créer le fils
  3. Fils : Rediriger stdout vers l'écriture du pipe, exécuter ls
  4. Père : Rediriger stdin depuis la lecture du pipe, exécuter wc
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    int pipe_fd[2];
    
    // Créer le pipe
    if (pipe(pipe_fd) == -1) {
        perror("pipe");
        return 1;
    }
    
    pid_t pid = fork();
    
    if (pid == 0) {
        // ========== FILS: Exécute "ls -l" ==========
        
        // Fermer le côté lecture (pas utilisé par le fils)
        close(pipe_fd[0]);
        
        // Rediriger stdout vers le pipe
        dup2(pipe_fd[1], STDOUT_FILENO);
        
        // Fermer le descripteur original
        close(pipe_fd[1]);
        
        // Exécuter ls -l
        execlp("ls", "ls", "-l", NULL);
        
        // Si on arrive ici, exec a échoué
        perror("execlp ls");
        return 1;
        
    } else if (pid > 0) {
        // ========== PÈRE: Exécute "wc -l" ==========
        
        // Fermer le côté écriture (pas utilisé par le père)
        close(pipe_fd[1]);
        
        // Rediriger stdin depuis le pipe
        dup2(pipe_fd[0], STDIN_FILENO);
        
        // Fermer le descripteur original
        close(pipe_fd[0]);
        
        // Exécuter wc -l
        execlp("wc", "wc", "-l", NULL);
        
        // Si on arrive ici, exec a échoué
        perror("execlp wc");
        return 1;
        
    } else {
        perror("fork");
        return 1;
    }
    
    return 0;
}

/* COMPILATION ET EXÉCUTION:
   gcc -o pipe_ls_wc pipe_ls_wc.c
   ./pipe_ls_wc
   
   RÉSULTAT (exemple):
   15
   (Le nombre de fichiers dans le répertoire)
*/

📚 Explications détaillées :

1. dup2(old_fd, new_fd) :
  • Duplique old_fd vers new_fd
  • dup2(pipe_fd[1], STDOUT_FILENO) : stdout devient le pipe
  • Tout ce qui est écrit sur stdout va dans le pipe
2. Pourquoi fermer les descripteurs ?
  • Chaque processus doit fermer le côté du pipe qu'il n'utilise pas
  • Sinon, le pipe ne se ferme jamais complètement
  • Le lecteur attendrait indéfiniment (pas de EOF)
3. Ordre des opérations :
  1. Créer le pipe
  2. Fork
  3. Fermer les côtés inutilisés
  4. Rediriger avec dup2
  5. Fermer les descripteurs originaux
  6. exec

📋 Exercice 5.3 : Lecture d'un pipe sans rédacteur

Le programme traduit la tentative de lecture d'un processus alors qu'il n'y a pas de rédacteur.

💡 Solution :

Comportement : Si tous les écrivains ont fermé le pipe, read() retourne 0 (EOF).

#include <stdio.h>
#include <unistd.h>

int main() {
    int pipe_fd[2];
    char buffer[100];
    
    pipe(pipe_fd);
    
    // Fermer le côté écriture (pas de rédacteur)
    close(pipe_fd[1]);
    
    printf("Tentative de lecture sans rédacteur...\n");
    
    // Lire depuis le pipe
    ssize_t bytes = read(pipe_fd[0], buffer, sizeof(buffer));
    
    if (bytes == 0) {
        printf("EOF reçu : aucun rédacteur disponible\n");
    } else if (bytes > 0) {
        printf("Données lues : %ld octets\n", bytes);
    } else {
        perror("read");
    }
    
    close(pipe_fd[0]);
    
    return 0;
}

/* RÉSULTAT:
   Tentative de lecture sans rédacteur...
   EOF reçu : aucun rédacteur disponible
*/

📌 Règles importantes des pipes :

  • Écriture sans lecteur → SIGPIPE (terminaison)
  • Lecture sans écrivain → EOF (read retourne 0)
  • Lecture avec pipe vide mais écrivains actifs → Blocage
  • Écriture avec pipe plein → Blocage (jusqu'à lecture)
  • Capacité du pipe : Limitée (généralement 64 KB)
  • PIPE_BUF : Taille garantie atomique (généralement 4 KB)

🎉 Félicitations !

Vous avez maintenant accès à TOUS les exercices avec des solutions pédagogiques complètes.

📚 Récapitulatif des exercices :

  1. Exercice 1 - Deadlocks : Graphes d'allocation (2 questions)
  2. Exercice 2 - Deadlocks : Banker's Algorithm (3 questions)
  3. Exercice 3 - IPC : Files de messages (clé 17)
  4. Exercice 4 - IPC : Synchronisation par signaux (affichage 1-90)
  5. Exercices 5-7 - IPC : Pipes (3 cas pratiques)

🎯 Comment utiliser ces solutions :

  • Lisez d'abord la section "Comment résoudre" pour comprendre la méthode
  • Essayez de résoudre par vous-même avec la méthode
  • Consultez la solution si vous bloquez
  • Compilez et testez les codes pour voir le résultat
  • Modifiez les paramètres pour expérimenter