Partie 2 - Fichiers¶
Pour les exercices ne demandant pas d’écrire du code C, créez un fichier Markdown (p2e[numéro exo].md) pour lister les commandes utilisées, leurs sorties et votre interprétation.
Pour les exercices en C, mettez #define _POSIX_C_SOURCE 200809L en première ligne du fichier (avant les #include) : c’est une bonne pratique, qui garantit que les fonctions et constantes POSIX sont déclarées même avec -std=c2x (voir la note du cours).
Note
Si vous travaillez dans le dossier partagé avec Windows, certaines opérations ne fonctionnent pas comme sous Linux : la création de liens échoue et le nombre de blocs affiché par stat peut différer.
Faites alors les exercices dans votre répertoire personnel (cd ~).
p2e1 - Métadonnées et types (shell)¶
Créez 3 fichiers :
un fichier texte :
echo "hello" > p2e1.txt,un fichier binaire :
head -c 5000 /dev/urandom > p2e1.dat,un script shell
p2e1.sh, créé avec votre éditeur, contenant :#!/usr/bin/env bash echo "coucou"
La première ligne (le shebang) indique quel interpréteur exécute le script (ici
bash). Rendez ensuite le script exécutable avecchmod +x p2e1.sh.
Affichez leurs métadonnées avec stat et leur nature avec file.
Comparez les champs clés (type, taille, blocs, droits) et répondez aux questions :
Quel est le type de contenu de chaque fichier d’après
file?Quelle différence de droits y a-t-il entre le script et les deux autres fichiers ?
Si les droits observés diffèrent de ceux du cours (par exemple rw-rw-r-- au lieu de rw-r--r--), c’est dû au masque de création de votre machine (voir Droits à la création : notation octale et umask dans le cours).
Commandes :
stat FICHIER: taille logique, blocs, mode (permissions), inode,file FICHIER: type de contenu (texte ASCII, binaire, script, etc.).
p2e2 - Liens physiques et symboliques (shell)¶
Créez un fichier p2e2_source.txt contenant une ligne de texte (echo "coucou" > p2e2_source.txt).
Créez un lien symbolique p2e2_sym.txt et un lien physique p2e2_hard.txt vers p2e2_source.txt.
Observez la sortie de ls -li et de stat pour les 3 fichiers.
Modifiez le contenu de p2e2_source.txt depuis le shell (echo "modif" >> p2e2_source.txt), puis affichez le contenu des deux liens.
Supprimez le fichier p2e2_source.txt, puis observez à nouveau ls -li, stat et cat sur p2e2_hard.txt et p2e2_sym.txt.
Répondez aux questions :
Quels fichiers ont le même numéro d’inode ?
Combien de liens (
Liens :) a chaque fichier ?Quelle est la taille du lien symbolique et à quoi correspond-elle ?
Que devient chaque lien après la suppression de
p2e2_source.txt?
Commandes :
ln CIBLE NOM_DU_LIEN(lien physique),ln -s CIBLE NOM_DU_LIEN(lien symbolique),ls -li: l’option-iaffiche le numéro d’inode de chaque fichier.
Schéma : répertoire, inodes et liens
Le schéma ci-dessous reprend la même démarche avec d’autres noms : un fichier original.txt, un lien physique lien_physique.txt et un lien symbolique lien_symbolique.txt, avant et après rm original.txt.
Un répertoire est une table nom → numéro d’inode : le nom du fichier n’est pas dans l’inode.
Un lien physique est une deuxième entrée de répertoire vers le même inode : le nombre de liens de l’inode passe à 2.
Un lien symbolique est un autre fichier (avec son propre inode) qui contient le chemin de la cible : sa taille est la longueur de ce chemin (
original.txt: 12 octets).rmsupprime une entrée de répertoire et diminue le nombre de liens : les données ne sont libérées que lorsque ce nombre atteint 0 (et que plus aucun programme n’a ouvert le fichier). Aprèsrm original.txt,lien_physique.txtdonne toujours accès aux données, maislien_symbolique.txtdésigne un nom qui n’existe plus : c’est un lien cassé.
p2e3 - E/S texte (libc)¶
Écrivez un programme p2e3.c qui utilise les fonctions de la libc pour manipuler les fichiers (fopen, fclose…). Il doit ouvrir un fichier p2e3_anneau.txt en écriture et écrire :
Un Anneau pour les gouverner tous,
un Anneau pour les trouver,
un Anneau pour les amener tous et dans les ténèbres les lier.
Puis le programme ferme le fichier, le rouvre en lecture et affiche son contenu ligne par ligne sur la sortie standard : chaque ligne est lue avec fgets et affichée avec fputs(ligne, stdout) (ou printf).
Fonctions :
FILE *fopen(const char *pathname, const char *mode)pour ouvrir le fichier en écriture ou en lecture ("w"/"r"),int fprintf(FILE *stream, const char *format, ...)pour écrire une chaîne formatéeformatdans le flux (fichier/stdout/stderr),int fputs(const char *s, FILE *stream)écritsdans le flux (fichier/stdout/stderr),char *fgets(char *s, int size, FILE *stream)lit au plussize - 1caractères depuis le fluxstreamet les place danss; la lecture s’arrête après un\n(qui est conservé), puis'\0'est ajouté ; retourneNULLen fin de fichier ou en cas d’erreur,int fclose(FILE *stream)ferme le flux.
p2e4 - E/S texte (POSIX)¶
Écrivez un programme p2e4.c qui utilise les fonctions POSIX pour manipuler les fichiers (open, close…). Il doit ouvrir un fichier p2e4_nazg.txt en écriture et écrire :
Ash nazg durbatulûk,
ash nazg gimbatul,
ash nazg thrakatulûk agh burzum-ishi krimpatul
Puis le programme ferme le fichier, le rouvre en lecture, lit son contenu avec read et l’affiche sur la sortie standard avec write(STDOUT_FILENO, …) (ou write(1, …)).
Pour les includes :
#define _POSIX_C_SOURCE 200809L // utilise la version de 2008 de POSIX
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
Fonctions :
int open(const char *pathname, int flags)ouint open(const char *pathname, int flags, mode_t mode)pour ouvrir le fichier : à la création, utilisez les flagsO_WRONLY | O_CREAT | O_TRUNCet le mode0644(le mode est obligatoire avecO_CREAT) ; pour la lecture, utilisez le flagO_RDONLY, sans mode,ssize_t write(int fd, const void *buf, size_t count)pour écrire dansfd(file descriptor)countoctets debuf; retourne le nombre d’octets écrits (qui peut être inférieur àcount) ou-1,ssize_t read(int fd, void *buf, size_t count)lit au pluscountoctets ; lectures partielles possibles → boucler jusqu’à0(fin de fichier) ; retourne-1en cas d’erreur (voir read),int close(int fd).
ssize_t est un size_t signé (il peut valoir -1), mode_t représente les droits du fichier (voir le cours).
write peut écrire moins d’octets que demandé, ou être interrompu par un signal (errno == EINTR, les signaux seront vus plus tard) : la fonction write_full du cours répète les appels à write jusqu’à ce que tout soit écrit.
Copiez-la dans votre programme et utilisez-la à la place de write (elle sera réutilisée dans les parties suivantes).
Avertissement
read n’ajoute pas de '\0' à la fin des octets lus : n’affichez pas le buffer avec printf("%s", buf), utilisez le nombre d’octets renvoyé par read.
Question : si le programme ouvrait le fichier une seule fois avec O_RDWR, écrivait le texte, puis appelait read sans fermer ni rouvrir le fichier, que lirait read ? Pourquoi (voir Position courante : lseek dans le cours) ?
p2e5 - E/S binaire (libc)¶
Écrivez un programme p2e5.c qui initialise un tableau d”int32_t avec {INT32_MIN, INT32_MIN + 1, -2048, -1, 0, 1, 2048, INT32_MAX - 1, INT32_MAX} (int32_t, INT32_MIN/MAX disponibles avec #include <stdint.h>), puis l’écrit dans un fichier binaire p2e5.dat via fwrite.
Puis rouvrez le fichier, lisez les valeurs avec fread, affichez-les et vérifiez qu’elles sont identiques au tableau initial.
Vérifiez la taille du fichier avec la commande stat et comparez-la à sizeof(int32_t) * 9.
Inspectez le contenu du fichier avec xxd -g 1 p2e5.dat (ou hexdump -C p2e5.dat) (octets bruts en hexadécimal, -g 1 sépare chaque octet ; par défaut xxd les groupe deux par deux), et avec od -A d -t d4 p2e5.dat (octets regroupés par 4 et affichés comme entiers signés : on doit retrouver les valeurs du tableau ; -A d affiche les décalages en décimal, sinon od les affiche en octal).
xxd affiche les octets dans l’ordre du fichier (ici l’ordre mémoire, puisque fwrite copie les octets tels quels). En little-endian, un mot 32 bits 0xAABBCCDD est stocké DD CC BB AA. Pour reconstituer la valeur, il faut réassembler les 4 octets dans l’ordre inverse (ou utiliser od/hexdump formatés).
Fonctions :
FILE *fopen(const char *pathname, const char *mode)pour ouvrir le fichier en écriture ou en lecture binaire ("wb"/"rb"),size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)écrit dansstreamnmembéléments desizeoctets chacun, lus à l’adresseptr(icisize = sizeof(int32_t)etnmemb= nombre d’éléments du tableau) ; retourne le nombre d’éléments écrits,size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)lit au plusnmembéléments desizeoctets depuis le fluxstreamet les place dansptr; retourne le nombre d’éléments lus,int fclose(FILE *stream)ferme le flux.
p2e6 - Renommer un fichier (POSIX)¶
Écrivez un programme p2e6.c (usage : ./p2e6 OLD NEW) qui renomme le fichier OLD en NEW si NEW n’existe pas ; sinon, il affiche un message d’erreur.
Fonctions :
int access(const char *path, int amode)(#include <unistd.h>, POSIX) avecamode = F_OK, retourne 0 si le fichierpathexiste,int rename(const char *old, const char *new)(#include <stdio.h>, libc) renommeoldennew(et écrasenews’il existe).
Note
Cette solution n’est pas parfaite : NEW peut être créé par un autre programme entre l’appel à access et l’appel à rename (on parle de race condition).
De plus, access suit les liens symboliques : si NEW est un lien symbolique cassé, access indique qu’il n’existe pas et rename l’écrase.
p2e7 - Lecture partielle (seek POSIX)¶
Écrivez un programme p2e7.c (usage : ./p2e7 FICHIER) qui lit, avec les fonctions POSIX, les 10 octets situés après les 15 premiers du fichier FICHIER, puis les affiche sur la sortie standard avec write, suivis d’un \n.
Avec un fichier contenant :
ABCDEFGHIJKLMNOPQRSTUVWXYZ
(créé avec echo ABCDEFGHIJKLMNOPQRSTUVWXYZ > p2e7.txt)
Le programme affiche :
PQRSTUVWXY
Fonctions :
int open(const char *pathname, int flags)avec le flagO_RDONLYpour ouvrir le fichier en lecture,off_t lseek(int fd, off_t offset, int whence)pour sauter les 15 premiers octets (offset= 15), avecwhence=SEEK_SET(décalage compté depuis le début du fichier) ;off_test un entier représentant une position dans un fichier,ssize_t read(int fd, void *buf, size_t count)lit au pluscountoctets depuisfdet les place dansbuf,ssize_t write(int fd, const void *buf, size_t count)avecfd = STDOUT_FILENO(ou1) pour écrire sur la sortie standard (voir p2e4),int close(int fd)pour fermer le fichier.
p2e8 - cat -n (libc)¶
Écrivez un programme p2e8.c (usage : ./p2e8 FICHIER) qui affiche le contenu du fichier FICHIER en numérotant les lignes (à la manière de cat -n, avec le format ci-dessous).
Chaque ligne est précédée de son numéro, affiché sur au moins 4 caractères (%4d), suivi de : et d’un espace.
Le fichier est lu ligne par ligne avec getline (qui nécessite #define _POSIX_C_SOURCE 200809L en première ligne).
Exemple avec le fichier de p2e3 :
$ ./p2e8 p2e3_anneau.txt
1: Un Anneau pour les gouverner tous,
2: un Anneau pour les trouver,
3: un Anneau pour les amener tous et dans les ténèbres les lier.
Fonctions de la libc :
FILE *fopen(..., "r")etfclose,ssize_t getline(char **lineptr, size_t *n, FILE *stream)lit la prochaine ligne du fluxstream(avec le\n) et la place dans*lineptr; retourne le nombre de caractères lus, ou-1en fin de fichier ou en cas d’erreur. Si*lineptrvautNULL(et*nvaut0),getlinealloue un buffer et met à jour*lineptret*n; le buffer est agrandi si nécessaire. Il faut le libérer avecfree, même quandgetlinerenvoie-1,int feof(FILE *stream)etint ferror(FILE *stream): après un-1, permettent de savoir si on est à la fin du fichier (feofnon nul) ou s’il y a eu une erreur (ferrornon nul).
Appel type de getline :
char *line = NULL; // getline alloue le buffer
size_t cap = 0; // taille du buffer alloué
ssize_t len;
while ((len = getline(&line, &cap, f)) != -1) {
// line contient la ligne lue (avec le \n), len est sa longueur
}
free(line);
p2e9 - head -n (libc)¶
Écrivez un programme p2e9.c (usage : ./p2e9 [-n N] FICHIER) qui affiche les N premières lignes du fichier FICHIER (défaut 10 lignes).
N doit être un entier positif ou nul ; sinon, affichez un message d’erreur.
Le fichier est lu ligne par ligne comme dans p2e8 (fopen, getline, fclose).
Fonctions :
int strcmp(const char *s1, const char *s2)retourne 0 sis1ets2sont égaux,INT_MAX(#include <limits.h>) : plus grande valeur d’unint, pour vérifier queNtient dans unint,long strtol(const char *nptr, char **endptr, int base)retourne unlongcorrespondant au nombre écrit dansnptravec la base donnée ;*endptrpointe ensuite sur le premier caractère non converti, ce qui permet de détecter une chaîne qui n’est pas un nombre ; si le nombre est trop grand pour unlong,strtolrenvoieLONG_MAX(ouLONG_MIN) et meterrnoàERANGE(#include <errno.h>) :
char *end;
errno = 0;
long v = strtol(argv[i], &end, 10);
if (end == argv[i] || *end != '\0' || errno == ERANGE) {
// argv[i] n'est pas un nombre entier, ou il est trop grand
}
p2e10 - Créer une arborescence + liens (POSIX)¶
Écrivez un programme p2e10.c (usage : ./p2e10 rep_name nb_d nb_f, on suppose nb_d >= 1 et nb_f >= 2) qui crée :
un répertoire
rep_name,pour chaque
d ∈ [1..nb_d]: un répertoirerep_name/sous_rep_d/,pour chaque
det chaquef ∈ [1..nb_f]: un fichier viderep_name/sous_rep_d/sous_rep_d_fichier_f.txt,au niveau racine
rep_name/:un lien physique
lien_physique_vers_sous_rep_1_fichier_1.txt→sous_rep_1/sous_rep_1_fichier_1.txt,un lien symbolique
lien_symbolique_vers_sous_rep_1_fichier_2.txt→sous_rep_1/sous_rep_1_fichier_2.txt.
Par exemple, ./p2e10 rep 2 3 crée :
$ tree rep
rep
├── lien_physique_vers_sous_rep_1_fichier_1.txt
├── lien_symbolique_vers_sous_rep_1_fichier_2.txt -> sous_rep_1/sous_rep_1_fichier_2.txt
├── sous_rep_1
│ ├── sous_rep_1_fichier_1.txt
│ ├── sous_rep_1_fichier_2.txt
│ └── sous_rep_1_fichier_3.txt
└── sous_rep_2
├── sous_rep_2_fichier_1.txt
├── sous_rep_2_fichier_2.txt
└── sous_rep_2_fichier_3.txt
2 directories, 8 files
Si le programme est relancé alors que les éléments existent déjà, il doit afficher un message d’erreur et se terminer avec un code d’erreur.
Fonctions (symlink nécessite #define _POSIX_C_SOURCE 200809L en première ligne) :
int mkdir(const char *path, mode_t mode)(#include <sys/stat.h>, ex. :mode = 0755, voir Droits à la création : notation octale et umask dans le cours pour la notation octale) ; échoue avecerrno == EEXISTsi le répertoire existe déjà,création d’un fichier :
open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644)puisclose,liens (
#include <unistd.h>) :int link(const char *oldpath, const char *newpath)(physique),int symlink(const char *target, const char *linkpath)(symbolique),
conversion des arguments en nombres :
strtol(voir p2e9),construction de chemins :
int snprintf(char *str, size_t size, const char *format, ...)(#include <stdio.h>) fonctionne commeprintfmais écrit au plussizecaractères ('\0'compris) dansstr; retourne le nombre de caractères qu’il aurait fallu écrire (une valeur>= sizeindique que le résultat a été tronqué) :
char path[256];
int d = 1;
int ret = snprintf(path, sizeof path, "%s/sous_rep_%d", "rep", d);
if (ret < 0 || (size_t)ret >= sizeof path) {
// erreur ou chemin tronqué
}
// path contient "rep/sous_rep_1"
Note
Pour link, oldpath est relatif au répertoire courant (il faut donc écrire rep_name/sous_rep_1/…).
Pour symlink, target est stocké tel quel dans le lien et sera interprété relativement au répertoire qui contient le lien : il faut donc écrire sous_rep_1/sous_rep_1_fichier_2.txt (et non rep_name/sous_rep_1/…, qui donnerait un lien cassé).
p2e11 - Copier un fichier (POSIX)¶
Écrivez un programme p2e11.c (usage : ./p2e11 SOURCE DEST) qui copie SOURCE vers DEST en utilisant les appels système POSIX de bas niveau.
Fonctions :
int open(const char *pathname, int flags)ouint open(const char *pathname, int flags, mode_t mode):source :
open(pathname, O_RDONLY),destination :
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, 0644),
ssize_t read(int fd, void *buf, size_t count),ssize_t write(int fd, const void *buf, size_t count),int close(int fd).
Utilisez un buffer (char buf[]) de 8192 octets : un buffer de cette taille limite le nombre d’appels système, et choisir un multiple de la taille d’E/S préférée (Blocs d'E/S : 4096 dans la sortie de stat) rend chaque appel efficace.
Pour les écritures, réutilisez la fonction write_full du cours (comme dans p2e4).
Avertissement
Avec O_TRUNC, ./p2e11 f f vide le fichier f avant de le lire : le contenu est perdu. Ne testez pas avec la même source et la même destination.
Testez avec un fichier de plus de 8192 octets et vérifiez que tout est copié :
# pour télécharger Dracula de Bram Stoker (domaine public - Project Gutenberg)
wget https://www.gutenberg.org/cache/epub/345/pg345.txt
Avec strace -e trace=read,write (qui n’affiche que les appels read et write), observez les appels read(3, …, 8192) = 8192 et write(4, …, 8192) = 8192 qui se répètent, puis le dernier read qui renvoie 0 (fin de fichier).
$ # -Wconversion signale les conversions implicites qui peuvent perdre de l'information
$ gcc -std=c2x -Wall -Wextra -pedantic -Wconversion -g p2/p2e11.c
$ strace -e trace=read,write ./a.out pg345.txt dracula.txt
...
...
$ # si les valeurs de sha256sum sont les mêmes, le contenu est le même
$ sha256sum dracula.txt pg345.txt
<empreinte> dracula.txt
<empreinte> pg345.txt
$ # si cmp n'affiche rien, les fichiers sont les mêmes
$ cmp dracula.txt pg345.txt