SE4 2022/2023 EC1
Objectifs
Il vous est demandé de :
- réaliser un micro système de fichiers ;
- le système de fichiers doit résider dans un fichier de 8 Mo ;
- le système de fichiers est géré par un exécutable obtenu à partir d'un programme C ;
- L'éxécutable prend deux arguments, le premier est le chemin du fichier dans lequel réside le système de fichiers, les paramètres suivants concernent l'action à appliquer sur le système de fichiers ;
- le micro système de fichier ne comporte qu'un répertoire : le répertoire principal, le répertoire principal peut comporter au maximum 64 fichiers, un fichier est caractérisé par un nom de 16 caractères au maximum et ses blocs de données, un fichier peut comporter au maximum 2040 blocs de données ;
- un bloc de données fait 256 octets et les blocs sont numérotés sur 2 octets ;
- les différentes actions possibles sur le système de fichiers sont :
TYPE
pour créer un fichier si possible, le nom du fichier suit la commande, le contenu du fichier est donné en entrée standard de l'exécutable ;CAT
pour afficher un fichier, le nom du fichier suit la commande ;RM
pour détruire un fichier, le nom du fichier suit la commande ;MV
pour renommer un fichier, les noms original et nouveau du fichier suivent la commande ;CP
pour copier un fichier, les noms de l'original et de la copie du fichier suivent la commande ;
Matériel nécessaire
Un PC sous Linux.
Travail réalisé
Semaine 1
ReX : attention, ce micro-système de fichiers est prévu pour un microcontrôleur, vos variables globales ne peuvent pas dépasser quelques centaines d'octets.
ReX : pour la création du système de fichiers la commande dd
suffit, vous voulez dire le formatage du système de fichiers ?
- création du programme programme.c contenant les différentes structures et les différentes fonctions.
- Piste d'amélioration: faire en sorte que la fonction CAT affiche le contenu exact des fichiers passés en argument.
Le code fourni est un programme en langage C qui simule un système de fichiers basique. Ce programme permet aux utilisateurs d'effectuer diverses actions telles que créer, afficher, supprimer, renommer et copier des fichiers dans un système de fichiers simulé. Le système de fichiers est stocké dans un fichier binaire.
ReX : vous n'avez pas tenu compte de la première remarque, vous chargez tout le superbloc et le répertoire racine, votre code ne convient pas.
ReX : vos structures utilisent des types entiers dont la taille n'est pas explicitée, utilisez les types de stdint.h
.
ReX : la création de fichier n'est pas fonctionnelle, vous ne cherchez pas les blocs libres, vous ne copiez pas le contenu du fichier dans les blocs, même remarque pour toutes les autres fonctions.
ReX : il manque les fonctions d'accès aux pages du système de fichiers.
Explication du programme programme.c
Voici une brève explication des principaux éléments et fonctions du code :
- Structures :
- Fichier : Représente un fichier dans le système de fichiers. Il contient un nom (nom) avec une longueur maximale de 16 caractères, un tableau de numéros de bloc (blocs) où le contenu du fichier est stocké (jusqu'à 2040 blocs), et le nombre de blocs utilisés par le fichier (nbBlocs).
- Repertoire : Représente le répertoire principal du système de fichiers. Il contient un tableau de fichiers (
fichiers
) et le nombre de fichiers dans le répertoire (nbFichiers
).
- Fonctions :
chargerSystemeFichiers
: Charge le système de fichiers à partir d'un fichier binaire donné (chemin) dans une structure Repertoire.sauvegarderSystemeFichiers
: Sauvegarde le système de fichiers stocké dans la structure Repertoire dans un fichier binaire (chemin).creerFichier
: Crée un nouveau fichier dans le système de fichiers avec un nom et un contenu donnés. Le contenu du fichier est simulé en le divisant en blocs.afficherFichier
: Affiche le contenu d'un fichier avec le nom spécifié.detruireFichier
: Supprime un fichier avec le nom spécifié du système de fichiers.renommerFichier
: Renomme un fichier avec l'ancien nom spécifié en un nouveau nom.copierFichier
: Copie un fichier avec le nom spécifié en créant un nouveau fichier avec le nom de copie spécifié.
- Fonction
main
:- La fonction
main
est le point d'entrée du programme. - Elle prend des arguments en ligne de commande :
<chemin_systeme_fichiers>
et<action>
. <chemin_systeme_fichiers>
est le chemin vers le fichier binaire où le système de fichiers est stocké.<action>
détermine quelle opération effectuer, commeTYPE
,CAT
,RM
,MV
ouCP
.- En fonction de l'action spécifiée, le programme appelle la fonction correspondante pour effectuer l'opération souhaitée sur le système de fichiers.
- La fonction
Remarque : Le code fourni une simulation basique d'un système de fichiers et de ses opérations, mais il n'interagit pas réellement avec un système de fichiers réel sur le disque.
Comment utiliser le programme programme.c
étape 1: création du système de fichiers respectant le cahier des charges:
dd if=/dev/zero of=systeme_fichiers.bin bs=1M count=8
étape 2: compilation du programme C:
gcc programme.c -o gestionnaire_fs
étape 3: création d'un fichier dans le système de fichier:
./gestionnaire_fs systeme_fichiers.bin TYPE fichier1.txt
-> entrer le texte en entrée standard
étape 4: manipulation des différentes fonctions. Exemples:
./gestionnaire_fs systeme_fichiers.bin CAT fichier1.txt ./gestionnaire_fs systeme_fichiers.bin CP fichier1.txt copie.txt ./gestionnaire_fs systeme_fichiers.bin RM copie.txt ./gestionnaire_fs systeme_fichiers.bin MV fichier1.txt fichier2.txt
Semaine 2
Amine: Merci pour vos remarques. Désolé de ne pas avoir suivi votre première remarque, je pensais avoir compris ce qu'il fallait faire mais je me rend compte que non. Je vais tout reprendre depuis le début afin de repartir sur de bonnes bases.
Création du système de fichier avec la commande "dd"
A quoi sert la commande dd?
La commande dd
permet de copier tout ou partie d'un disque par blocs d'octets, indépendamment de la structure du contenu du disque en fichiers et en répertoires (source : [1]).
Structure de la commande
dd if=<source> of=<cible> bs=<taille des blocs>
source
= données à copier
cible
= endroit où les copier
if
= input file
of
= output file
bs
= block size, habituellement une puissance de 2 supérieure ou égale à 512, représentant un nombre d'octets
Choix de la commande:
Pour la source, on prends /dev/zero
: cela permet de créer un fichier rempli de 0. Ce fichier servira de base pour notre système de fichiers.
Pour la cible, on va l'appeler filesystem.bin
: ce sera notre système de fichier.
Pour la taille des blocs, on veut des blocs de données de 256 octets d'après l'enoncé. On va donc prendre bs=256
.
Enfin, on veut que le système de fichier réside dans un fichier de 8 Mo. On va donc ajouter count=31250
: cela indique que nous voulons écrire 32768 blocs de données dans le fichier (31250 * 256 octets = 8 Mo).
Résultat:
dd if=/dev/zero of=filesystem.bin bs=256 count=31250
Question pour M. Redon : j'ai ici essayé de respecter votre remarque "pour la création du système de fichiers la commande dd
suffit". Cependant, pour votre seconde remarque "vous chargez tout le superbloc et le répertoire racine, votre code ne convient pas.", je ne suis pas sûr de comprendre où je fais cela: est-ce lors de la commande dd ou est-ce par la suite avec mon programme C? J'ai un peu modifié la commande dd comme vous pouvez le voir ci-dessus. Ai-je corrigé mon erreur avec cette nouvelle commande? J'ai essayé de justifier au maximum la commande dd que j'ai choisie.
ReX : Pas de problème avec la commande dd
(mettez tous les extraits de code entre des balises code
). Le problème est au niveau du programme C.
Amine: Ok je comprends mieux merci. Je vais donc reprendre le code étape par étape afin de mieux répondre au cahier des charges.
Gestion du système de fichier par un programme C
Etape 1: création des structures
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> // Structure pour représenter un bloc de données struct DataBlock { uint8_t data[256]; // 256 octets pour chaque bloc de données }; // Structure pour représenter un fichier struct File { char filename[17]; // 16 caractères pour le nom du fichier + 1 caractère null-terminator struct DataBlock data_blocks[2040]; // Tableau de blocs de données pour stocker le contenu du fichier uint16_t num_data_blocks; // Nombre de blocs de données utilisés par le fichier }; // Structure pour représenter un répertoire struct Directory { struct File files[64]; // Tableau de fichiers pour le répertoire principal uint8_t num_files; // Nombre de fichiers dans le répertoire principal };
Modification faite : suite à votre remarque, j'ai explicité la taille des entiers grâce aux types de stdint.h.
(j'ai également mis les variables en anglais)
ReX : Vous ne pouvez pas utiliser ces structures, une instanciation d'une de ces structure prend trop d'espace mémoire, vous devez faire sans structure. Par ailleurs la façon dont vous représentez vos fichiers ne convient pas, la description d'un fichier contient des numéros de blocs, pas les blocs eux-même. Faites aussi attention qu'un fichier soit décrit par un nombre entier de blocs.
Question pratique: je n'arrive pas à mettre tout le code dans le même bloc comme vous l'avez fait ci-dessus dans l'étape 4 de la semaine 1. Je vois que c'est en format "préformaté" mais j'obtiens des blocs séparés lorsque j'applique ce format à mon code.
ReX : j'ai corrigé, pour un code entier la syntaxe n'est pas la même (un espace en début de chaque ligne), la balise c'est uniquement pour de très courts extraits de code.
Etape 2: Fonction readBlock
Cette fonction est utilisée pour lire un bloc de données à partir du fichier filesystem.bin
et le stocker dans un tableau de caractères (storage
).
Fonction:
#define BLOCK_SIZE 256 // Fonction pour lire un bloc de données void readBlock(unsigned int num, int offset, unsigned char *storage, int size) { FILE *file = fopen("filesystem.bin", "rb"); if (file == NULL) { perror("Erreur lors de l'ouverture du fichier"); return; } fseek(file, num * BLOCK_SIZE + offset, SEEK_SET); fread(storage, 1, size, file); fclose(file); }
Paramètres :
num
: Numéro du bloc à lire. Chaque bloc contient 256 octets (1 bloc = 256 octets).offset
: Décalage (en octets) à partir du début du bloc pour commencer la lecture.storage
: Pointeur vers un tableau de caractères où les données lues seront stockées.size
: Taille du tableau de caractèresstorage
.
Fonctionnement :
- La fonction commence par ouvrir le fichier
filesystem.bin
en mode lecture binaire ("rb"
). - Elle utilise
fseek
pour positionner le curseur de lecture dans le fichier à l'endroit approprié pour commencer la lecture du bloc spécifié (num
) à partir de l'offset (offset
). - Elle utilise ensuite
fread
pour liresize
octets à partir du fichier et les stocker dans le tableaustorage
. - Enfin, elle ferme le fichier avec
fclose
.
Note : Le paramètre offset
est utilisé pour spécifier à partir de quel octet du bloc on souhaite commencer la lecture. Si offset
est égal à 0, la lecture commencera depuis le début du bloc. Si offset
est différent de 0, la lecture commencera à l'octet spécifié.
Remarque: dans votre mail, vous définissez "readBlock(unsigned int num,int offset,unsigned storage,int size)": storage n'est alors pas un pointeur vers un tableau de caractère. Je me suis donc permis de faire la modification.
ReX : OK pour la fonction, pas la peine d'en mettre autant dans le Wiki pour une fonction aussi simple.
Etape 3: Fonction writeBlock
Cette fonction est utilisée pour écrire un bloc de données dans le fichier filesystem.bin
à partir d'un tableau de caractères (storage
).
Fonction:
// Fonction pour écrire un bloc de données void writeBlock(unsigned int num, int offset, const unsigned char *storage, int size) { FILE *file = fopen("filesystem.bin", "rb+"); if (file == NULL) { perror("Erreur lors de l'ouverture du fichier"); return; } fseek(file, num * BLOCK_SIZE + offset, SEEK_SET); fwrite(storage, 1, size, file); fclose(file); }
Paramètres :
num
: Numéro du bloc où écrire.offset
: Décalage (en octets) à partir du début du bloc pour commencer l'écriture.storage
: Pointeur vers un tableau de caractères contenant les données à écrire dans le fichier.size
: Taille du tableau de caractèresstorage
.
Fonctionnement :
- La fonction commence par ouvrir le fichier
filesystem.bin
en mode lecture et écriture binaire ("rb+"
). - Elle utilise
fseek
pour positionner le curseur de lecture/écriture dans le fichier à l'endroit approprié pour commencer l'écriture du bloc spécifié (num
) à partir de l'offset (offset
). - Elle utilise ensuite
fwrite
pour écriresize
octets à partir du tableaustorage
dans le fichier. - Enfin, elle ferme le fichier avec
fclose
.
ReX : Même remarque que pour la fonction précédente.
Etape 4: test des fonctions readBlock
et writeBlock
dans le main
// Exemple de fonction principale pour tester les opérations de lecture et d'écriture int main() { unsigned char data[BLOCK_SIZE]; // Test de la fonction readBlock readBlock(0, 0, data, BLOCK_SIZE); printf("Block 0, offset 0: "); for (int i = 0; i < BLOCK_SIZE; i++) { printf("%02x ", data[i]); } printf("\n"); // Test de la fonction writeBlock unsigned char newData[BLOCK_SIZE] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // ... Autres données du bloc ... }; writeBlock(1, 0, newData, BLOCK_SIZE); // Lecture du bloc nouvellement écrit pour vérifier readBlock(1, 0, data, BLOCK_SIZE); printf("Block 1, offset 0: "); for (int i = 0; i < BLOCK_SIZE; i++) { printf("%02x ", data[i]); } printf("\n"); return 0; }
Fonctionnement :
- On déclare tout d'abord un tableau de caractères
data
de tailleBLOCK_SIZE
pour stocker les données lues du bloc. - Ensuite, la fonction
readBlock
est appelée avec les arguments (0, 0, data, BLOCK_SIZE) pour lire le premier bloc du fichier (num=0
,offset=0
) et stocker les données dansdata
. - Ensuite, la fonction
printf
est utilisée pour afficher les données lues du bloc (data
) en format hexadécimal. - Ensuite, un nouveau tableau de caractères
newData
est créé avec des données spécifiques pour tester la fonctionwriteBlock
. - La fonction
writeBlock
est appelée avec les arguments (1, 0, newData, BLOCK_SIZE) pour écrire le tableaunewData
dans le deuxième bloc du fichier (num=1
,offset=0
). - Enfin, la fonction
readBlock
est appelée à nouveau pour lire le deuxième bloc nouvellement écrit et afficher les données lues en format hexadécimal.
Question générale : ce que j'avais la première semaine n'est plus forcément d'actualité. Puis-je supprimer les choses qui ne le sont plus afin d'alléger la page ou préférez-vous que je laisse?
ReX : Laissez.
Pour la suite : j'ai donc pour l'instant crée deux fonctions, l'une permettant la lecture et l'autre l'écriture d'un bloc, de manière à économiser la mémoire. Pour la suite, je vais essayer de créer la fonction LS
en utilisant la fonction readBlock
.
ReX : Oui c'est le début du projet. Mais si vous n'avez pas une bonne description de fichier cela ne donnera rien. Voir remarques plus haut.
Etape 5: fonction LS
Objectif: créer la fonction LS
avec la fonction readBlock.
Question: Souhaitez-vous la fonction LS
classique, c'est à dire la fonction LS
qui affiche simplement le nom des fichiers présent dans le répertoire ?
ReX : Oui. Tu peux ajouter la taille si tu trouves cela trop simple.
Piste : si c'est ce que vous voulez:
- il va tout d'abord falloir que je crée des fichiers, un fichier étant composé d'un nom et de blocs (création d'une fonction createFile)
- Il faudra ensuite que je stocke ces fichiers dans le répertoire (création d'une fonction addFileToDirectory par exemple)
- enfin, il faudra que j'affiche le nom des fichiers. Pour cela, il faudra que je parcours le répertoire (structure "Directory"), puis que pour chaque fichier présent dans le répertoire que j'affiche son nom (création de la fonction LS)
Je devrais surement utiliser la fonction readBlock afin de transférer les blocs dans le fichier au travers de la variable "storage".
ReX : Créer des fichiers n'est pas nécessaire tout de suite je verrais bien si ton code est bon sans test. Laisse tomber createFile
et addFileToDirectory
pour l'instant.
ReX : Ton algorithme ne fonctionne pas pour un microcontrôleur où il faut économiser la mémoire, tu ne peux pas t'aider de structures. Il faudra que tu charges les noms et seulement les noms, pour la taille il faudra se baser sur le nombre de blocs.
Etape 6: rectification du code suite à vos remarques
Modifications apportées:
- suppression des structures afin d’économiser de la mémoire.
- le problème quant à la description des fichiers est normalement résolu puisque plus de structures. On ne charge plus les blocs mais seulement les noms de fichiers.
ReX : Non car si les noms ne sont pas répartis de façon régulière dans les blocs de 256 octets tu vas avoir du mal à les charger. Mais écrit la fonction LS
et tu verras ce que je veux dire.
J'ai également ajouté deux nouvelles fonctions:
void readFileName(unsigned int file_num, char *file_name)
: Cette fonction permet de lire le nom du fichier associé au numéro de fichier donné (file_num
). Elle stocke le nom du fichier lu dans le tableaufile_name
.void writeFileName(unsigned int file_num, const char *file_name)
: Cette fonction permet d'écrire le nom du fichier dans le système de fichiers, associé au numéro de fichier donné (file_num
). Elle prend en entrée le nom du fichier à écrire (file_name
).
Suite: si vous validez cette base, je pourrai passer à la création de la fonction LS.
ReX : tu peux y aller. Je ne vois pas le code des tes deux fonctions readFileName
et writeFileName
.
Voici le code des fonctions readFileName
et writeFileName
:
Fonction readFileName:
// Fonction pour lire le nom du fichier void readFileName(unsigned int file_num, char *file_name) { readBlock(file_num, 0, (unsigned char *)file_name, MAX_FILENAME_LENGTH); file_name[MAX_FILENAME_LENGTH] = '\0'; // Ajout du caractère null-terminator pour former une chaîne de caractères }
ReX : Non, aucune raison que le fichier de numéro n soit au bloc n. Dessine la représentation d'un fichier sur le SF en comptant les octets si cela peut aider.
Fonction writeFileName:
// Fonction pour écrire le nom du fichier void writeFileName(unsigned int file_num, const char *file_name) { writeBlock(file_num, 0, (const unsigned char *)file_name, MAX_FILENAME_LENGTH); }
ReX : Faux et inutile pour l'instant. Problème avec la constante MAX_FILENAME_LENGTH.
Fonction LS:
// Fonction LS pour afficher les fichiers présents dans le système de fichiers void LS(const char *filesystem_path) { FILE *file = fopen(filesystem_path, "rb"); if (file == NULL) { perror("Erreur lors de l'ouverture du fichier système"); return; } uint16_t num_files; fread(&num_files, sizeof(uint16_t), 1, file); // Lecture du nombre de fichiers dans le système char filename[17]; printf("Fichiers présents dans le système de fichiers:\n"); for (int i = 0; i < num_files; i++) { readFileName(i, filename); // Lecture du nom du fichier printf("%s\n", filename); // Affichage du nom du fichier } fclose(file); }
La fonction LS
ouvre le fichier système, lit le nombre de fichiers qu'il contient, puis affiche les noms des fichiers un par un, chacun sur une ligne séparée.
ReX : Il y a le nombre de fichiers dans ton superbloc ? Un dessin du format du superbloc avec les numéros des octets ?
ReX : Tout accès au système de fichiers doit se faire avec les deux fonctions readBlock
et writeBlock
.
Semaine 3
Question 1: cela fait plusieurs fois que vous mentionnez dans le wiki un "superbloc". Celui-ci n'étant pas clairement défini dans le sujet, je me demande s'il s'agit du bloc crée grâce à la fonction dd, c'est à dire que le superbloc serait en faite le bloc constitué des 31250 blocs de taille 256 octets, où s'il s'agit d'un bloc qui vient en entête, celui contenant alors des informations décrivant le système de fichier (les noms de fichiers...).
ReX : Pour ton pico système de fichiers, le superbloc consiste en les premiers blocs (de 256 octets) qui définissent les 64 fichiers possibles.
Proposition de structure: on pourrait réserver les premiers blocs du système de fichiers aux noms de fichiers ainsi qu'aux numéros des blocs correspondant à chaque fichier.
ReX : Oui c'est évident.
On sait que les noms de fichiers font au maximum 16 caractères + 1 caractère pour le '\0' (donc 17 octets car 1 char=1 octet).
ReX : Non, 16 octets suffisent, des '\0' en fin de nom uniquement si le nom fait moins de 16 caractères.
On sait également qu'un fichier contient au maximum 2040 blocs, chaque bloc étant numéroté sur 2 octets. On pourrait donc avoir en début du système de fichier: 17 octets+2 octets*2040 blocs = 4097 octets pour la description d'un fichier.
ReX : Non, 4096 octets soit 16 blocs de 256.
Or d'après l'une de vos remarques dans le wiki, un fichier doit être décrit par un nombre entier de blocs. Pour un fichier, il nous faudra donc 4097/256=16.004 soit 17 blocs. Il peut y avoir jusqu'à 64 fichiers dans le répertoire, il nous faudra donc 17 blocs*64 fichiers= 1088 blocs pour la description des fichiers.
ReX : Non 1024 blocs de 256 octets dans le superbloc.
Ainsi, pour la fonction LS, il suffira de lire les premiers caractères tous les 17 blocs ( du premier caractère au caractère '\0').
ReX : Non, tous les 16 blocs.
Question 2: Ne faudrait-il pas également stocker quelque part le nombre de fichier qu'il y a dans le répertoire afin de savoir jusqu'à quel bloc la fonction LS doit aller ?
ReX : Non, un fichier dont le nom n'est constitué que de '\0' est réputé ne pas exister.
Question 3: on a donc vu que les 1088 premiers blocs au maximum serviraient à la description du système de fichier. Il reste donc 31250-1088=30132 blocs dans le système de fichier.
ReX : Non, 32768-1024=31744 blocs.
Comment utiliser ces blocs? Ces blocs doivent-ils stocker le contenu des fichiers ?
ReX : Oui, c'et évident.
En effet, avec la fonction TYPE, nous allons créer des fichiers et ces fichiers devront être stockés quelque part. Cependant, étant donné que ce pico système de fichiers est prévu pour un microcontrôleur, je ne sais pas si c'est le contenu des fichiers qui doit être stocké dans les blocs ou autre chose (peut être l'adresse des fichiers, le fichiers se trouvant autre part). En effet, je me dis que si un fichier est très volumineux, il ne pourra pas rentrer dans le système de fichier, ce dernier étant composé d'un nombre limité de blocs, et les blocs étant eux même limité en nombre d'octets.
ReX : Je ne comprend pas ton problème. De tout façon comme dit plus haut, les 31744 blocs suivant le superbloc doivent contenir les données des fichiers.
Désolé de poser des questions peut être basique aussi tard, mais je pense qu'une fois que j'aurai ces réponses, cela ira beaucoup mieux pour la suite. Merci d'avance pour vos réponses.
ReX : Les questions sont effectivement triviales et il est effectivement inquiétant que voir que la travail n'avance pas.
Amine: merci pour vos corrections.
D'après votre commentaire "32768-1024=31744 blocs", j'en déduis qu'il faut que je modifie ma commande dd (qui est actuellement "dd if=/dev/zero of=filesystem.bin bs=256 count=31250" pour avoir le bon filesystem).
ReX : Je comprends pas d'où tu sors le 31250. D'autant plus que la commande ci-dessous est bonne.
Amine : 31250 car 31250 blocs * 256 octets = 8 Mo.
ReX : Haaaa je vois tu utilises le système métrique international où le mégaoctet vaut 10^6 octets, sauf qu'aucun informaticien n'utilisera cette norme hors sol. Quand je parle de 8 Mo c'est 8x1024x1024 octets. Tout simplement parce qu'une puce mémoire de 8 Mo c'est bien 8x1024x1024 octets.
Amine: D'accord merci pour l'information !
Nouvelle commande dd:
dd if=/dev/zero of=filesystem.bin bs=256 count=32768
Fonction LS:
Dans notre structure du système de fichier, le superbloc est constitué de 1024 blocs (16 blocs * 64 fichiers), un fichier étant décrit par 16 blocs. Le nom du fichier est donc écrit au début de tous les blocs multiples de 16. Par exemple, le nom du premier fichier est dans les 16 premiers octets du premier bloc, le nom du 2ème fichier est dans les 16 premiers octets du 32ème bloc et ainsi de suite, tant que des fichiers existent. Lorsque qu'il n'y a plus de fichier, le nom du premier caractère du bloc multiple de 16 est un 0.
Voici le code:
// Fonction pour lister les noms de fichiers présents dans le système de fichiers void LS() { unsigned char buffer[BLOCK_SIZE]; int fileCount = 0; for (int blockNum = 1; blockNum <= MAX_FILES_IN_DIRECTORY; blockNum += 16) { readBlock(blockNum, 0, buffer, BLOCK_SIZE); // Vérifier si le bloc est vide if (buffer[0] == 0) { break; // Plus de fichiers à lire } char filename[MAX_FILENAME_LENGTH]; memcpy(filename, buffer, MAX_FILENAME_LENGTH); // Afficher le nom du fichier printf("%s\n", filename); fileCount++; } if (fileCount == 0) { printf("Aucun fichier trouvé.\n"); } }
ReX : Tu supposes que si un fichier a un nom vide, les suivants auront aussi un nom vide. C'est possible mais dans ce cas la commande RM
ca être plus compliquée à écrire.
ReX : Tu ne sembles pas comprendre que la mémoire d'un microcontrôleur est limitée. Pourquoi lire le premier bloc de description d'un fichier en entier ? Dit autrement c'est quoi l'intérêt de lire 256 octets alors que 16 suffisent ?
ReX : Pourquoi tu ne lis pas le premier fichier ? Autrement dit pourquoi décaler tout d'un bloc ?
Amine: Merci pour vos remarques. J'ai apporté les modifications à LS dans la section "Semaine 4" du wiki.
Réflexion par rapport aux autres fonctions:
J'ai créé les fonctions TYPE et CAT mais il y a malheureusement un problème pour l'instant. Vous pouvez les retrouver en pièce jointe dans l'archive du fichier programme.c.
Le problème que j'ai est que lorsque j'affiche le contenu du fichier créé avec TYPE, j'obtiens plein de caractères spéciaux. Par exemple, je créé le fichier "mon_fichier.txt" avec la fonction TYPE, et je mets en entré standard "hello". Ensuite, je veux afficher le contenu de ce fichier grâce à la fonction CAT. Cependant, ce qui s'affiche dans le terminal n'est pas ce que je veux. De la même manière, lorsque je fais la commande "cat filesystem.bin" dans le terminal, c'est la même chose s'affiche, des donnés parasites.
ReX : Avant même de lire ton code je vais te poser la question de savoir comment tu récupères un bloc libre ? Quel algorithme tu utilises. Personnellement je ne sais pas faire sans ajouter au superbloc un tableau bit à bit des blocs libres. Pour avoir un bit pour chaque bloc du système de fichiers, il faut 32768/8=4096 octets soit 16 blocs.
Amine: ok je vais donc ajouter ce tableau de 16 blocs à la suite de mes premiers 1024 blocs servant à la description de fichier. Le superbloc fera maintenant 1024+16=1040 blocs (plus de détail à la fonction RM de la semaine 4).
Question 1: est ce que la fonction CAT que je dois créer doit renvoyer la même chose que "cat filesystem.bin" ?
ReX : Je préfère ne pas répondre tellement la question montre un manque de réflexion.
Question 2: comment savoir combien de blocs doivent etre attribué à un fichier? Par exemple, si je créé un fichier "mon_fichier.txt" et que je mets en entrée standard le texte "hello", un seul bloc suffi pour stocker ce texte. Cependant, si j'avais mis beaucoup de texte en entrée standard, il aurait fallu plus de blocs. Doit-on calculer la taille de l'entrée standard ou doit-on attribuer toujours le meme nombre de blocs à un fichier? Peut-etre ainsi attribuer 2040 blocs par fichier, meme s'il n'en utilise que 1.
ReX : Là encore c'est une question stupéfiante tellement la réponse est évidente. Il suffit de lire les données sur l'entrée standard et de passer au bloc de données suivant dès que le bloc courant est rempli. La question à poser c'était de se demander comment il est possible d'avoir la taille exacte du fichier pour un CAT
. Il est uniquement possible de l'avoir à 256 octets près puisque rien n'indique si le dernier block est vide. Le mieux aurait été de prévoir 4 octets dans la description d'un fichier pour stocker la taille. En l'espèce on considérera que les fichiers sont des fichiers ASCII et qu'ils seront terminés par des \'0
qui ne seront pas affichés.
Semaine 4
Voici un bilan de ce que j'ai fait à ce stade du projet:
- j'ai choisi une structure du système de fichier
- j'ai créé les fonctions readBlock et writeBlock permettant de lire et d'écrire des blocs
- j'ai crée la fonction LS permettant d'afficher le nom des fichiers présents dans le système de fichiers
- j'ai programmer un main permettant d'utiliser les fonctions (LS, TYPE...) depuis le terminal
- j'ai réflechi aux fonctions TYPE et CAT.
Actuellement, je suis en train de créer les fonctions TYPE et CAT.
Nouvelle fonction LS
void LS() { unsigned char buffer[MAX_FILENAME_LENGTH]; int fileCount = 0; // Parcourir tous les 16 blocs de 0 jusqu'à MAX_FILES_IN_DIRECTORY for (int blockNum = 0; blockNum < MAX_FILES_IN_DIRECTORY; blockNum += 16) { readBlock(blockNum, 0, buffer, MAX_FILENAME_LENGTH); // Vérifier si le nom de fichier est vide if (buffer[0] != 0) { // Afficher le nom du fichier printf("%s\n", buffer); fileCount++; } } if (fileCount == 0) { printf("Aucun fichier trouvé.\n"); } }
Suite à vos remarques, j'ai effectué les modifications suivantes de la fonction LS:
- Je ne suppose plus que si un fichier a un nom vide, les autres auront aussi un nom vide.
- Je ne lis plus les 256 octets mais seulement les 16 premiers octets car cela suffit.
- Je ne lisais en effet pas le premier bloc. Je pensais que le premier bloc était d'indice 1 mais ce n'est pas le cas.
ReX : Ouiiii ! Une fonction correcte. Passe à RM
maintenant, c'est la plus facile à faire concernant l'image bit à bit des blocs libres.
Fonction setBlockAvailability:
Comme vous l'avez indiqué dans votre remarque, il faut un tableau dans le superbloc qui nous permettra de connaitre la disponibilité bit par bit de chaque bloc. Sachant qu'il y a dans notre système de fichiers 32768 blocs, et qu'il faut 1 bit par bloc, nous avons besoin de 32768 bits, soit 32768/8=4096 octets soit 4096/256=16 blocs. Notre superbloc sera donc comme suit: les 1024 premiers blocs servent à la description des fichiers, c'est à dire à leur nom suivi des numéros de blocs qui leur sont associés, puis ces 1024 blocs sont suivi de 16 blocs listant la disponibilité de chaque bloc.
ReX : Bien résumé.
Nous allons tout d'abord écrire la fonction "setBlockAvailability" prenant en paramètre le numéro du bloc à traiter (blockNum
) et la disponibilité souhaitée pour ce bloc (availability
). Cette fonction permet de marquer la disponibilité d'un bloc spécifique dans le système de fichiers. Nous utiliserons la convention suivante: un bloc disponible sera marqué par un 1 tandis qu'un bloc non disponible par un 0.
#define SUPERBLOCK_START_BLOCK 1024 void setBlockAvailability(int blockNum, int availability) { // Calculer l'index du byte et le bit offset correspondant int byteIndex = blockNum / 8; int bitOffset = blockNum % 8; // Lire le byte existant du bloc de disponibilité des blocs unsigned char buffer[1]; readBlock(SUPERBLOCK_START_BLOCK + byteIndex, 0, buffer, 1); // Mettre à jour le bit d'offset correspondant if (availability) { buffer[0] |= (1 << bitOffset); } else { buffer[0] &= ~(1 << bitOffset); } // Écrire le byte mis à jour dans le bloc de disponibilité des blocs writeBlock(SUPERBLOCK_START_BLOCK + byteIndex, 0, buffer, 1); }
ReX : Un tableau de un octet c'est un octet (variable buffer
).
Amine: je vais utiliser un unsigned char buffer.
Je suis conscient qu'un char vaut un octet mais je ne pense pas qu'il y ait un type de donnée de la taille d'un bit, c'est pourquoi je travaille avec un octet mais je modifie les bits de cet octet grâce aux algorithmes.
ReX : Il faut bien que tu travailles sur un octet vu que l'état des blocs est stocké sous la forme d'octets. Je disais juste qu'un tableau de 1 élément n'a pas d'intérêt par rapport à l'élément lui-même.
Amine: c'est vrai, modification faite.
ReX : Non il faut calculer le numéro du bloc avant de faire le readBlock
.
Amine: ok je vais calculer le numéro de bloc avant.
ReX : La mise à jour du bit est correcte mais le block et le déplacement dans le block ne sont pas correctement calculés.
Amine: je vais vous expliquer le raisonnement que j'avais. Prenons un exemple concret, imaginons que je veuille marquer le bloc 1030 comme disponible:
- Calcul de l'index de l'octet et du bit offset :
blockNum
= 1030- Index du byte = 1030 / 8 = 128
- Bit offset = 1030 % 8 = 6
- Lecture de l'octet existant de disponibilité:
- Nous lisons l'octet existant à l'index 128 dans les blocs de disponibilité.
- Mise à jour du bit de disponibilité :
- Nous voulons marquer le bloc 1030 comme disponible, donc nous utilisons le masque
(1 << 6)
pour définir le bit 6 à 1 dans l'octet. Le résultat sera, par exemple,00100000
.
- Nous voulons marquer le bloc 1030 comme disponible, donc nous utilisons le masque
- Écriture du byte mis à jour :
- Nous écrivons le byte mis à jour
00100000
à l'index 128 dans les blocs de disponibilité.
- Nous écrivons le byte mis à jour
ReX : Ben non, un vrai exemple :
- Il faut prendre un n° de bloc plus grand : 3072 ;
- Numéro d'octet dans la carte des blocs libres : 3072/8=384 ;
- Numéro du bloc dans la carte des blocs libres : 384/256=1 ;
- Numéro du bit
b
dans l'octet n° 384-256=128 du bloc 1 : 3072%8=0 ; - Il faut donc lire l'octet
o
de numéro 128 du bloc 1, puis modifier cet octet paro |= (1<<b)
ouo &= ~(1<<b)
; - Enfin il faut écrire l'octet modifié dans le bloc 1.
Amine: merci pour cet exemple! J'ai compris d'où vient mon erreur. Voir fonction setBlockAvailability version 3.
Remarque: par convention, j'apellerai dans la suite de ce projet "premier superbloc" le superbloc correspondant à la description des fichiers, c'est à dire les 1024 premiers blocs, et j'appellerai "second superbloc" les 16 blocs qui suivent servant à décrire la disponibilité des blocs (du bloc 1024 au bloc 1039 inclu). Le reste des blocs servira à stocker les données.
ReX : Non, le superblc est l'ensemble des informations hors blocs de données, tu ne peux pas utiliser un vocabulaire au hasard. Parle de blocs de description des fichiers ou de carte des blocs libres.
Amine: ok
Je pense que le problème qui se pose est que l'indice du bit dans le second superbloc ne correspond pas à l'indice du bloc dans le système de fichier. Par exemple, nous voudrions que si le bloc 1030 est disponible dans le système de fichier, alors le bit numéro 1030 du deuxième superbloc soit à 1, ce qui n'est pas le cas ici. En effet, on se trouve bien dans le bon octet avec ma méthode mais l'écriture du bit se fait de la droite vers la gauche alors qu'on voudrait l'inverse. Ainsi, si le 6ème bit de l'octet doit être à 1, alors il faudrait qu'on obtienne 00000100
et non 00100000.
L'indice du bit coinciderait ainsi avec le numéro du bloc.
ReX : Non, réfléchit à nouveau.
Voir plus bas la nouvelle version de setBlockAvailability.
Explication de l'algorithme pour mettre le bit à 1 ou 0:
buffer[0] |= (1 << bitOffset);
: Cette ligne utilise l'opérateur OR bit à bit (|=
) pour activer (mettre à 1) le bit spécifié parbitOffset
dans le premier octet (buffer[0]
). Cela se fait en décalant le bit 1 vers la gauche debitOffset
positions, puis en effectuant une opération OR bit à bit avec le contenu actuel debuffer[0]
. Cette opération modifie uniquement le bit ciblé, laissant les autres bits inchangés.
ReX : Oui ça c'est bon.
buffer[0] &= ~(1 << bitOffset);
: Cette ligne utilise l'opérateur AND bit à bit (&=
) pour désactiver (mettre à 0) le bit spécifié parbitOffset
dansbuffer[0]
. Pour ce faire, l'expression~(1 << bitOffset)
est utilisée pour créer un masque où tous les bits sont à 1, sauf celui correspondant àbitOffset
, qui est à 0. En effectuant ensuite une opération AND bit à bit entre le masque et le contenu actuel debuffer[0]
, on met à 0 le bit ciblé sans affecter les autres bits.
ReX : Ca aussi.
Fonction setBlockAvailability version 2:
void setBlockAvailability(int blockNum, int availability) { // Calculer l'indice de l'octet et le bit offset correspondant int byteIndex = blockNum / 8; int bitOffset = 7 - (blockNum % 8); // Inversion de l'ordre des bits // Calculer le numéro du bloc du super bloc où se trouve l'octet de disponibilité correspondant int availabilityBlockNum = SUPERBLOCK_START_BLOCK + byteIndex; // Lire l'octet existant dans le bloc de disponibilité du super bloc unsigned char buffer; readBlock(availabilityBlockNum, 0, &buffer, 1); // Mettre à jour le bit d'offset correspondant : if (availability) { buffer |= (1 << bitOffset); } else { buffer &= ~(1 << bitOffset); } // Écrire l'octet mis à jour dans le bloc de disponibilité du super bloc writeBlock(availabilityBlockNum, 0, &buffer, 1); }
J'ai modifié la ligne int bitOffset = 7 - (blockNum % 8);
pour inverser l'ordre des bits sélectionnés, de sorte que le bit correspondant au bloc disponible soit correct.
ReX : Encore plus faux.
Si on veut rendre le bloc 1030 indisponible alors que les autres blocs sont disponibles:
ReX : Pardon ? Le bloc de données est libre ou pas suivant la carte des blocs libres. Le rendre disponible n'est possible que si un fichier le comportant est détruit.
- Calcul de l'indice de l'octet et du bit offset correspondant :
blockNum = 1030
byteIndex = 1030 / 8 = 128
bitOffset = 7 - 1030 % 8 = 1
Cela signifie que le bit d'intérêt se trouve à la position 2 dans l'octet.
- Calcul du numéro du bloc du super bloc où se trouve l'octet de disponibilité correspondant :
availabilityBlockNum = SUPERBLOCK_START_BLOCK + 128 = 1024 + 128 = 1152
Donc, nous devons accéder à l'octet 1152 pour mettre à jour la disponibilité du bloc 1030.
- Lecture de l'octet existant dans le bloc de disponibilité du super bloc :
- Le contenu de l'octet dans le bloc 1152 avant la mise à jour : 11111111 (en binaire)
- Mise à jour du bit d'offset correspondant :
- Supposons que nous voulions marquer le bloc 1030 comme non disponible (
availability = 0
). - Le bitOffset est 1.
- Nous effectuons une opération AND avec le complément du bit à la position 1 :
11111111 & 11111101 = 11111101
- Supposons que nous voulions marquer le bloc 1030 comme non disponible (
- Écriture de l'octet mis à jour dans le bloc de disponibilité du super bloc :
- Le contenu de l'octet 128 du second superbloc après la mise à jour : 11111101 (en binaire)
ReX : Toujours aussi faux.
Maintenant, le bit d'indice 1 du 128 ème octet du second superbloc est mis à 0, indiquant que le bloc 1030 n'est plus disponible. Les autres bits restent inchangés car nous avons effectué un AND avec un masque binaire qui avait des 1 partout sauf à la position du bit que nous souhaitions mettre à 0.
ReX : Erreur sur erreur.
Fonction setBlockAvailability version 3:
J'ai compris d'où vient l'erreur. La fonction readBlock prends en premier paramètre le numéro du bloc à lire, je ne peux donc pas lui donner la valeur "SUPERBLOCK_START_BLOCK + byteIndex" car byteIndex s'exprime en octet alors qu'on s'attend à un nombre de bloc ici. Il faut donc divisé byteIndex par 256 pour être en unité "bloc", et préciser le bit qu'on veut lire grâce à l'offset.
Pour obtenir l'octet que l'on veut, il faut prendre le reste de la division de byteIndex par 256. On va ensuite stocker cet octet dans notre "buffer".
Pour savoir quel bit modifier dans ce "buffer", on va prendre le reste de la division du bloc dont on veut mettre à jour la disponibilité par 8. On va ainsi pouvoir modifier ce bit grâce à l'algorithme, puis réécrire l'octet à son emplacement d'origine.
void setBlockAvailability(int blockNum, int availability) { int byteIndexInCard = blockNum / 8; //Numéro d'octet dans la carte des blocs libres int blockIndex = byteIndexInCard / 256; //Numéro du bloc dans la carte des blocs libres int byteIndexInBlock = byteIndexInCard % 256; //Numéro d'octet dans le bloc contenant le bit de disponibilité int bitOffset = blockNum % 8; //Numéro du bit dans l'octet n°byteIndexInBlock du bloc blockIndex //indice du bloc contenant le bit à mettre à jour dans le bloc de description int availabilityBlockNum = SUPERBLOCK_START_BLOCK + blockIndex; // Lire l'octet existant dans le bloc de disponibilité du super bloc unsigned char buffer; readBlock(availabilityBlockNum, byteIndexInBlock, &buffer, 1); // Mettre à jour le bit d'offset correspondant : if (availability) { buffer |= (1 << bitOffset); // Mettre le bit à 1 } else { buffer &= ~(1 << bitOffset); // Mettre le bit à 0 } // Écrire l'octet mis à jour dans le bloc de disponibilité du super bloc writeBlock(availabilityBlockNum, byteIndexInBlock, &buffer, 1); }
fonction RM:
void RM(const char *filesystem_path, const char *filename) { unsigned char buffer[BLOCK_SIZE]; int fileNum = -1; // Parcourir les blocs réservés pour la description des fichiers (superbloc) for (int blockNum = 0; blockNum < MAX_FILES_IN_DIRECTORY; blockNum += 16) { unsigned char filenameBuffer[MAX_FILENAME_LENGTH]; readBlock(blockNum, 0, filenameBuffer, MAX_FILENAME_LENGTH); // Vérifier si le bloc contient le nom du fichier recherché if (memcmp(filenameBuffer, filename, MAX_FILENAME_LENGTH) == 0) { // Effacer le nom du fichier dans le superbloc memset(filenameBuffer, 0, MAX_FILENAME_LENGTH); writeBlock(blockNum, 0, filenameBuffer, MAX_FILENAME_LENGTH); unsigned char fileBuffer[MAX_BLOCKS_PER_FILE * 2]; readBlock(blockNum, MAX_FILENAME_LENGTH, fileBuffer, MAX_BLOCKS_PER_FILE * 2); // Libérer les blocs associés au fichier et réinitialiser les numéros de blocs for (int i = 0; i < MAX_BLOCKS_PER_FILE * 2; i += 2) { int blockNum = ((int)fileBuffer[i] << 8) + (int)fileBuffer[i + 1]; if (blockNum == 0) { break; // Fin du fichier } setBlockAvailability(blockNum, 1); // Marquer le bloc comme disponible fileBuffer[i] = 0; fileBuffer[i + 1] = 0; } // Mettre à jour les numéros de blocs associés au fichier writeBlock(blockNum, MAX_FILENAME_LENGTH, fileBuffer, MAX_BLOCKS_PER_FILE * 2); fileNum = blockNum; break; // Fichier trouvé, sortir de la boucle } } // Afficher le résultat de l'opération if (fileNum == -1) { printf("Le fichier \"%s\" n'a pas été trouvé.\n", filename); } else { printf("Le fichier \"%s\" a été supprimé avec succès.\n", filename); } }
ReX : Vous utilisez strcmp
comme strncmp
. C'est bien la dernière qu'il faut utiliser mais en précisant la longueur du nom en paramètre, pas la taille maximale.
ReX : Le parcours des blocs de données du fichier est atroce. Il faut parcourir les numéros de blocs dans le premier bloc du fichier derrière le nom du fichier puis il faut parcourir le 15 autres blocs.
Fonctionnement de la fonction RM:
- Parcours des blocs réservés pour la description des fichiers (superbloc) à partir du bloc 0, en incrémentant de 16 à chaque itération pour accéder aux blocs de noms de fichiers.
ReX : OK.
- Dans chaque bloc de noms de fichiers, la fonction compare le nom de fichier recherché avec les noms stockés dans les 16 octets de chaque bloc.
ReX : Non la fonction ne fait pas ça par contre il faudrait, oui.
- Si le nom de fichier est trouvé, la fonction efface le nom du fichier dans le superbloc en remplaçant les 16 octets du bloc par des zéros.
ReX : OK.
- Ensuite, la fonction accède aux octets suivants du même bloc pour obtenir les numéros de blocs associés au fichier.
ReX : Non, la boucle sort largement du premier bloc.
- Pour chaque paire de numéros de blocs (2 octets chacun), la fonction convertit les octets en un numéro de bloc. Si le numéro de bloc est nul, cela signifie la fin des blocs associés au fichier, sinon, la fonction marque ce bloc comme disponible en utilisant la fonction
setBlockAvailability
et réinitialise les numéros de blocs dans le tableau à zéro. - Les numéros de blocs réinitialisés sont ensuite réécrits dans le bloc de description du fichier.
ReX : L'idée est là mais vu que la boucle n'est pas bonne, rien ne peut marcher.
- Si le nom de fichier est trouvé, la boucle est arrêtée, et le fichier est supprimé.
ReX : ??
- La fonction affiche ensuite un message indiquant le résultat de l'opération : soit que le fichier a été supprimé avec succès, soit que le fichier n'a pas été trouvé.
Fonction TYPE:
J'ai également commencé à réfléchir à la fonction TYPE. Pour l'instant, elle ne fait qu'ajouter les noms de fichier dans le superbloc. Cela permet de pouvoir tester les fonctions LS et RM (voir dans la rubrique compilation et exécution comment utiliser le programme).
// Fonction pour créer un fichier avec le nom donné et le contenu fourni en entrée standard void TYPE(const char *filesystem_path, const char *filename) { unsigned char buffer[MAX_FILENAME_LENGTH]; // Parcours des blocs réservés pour la description des fichiers for (int blockNum = 0; blockNum <= MAX_FILES_IN_DIRECTORY; blockNum += 16) { readBlock(blockNum, 0, buffer, MAX_FILENAME_LENGTH); // Vérifier si le bloc est vide (pas de nom de fichier) if (buffer[0] == 0) { // Écrire le nom du fichier dans l'emplacement vide du superbloc writeBlock(blockNum, 0, (const unsigned char *)filename, MAX_FILENAME_LENGTH); break; // Fichier créé, sortir de la boucle } } printf("Le fichier \"%s\" a été créé avec succès.\n", filename); }
ReX : Si la boucle se termine sans trouver de slot libre le message de succès s'affiche tout de même.
ReX : Le paramètre filename
n'a pas forcément une taille de MAX_FILENAME_LENGTH
.
Selon moi, la fonction TYPE devra respecter 3 étapes:
- Enregistrement du Nom du Fichier: L'ajout du nom du fichier dans un des blocs de la première partie du superbloc.
- Stockage des Données du Fichier: La récupération des données saisies en entrée standard par l'utilisateur et leur stockage dans des blocs du système de fichiers à partir d'une certaine position, comme le bloc 1040.
- Mise à Jour de la Disponibilité des Blocs: L'indication que les blocs dans lesquels les données ont été écrites ne sont plus disponibles en modifiant les bits correspondants dans la deuxième partie du superbloc (les 16 derniers blocs du super bloc).
ReX : Relis le point 3 et dit moi si tu te comprends toi même ?
Compilation et exécution
Création du système de fichiers:
dd if=/dev/zero of=filesystem.bin bs=256 count=32768
Compilation:
gcc programme.c -o programme
Exécution de LS:
./programme filesystem.bin LS
Exécution de TYPE:
./programme filesystem.bin TYPE mon_fichier.txt
Faire entrer
Ecrire le contenu du fichier
Ctrl D
Exécution de CAT:
./programme filesystem.bin CAT mon_fichier.txt
Exécution de RM:
./programme filesystem.bin RM mon_fichier.txt
Documents Rendus
Fichier:SE4 2022 2023 EC1 V1.tar
30/07/23: j'ai téléverser une nouvelle version de programme.c
contenant les fonctions LS, TYPE et CAT pour que vous puissiez les tester.
ReX : C'est une blague ? Il est où le lien du fichier téléversé ?