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.txt

  • un fichier binaire : head -c 5000 /dev/urandom > p2e1.dat

  • un script shell p2e1.sh que 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 : voir man pour les options physique/symbolique.

  • ls -li : l’option -i affiche 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ée format dans le flux (fichier/stdout/stderr)

  • int fputs(const char *s, FILE *stream) écrit s dans le flux (fichier/stdout/stderr)

  • char *fgets(char *s, int size, FILE *stream) lit au plus size - 1 caractères depuis le flux stream et les place dans s ; la lecture s’arrête après un \n (qui est conservé), puis '\0' est ajouté ; retourne NULL en fin de fichier ou en cas d’erreur

  • int 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 flags O_WRONLY|O_CREAT|O_TRUNC et le mode 0644 (mode obligatoire si création du fichier avec O_CREAT)

  • int open(const char *pathname, int flags) pour ouvrir le fichier avec le flag O_RDONLY pour la lecture

  • ssize_t write(int fd, const void *buf, size_t count) pour écrire dans fd (file descriptor) count octets de buf ; retourne le nombre d’octets écrits (qui peut être inférieur à count, voir p2e11) ou -1

  • ssize_t read(int fd, void *buf, size_t count) lit au plus count octets ; 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é par ptr, dont les éléments ont une taille de size octets (ici sizeof(int32_t)) et sont au nombre de nmemb (le nombre d’éléments dans le tableau), dans le flux stream ; retourne le nombre d’éléments écrits

  • size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) lit au plus nmemb éléments de size octets depuis le flux stream et les place dans ptr ; retourne le nombre d’éléments lus

  • int 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) avec amode = F_OK, retourne 0 si le fichier path existe

  • int rename(const char *old, const char *new) (#include <stdio.h>, libc) renomme old en new (et écrase new s’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 flag O_RDONLY pour ouvrir le fichier en lecture

  • off_t lseek(int fd, off_t offset, int whence) pour sauter les 15 premiers octets (offset = 15), avec whence = SEEK_SET (décalage compté depuis le début du fichier) ; off_t est un entier représentant une position dans un fichier

  • ssize_t read(int fd, void *buf, size_t count) lit au plus count octets depuis fd et les place dans buf

  • int 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") et fclose

  • ssize_t getline(char **lineptr, size_t *n, FILE *stream) lit la prochaine ligne du flux stream (avec le \n) et la place dans *lineptr ; retourne le nombre de caractères lus, ou -1 en fin de fichier ou en cas d’erreur. Si *lineptr vaut NULL (et *n vaut 0), getline alloue un tampon et met à jour *lineptr et *n ; le tampon est agrandi si nécessaire. Il faut le libérer avec free, même quand getline renvoie -1

  • int feof(FILE *stream) et int ferror(FILE *stream) : après un -1, permettent de savoir si on est à la fin du fichier (feof non nul) ou s’il y a eu une erreur (ferror non 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 si s1 et s2 sont égaux

  • long strtol(const char *nptr, char **endptr, int base) retourne un long correspondant au nombre écrit dans nptr avec la base donnée ; *endptr pointe 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 vides rep_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 avec errno == EEXIST si le répertoire existe déjà

  • Création fichier : open(path, O_WRONLY|O_CREAT|O_TRUNC, 0644) puis close

  • 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 comme printf mais écrit au plus size caractères ('\0' compris) dans str ; retourne le nombre de caractères qu’il aurait fallu écrire (une valeur >= size indique 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 flags O_RDWR | O_CREAT et le mode 0644

  • int flock(int fd, int operation) (#include <sys/file.h>) avec LOCK_EX pour prendre le verrou exclusif (attend s’il est déjà pris) et LOCK_UN pour le libérer

  • ssize_t read(int fd, void *buf, size_t count) et ssize_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 valeur

  • int 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