Préalable

Le rendu des TP se fera via GitHub ou GitLab. Avant de commencer les TP/TD, vous devrez donc impérativement suivre les étapes décrites en introduction de ce cours. Même si ce TD n’est, en principe, pas noté, la configuration initiale fait partie du travail demandé. Afin de préparer la mise en place des TP suivants et de ne pas perdre de temps. Normalement vous avez déjà vue cela, donc pas d’excuse. Une fois votre dépôt créé et avant d’ajouter le répertoire pour le premier TP, créez un fichier .gitignore à la racine de votre dépôt contenant *.class et *.jar.

echo "*.class" >> .gitignore
echo "*.jar" >> .gitignore
echo "*.o" >> .gitignore
echo "*.obj" >> .gitignore
echo "*.a" >> .gitignore

Ce fichier .gitignore doit donc se trouver à la racine de votre dépôt, valable pour tous les TPs. Une fois les dépôts Git (local et distant) créés, vous pouvez passer à la suite :

Le répertoire de ce TP dans votre dépôt s’appellera “SystemeFichiersVirtuel”.

mkdir SystemeFichiersVirtuel

Créez ensuite un fichier .gitignore dans ce répertoire, contenant le nom des fichiers compilés et exécutables.

Organisation attendue

OS_Nom1_Nom2/
├─ .gitignore                # général (*.class, *.jar, etc.)
├─ README.md                 # informations additionnelles
└─ SystemeFichiersVirtuel/
   ├─ .gitignore             # spécifique à ce TP
   ├─ MemoryManager.java
   ├─ Inode.java
   ├─ Directory.java
   ├─ VirtualFileSystem.java
   ├─ FileSystemUtils.java
   ├─ Utils.java
   └─ Main.java

Ce qui n’est pas poussé sur votre dépôt GitLab n’est pas corrigé. Pensez donc bien à effectuer des commit et des push régulièrement. Afin de comprendre les mécanismes fondamentaux des systèmes de fichiers, vous allez implémenter un système de fichiers virtuel entièrement géré en mémoire RAM dans un espace fixe de 1 Mo.

Introduction aux systèmes de fichiers

Un système de fichiers est une méthode d’organisation, de stockage et de récupération de données sur un support de stockage. Dans ce TP, nous allons créer un système de fichiers virtuel (VFS) qui simule le fonctionnement d’un système de fichiers réel, mais entièrement en mémoire RAM. Cette mémoire peut néanmoins être sauvegardée dans un fichier, voir TP précédent. Vous allez comprendre et mettre en pratique, comment un ordinateur organise et stocke les fichiers, mais de manière simplifiée.

Concepts fondamentaux

Blocs de données : L’unité de base d’allocation dans notre système de fichiers. Nous utiliserons des blocs de 512 octets, similaires aux secteurs d’un disque dur traditionnel. Avec 1 Mo de mémoire disponible, nous disposons de 2048 blocs (1 048 576 ÷ 512 = 2048).

Inodes (Index Nodes) : Structure de données qui contient les métadonnées d’un fichier : taille, permissions, horodatage, et pointeurs vers les blocs de données. Chaque fichier et répertoire possède un inode unique identifié par un numéro.

Table d’allocation : Bitmap qui indique quels blocs sont libres ou occupés. Essentielle pour éviter les conflits lors de l’allocation de nouveaux blocs.

Répertoires : Fichiers spéciaux contenant une liste d’entrées, chaque entrée associant un nom de fichier à un numéro d’inode.

La figure ci-dessous illustre l’organisation générale de notre système :

┌─────────────────────────────────────────────────────────────┐
│                    MÉMOIRE VIRTUELLE 1 Mo                   │
├─────────────────────────────────────────────────────────────┤
│ Superbloc (1 bloc)    │ Bitmap allocation (1 bloc)          │
├─────────────────────────────────────────────────────────────┤
│ Table des inodes (127 blocs) │ Données (1919 blocs)         │
└─────────────────────────────────────────────────────────────┘

CONTRAINTE MÉMOIRE CRITIQUE : Contrairement aux exemples classiques qui utilisent HashMap, ArrayList, BitSet de Java, TOUTES nos structures doivent être stockées dans le tableau byte[] de 1Mo. Aucune allocation mémoire externe n’est autorisée. Cela impose une gestion manuelle des offsets et la sérialisation/désérialisation de toutes les structures.

Dit autrement : la mémoire est géré comme un classeur

  1. Les pages = Blocs de données (512 octets chacun)
    • Votre classeur a exactement 2048 pages de 512 octets chacune
    • Total = 1 Mo de mémoire
  2. L’index = Table des Inodes
    • Comme l’index d’un livre : “Le document X se trouve aux pages 15, 16, 17”
    • Chaque fichier a une “fiche” (inode) qui dit où il est stocké
  3. Le plan d’occupation = Bitmap
    • Une liste qui dit : “page 1: occupée, page 2: libre, page 3: occupée…”

Comparaison avec les systèmes réels

En comparaison avec EXT4 : Notre système simplifié reprend les concepts fondamentaux d’EXT4 (inodes, blocs, bitmap d’allocation) mais sans les optimisations complexes comme les groupes de blocs, les extents, ou la journalisation.

Attention : Notre système de fichiers virtuel ne gère pas la persistance automatique. Toutes les données sont perdues à la fin du programme sauf si explicitement sauvegardées. C’est une limitation volontaire pour se concentrer sur les algorithmes de base.

Gestion de la mémoire et allocation de blocs

La gestion efficace de la mémoire est cruciale dans un système de fichiers. Notre approche utilise un bitmap pour tracker l’état de chaque bloc, offrant un accès en O(1) pour vérifier la disponibilité. MAIS ce bitmap doit résider dans notre zone mémoire de 1Mo.

Layout mémoire détaillé

Notre espace de 1Mo (1 048 576 octets) est organisé comme suit :

Offset 0-511      : Superbloc (bloc 0)
Offset 512-1023   : Bitmap d'allocation (bloc 1) - 256 octets utiles
Offset 1024-66047 : Table des inodes (blocs 2-128) - 127 blocs = 512 inodes
Offset 66048-...  : Zone de données (blocs 129-2047) - 1919 blocs

En C (approche bas niveau) :

#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define BLOCK_SIZE 512
#define TOTAL_MEMORY (1024 * 1024)  // 1 Mo
#define NUM_BLOCKS (TOTAL_MEMORY / BLOCK_SIZE)  // 2048 blocs
#define BITMAP_SIZE (NUM_BLOCKS / 8)  // 256 octets pour le bitmap

typedef struct {
    // also contain super block data
    uint32_t superblock_offset;   // = 0
    uint32_t bitmap_offset;       // = 512
    uint32_t inode_table_offset;  // = 1024
    uint32_t data_offset;         // = 66048
} FileSystem;

FileSystem* init_filesystem() {
    FileSystem* fs = malloc(TOTAL_MEMORY);
    memset(fs->filesystem_memory, 0, TOTAL_MEMORY);

    fs->superblock_offset = 0;
    fs->bitmap_offset = BLOCK_SIZE;
    fs->inode_table_offset = 2 * BLOCK_SIZE;
    fs->data_offset = 129 * BLOCK_SIZE;

    // Initialiser le superbloc
    write_superblock(fs);

    // Marquer les blocs système comme occupés dans le bitmap
    set_block_used(fs, 0);  // superbloc
    set_block_used(fs, 1);  // bitmap

    for (int i = 2; i < 129; i++)
        set_block_used(fs, i);  // table des inodes

    return fs;
}

void set_block_used(FileSystem* fs, int block_number) {
    int byte_index = block_number / 8;
    int bit_position = block_number % 8;
    uint8_t* bitmap = (uint8_t*)fs + fs->bitmap_offset;
    bitmap[byte_index] |= (1 << bit_position);
}

int is_block_used(FileSystem* fs, int block_number) {
    int byte_index = block_number / 8;
    int bit_position = block_number % 8;
    uint8_t* bitmap = (uint8_t*)fs + fs->bitmap_offset;
    return (bitmap[byte_index] & (1 << bit_position)) != 0;
}

Cette approche en C donne un contrôle total sur la gestion mémoire et respecte notre contrainte de tout stocker dans 1Mo.

En Java a vous de jouer :

import java.io.*;

public class MemoryManager {
    public static final int BLOCK_SIZE = 512;
    public static final int TOTAL_MEMORY = 1024 * 1024; // 1 Mo
    public static final int NUM_BLOCKS = TOTAL_MEMORY / BLOCK_SIZE; // 2048

    // Offsets des différentes zones
    public static final int SUPERBLOCK_OFFSET = 0;
    public static final int BITMAP_OFFSET = BLOCK_SIZE;
    public static final int INODE_TABLE_OFFSET = 2 * BLOCK_SIZE;
    public static final int DATA_OFFSET = 129 * BLOCK_SIZE;

    public static final int INODE_SIZE = 128; // Taille d'un inode en octets
    public static final int INODE_TABLE_SIZE = DATA_OFFSET - INODE_TABLE_OFFSET;
    public static final int MAX_INODES = INODE_TABLE_SIZE / INODE_SIZE; // => 508

    // CONTRAINTE : Un seul tableau pour TOUT le système de fichiers
    private byte[] memory;

    public MemoryManager() {
        this.memory = new byte[TOTAL_MEMORY];
        initializeFilesystem();
    }

    private void initializeFilesystem() {
        // Initialiser le superbloc
        writeSuperblock();

        // Marquer les blocs système comme occupés
        setBlockUsed(0);  // superbloc
        setBlockUsed(1);  // bitmap

        for (int i = 2; i < 129; i++)
            setBlockUsed(i);  // table des inodes
    }

    private void writeSuperblock() {

        // Exemple minimal (tu peux stocker plus d’infos si tu veux)
        String signature = "MYFS1.0";

        //! Complétez la fonction
        // Utiliser System.arraycopy pour stocker MYFS1.0 dans le tableau de 1Mo
        // Sauvegarder les variables du systeme (block size, total memory, etc, max inodes)
        // exemple
        // new byte[] { (byte)(value >>> 24), (byte)(value >>> 16), (byte)(value >>> 8), (byte)value};

        //! correction
        // ÉTAPE 1: Écrire la signature du système
        for (int i = 0; i < Math.min(27, data.length); i++) {
                memory[i] = (byte) signature.charAt(i);
        }

        // byte [] data = signature.getBytes()
        // equivalent System.arraycopy(data, 0, memory, 0, Math.min(27, data.length));

        // ÉTAPE 2: Écrire quelques infos importantes à des positions fixes
        Utils.writeInt(memory, 16, BLOCK_SIZE);     // Position 16: taille des blocs
        Utils.writeInt(memory, 20, TOTAL_MEMORY);   // Position 20: taille totale
        Utils.writeInt(memory, 24, NUM_BLOCKS);     // Position 24: nombre de blocs
        // Possible d'utiliser System.arraycopy
    }

    public boolean setBlockUsed(int blockNumber, boolean used) {
        if (blockNumber < 0 || blockNumber >= NUM_BLOCKS)
            return false;

        int byteIndex = blockNumber / 8;
        int bitPosition = blockNumber % 8;
        int offset = BITMAP_OFFSET + byteIndex;

        // TODO: Complétez cette partie !
        // INDICE: Utilisez les opérations | (OR) et & (AND) avec des masques

        if (used) {
            // TODO: Mettre le bit à 1 (bloc occupé)
        } else {
            // TODO: Mettre le bit à 0 (bloc libre)
        }

        return true;
    }

    public int isBlockUsed(int blockNumber) {
        if (blockNumber < 0 || blockNumber >= NUM_BLOCKS)
                    return -1;

        int byteIndex = blockNumber / 8;
        int bitPosition = blockNumber % 8;
        int offset = BITMAP_OFFSET + byteIndex;

        return (filesystemMemory[offset] >> bitPosition) & 1;
    }

    public int allocateBlock() {
        // TODO: Complétez cette méthode étape par étape
        // ÉTAPE 1: Boucle de la page 129 à la fin (les pages de données)
        // ÉTAPE 2: Pour chaque page, vérifier si elle est libre
        // ÉTAPE 3: Si libre, la marquer comme occupée
        // ÉTAPE 4: Retourner son numéro

        // AIDE: Commencer par cette structure
        /*
        for (int i = 129; i < NUM_BLOCKS; i++) {
            if (isBlockUsed(i) == 0) {  // Bloc libre trouvé !
                // TODO: Le marquer comme occupé
                // TODO: L'annoncer à l'utilisateur
                // TODO: Le retourner
            }
        }
        */
        return -1; // Pas de bloc libre
    }

    public byte[] getFilesystemMemory() {
        return filesystemMemory;
    }

    public void saveToFile() throws IOException {
        // Complété la sauvegarde du system avec FileOutputStream

        //! correction
        FileOutputStream fos = new FileOutputStream("filesystem.img");
        fos.write(memory);
        fos.close();
    }

    public void loadFromFile() throws IOException {
            // Complété la sauvegarde du system avec FileInputStream
        // AIDE: Utilisez FileInputStream
    }
}

Pourquoi cette contrainte mémoire ? Dans un vrai système de fichiers, toutes les structures sont stockées sur le disque dans des zones bien définies. En imposant cette contrainte, nous simulons fidèlement le comportement réel où chaque bit de métadonnée a un coût en espace et doit être optimisé.

Question : Avec 10 pointeurs directs par inode, quelle est la taille maximale d’un fichier? Comment pourrait-on l’augmenter?

Question 1: Pourquoi utilise-t-on des bits plutôt que des octets entiers pour le bitmap ?
Question 2: Combien d’octets faut-il pour stocker l’état de 2048 blocs ?
Question 3: Que se passe-t-il si on essaie d’accéder au bloc 3000 ?

Gestion mémoire manuelle

L’inode (Index Node) est le cœur de notre système de fichiers. Contrainte critique : chaque inode doit être sérialisé dans notre zone mémoire de 1Mo.

Structure mémoire d’un Inode (128 octets)

Offset 0-3   : Numéro d'inode (int)
Offset 4-7   : Type de fichier (int) - 0=fichier, 1=répertoire
Offset 8-11  : Taille du fichier (int)
Offset 12-19 : Timestamp création (long)
Offset 20-27 : Timestamp modification (long)
Offset 28-67 : Pointeurs directs (10 × int = 40 octets)
Offset 68-71 : Pointeur indirect (int)
Offset 72-73 : Permissions (short)
Offset 74-127: Réservé pour extensions futures (54 octets)
public class Inode {
    private MemoryManager memoryManager;
    private int inodeNumber;

    // Taille fixe d'un inode en mémoire
    public static final int INODE_SIZE = 128;
    public static final int DIRECT_POINTERS = 10;

    public Inode(MemoryManager memoryManager, int inodeNumber) {
        this.memoryManager = memoryManager;
        this.inodeNumber = inodeNumber;
    }

    private int getInodeOffset() {
        return MemoryManager.INODE_TABLE_OFFSET + (inodeNumber * INODE_SIZE);
    }

    // QUESTION 3: Pourquoi calcule-t-on un offset ?
    public void writeToMemory(int fileType, int fileSize, long creationTime, 
                             long modificationTime, int[] directPointers, 
                             int indirectPointer, short permissions, int linkCount) {
        byte[] memory = memoryManager.getFilesystemMemory();
        int offset = getInodeOffset();

        // Écrire le numéro d'inode
        Utils.writeInt(memory, offset, inodeNumber);
        offset += 4;  // Avancer de 4 octets
                // Il exist une meilleur solution, vérifier la définition de writeInt

                // TODO: Complétez les étapes suivantes !
        // Écrire le type de fichier
        // Écrire la taille
        // Écrire les timestamps
        // Écrire les pointeurs directs
        for (int i = 0; i < DIRECT_POINTERS; i++) {
        }
        // Écrire le pointeur indirect
        // Écrire les permissions
        // Écrire le nombre de liens
    }

    public int getFileType() {
        byte[] memory = memoryManager.getFilesystemMemory();
        int offset = getInodeOffset(); // Skip inode number
        return readInt(memory, offset);
    }

    public int getFileSize() {
        byte[] memory = memoryManager.getFilesystemMemory();
        int offset = getInodeOffset() + 8; // Skip inode number + file type
        return readInt(memory, offset);
    }

    public void setFileSize(int newSize) {
        byte[] memory = memoryManager.getFilesystemMemory();
        int offset = getInodeOffset(); // Skip ...
        writeInt(memory, offset, newSize);
        // Mettre à jour le timestamp de modification
    }

    public int[] getDirectPointers() {
        byte[] memory = memoryManager.getFilesystemMemory();
        int offset = getInodeOffset(); // Skip header fields
        int[] pointers = new int[DIRECT_POINTERS];

        for (int i = 0; i < DIRECT_POINTERS; i++) {
            // pointers[i] = ?
            // offset += ?;
        }
        return pointers;
    }
}

Pourquoi 128 octets par inode ? Cette taille permet de stocker 512 inodes dans 127 blocs (512 × 128 = 65536 octets = 128 blocs). C’est un bon compromis entre le nombre de fichiers supportés et l’espace occupé par les métadonnées.

{{info Question 3: Pourquoi calcule-t-on un offset pour chaque inode ?
Question 4: Combien de fichiers maximum peut-on créer ?
Question 5: Quelle est la taille maximum d’un fichier avec 10 pointeurs ?

}}}

Fragmentation

Dans un système de fichiers, il existe deux grands types de fragmentation : externe et interne.

Fragmentation externe : L’espace libre sur le disque (ou en mémoire dans le VFS) finit par être découpé en beaucoup de petits bloques disséminés en fonction des allocations et désallocations. Quand on veut enregistrer un gros fichier, il ne peut pas rentrer dans un seul espace contigu : il doit être réparti dans plusieurs blocs, là où il y a de la place. Cela oblige à découper le fichier en plusieurs morceaux (blocs non contigus) et permet de géré l’espace éfficassement. Mais cause du ralentissement en lecture et écriture.

Exemple simplifié : Après plusieurs créations/suppressions on pourrais ce retrouvé avec ces espaces :

  • Bloc 0-129 : occupé par le VFS
  • Bloc 130 : occupé
  • Bloc 131 : libre
  • Bloc 132 : occupé
  • Bloc 133 : libre
  • Bloc 134 : libre
  • Bloc 135 : occupé

Si un fichier de 3 blocs dois être stocké, il aura obligatoirement les blocs 1, 3 et 4.

Fragmentation interne : Cela arrive quand les blocs alloués à un fichier ne sont pas entièrement remplis, par exemple chaque bloc fait 512 octets mais le fichier ne finit pas pile sur cette taille. Le “reste” à la fin du fichier (dans le dernier bloc) est de la mémoire perdue. C’est donc de l’espace gaspillé à l’intérieur d’un bloc réservé et invisible pour le gestionnaire du système de fichiers.

Le VFS ne peut pas “voir” la fragmentation interne (ce qu’il gaspille dans chaque bloc) : il ne peut mesurer que la fragmentation externe (= comment sont répartis les blocs libres/occupés).

Gestionnaire du système de fichiers

La classe VirtualFileSystem constitue l’interface principale entre l’utilisateur et notre système de fichiers. Elle coordonne les interactions entre les différents composants tout en maintenant la cohérence des métadonnées.

Parallèle avec les systèmes réels : Cette classe joue un rôle similaire au VFS (Virtual File System) de Linux, qui fournit une interface unifiée pour différents systèmes de fichiers sous-jacents. Dans notre cas, nous n’avons qu’un seul système, mais l’architecture reste la même.

import java.util.*;

public class VirtualFileSystem {
    private MemoryManager memoryManager;

    public VirtualFileSystem() {
        this.memoryManager = new MemoryManager();
    }

    // TROUVER UN INODE LIBRE
    private int allocateInode() {
                byte[] memory = memoryManager.getFilesystemMemory();
        // TODO: Complétez cette méthode étape par étape

        // ÉTAPE 1: Parcourir tous les inodes possibles (0 à MAX_INODES)
        // ÉTAPE 2: Pour chaque position, lire le numéro d'inode stocké
        // ÉTAPE 3: Si le numéro ne correspond pas à la position, l'inode est libre
        // ÉTAPE 4: Retourner le numéro de l'inode libre trouvé

        /* AIDE: Structure de base
        for (int i = 0; i < MemoryManager.MAX_INODES; i++) {
            int offset = 0; // MemoryManager.INODE_TABLE_OFFSET  Inode.INODE_SIZE;
            int storedInodeNumber = 0;

            if (storedInodeNumber != i) {
                System.out.println("Inode libre trouvé: " + i);
                return i;
            }
        }
        */

        return -1; // Aucun inode libre
    }

    public boolean createFile(String directory, String filename) {
        System.out.println("\n=== Création du fichier: " + filename + " ===");

        // ÉTAPE 1: Trouver un inode libre
        int inodeNum = allocateInode();
        if (inodeNum == -1) {
            System.err.println("Impossible de créer " + filename + " - Plus d'inodes !");
            return false;
        }

        // ÉTAPE 2: Créer l'objet inode
        Inode inode = new Inode(memoryManager, inodeNum);
        long now = System.currentTimeMillis();

        // ÉTAPE 3: Initialiser l'inode avec des valeurs par défaut
        int[] emptyPointers = new int[Inode.DIRECT_POINTERS]; // Tous à 0

        // TODO: Complétez cet appel !
        // inode.writeToMemory(  ....  );

        System.out.println("Fichier " + filename + " créé avec l'inode " + inodeNum);
        return true;
    }

    // 📋 LISTER TOUS LES FICHIERS (= nos inodes utilisés)
    public List<Integer> getRootDirectory() {
                byte[] memory = memoryManager.getFilesystemMemory();
        List<Integer> usedInodes = new ArrayList<>();

        // TODO: Complétez cette méthode !
        // Parcourir tous les inodes et ajouter ceux qui sont utilisés à la liste

        /* AIDE:
        for (int i = 0; i < MemoryManager.MAX_INODES; i++) {
            int offset = 0; // MemoryManager.INODE_TABLE_OFFSET Inode.INODE_SIZE
            int storedInodeNumber = Utils.readInt(memory, offset);

            // Si le numéro correspond à la position, l'inode est utilisé
            if (storedInodeNumber == i) {
                usedInodes.add(i);
            }
        }
        */

        System.out.println("" + usedInodes.size() + " fichier(s) trouvé(s)");
        return usedInodes;
    }

    // TROUVER UN FICHIER PAR "NOM" (version ultra-simplifiée)
    private int findInodeByName(String directory, String filename) {
        System.out.println("🔍 Recherche du fichier: " + filename);

        // On prend le dernier inode créé (pour les tests)
        List<Integer> inodes = getRootDirectory();

        if (!inodes.isEmpty()) {
            int lastInode = inodes.get(inodes.size() - 1);
            System.out.println("Fichier trouvé à l'inode: " + lastInode);
            return lastInode;
        }

        System.err.println("Fichier " + filename + " non trouvé");
        return -1;
    }

    // MÉTHODES UTILITAIRES
    public MemoryManager getMemoryManager() {
        return memoryManager;
    }

    public int getUsedInodesCount() {
        return getRootDirectory().size();
    }
}

Écriture et lecture de données

Vous allez maintenant compléter des méthodes essentielles d’un système de fichiers virtuel. Ces méthodes permettent d’écrire, de lire et de supprimer des fichiers. Chaque opération correspond à un enchaînement d’étapes importantes que vous devrez analyser. L’écriture de données dans notre système de fichiers nécessite l’allocation de blocs dans notre zone mémoire de 1Mo et la mise à jour des pointeurs dans l’inode correspondant.

public boolean writeFile(String directory, String filename, byte[] data) {
    System.out.println("\n=== Écriture de " + filename + " (" + data.length + " octets) ===");

    // ÉTAPE 1: Trouver le fichier
    int inodeNum = findInodeByName(directory, filename);
    if (inodeNum == -1) {
        System.err.println("Fichier " + filename + " non trouvé !");
        return false;
    }

    // ÉTAPE 2: Calculer combien de blocs il nous faut
    int blocksNeeded = (data.length + MemoryManager.BLOCK_SIZE - 1) / MemoryManager.BLOCK_SIZE;
    System.out.println("Blocs nécessaires: " + blocksNeeded);

    if (blocksNeeded > Inode.DIRECT_POINTERS) {
        System.err.println("Fichier trop volumineux ! Max: " + 
                          (Inode.DIRECT_POINTERS * MemoryManager.BLOCK_SIZE) + " octets");
        return false;
    }

    // ÉTAPE 3: Allouer les blocs nécessaires
    int[] blockPointers = new int[Inode.DIRECT_POINTERS];
    System.out.println("Allocation des blocs...");

    // TODO: Complétez cette boucle !
    /*
    for (int i = 0; i < blocksNeeded; i++) {
    }
    */

    // ÉTAPE 4: Écrire les données dans les blocs
    System.out.println("Écriture des données...");
    byte[] memory = memoryManager.getFilesystemMemory();
    int dataOffset = 0; // Position dans notre fichier

    // TODO: Complétez cette boucle d'écriture !
    /*
    for (int i = 0; i < blocksNeeded; i++) {
    }
    */

    // ÉTAPE 5: Mettre à jour l'inode
    Inode inode = new Inode(memoryManager, inodeNum);
    long now = System.currentTimeMillis();

        // TODO
    // inode.writeToMemory( ...  );

    System.out.println("Fichier " + filename + " écrit avec succès !");
    return true;
}


// LIRE UN FICHIER
public byte[] readFile(String directory, String filename) {
    System.out.println("\n=== Lecture de " + filename + " ===");

    // ÉTAPE 1: Trouver le fichier
    int inodeNum = findInodeByName(directory, filename);
    if (inodeNum == -1) {
        return null;
    }

    // ÉTAPE 2: Lire les infos du fichier
    Inode inode = new Inode(memoryManager, inodeNum);
    int fileSize = inode.getFileSize();
    System.out.println("Taille du fichier: " + fileSize + " octets");

    if (fileSize == 0) {
        System.out.println("Fichier vide");
        return new byte[0];
    }

    // ÉTAPE 3: Préparer le buffer de lecture
    byte[] fileData = new byte[fileSize];
    byte[] memory = memoryManager.getFilesystemMemory();

    // ÉTAPE 4: Obtenir les pointeurs vers les blocs
    int[] blockPointers = inode.getDirectPointers();
    int dataOffset = 0;
    int blocksToRead = (fileSize + MemoryManager.BLOCK_SIZE - 1) / MemoryManager.BLOCK_SIZE;

    System.out.println("Lecture de " + blocksToRead + " bloc(s)...");

    // TODO: Complétez cette boucle de lecture !
    /*
    for (int i = 0; i < blocksToRead && dataOffset < fileSize; i++) {
        if (blockPointers[i] == 0) {
            System.err.println("Pointeur de bloc manquant !");
            break;
        }
    }
    */

    return fileData;
}

// SUPPRIMER UN FICHIER
public boolean deleteFile(String directory, String filename) {
    // TODO: Complétez cette méthode !
    // ÉTAPE 1: Trouver le fichier
    // ÉTAPE 2: Libérer tous ses blocs
    // ÉTAPE 3: Marquer l'inode comme libre

    System.out.println("Fichier " + filename + " supprimé !");
    return true;
}

Statistiques et analyse du système

public class FileSystemUtils {

    public static void displayStats(VirtualFileSystem vfs) {
        MemoryManager mm = vfs.getMemoryManager();

        System.out.println("=== Statistiques du système de fichiers ===");
        System.out.println("Taille totale : " + (MemoryManager.TOTAL_MEMORY / 1024) + " Ko");
        System.out.println("Nombre total de blocs : " + MemoryManager.NUM_BLOCKS);

        // Compter les blocs libres
        int freeBlocks = 0;
        for (int i = 129; i < MemoryManager.NUM_BLOCKS; i++) { // Commencer après les blocs système
            if (!mm.isBlockUsed(i))
                freeBlocks++;
        }

        int usedBlocks = MemoryManager.NUM_BLOCKS - freeBlocks;
        System.out.println("Blocs libres : " + freeBlocks);
        System.out.println("Blocs utilisés : " + usedBlocks);

        double utilization = (double) usedBlocks / MemoryManager.NUM_BLOCKS * 100;
        System.out.printf("Taux d'utilisation : %.2f%%\n", utilization);

        // Analyser la fragmentation
        double fragmentation = calculateFragmentation(mm);
        System.out.printf("Fragmentation : %.2f%%\n", fragmentation * 100);

        // Compter les inodes utilisés
        int usedInodes = countUsedInodes(vfs);
        System.out.println("Inodes utilisés : " + usedInodes + "/" + MemoryManager.MAX_INODES);
    }

    private static double calculateFragmentation(MemoryManager mm) {
        int consecutiveGroups = 0;
        int totalFreeBlocks = 0;
        boolean inFreeGroup = false;

        for (int i = 129; i < MemoryManager.NUM_BLOCKS; i++) {
            if (!mm.isBlockUsed(i)) {
                totalFreeBlocks++;
                if (!inFreeGroup) {
                    consecutiveGroups++;
                    inFreeGroup = true;
                }
            } else {
                inFreeGroup = false;
            }
        }

        if (totalFreeBlocks == 0)
            return 0.0;

        return (double) consecutiveGroups / totalFreeBlocks;
    }

    private static int countUsedInodes(VirtualFileSystem vfs) {
        // Parcourir la table des inodes et compter ceux qui sont utilisés
        byte[] memory = vfs.getMemoryManager().getFilesystemMemory();
        int count = 0;

        for (int i = 0; i < MemoryManager.MAX_INODES; i++) {
            int offset = MemoryManager.INODE_TABLE_OFFSET + (i * Inode.INODE_SIZE);
            int inodeNumber = readInt(memory, offset);

            // Si le numéro d'inode correspond à l'index, l'inode est utilisé
            if (inodeNumber == i)
                count++;
        }

        return count;
    }

    public static void dumpMemoryLayout(VirtualFileSystem vfs) {
        System.out.println("\n=== Layout mémoire détaillé ===");
        System.out.printf("Superbloc        : %d - %d\n", 0, MemoryManager.BLOCK_SIZE - 1);
        System.out.printf("Bitmap           : %d - %d\n", MemoryManager.BITMAP_OFFSET, 
                         MemoryManager.BITMAP_OFFSET + MemoryManager.BLOCK_SIZE - 1);
        System.out.printf("Table inodes     : %d - %d\n", MemoryManager.INODE_TABLE_OFFSET,
                         MemoryManager.DATA_OFFSET - 1);
        System.out.printf("Zone données     : %d - %d\n", MemoryManager.DATA_OFFSET,
                         MemoryManager.TOTAL_MEMORY - 1);
    }
}

Classe et méthodes utilitaires

class Utils {
    public static int writeInt(byte[] memory, int offset, int value) {
        memory[offset] = (byte) (value >> 24);
        memory[offset + 1] = (byte) (value >> 16);
        memory[offset + 2] = (byte) (value >> 8);
        memory[offset + 3] = (byte) value;
                return 4;
    }

    public static int readInt(byte[] memory, int offset) {
        return ((memory[offset] & 0xFF) << 24) |
               ((memory[offset + 1] & 0xFF) << 16) |
               ((memory[offset + 2] & 0xFF) << 8) |
               (memory[offset + 3] & 0xFF);
    }

    public static int writeLong(byte[] memory, int offset, long value) {
        for (int i = 0; i < 8; i++)
            memory[offset + i] = (byte) (value >> (56 - i * 8));
                return 8;
    }

    public static long readLong(byte[] memory, int offset) {
        long value = 0;

        for (int i = 0; i < 8; i++)
            value |= ((long) (memory[offset + i] & 0xFF)) << (56 - i * 8);

        return value;
    }

    public static int writeShort(byte[] memory, int offset, short value) {
        memory[offset] = (byte) (value >> 8);
        memory[offset + 1] = (byte) value;
                return 2;
    }

    public static String readString(byte[] memory, int offset, int maxLength) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < maxLength; i++) {
            byte b = memory[offset + i];

            if (b == 0)
                break; // Fin de chaîne

            sb.append((char) b);
        }
        return sb.toString();
    }

    public static int writeString(byte[] memory, int offset, String str, int maxLength) {
        byte[] strBytes = str.getBytes();
        int len = Math.min(strBytes.length, maxLength - 1); // Garder place pour \0

        System.arraycopy(strBytes, 0, memory, offset, len);
        // Compléter avec des zéros
        for (int i = len; i < maxLength; i++)
            memory[offset + i] = 0;

                return maxLength;
    }
}

Classe principale et tests

Créez le fichier Main.java pour tester votre système :

public class Main {
    public static void main(String[] args) {
        System.out.println("=== Initialisation du système de fichiers virtuel ===");
        VirtualFileSystem vfs = new VirtualFileSystem();

        try {
            // Afficher le layout initial
            FileSystemUtils.dumpMemoryLayout(vfs);
            FileSystemUtils.displayStats(vfs);

            // Test 1 : Création de fichiers
            System.out.println("\n--- Test 1 : Création de fichiers ---");
            vfs.createFile("/", "readme.txt");
            vfs.createFile("/", "test.dat");
            vfs.createFile("/", "config.conf");

            // Test 2 : Écriture de données
            System.out.println("\n--- Test 2 : Écriture de données ---");
            String contenu1 = "Ceci est un fichier de test pour notre système de fichiers virtuel.\n";
            contenu1 += "Toutes les données sont stockées dans 1Mo de mémoire.\n";
            vfs.writeFile("/", "readme.txt", contenu1.getBytes());

            // Créer un fichier plus volumineux
            StringBuilder bigContent = new StringBuilder();
            for (int i = 0; i < 50; i++)
                bigContent.append("Ligne ").append(i).append(" - Test de données répétées.\n");
            vfs.writeFile("/", "test.dat", bigContent.toString().getBytes());

            // Test 3 : Lecture de fichiers
            System.out.println("\n--- Test 3 : Lecture de fichiers ---");
            byte[] data = vfs.readFile("/", "readme.txt");

            if (data != null) {
                System.out.println("Contenu lu (" + data.length + " octets) :");
                System.out.println(new String(data));
            }

            // Test 4 : Statistiques finales
            System.out.println("\n--- Test 4 : Statistiques finales ---");
            FileSystemUtils.displayStats(vfs);

            // Test 5 : Stress test - créer beaucoup de petits fichiers
            System.out.println("\n--- Test 5 : Stress test ---");
            int successCount = 0;

            for (int i = 0; i < 100; i++) {
                String filename = "file" + i + ".tmp";
                String content = "Contenu du fichier " + i;

                if (vfs.createFile("/", filename) && 
                    vfs.writeFile("/", filename, content.getBytes())) {
                    successCount++;
                } else {
                    System.out.println("Échec création fichier " + i + " (limite atteinte)");
                    break;
                }
            }
            System.out.println("Fichiers créés avec succès : " + successCount);

            // Statistiques finales
            FileSystemUtils.displayStats(vfs);

        } catch (Exception e) {
            System.err.println("Erreur pendant les tests : " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Comment optimiser l’utilisation mémoire ? Surveillez le taux de fragmentation, le nombre d’inodes utilisés, et l’efficacité de l’allocation. Un bon système de fichiers maintient une fragmentation faible tout en maximisant l’utilisation de l’espace disponible.