Partie 2 - Fichiers¶
Pour les exercices ne demandant pas d’écrire un code C, créez un fichier Markdown (p2e[numéro exo].md) pour lister les commandes utilisées, leurs sorties et votre interprétation.
p2e1 - Métadonnées et types (shell)¶
Créez 3 fichiers :
un fichier texte :
echo "hello" > p2e1.txtun fichier binaire :
head -c 5000 /dev/urandom > p2e1.datun script shell
p2e1.shque vous rendrez exécutable (chmod +x p2e1.sh) contenant :
#!/usr/bin/env bash
echo "coucou"
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?Pourquoi le fichier texte occupe-t-il 8 blocs pour seulement 6 octets ?
Quelle différence de droits y a-t-il entre le script et les deux autres fichiers ?
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)¶
Note
Si vous ne pouvez pas créer les liens dans le dossier courant, c’est probablement parce que vous être dans le dossier en commun avec la partition Windows qui ne permet pas leur création, vous pouvez vous placer dans le home pour faire l’exercice.
Créez un fichier p2e2_source.txt.
Créez un lien symbolique p2e2_sym.txt et un lien physique p2e2_hard.txt vers p2e2_source.txt.
Observez ls -li et stat des 3 fichiers.
Modifiez le contenu de 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.
Commandes :
ln SOURCE DEST: voirmanpour les options physique/symbolique.ls -li: l’option-iaffiche le numéro d’inode de chaque fichier.
p2e3 - E/S fichier (libc)¶
Écrire 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 fermer le fichier, le rouvrir en lecture et afficher son contenu ligne par ligne avec fgets.
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’erreurint fclose(FILE *stream)ferme le flux
p2e4 - E/S texte (POSIX)¶
Écrire 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 fermer le fichier, le rouvrir en lecture, lire son contenu avec read et l’afficher sur la sortie standard avec 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, mode_t mode)pour ouvrir le fichier ; à la création, utiliser les flagsO_WRONLY|O_CREAT|O_TRUNCet le mode0644(mode obligatoire si création du fichier avecO_CREAT)int open(const char *pathname, int flags)pour ouvrir le fichier avec le flagO_RDONLYpour la lecturessize_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, voir p2e11) ou-1ssize_t read(int fd, void *buf, size_t count)lit au pluscountoctets ; lectures partielles possibles → boucler jusqu’à0(fin de fichier)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).
p2e5 - E/S binaire (libc)¶
Écrire 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 rouvrir le fichier, lire les valeurs avec fread, les afficher et vérifier qu’elles sont identiques au tableau initial.
Vérifier la taille du fichier avec la commande stat et la comparer à sizeof(int32_t) * 9.
Inspecter le contenu du fichier avec xxd p2e5.dat ou hexdump -C p2e5.dat (octets bruts en hexadécimal), et avec od -t d4 p2e5.dat (octets regroupés par 4 et affichés comme entiers signés : on doit retrouver les valeurs du tableau).
xxd affiche les octets dans l’ordre mémoire. 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)pour écrire le contenu pointé parptr, dont les éléments ont une taille desizeoctets (icisizeof(int32_t)) et sont au nombre denmemb(le nombre d’éléments dans le tableau), dans le fluxstream; retourne le nombre d’éléments écritssize_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 lusint fclose(FILE *stream)ferme le flux
p2e6 - Renommer un fichier (POSIX)¶
Écrire un programme p2e6.c : p2e6 OLD NEW 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 fichierpathexisteint 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 condition de course).
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)¶
Écrire un programme p2e7 FICHIER qui lit les 10 octets après les 15 premiers du fichier FICHIER avec les fonctions POSIX.
Avec un fichier contenant :
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Le programme affiche :
PQRSTUVWXY
Fonctions :
int open(const char *pathname, int flags)avec le flagO_RDONLYpour ouvrir le fichier en lectureoff_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 fichierssize_t read(int fd, void *buf, size_t count)lit au pluscountoctets depuisfdet les place dansbufint close(int fd)pour fermer le fichier
p2e8 - cat (libc)¶
Écrire un programme p2e8 FICHIER qui affiche le contenu du fichier FICHIER en numérotant les lignes (comme cat -n).
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.
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")etfclosessize_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 tampon et met à jour*lineptret*n; le tampon est agrandi si nécessaire. Il faut le libérer avecfree, même quandgetlinerenvoie-1int 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)
p2e9 - head -n (libc)¶
Écrire un programme p2e9 [-n N] FICHIER qui affiche les N premières lignes du fichier FICHIER (défaut 10 lignes).
Si N n’est pas un nombre valide, afficher un message d’erreur.
Fonctions :
int strcmp(const char *s1, const char *s2)retourne 0 sis1ets2sont égauxlong 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 erreur :
char *end;
long v = strtol(argv[i], &end, 10);
if (end == argv[i] || *end != '\0') {
// argv[i] n'est pas un nombre entier
}
p2e10 - Créer une arborescence + liens (POSIX)¶
Écrire un programme 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]:rep_name/sous_rep_d/,pour chaque
f ∈ [1..nb_f]: des fichiers videsrep_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.
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 :
int mkdir(const char *path, mode_t mode);(#include <sys/stat.h>, ex. :mode = 0755) ; échoue avecerrno == EEXISTsi le répertoire existe déjàCréation fichier :
open(path, O_WRONLY|O_CREAT|O_TRUNC, 0644)puiscloseLiens (
#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)¶
Écrire un programme p2e11 SOURCE DEST qui copie SOURCE vers DEST en utilisant les appels système POSIX de bas niveau.
Fonctions :
int open(const char *path, int flags, ...);Source :
open(path, O_RDONLY)Destination :
open(path, 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 tampon (char buf[]) de 8192 octets (2 pages mémoire de 4 Kio sur x86-64, voir getconf PAGESIZE).
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 ci-dessous répète les appels à write jusqu’à ce que tout soit écrit.
// Active les extensions POSIX récentes (getline, etc.)
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
/**
* Écrit exactement `count` octets de `buf` dans le descripteur `fd`,
* en répétant les appels à write() si nécessaire (cas d'écritures partielles
* ou interruptions par un signal EINTR).
* Retourne le nombre total d'octets écrits, ou -1 en cas d'erreur.
*/
ssize_t write_full(int fd, const void *buf, size_t count) {
size_t done = 0;
while (done < count) {
ssize_t w = write(fd, (const char *)buf + done, count - done);
if (w > 0) {
done += (size_t)w;
} else if (w < 0 && errno == EINTR) {
continue;
} else {
return -1;
}
}
return (ssize_t)done;
}
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "Usage: %s SOURCE DEST\n", argv[0]);
return EXIT_FAILURE;
}
// TODO
return EXIT_SUCCESS;
}
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, 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 ./a.out pg345.txt dracula.txt
...
...
$ # si les valeurs de sha256sum sont les mêmes, le contenu est le même
$ # (la valeur elle-même peut différer si le fichier du Project Gutenberg a été mis à jour)
$ sha256sum dracula.txt pg345.txt
50a66d0773e9476e97bd731947ba03ecf7f7f92eefc59c0027acf48129dc6cbf dracula.txt
50a66d0773e9476e97bd731947ba03ecf7f7f92eefc59c0027acf48129dc6cbf pg345.txt
$ # si cmp n'affiche rien, les fichiers sont les mêmes
$ cmp dracula.txt pg345.txt
p2e12 - Éviter les accès concurrents (POSIX)¶
Écrire un programme p2e12 qui ouvre un fichier p2e12.txt (en le créant s’il n’existe pas ; un fichier vide vaut 0), lit un entier, l’incrémente et réécrit la valeur, en utilisant flock pour empêcher deux instances de modifier le fichier en même temps.
Fonctions :
int open(const char *pathname, int flags, mode_t mode)avec les flagsO_RDWR | O_CREATet le mode0644int flock(int fd, int operation)(#include <sys/file.h>) avecLOCK_EXpour prendre le verrou exclusif (attend s’il est déjà pris) etLOCK_UNpour le libérerssize_t read(int fd, void *buf, size_t count)etssize_t write(int fd, const void *buf, size_t count)long strtol(const char *nptr, char **endptr, int base)pour convertir le texte lu en nombre (voir p2e9)int snprintf(char *str, size_t size, const char *format, ...)pour convertir le nombre en texte (voir p2e10)off_t lseek(int fd, off_t offset, int whence)pour revenir au début du fichier avant d’écrire (voir p2e7)int ftruncate(int fd, off_t length)(#include <unistd.h>) pour vider le fichier (length = 0) avant d’écrire la nouvelle valeurint close(int fd)
Lancez 1000 instances du programme (dont 100 en parallèle) : après les 1000 exécutions, le fichier doit contenir 1000.
Comparez avec une version sans flock.
seq 1000 affiche les nombres de 1 à 1000 (un par ligne) ; xargs lance la commande donnée pour chaque ligne reçue (-I{} : une exécution par ligne, -P100 : jusqu’à 100 exécutions simultanées).
$ rm -f p2e12.txt
$ seq 1000 | xargs -P100 -I{} ./p2e12
$ cat p2e12.txt
1000