Se connecterS'abonner

Chapitre 17

Programming, data structures & operating systems

Lecture : 23 minQuiz de 12 questions

# Programming, data structures & operating systems ## Objectifs et prérequis Ce chapitre vise à consolider les connaissances fondamentales en programmation C, structures de données, systèmes d'exploitation et logiciels système, tout en développant des compétences pratiques à travers deux projets concrets. À l'issue de ce chapitre, l'étudiant sera capable de : - Implémenter et manipuler des structures de données linéaires et non linéaires en C (listes, piles, files, arbres binaires de recherche). - Comprendre le rôle du système d'exploitation dans la gestion des processus, de la mémoire et des périphériques. - Développer un module d'indexation et de recherche de données en C utilisant des structures de données appropriées. - Concevoir un simulateur de allocation CPU et mémoire basé sur des algorithmes de planification et de gestion de mémoire réels. - Appliquer les bonnes pratiques de programmation système : gestion de la mémoire dynamique, pointeurs, fichiers, et modularité. **Prérequis** : - Maîtrise des bases du langage C (variables, types, opérateurs, structures de contrôle, fonctions). - Notions d'algorithmique de base (complexité en O(n), recherche linéaire/dichotomique). - Familiarité avec l'environnement de développement (compilateur GCC, éditeur de texte, ligne de commande sous Linux ou équivalent). - Compréhension des concepts de mémoire (stack vs heap) et de compilation (préprocessing, compilation, assemblage, édition de liens). --- ## I. Data Structures & C Programming Foundations ### 1.1. Structures de données linéaires : listes simplement chaînées Une **liste simplement chaînée** est une structure de données dynamique où chaque élément (nœud) contient une donnée et un pointeur vers le nœud suivant. Elle permet une insertion/suppression efficace en O(1) à la tête, mais une recherche en O(n). > **Définition** : Une liste simplement chaînée est définie par un pointeur `head` vers le premier nœud. Chaque nœud est une structure : > ```c > typedef struct Node { > int data; > struct Node* next; > } Node; > ``` **Propriétés** : - Allocation dynamique : chaque nœud est créé avec `malloc()`. - Pas d'accès aléatoire : il faut parcourir la liste depuis la tête. - Insertion en tête : O(1). - Suppression d'un nœud donné : O(n) si on ne connaît pas son prédécesseur. - Utilisation courante : implémentation de piles, files, gestion de listes de tâches. **Exemple résolu** : Insertion en tête et affichage d'une liste. ```c #include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node* next; } Node; void push(Node** head_ref, int new_data) { Node* new_node = (Node*)malloc(sizeof(Node)); new_node->data = new_data; new_node->next = (*head_ref); (*head_ref) = new_node; } void printList(Node* node) { while (node != NULL) { printf("%d -> ", node->data); node = node->next; } printf("NULL\n"); } int main() { Node* head = NULL; push(&head, 3); push(&head, 2); push(&head, 1); printList(head); // Output: 1 -> 2 -> 3 -> NULL return 0; } ``` **Exercice 1** : Écrire une fonction `deleteNode(Node** head_ref, int key)` qui supprime la première occurrence d'un nœud contenant `key`. **Corrigé** : ```c void deleteNode(Node** head_ref, int key) { Node* temp = *head_ref; Node* prev = NULL; if (temp != NULL && temp->data == key) { *head_ref = temp->next; free(temp); return; } while (temp != NULL && temp->data != key) { prev = temp; temp = temp->next; } if (temp == NULL) return; prev->next = temp->next; free(temp); } ``` ### 1.2. Piles et files Une **pile** (stack) est une structure LIFO (Last In, First Out). Une **file** (queue) est une structure FIFO (First In, First Out). > **Définition (pile)** : Les opérations principales sont `push` (empiler) et `pop` (dépiler), toutes deux en O(1). > **Définition (file)** : Les opérations sont `enqueue` (ajouter à la fin) et `dequeue` (retirer du début), en O(1) si implémentée avec une liste chaînée ou un tableau circulaire. **Implémentation en C (pile avec liste chaînée)** : ```c typedef struct Stack { Node* top; } Stack; void stackPush(Stack* s, int value) { Node* new_node = (Node*)malloc(sizeof(Node)); new_node->data = value; new_node->next = s->top; s->top = new_node; } int stackPop(Stack* s) { if (s->top == NULL) { fprintf(stderr, "Erreur : pile vide\n"); exit(EXIT_FAILURE); } Node* temp = s->top; int value = temp->data; s->top = s->top->next; free(temp); return value; } ``` **Implémentation en C (file avec liste chaînée)** : ```c typedef struct Queue { Node* front; Node* rear; } Queue; void enqueue(Queue* q, int value) { Node* new_node = (Node*)malloc(sizeof(Node)); new_node->data = value; new_node->next = NULL; if (q->rear == NULL) { q->front = q->rear = new_node; return; } q->rear->next = new_node; q->rear = new_node; } int dequeue(Queue* q) { if (q->front == NULL) { fprintf(stderr, "Erreur : file vide\n"); exit(EXIT_FAILURE); } Node* temp = q->front; int value = temp->data; q->front = q->front->next; if (q->front == NULL) q->rear = NULL; free(temp); return value; } ``` **Exercice 2** : Implémenter une fonction `isPalindrome(Node* head)` qui vérifie si une liste d'entiers forme un palindrome en utilisant une pile. **Corrigé** : 1. Parcourir la liste et empiler chaque élément. 2. Parcourir à nouveau la liste, dépiler et comparer chaque élément. 3. Si tous correspondent → palindrome. Complexité : O(n) en temps, O(n) en espace. ### 1.3. Arbres binaires de recherche (BST) Un **arbre binaire de recherche** est un arbre où pour chaque nœud, toutes les valeurs du sous-arbre gauche sont inférieures à la valeur du nœud, et toutes celles du sous-arbre droit sont supérieures. > **Définition** : > ```c > typedef struct BSTNode { > int data; > struct BSTNode* left; > struct BSTNode* right; > } BSTNode; > ``` **Propriétés** : - Recherche, insertion, suppression : O(h), où h est la hauteur de l'arbre. - Dans le pire cas (arbre dégénéré) : O(n). - Dans le meilleur cas (arbre équilibré) : O(log n). - Parcours : in-order (trié), pre-order, post-order. **Exemple résolu** : Insertion et recherche dans un BST. ```c BSTNode* insert(BSTNode* root, int value) { if (root == NULL) { BSTNode* new_node = (BSTNode*)malloc(sizeof(BSTNode)); new_node->data = value; new_node->left = new_node->right = NULL; return new_node; } if (value < root->data) root->left = insert(root->left, value); else if (value > root->data) root->right = insert(root->right, value); return root; } BSTNode* search(BSTNode* root, int value) { if (root == NULL || root->data == value) return root; if (value < root->data) return search(root->left, value); return search(root->right, value); } ``` **Exercice 3** : Écrire une fonction `inorderTraversal(BSTNode* root)` qui affiche les éléments du BST dans l'ordre croissant. **Corrigé** : ```c void inorderTraversal(BSTNode* root) { if (root != NULL) { inorderTraversal(root->left); printf("%d ", root->data); inorderTraversal(root->right); } } ``` **Exercice 4** : Écrire une fonction `minValueNode(BSTNode* node)` qui retourne le nœud avec la valeur minimale dans un BST. **Corrigé** : ```c BSTNode* minValueNode(BSTNode* node) { BSTNode* current = node; while (current && current->left != NULL) current = current->left; return current; } ``` --- ## II. Operating Systems & System Software ### 2.1. Rôle et fonctions du système d'exploitation Un **système d'exploitation (SE)** est un logiciel système qui gère les ressources matérielles d'un ordinateur et fournit des services communs aux programmes informatiques. > **Définition** : Le SE agit comme une interface entre le matériel et les logiciels applicatifs, assurant l'abstraction, la protection, et la gestion efficace des ressources. **Fonctions principales** : - **Gestion des processus** : création, planification, synchronisation, communication inter-processus (IPC). - **Gestion de la mémoire** : allocation, protection, échange (swapping), mémoire virtuelle. - **Gestion des fichiers** : création, lecture, écriture, organisation des systèmes de fichiers (ex: ext4, NTFS). - **Gestion des périphériques** : pilotes, interruptions, accès aux entrées/sorties (I/O). - **Protection et sécurité** : contrôle d'accès, authentification, isolation des processus. - **Interface utilisateur** : ligne de commande (CLI) ou interface graphique (GUI). **Logiciels système** : - **Noyau (kernel)** : cœur du SE, s'exécute en mode privilégié. - **Bibliothèques système** : fournissent des API pour accéder aux services du noyau (ex: libc, POSIX). - **Utilitaires** : commandes de base (ls, cp, grep, etc.). - **Shell** : interpréteur de commandes (ex: bash, sh). ### 2.2. Gestion des processus Un **processus** est un programme en cours d'exécution, comprenant son code, ses données, sa pile, ses registres, et son état. > **Définition** : Le SE maintient un **bloc de contrôle de processus (PCB)** pour chaque processus, contenant : > - État du processus (nouveau, prêt, exécutant, bloqué, terminé) > - Compteur d'instruction (PC) > - Registres CPU > - Informations de gestion de mémoire > - Informations de comptabilité > - État des E/S **Transitions d'état** : - **Nouveau → Prêt** : lors de la création. - **Prêt → Exécutant** : lorsque le planificateur choisit le processus. - **Exécutant → Bloqué** : lors d'une attente d'E/S ou d'un verrou. - **Bloqué → Prêt** : lorsque l'événement attendu se produit. - **Exécutant → Terminé** : après fin d'exécution ou appel à `exit()`. **Planification (scheduling)** : - **Objectif** : maximiser l'utilisation du CPU, minimiser le temps de réponse, assurer l'équité. - **Algorithmes courants** : - **FCFS (First Come, First Served)** : non préemptif, simple mais peut entraîner l'effet de convoi. - **SJF (Shortest Job First)** : préemptif ou non, optimal pour le temps moyen d'attente mais nécessite connaissance du temps d'exécution. - **Round Robin (RR)** : préemptif, chaque processus reçoit un quantum de temps fixe. - **Priorité** : attribue une priorité à chaque processus ; peut entraîner la famine. - **Multilevel Feedback Queue** : combine plusieurs files avec des quantums et priorités différents. **Exemple résolu** : Calcul du temps moyen d'attente avec FCFS pour 3 processus : P1: arrivée à 0, durée 6 P2: arrivée à 1, durée 3 P3: arrivée à 2, durée 4 Ordre d'exécution : P1 (0-6), P2 (6-9), P3 (9-13) Temps d'attente : P1: 0 - 0 = 0 P2: 6 - 1 = 5 P3: 9 - 2 = 7 Moyenne = (0+5+7)/3 = 4 **Exercice 5** : Calculer le temps moyen d'attente avec SJF non préemptif pour les mêmes processus. **Corrigé** : À t=0 : seul P1 disponible → exécuter P1 (0-6) À t=6 : P2 (durée 3) et P3 (durée 4) disponibles → choisir P2 (6-9) À t=9 : exécuter P3 (9-13) Temps d'attente : P1: 0 P2: 6-1=5 P3: 9-2=7 Moyenne = 4 (identique à FCFS ici car P1 est le plus court au départ) ### 2.3. Gestion de la mémoire La **gestion de la mémoire** vise à allouer de la mémoire aux processus de manière efficace et sécurisée. > **Définition** : La **mémoire virtuelle** permet à chaque processus de disposer d'un espace d'adressage logique indépendant, plus grand que la mémoire physique disponible, grâce à la pagination et à l'échange sur disque. **Mécanismes** : - **Pagination** : division de la mémoire en blocs de taille fixe appelés **pages** (généralement 4 Ko). - **Table des pages** : associe chaque numéro de page logique à un numéro de cadre physique (ou indique qu'elle est sur disque). - **TLB (Translation Lookaside Buffer)** : cache matériel pour accélérer la traduction d'adresses. - **Échange (swapping)** : déplacement d'une page entière entre mémoire vive et disque lorsqu'elle n'est pas utilisée récemment. - **Algorithmes de remplacement de page** : - **FIFO (First In, First Out)** : remplace la page la plus ancienne. - **LRU (Least Recently Used)** : remplace la page la moins récemment utilisée (optimal en pratique). - **Optimal** : remplace la page qui ne sera pas utilisée le plus longtemps dans le futur (inutile en pratique car nécessite connaissance du futur). - **Clock** : approximation de LRU avec un bit d'utilisation. **Exemple résolu** : Simulation de FIFO avec 3 cadres et séquence de références : 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 Étapes : - Réf 1 : défaut → charger 1 → [1] - Réf 2 : défaut → charger 2 → [1,2] - Réf 3 : défaut → charger 3 → [1,2,3] - Réf 4 : défaut → remplacer 1 (FIFO) → [4,2,3] - Réf 1 : défaut → remplacer 2 → [4,1,3] - Réf 2 : défaut → remplacer 3 → [4,1,2] - Réf 5 : défaut → remplacer 4 → [5,1,2] - Réf 1 : hit - Réf 2 : hit - Réf 3 : défaut → remplacer 1 → [5,3,2] - Réf 4 : défaut → remplacer 5 → [4,3,2] - Réf 5 : défaut → remplacer 3 → [4,5,2] Total défauts : 9 **Exercice 6** : Répéter l'exercice avec l'algorithme LRU. **Corrigé** : - Réf 1 : défaut → [1] - Réf 2 : défaut → [1,2] - Réf 3 : défaut → [1,2,3] - Réf 4 : défaut → remplacer 1 (LRU) → [2,3,4] - Réf 1 : défaut → remplacer 2 → [3,4,1] - Réf 2 : défaut → remplacer 3 → [4,1,2] - Réf 5 : défaut → remplacer 4 → [1,2,5] - Réf 1 : hit → [2,5,1] (mettre à jour l'usage) - Réf 2 : hit → [5,1,2] - Réf 3 : défaut → remplacer 5 → [1,2,3] - Réf 4 : défaut → remplacer 1 → [2,3,4] - Réf 5 : défaut → remplacer 2 → [3,4,5] Total défauts : 9 (dans ce cas, même résultat que FIFO) ### 2.4. Logiciels système et appels système Les **appels système (system calls)** sont l'interface par laquelle un programme utilisateur demande un service au noyau du SE. > **Définition** : Un appel système provoque une interruption logicielle qui bascule le processeur du mode utilisateur au mode noyau, permettant l'exécution sécurisée de privilégiés. **Exemples courants (POSIX/Linux)** : - `fork()` : crée un nouveau processus (copie du processus appelant). - `exec()` : remplace l'image mémoire du processus par un nouveau programme. - `wait()` : attend la fin d'un processus fils. - `open()`, `read()`, `write()`, `close()` : opérations sur les fichiers. - `malloc()` : bien que souvent perçu comme une fonction de bibliothèque, il utilise finalement `brk()` ou `mmap()` (appels système) pour obtenir de la mémoire du noyau. - `signal()` : gère les signaux (ex: SIGINT, SIGTERM). **Différence entre appel système et fonction de bibliothèque** : - Une **fonction de bibliothèque** (ex: `printf()`) peut exécuter du code en mode utilisateur avant d'effectuer un appel système. - Un **appel système** est toujours une transition vers le noyau et implique un coût de contexte plus élevé. **Exemple résolu** : Utilisation de `fork()` et `exec()` pour exécuter une commande externe. ```c #include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid == 0) { // Processus fils execlp("ls", "ls", "-l", NULL); perror("execlp failed"); // Seulement si execlp échoue exit(EXIT_FAILURE); } else if (pid > 0) { // Processus père wait(NULL); // Attend la fin du fils printf("Child process completed.\n"); } else { perror("fork failed"); exit(EXIT_FAILURE); } return 0; } ``` **Exercice 7** : Modifier l'exemple ci-dessus pour exécuter `ls -la /tmp` au lieu de `ls -l`. **Corrigé** : Remplacer `execlp("ls", "ls", "-l", NULL);` par `execlp("ls", "ls", "-la", "/tmp", NULL);` --- ## III. Hands-on C Project: Data Indexing & Search Module ### Objectif du projet Développer un module en C permettant d'indexer un ensemble de documents texte (fichiers .txt) et d'effectuer des recherches rapides par mot-clé, en utilisant une table de hachage (hash table) associée à des listes simplement chaînées pour gérer les collisions. ### Spécifications - Lire tous les fichiers .txt d'un répertoire spécifié. - Pour chaque mot (séparé par des espaces, ponctuation, retour ligne), enregistrer : - Le mot lui-même (en minuscules). - Le nom du fichier où il apparaît. - Le numéro de ligne dans le fichier. - Construire un index : table de hachage où chaque entrée pointe vers une liste chaînée d'occurrences. - Permettre à l'utilisateur de saisir un mot-clé et afficher toutes les occurrences (fichier + ligne). - Gérer la mémoire dynamiquement : libérer toutes les allocations à la fin. - Ignorer les mots vides (stop words) courants : "the", "and", "or", "but", "in", "on", "at", "to", "a", "an". ### Structure de données proposée ```c #define TABLE_SIZE 101 typedef struct Occurrence { char* filename; int line_number; struct Occurrence* next; } Occurrence; typedef struct HashNode { char* word; Occurrence* occurrences; struct HashNode* next; // pour gérer les collisions dans la table de hachage } HashNode; typedef struct { HashNode* table[TABLE_SIZE]; } HashTable; ``` ### Fonctions clés 1. **`unsigned int hash(const char* str)`** : fonction de hachage simple (ex: somme des codes ASCII modulo TABLE_SIZE). 2. **`void toLowerCase(char* str)`** : convertit une chaîne en minuscules. 3. **`int isStopWord(const char* word)`** : vérifie si le mot est dans la liste des mots vides. 4. **`void insertWord(HashTable* ht, const char* word, const char* filename, int line_num)`** : - Calcule l'index de hachage. - Recherche si le mot existe déjà dans la chaîne à cet index. - Si oui : ajoute une nouvelle occurrence à la liste des occurrences du mot. - Si non : crée un nouveau nœud HashNode, l'insère en tête de la chaîne, puis ajoute l'occurrence. 5. **`void searchWord(HashTable* ht, const char* word)`** : - Calcule l'index de hachage. - Parcourt la chaîne à cet index pour trouver le mot. - Si trouvé : affiche toutes les occurrences (fichier, ligne). - Sinon : affiche "Mot non trouvé". 6. **`void freeHashTable(HashTable* ht)`** : libère récursivement toute la mémoire allouée (mots, occurrences, nœuds). ### Étapes d'implémentation 1. Parcourir le répertoire avec `opendir()`, `readdir()`, `closedir()`. 2. Pour chaque fichier .txt : - L'ouvrir avec `fopen()`. - Lire ligne par ligne avec `fgets()`. - Pour chaque ligne : - Extraire les mots (en utilisant `strtok()` ou parsing manuel). - Pour chaque mot : - Appliquer `toLowerCase()`. - Vérifier `isStopWord()`. - Si valide : appeler `insertWord()`. 3. Après l'indexation : - Boucle de recherche : demander un mot à l'utilisateur, appeler `searchWord()`, répéter jusqu'à saisie de "exit". 4. Libérer la table de hachage avec `freeHashTable()`. ### Exemple résolu (extrait) Fichier `doc1.txt` : ``` Hello world This is a test ``` Après traitement : - "hello" → fichier: doc1.txt, ligne: 1 - "world" → fichier: doc1.txt, ligne: 1 - "this" → fichier: doc1.txt, ligne: 2 - "is" → stop word → ignoré - "a" → stop word → ignoré - "test" → fichier: doc1.txt, ligne: 2 Recherche de "world" : ``` Found in: doc1.txt (line 1) ``` ### Exercice 8** : Écrire la fonction `toLowerCase(char* str)` en C. **Corrigé** : ```c void toLowerCase(char* str) { for (int i = 0; str[i]; i++) { if (str[i] >= 'A' && str[i] <= 'Z') str[i] = str[i] + ('a' - 'A'); } } ``` ### Exercice 9** : Écrire la fonction `isStopWord(const char* word)` en utilisant un tableau statique de mots vides. **Corrigé** : ```c int isStopWord(const char* word) { const char* stop_words[] = {"the", "and", "or", "but", "in", "on", "at", "to", "a", "an"}; int n = sizeof(stop_words) / sizeof(stop_words[0]); for (int i = 0; i < n; i++) { if (strcmp(word, stop_words[i]) == 0) return 1; } return 0; } ``` ### Exercice 10** : Écrire la fonction `insertWord()` en détail. **Corrigé** : ```c void insertWord(HashTable* ht, const char* word, const char* filename, int line_num) { unsigned int index = hash(word) % TABLE_SIZE; HashNode* current = ht->table[index]; // Rechercher si le mot existe déjà while (current != NULL) { if (strcmp(current->word, word) == 0) { // Mot trouvé : ajouter une occurrence Occurrence* new_occ = (Occurrence*)malloc(sizeof(Occurrence)); new_occ->filename = strdup(filename); new_occ->line_number = line_num; new_occ->next = current->occurrences; current->occurrences = new_occ; return; } current = current->next; } // Mot non trouvé : créer un nouveau nœud HashNode* new_node = (HashNode*)malloc(sizeof(HashNode)); new_node->word = strdup(word); new_node->occurrences = NULL; new_node->next = ht->table[index]; ht->table[index] = new_node; // Ajouter la première occurrence Occurrence* new_occ = (Occurrence*)malloc(sizeof(Occurrence)); new_occ->filename = strdup(filename); new_occ->line_number = line_num; new_occ->next = NULL; new_node->occurrences = new_occ; } ``` --- ## IV. Hands-on OS Project: CPU & Memory Allocation Simulator ### Objectif du projet Simuler les comportements d'un noyau de système d'exploitation concernant : 1. La **planification du CPU** avec l'algorithme Round Robin (RR). 2. La **gestion de la mémoire** avec l'algorithme de remplacement de page LRU (Least Recently Used). Le simulateur lit une séquence d'instructions provenant d'un fichier d'entrée et affiche l'état du système à chaque étape. ### Spécifications d'entrée Fichier d'entrée (`input.txt`) contenant des lignes de la forme : `<timestamp> <process_id> <instruction_type> <address>` - `timestamp` : entier, temps auquel l'instruction est émise (non nécessairement séquentiel). - `process_id` : entier identifiant le processus (0, 1, 2, ...). - `instruction_type` : soit `CPU` (instruction de calcul), soit `MEM` (accès mémoire à `address`). - `address` : adresse logique entière (pour les instructions MEM uniquement). ### Comportement attendu - À chaque unité de temps (à partir de t=0), le simulateur : 1. Active tous les processus dont le `timestamp` <= temps courant et qui ne sont pas encore terminés. 2. Applique le planificateur RR avec un quantum fixe (ex: q=2 unités de temps). 3. Pour le processus sélectionné : - Si l'instruction suivante est `CPU` : l'exécuter pendant 1 unité de temps (décrémenter son compteur d'instructions CPU restantes). - Si l'instruction suivante est `MEM` : - Traduire l'adresse logique en adresse physique via la table des pages (simulée). - Vérifier si la page est en mémoire (hit) ou sur disque (fault). - En cas de fault : charger la page depuis le disque, appliquer LRU pour choisir une page à évincer si la mémoire est pleine. - Mettre à jour l'historique d'utilisation (pour LRU). 4. Afficher l'état : temps courant, processus en cours d'exécution, file prête, état de la mémoire (cadres occupés, pages présentes), nombre de faults. 5. Passer au temps suivant. ### Structures de données ```c #define MAX_PROCESSES 10 #define MEMORY_FRAMES 3 #define PAGE_SIZE 1024 // taille logique d'une page (ex: adresses 0-1023 = page 0) typedef struct { int pid; int remaining_cpu_time; // temps CPU restant pour l'instruction courante int next_instruction_index; // index dans la liste d'instructions du processus int is_complete; } Process; typedef struct { int page_number; // numéro de page logique int frame_number; // numéro de cadre physique (-1 si pas en mémoire) int last_used; // timestamp d'utilisation récente (pour LRU) } PageTableEntry; typedef struct { int frame_number; int page_number; // page logique actuellement chargée dans ce cadre int last_used; // pour LRU } MemoryFrame; ``` ### Algorithmes - **Planification RR** : - File prête (FIFO) des processus actifs. - Chaque processus sélectionné s'exécute pendant min(quantum, temps CPU restant de l'instruction courante). - Après expiration du quantum ou fin de l'instruction, il est remis à la fin de la file s'il n'est pas terminé. - **Remplacement de page LRU** : - Chaque accès à une page met à jour son champ `last_used` au timestamp courant. - En cas de fault et mémoire pleine : remplacer la page avec la plus petite valeur de `last_used` (la moins récemment utilisée). ### Étapes d'implémentation 1. Lire le fichier d'entrée et construire pour chaque processus une liste d'instructions (type, adresse si MEM). 2. Initialiser : - Tous les processus à l'état non actif. - Table des pages vide pour chaque processus (toutes les entrées pointent vers -1). - Mémoire physique : tous les cadres libres. - Temps courant = 0. 3. Boucle principale jusqu'à ce que tous les processus soient terminés : - Activer les nouveaux processus (timestamp <= temps courant). - Si la file prête est vide : avancer le temps au prochain timestamp d'arrivée. - Sinon : - Défilez le premier processus de la file prête. - Déterminez le temps d'exécution : `time_slice = min(quantum, remaining_cpu_time)` si CPU, ou 1 unité si MEM (on suppose qu'une accès mémoire prend 1 unité). - Exécuter l'instruction : - Si CPU : décrémenter `remaining_cpu_time` de `time_slice`. - Si MEM : - Calculer numéro de page : `page = address / PAGE_SIZE`. - Vérifier la table des pages du processus pour cette page. - Si `frame_number == -1` : fault → - Si un cadre libre existe : le prendre. - Sinon : choisir le cadre avec le plus petit `last_used` (LRU), sauvegarder sa page si nécessaire (on ignore l'écriture ici), charger la nouvelle page. - Mettre à jour `last_used` de la page et du cadre au timestamp courant. - Mettre à jour la table des pages. - Après exécution : - Si l'instruction courante est terminée (remaining_cpu_time == 0 pour CPU, ou après 1 unité pour MEM) : - Passer à l'instruction suivante du processus. - Si aucune instruction restante : marquer le processus comme terminé. - Sinon : remettre le processus en file prête avec son temps restant. - Afficher l'état. - Incrémenter le temps courant de 1 (ou de `time_slice` si on veut optimiser, mais pour simplifier on fait pas à pas). ### Exemple résolu (extrait) Fichier `input.txt` : ``` 0 0 CPU 0 0 0 MEM 500 1 1 CPU 0 1 1 MEM 2000 2 0 CPU 0 ``` PAGE_SIZE = 1024 → pages : - 500 → page 0 - 2000 → page 1 (puisque 2000/1024 = 1) Supposons quantum = 2, MEMORY_FRAMES = 2. **Temps t=0** : - Activer P0 (timestamp 0). - File prête : [P0] - P0 sélectionné : instruction CPU (temps restant CPU = on suppose 1 unité par instruction CPU pour simplifier → disons que chaque ligne "CPU" = 1 unité CPU). - Exécuter 1 unité de CPU (quantum=2, mais on ne prend que ce qui est nécessaire). - Après : P0 a encore une instruction MEM à faire. - Remettre P0 en file prête. - État : temps=0, P0 en cours, file prête=[P0], mémoire vide, faults=0. **Temps t=1** : - Aucun nouveau processus (timestamp suivant à 1 pour P1). - Activer P1 (timestamp 1 <= 1). - File prête : [P0, P1] - Sélectionner P0 : prochaine instruction MEM 500 → page 0. - Page 0 non en mémoire → fault. - Charger page 0 dans cadre 0 (libre). - Mettre à jour last_used de cadre 0 et page 0 de P0 à t=1. - faults=1. - Après exécution : P0 terminé (plus d'instructions). - État : temps=1, P0 terminé, file prête=[P1], mémoire: cadre0=page0(P0), faults=1. **Temps t=2** : - Activer P2? Non, pas de P2. - File prête : [P1] - Sélectionner P1 : instruction CPU → exécuter 1 unité. - Après : P1 a encore instruction MEM. - Remettre P1 en file prête. - État : temps=2, P1 en cours, file prête=[P1], mémoire inchangée, faults=1. **Temps t=3** : - File prête : [P1] - Sélectionner P1 : instruction MEM 2000 → page 1. - Page 1 non en mémoire → fault. - Charger page 1 dans cadre 1 (libre). - Mettre à jour last_used de cadre 1 et page 1 de P1 à t=3. - faults=2. - Après : P1 terminé. - État : temps=3, P1 terminé, file prête=[], mémoire: cadre0=page0(P0), cadre1=page1(P1), faults=2. **Temps t=4+** : aucun processus actif → fin. ### Exercice 11** : Écrire la fonction qui calcule le numéro de page à partir d'une adresse logique. **Corrigé** : ```c int getPageNumber(int address, int page_size) { return address / page_size; } ``` ### Exercice 12** : Écrire la fonction qui sélectionne le cadre à remplacer en utilisant LRU (le cadre avec la plus petite valeur de `last_used`). **Corrigé** : ```c int selectLRUFrame(MemoryFrame frames[], int num_frames) { int lru_index = 0; for (int i = 1; i < num_frames; i++) { if (frames[i].last_used < frames[lru_index].last_used) { lru_index = i; } } return lru_index; } ``` ### Exercice 13** : Dans le simulateur, après un accès mémoire réussi (hit), que faut-il mettre à jour pour maintenir la propriété LRU ? **Corrigé** : Il faut mettre à jour le champ `last_used` de la page accédée (dans la table des pages) et du cadre mémoire où elle réside, en le définissant égal au timestamp courant. ### Exercice 14** : Pourquoi utilise-t-on un timestamp global dans le simulateur plutôt que le temps d'exécution réel de chaque processus ? **Corrigé** : Le timestamp global permet de synchroniser l'état de tous les processus et de la mémoire selon un temps logique commun, essentiel pour déterminer lequel des processus est arrivé (timestamp <= temps courant) et pour appliquer de manière cohérente l'algorithme LRU basé sur la récence d'accès. ---

Quiz du chapitre

12 questions — vérifie chaque réponse pour voir l'explication.

  1. 1. Which of the following best describes the time complexity of binary search on a sorted array of size n?
  2. 2. In a singly linked list, what is the time complexity to insert a node at the beginning?
  3. 3. Which data structure is most appropriate for implementing a breadth-first search (BFS) algorithm?
  4. 4. What is the primary purpose of virtual memory in an operating system?
  5. 5. A programmer writes a function that recursively calculates the nth Fibonacci number without memoization. What is the time complexity of this approach?
  6. 6. In the context of operating systems, what does a 'context switch' involve?
  7. 7. Which of the following statements about hash tables is TRUE when using chaining for collision resolution?
  8. 8. Consider a binary search tree (BST) where all nodes have only left children. What is the shape and height of this tree?
  9. 9. In a paged virtual memory system, what happens when a process accesses a page that is not currently in physical memory?
  10. 10. Which of the following best describes the difference between a mutex and a semaphore in process synchronization?
  11. 11. A hash function h(k) = k mod 10 is used to insert keys 15, 25, 35, and 45 into a hash table of size 10 using chaining. What is the length of the chain at index 5?
  12. 12. In a multi-level feedback queue (MLFQ) scheduler, what is the primary goal of demoting a process to a lower-priority queue after it uses its full time slice?