Répertoires et liens sous Linux

Ce forum est dédié à apprendre le développement de programmes user mode sur Linux

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 46
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Répertoires et liens sous Linux

Post by Hydraxx »

Répertoires et liens sous Linux

Objectif du cours

Ce cours explique comment Linux représente les fichiers, les répertoires, les chemins, les liens physiques, les liens symboliques, et comment utiliser les principales fonctions système associées :
  • link()
  • unlink()
  • symlink()
  • readlink()
  • stat() / lstat()
  • rename()
  • mkdir()
  • rmdir()
  • remove()
  • opendir()
  • readdir()
  • closedir()
  • rewinddir()
  • dirfd()
  • nftw()
  • getcwd()
  • chdir()
  • fchdir()
  • openat() et les fonctions *at()
  • chroot()
  • realpath()
  • dirname() / basename()
Le chapitre est fondamental parce qu’il casse une idée fausse fréquente : sous Linux, un fichier n’est pas simplement “son nom”.

Un nom de fichier est seulement une entrée dans un répertoire. Cette entrée pointe vers un inode. L’inode décrit réellement le fichier.

1. Le modèle mental fondamental

Sous Linux, il faut distinguer plusieurs notions :
  • le chemin
  • le nom du fichier
  • l’entrée de répertoire
  • l’inode
  • les données du fichier
  • le descripteur de fichier ouvert
Exemple :

Code: Select all

/home/user/demo.txt
Ce chemin est une chaîne de caractères qui permet au noyau de retrouver un fichier.

Mais le fichier n’est pas vraiment cette chaîne.

Le répertoire /home/user contient une entrée qui ressemble conceptuellement à ceci :

Code: Select all

demo.txt  ->  inode 12345
L’inode 12345 contient ensuite les métadonnées du fichier :
  • type du fichier ;
  • permissions ;
  • propriétaire ;
  • groupe ;
  • taille ;
  • timestamps ;
  • nombre de liens physiques ;
  • emplacement des blocs de données.
Donc le modèle mental correct est :

Code: Select all

chemin -> répertoires traversés -> entrée de répertoire -> inode -> données
Et non pas :

Code: Select all

nom = fichier
Cette différence explique énormément de comportements Linux : link(), unlink(), rename(), les fichiers supprimés mais encore ouverts, les liens physiques, etc.

2. Qu’est-ce qu’un répertoire ?

Un répertoire est un fichier spécial.

Il ne contient pas du texte classique. Il contient une liste d’associations :

Code: Select all

nom -> numéro d’inode
Exemple conceptuel :

Code: Select all

/home/user
    notes.txt      -> inode 1001
    image.png      -> inode 1002
    projet         -> inode 1003
    lien_symbolique -> inode 1004
Quand un programme appelle :

Code: Select all

open("/home/user/notes.txt", O_RDONLY);
Le noyau doit résoudre le chemin étape par étape :

Code: Select all

/
 -> home
    -> user
       -> notes.txt
À chaque étape, le noyau lit le répertoire courant de la résolution pour trouver l’entrée suivante.

3. Chemin absolu et chemin relatif

Un chemin absolu commence par /.

Exemple :

Code: Select all

/home/user/demo.txt
/etc/passwd
/tmp/test.txt
Il est résolu depuis la racine du système de fichiers visible par le processus.

Un chemin relatif ne commence pas par /.

Exemple :

Code: Select all

demo.txt
../test.txt
src/main.c
Il est résolu par rapport au répertoire courant du processus.

Exemple :

Code: Select all

répertoire courant = /home/user/projet
chemin relatif     = src/main.c
chemin réel        = /home/user/projet/src/main.c
4. Les entrées spéciales . et ..

Dans un répertoire, on rencontre souvent deux entrées spéciales :

Code: Select all

.   -> le répertoire courant lui-même
..  -> le répertoire parent
Exemple :

Code: Select all

/home/user/projet
Dans ce dossier :

Code: Select all

.   désigne /home/user/projet
..  désigne /home/user
Quand on parcourt un répertoire avec readdir(), on les ignore souvent :

Code: Select all

if (strcmp(entry->d_name, ".") == 0 ||
    strcmp(entry->d_name, "..") == 0) {
    continue;
}
5. Les liens physiques

Un lien physique, ou hard link, est un autre nom pour le même inode.

Exemple shell :

Code: Select all

echo "hello" > a.txt
ln a.txt b.txt
Après cette opération :

Code: Select all

a.txt -> inode 500
b.txt -> inode 500
Les deux noms pointent vers le même fichier réel.

Si on modifie a.txt, on verra la modification via b.txt, parce que ce sont deux noms pour le même inode.

On peut vérifier avec :

Code: Select all

ls -li
Exemple :

Code: Select all

500 -rw-r--r-- 2 user user 6 a.txt
500 -rw-r--r-- 2 user user 6 b.txt
Le premier nombre est l’inode.

Le 2 indique le nombre de liens physiques vers cet inode.

À retenir :
  • un lien physique ne contient pas un chemin ;
  • il est simplement une autre entrée de répertoire vers le même inode ;
  • il ne copie pas les données ;
  • il ne crée pas un nouveau fichier indépendant ;
  • il ne fonctionne généralement pas entre deux systèmes de fichiers différents ;
  • les utilisateurs normaux ne peuvent généralement pas créer de hard links vers des répertoires.
6. link() — créer un lien physique

Prototype :

Code: Select all

#include <unistd.h>

int link(const char *oldpath, const char *newpath);
oldpath est le nom existant.

newpath est le nouveau nom à créer.

Exemple :

Code: Select all

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    if (link("a.txt", "b.txt") == -1) {
        perror("link");
        return 1;
    }

    printf("Lien physique créé.\n");
    return 0;
}
Après cela :

Code: Select all

a.txt -> même inode
b.txt -> même inode
Piège important :

Code: Select all

link("a.txt", "b.txt");
ne copie pas a.txt.

Cela ajoute seulement une nouvelle entrée de répertoire.

7. unlink() — supprimer un nom

Prototype :

Code: Select all

#include <unistd.h>

int unlink(const char *pathname);
unlink() supprime une entrée de répertoire.

Il ne faut pas penser :

Code: Select all

unlink() détruit toujours immédiatement le fichier
Il faut penser :

Code: Select all

unlink() retire un nom qui pointait vers un inode
Exemple :

Code: Select all

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    if (unlink("a.txt") == -1) {
        perror("unlink");
        return 1;
    }

    printf("Nom supprimé.\n");
    return 0;
}
Si on avait :

Code: Select all

a.txt -> inode 500
b.txt -> inode 500
Après :

Code: Select all

unlink("a.txt");
On obtient :

Code: Select all

b.txt -> inode 500
Le fichier existe encore, car il reste un nom vers l’inode.

L’inode et les blocs de données sont réellement libérés seulement quand :
  • le nombre de liens physiques tombe à 0 ;
  • aucun processus ne garde encore le fichier ouvert.
8. Fichier supprimé mais encore ouvert

C’est un point très important.

Exemple :

Code: Select all

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
    int fd = open("temp.txt", O_CREAT | O_RDWR | O_TRUNC, 0600);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    write(fd, "hello\n", 6);

    if (unlink("temp.txt") == -1) {
        perror("unlink");
        close(fd);
        return 1;
    }

    /* Le nom temp.txt n'existe plus dans le répertoire. */
    /* Mais le fichier existe encore tant que fd est ouvert. */

    write(fd, "world\n", 6);

    close(fd);

    /* Maintenant, si aucun autre descripteur n'existe,
       le fichier peut être réellement libéré. */

    return 0;
}
Après unlink(), le fichier n’a plus de nom visible dans le répertoire.

Mais il reste accessible via fd.

C’est souvent utilisé pour créer des fichiers temporaires qui disparaissent automatiquement quand le processus termine ou ferme le descripteur.

9. Les liens symboliques

Un lien symbolique, ou symlink, est différent d’un lien physique.

Un lien symbolique est un fichier spécial qui contient un chemin.

Exemple shell :

Code: Select all

ln -s original.txt lien.txt
Modèle mental :

Code: Select all

lien.txt contient le texte "original.txt"
Donc :

Code: Select all

lien.txt -> "original.txt"
original.txt -> inode 700
Un symlink ne pointe pas directement vers l’inode. Il pointe vers un chemin.

Comparaison :

Code: Select all

Lien physique :
    a.txt -> inode 100
    b.txt -> inode 100

Lien symbolique :
    lien.txt contient "a.txt"
    a.txt -> inode 100
Conséquence importante :

Si la cible du lien symbolique disparaît, le lien symbolique peut devenir cassé.

10. symlink() — créer un lien symbolique

Prototype :

Code: Select all

#include <unistd.h>

int symlink(const char *target, const char *linkpath);
Exemple :

Code: Select all

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    if (symlink("original.txt", "lien.txt") == -1) {
        perror("symlink");
        return 1;
    }

    printf("Lien symbolique créé.\n");
    return 0;
}
Cela crée lien.txt, qui contient le chemin :

Code: Select all

original.txt
Piège important :

La cible n’a pas besoin d’exister au moment de la création du lien symbolique.

Exemple :

Code: Select all

symlink("fichier_inexistant.txt", "lien.txt");
Cela peut réussir.

Mais le lien sera cassé tant que fichier_inexistant.txt n’existe pas.

11. readlink() — lire la cible d’un lien symbolique

Prototype :

Code: Select all

#include <unistd.h>

ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
readlink() lit le contenu d’un lien symbolique.

Elle ne lit pas les données de la cible.

Elle lit le chemin stocké dans le lien.

Exemple :

Code: Select all

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    char buffer[4096];
    ssize_t n;

    n = readlink("lien.txt", buffer, sizeof(buffer) - 1);
    if (n == -1) {
        perror("readlink");
        return 1;
    }

    buffer[n] = '\0';

    printf("Le lien pointe vers : %s\n", buffer);
    return 0;
}
Piège classique :

readlink() ne met pas automatiquement le caractère nul final \0.

Il faut le faire soi-même :

Code: Select all

buffer[n] = '\0';
12. stat() et lstat()

Ces deux fonctions servent à obtenir des informations sur un fichier.

Mais leur comportement diffère avec les liens symboliques.

stat() suit le lien symbolique.

lstat() inspecte le lien symbolique lui-même.

Prototype :

Code: Select all

#include <sys/stat.h>

int stat(const char *pathname, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
Exemple :

Code: Select all

original.txt -> inode 100
lien.txt     -> "original.txt"
Si on fait :

Code: Select all

stat("lien.txt", &st);
On obtient les informations de original.txt.

Si on fait :

Code: Select all

lstat("lien.txt", &st);
On obtient les informations du lien symbolique lui-même.

Exemple de code :

Code: Select all

#include <stdio.h>
#include <sys/stat.h>

int main(void)
{
    struct stat st;

    if (lstat("lien.txt", &st) == -1) {
        perror("lstat");
        return 1;
    }

    if (S_ISLNK(st.st_mode)) {
        printf("C'est un lien symbolique.\n");
    }

    return 0;
}
À retenir :

Code: Select all

stat()  -> suit le lien
lstat() -> regarde le lien lui-même
13. Liens symboliques cassés

Un lien symbolique cassé est un lien dont la cible n’existe plus.

Exemple :

Code: Select all

echo "hello" > original.txt
ln -s original.txt lien.txt
rm original.txt
Maintenant :

Code: Select all

lien.txt -> "original.txt"
Mais original.txt n’existe plus.

Le lien symbolique existe encore, mais il est cassé.

On appelle cela un dangling symlink.

14. rename() — renommer ou déplacer

Prototype :

Code: Select all

#include <stdio.h>

int rename(const char *oldpath, const char *newpath);
rename() renomme ou déplace un fichier.

Exemple :

Code: Select all

rename("ancien.txt", "nouveau.txt");
Sur le même système de fichiers, les données ne sont pas recopiées.

Le noyau modifie surtout les entrées de répertoire.

Modèle mental :

Code: Select all

avant :
    ancien.txt -> inode 300

après :
    nouveau.txt -> inode 300
L’inode reste le même.

Les blocs de données ne bougent pas.

C’est pour cela que rename() est très rapide.

15. Atomicité de rename()

rename() est important parce que l’opération est généralement atomique.

Cela signifie qu’on n’a pas un état intermédiaire visible où le fichier serait “à moitié renommé”.

C’est très utile pour remplacer un fichier proprement.

Exemple classique :

Code: Select all

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    FILE *f = fopen("config.tmp", "w");
    if (f == NULL) {
        perror("fopen");
        return 1;
    }

    fprintf(f, "option=42\n");
    fclose(f);

    if (rename("config.tmp", "config.txt") == -1) {
        perror("rename");
        return 1;
    }

    return 0;
}
Idée :
  • on écrit dans un fichier temporaire ;
  • quand l’écriture est terminée, on remplace l’ancien fichier par rename() ;
  • les autres programmes voient soit l’ancien fichier, soit le nouveau, mais pas un fichier à moitié écrit.
16. mkdir() — créer un répertoire

Prototype :

Code: Select all

#include <sys/stat.h>
#include <sys/types.h>

int mkdir(const char *pathname, mode_t mode);
Exemple :

Code: Select all

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>

int main(void)
{
    if (mkdir("demo", 0755) == -1) {
        perror("mkdir");
        return 1;
    }

    printf("Répertoire créé.\n");
    return 0;
}
Le paramètre mode indique les permissions souhaitées.

Exemple :

Code: Select all

0755
Signifie généralement :

Code: Select all

propriétaire : lecture + écriture + exécution
 groupe      : lecture + exécution
 autres      : lecture + exécution
Pour un répertoire, le bit x signifie qu’on peut le traverser.

Piège : umask

Les permissions finales sont influencées par umask.

Si tu demandes :

Code: Select all

mkdir("demo", 0777);
et que l’umask vaut :

Code: Select all

0022
les permissions finales seront souvent :

Code: Select all

0755
17. rmdir() — supprimer un répertoire vide

Prototype :

Code: Select all

#include <unistd.h>

int rmdir(const char *pathname);
rmdir() supprime un répertoire, mais uniquement s’il est vide.

Exemple :

Code: Select all

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    if (rmdir("demo") == -1) {
        perror("rmdir");
        return 1;
    }

    printf("Répertoire supprimé.\n");
    return 0;
}
Si demo contient des fichiers, rmdir() échoue.

Pour supprimer un arbre complet, il faut :
  • ouvrir le répertoire ;
  • parcourir ses entrées ;
  • supprimer les fichiers avec unlink() ;
  • supprimer récursivement les sous-répertoires ;
  • terminer par rmdir() sur le dossier parent.
18. remove() — supprimer fichier ou répertoire vide

Prototype :

Code: Select all

#include <stdio.h>

int remove(const char *pathname);
remove() est une fonction de la bibliothèque C.

Elle peut supprimer :
  • un fichier ;
  • un répertoire vide.
Modèle simple :

Code: Select all

si pathname est un fichier     -> comportement proche de unlink()
si pathname est un répertoire  -> comportement proche de rmdir()
Exemple :

Code: Select all

#include <stdio.h>

int main(void)
{
    if (remove("test.txt") == -1) {
        perror("remove");
        return 1;
    }

    return 0;
}
Différence entre unlink(), rmdir() et remove()

Code: Select all

unlink() -> supprime un nom de fichier
rmdir()  -> supprime un répertoire vide
remove() -> supprime un fichier ou un répertoire vide
Attention :

remove() ne supprime pas récursivement un répertoire non vide.

19. Lire un répertoire avec opendir(), readdir(), closedir()

Pour parcourir un répertoire en C, on utilise :

Code: Select all

#include <dirent.h>

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);
Exemple minimal :

Code: Select all

#include <stdio.h>
#include <dirent.h>

int main(void)
{
    DIR *dirp;
    struct dirent *entry;

    dirp = opendir(".");
    if (dirp == NULL) {
        perror("opendir");
        return 1;
    }

    while ((entry = readdir(dirp)) != NULL) {
        printf("%s\n", entry->d_name);
    }

    closedir(dirp);
    return 0;
}
opendir() ouvre un flux de répertoire.

readdir() lit les entrées une par une.

closedir() ferme le flux.

20. struct dirent

readdir() retourne un pointeur vers une structure struct dirent.

Elle contient notamment :

Code: Select all

ino_t d_ino;
char  d_name[];
Le champ le plus utilisé est :

Code: Select all

d_name
Il contient le nom de l’entrée.

Exemple :

Code: Select all

.
..
main.c
demo.txt
include
Attention :

readdir() ne donne pas toutes les informations du fichier.

Pour obtenir la taille, les permissions, le type exact, etc., il faut souvent appeler :

Code: Select all

stat()
lstat()
fstatat()
21. rewinddir() — revenir au début du répertoire

Prototype :

Code: Select all

#include <dirent.h>

void rewinddir(DIR *dirp);
rewinddir() replace le flux de répertoire au début.

Exemple :

Code: Select all

#include <stdio.h>
#include <dirent.h>

int main(void)
{
    DIR *dirp;
    struct dirent *entry;

    dirp = opendir(".");
    if (dirp == NULL) {
        perror("opendir");
        return 1;
    }

    printf("Premier parcours :\n");
    while ((entry = readdir(dirp)) != NULL) {
        printf("%s\n", entry->d_name);
    }

    rewinddir(dirp);

    printf("Deuxième parcours :\n");
    while ((entry = readdir(dirp)) != NULL) {
        printf("%s\n", entry->d_name);
    }

    closedir(dirp);
    return 0;
}
Cela sert quand on veut relire un répertoire depuis le début sans le fermer et le rouvrir.

22. dirfd() — récupérer le descripteur associé à un DIR *

Prototype :

Code: Select all

#include <dirent.h>

int dirfd(DIR *dirp);
dirfd() retourne le descripteur de fichier associé à un flux de répertoire.

Exemple :

Code: Select all

#include <stdio.h>
#include <dirent.h>
#include <unistd.h>

int main(void)
{
    DIR *dirp = opendir(".");
    if (dirp == NULL) {
        perror("opendir");
        return 1;
    }

    int fd = dirfd(dirp);
    if (fd == -1) {
        perror("dirfd");
        closedir(dirp);
        return 1;
    }

    printf("Descripteur du répertoire : %d\n", fd);

    closedir(dirp);
    return 0;
}
Ce descripteur est utile avec les fonctions *at(), comme :
  • openat()
  • fstatat()
  • unlinkat()
  • mkdirat()
  • renameat()
23. Parcours récursif manuel d’un répertoire

Un parcours récursif manuel ressemble à ceci :

Code: Select all

opendir()
readdir()
ignorer . et ..
lstat()
si fichier -> traiter
si répertoire -> appeler récursivement la fonction
closedir()
Exemple simplifié :

Code: Select all

#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <limits.h>

static void walk(const char *path)
{
    DIR *dirp;
    struct dirent *entry;

    dirp = opendir(path);
    if (dirp == NULL) {
        perror(path);
        return;
    }

    while ((entry = readdir(dirp)) != NULL) {
        char fullpath[PATH_MAX];
        struct stat st;

        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name);

        if (lstat(fullpath, &st) == -1) {
            perror(fullpath);
            continue;
        }

        printf("%s\n", fullpath);

        if (S_ISDIR(st.st_mode)) {
            walk(fullpath);
        }
    }

    closedir(dirp);
}

int main(void)
{
    walk(".");
    return 0;
}
Pourquoi lstat() ici ?

Parce qu’on ne veut pas suivre aveuglément les liens symboliques.

Un lien symbolique peut créer une boucle ou pointer vers un endroit inattendu.

24. nftw() — parcourir une arborescence

nftw() signifie souvent new file tree walk.

Cette fonction parcourt une arborescence de fichiers et appelle une fonction callback pour chaque élément rencontré.

Prototype simplifié :

Code: Select all

#define _XOPEN_SOURCE 500
#include <ftw.h>

int nftw(const char *dirpath,
         int (*func)(const char *pathname,
                     const struct stat *statbuf,
                     int typeflag,
                     struct FTW *ftwbuf),
         int nopenfd,
         int flags);
Au lieu de faire manuellement :

Code: Select all

opendir()
readdir()
lstat()
récursion
closedir()
nftw() le fait pour toi.

Exemple minimal :

Code: Select all

#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <ftw.h>

static int callback(const char *pathname,
                    const struct stat *statbuf,
                    int typeflag,
                    struct FTW *ftwbuf)
{
    (void)statbuf;
    (void)ftwbuf;

    printf("%s\n", pathname);

    return 0;
}

int main(void)
{
    if (nftw(".", callback, 10, FTW_PHYS) == -1) {
        perror("nftw");
        return 1;
    }

    return 0;
}
nopenfd indique combien de descripteurs peuvent être utilisés simultanément pendant le parcours.

flags permet de modifier le comportement du parcours.

25. typeflag dans nftw()

La callback reçoit un paramètre typeflag.

Il indique le type d’objet rencontré.

Valeurs importantes :

Code: Select all

FTW_F
    Fichier normal.

FTW_D
    Répertoire visité avant ses enfants.

FTW_DNR
    Répertoire non lisible.

FTW_DP
    Répertoire visité après ses enfants.

FTW_NS
    stat() impossible sur cet élément.

FTW_SL
    Lien symbolique.

FTW_SLN
    Lien symbolique cassé.
Exemple :

Code: Select all

#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <ftw.h>

static int callback(const char *pathname,
                    const struct stat *statbuf,
                    int typeflag,
                    struct FTW *ftwbuf)
{
    (void)statbuf;
    (void)ftwbuf;

    switch (typeflag) {
    case FTW_F:
        printf("Fichier : %s\n", pathname);
        break;

    case FTW_D:
        printf("Dossier : %s\n", pathname);
        break;

    case FTW_SL:
        printf("Lien symbolique : %s\n", pathname);
        break;

    case FTW_SLN:
        printf("Lien symbolique cassé : %s\n", pathname);
        break;

    default:
        printf("Autre : %s\n", pathname);
        break;
    }

    return 0;
}

int main(void)
{
    nftw(".", callback, 10, FTW_PHYS);
    return 0;
}
26. Flags importants de nftw()

FTW_PHYS

Ne suit pas les liens symboliques.

C’est souvent le choix le plus sûr.

Code: Select all

nftw(".", callback, 10, FTW_PHYS);
FTW_DEPTH

Visite les enfants avant le répertoire parent.

Très utile pour supprimer récursivement :

Code: Select all

fichiers d'abord
sous-répertoires ensuite
répertoire parent à la fin
FTW_MOUNT

Ne traverse pas vers un autre système de fichiers.

FTW_CHDIR

Change le répertoire courant pendant le parcours.

À utiliser avec prudence, surtout dans un programme multi-thread.

27. Exemple avec nftw() : calculer la taille totale

Code: Select all

#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <ftw.h>
#include <sys/stat.h>

static long long total_size = 0;

static int callback(const char *pathname,
                    const struct stat *statbuf,
                    int typeflag,
                    struct FTW *ftwbuf)
{
    (void)pathname;
    (void)ftwbuf;

    if (typeflag == FTW_F) {
        total_size += statbuf->st_size;
    }

    return 0;
}

int main(int argc, char *argv[])
{
    const char *path = ".";

    if (argc > 1) {
        path = argv[1];
    }

    if (nftw(path, callback, 10, FTW_PHYS) == -1) {
        perror("nftw");
        return 1;
    }

    printf("Taille totale : %lld octets\n", total_size);
    return 0;
}
28. Le répertoire courant d’un processus

Chaque processus possède un current working directory, c’est-à-dire un répertoire courant.

Les chemins relatifs sont interprétés par rapport à ce répertoire.

Exemple :

Code: Select all

répertoire courant = /home/user/projet
open("main.c", O_RDONLY)
Le noyau cherche :

Code: Select all

/home/user/projet/main.c
29. getcwd() — connaître le répertoire courant

Prototype :

Code: Select all

#include <unistd.h>

char *getcwd(char *buf, size_t size);
Exemple :

Code: Select all

#include <stdio.h>
#include <unistd.h>
#include <limits.h>

int main(void)
{
    char buffer[PATH_MAX];

    if (getcwd(buffer, sizeof(buffer)) == NULL) {
        perror("getcwd");
        return 1;
    }

    printf("Répertoire courant : %s\n", buffer);
    return 0;
}
getcwd() renvoie le chemin absolu du répertoire courant.

30. chdir() — changer le répertoire courant

Prototype :

Code: Select all

#include <unistd.h>

int chdir(const char *path);
Exemple :

Code: Select all

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    if (chdir("/tmp") == -1) {
        perror("chdir");
        return 1;
    }

    printf("Répertoire courant changé.\n");
    return 0;
}
Après :

Code: Select all

chdir("/tmp");
un appel comme :

Code: Select all

open("test.txt", O_RDONLY);
visera :

Code: Select all

/tmp/test.txt
Attention multi-thread :

Le répertoire courant appartient au processus.

Dans un programme multi-thread, changer le répertoire courant peut perturber les autres threads.

31. fchdir() — changer de répertoire avec un descripteur

Prototype :

Code: Select all

#include <unistd.h>

int fchdir(int fd);
fchdir() change le répertoire courant vers un répertoire déjà ouvert.

Exemple :

Code: Select all

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
    int olddir;

    olddir = open(".", O_RDONLY | O_DIRECTORY);
    if (olddir == -1) {
        perror("open");
        return 1;
    }

    if (chdir("/tmp") == -1) {
        perror("chdir");
        close(olddir);
        return 1;
    }

    printf("On est dans /tmp.\n");

    if (fchdir(olddir) == -1) {
        perror("fchdir");
        close(olddir);
        return 1;
    }

    printf("On est revenu dans l'ancien répertoire.\n");

    close(olddir);
    return 0;
}
C’est utile pour sauvegarder/restaurer un répertoire courant.

32. Les fonctions *at()

Linux propose des fonctions qui travaillent relativement à un descripteur de répertoire :

Code: Select all

openat()
fstatat()
unlinkat()
renameat()
mkdirat()
symlinkat()
readlinkat()
L’idée est de remplacer :

Code: Select all

chemin relatif par rapport au répertoire courant global
par :

Code: Select all

chemin relatif par rapport à un répertoire déjà ouvert
33. openat()

Prototype simplifié :

Code: Select all

#include <fcntl.h>

int openat(int dirfd, const char *pathname, int flags, mode_t mode);
Exemple :

Code: Select all

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
    int dfd;
    int fd;

    dfd = open("/tmp", O_RDONLY | O_DIRECTORY);
    if (dfd == -1) {
        perror("open directory");
        return 1;
    }

    fd = openat(dfd, "test.txt", O_RDONLY);
    if (fd == -1) {
        perror("openat");
        close(dfd);
        return 1;
    }

    printf("/tmp/test.txt ouvert.\n");

    close(fd);
    close(dfd);
    return 0;
}
Ici :

Code: Select all

openat(dfd, "test.txt", O_RDONLY)
signifie :

Code: Select all

ouvre test.txt relativement au répertoire représenté par dfd
Donc si dfd représente /tmp, alors on ouvre :

Code: Select all

/tmp/test.txt
34. Pourquoi les fonctions *at() sont importantes

Les fonctions *at() sont importantes pour :
  • éviter de dépendre du répertoire courant global ;
  • écrire du code plus robuste ;
  • réduire certains risques de race conditions ;
  • travailler proprement dans des programmes multi-thread ;
  • garder une référence stable vers un répertoire déjà ouvert.
Problème classique :

Code: Select all

1. Je vérifie un chemin.
2. Quelqu'un modifie le système de fichiers.
3. J'utilise le chemin.
Entre le moment de la vérification et le moment de l’utilisation, la cible peut avoir changé.

C’est le problème TOCTOU :

Code: Select all

Time Of Check To Time Of Use
Les fonctions *at() ne règlent pas magiquement tous les problèmes, mais elles permettent de construire du code beaucoup plus propre et plus sûr.

35. AT_FDCWD

Certaines fonctions *at() acceptent la constante :

Code: Select all

AT_FDCWD
Elle signifie :

Code: Select all

utilise le répertoire courant comme base
Exemple :

Code: Select all

openat(AT_FDCWD, "test.txt", O_RDONLY);
est proche de :

Code: Select all

open("test.txt", O_RDONLY);
36. chroot() — changer la racine visible

Prototype :

Code: Select all

#include <unistd.h>

int chroot(const char *path);
chroot() change la racine visible d’un processus.

Normalement, / désigne la racine du système.

Après :

Code: Select all

chroot("/srv/jail");
le processus voit :

Code: Select all

/  comme  /srv/jail
Donc :

Code: Select all

/etc/passwd
sera résolu comme :

Code: Select all

/srv/jail/etc/passwd
On parle souvent de chroot jail.

37. chroot() n’est pas une sécurité parfaite

chroot() seul ne suffit pas pour faire une sandbox moderne fiable.

Problèmes possibles :
  • le processus peut garder des privilèges élevés ;
  • il peut garder des descripteurs ouverts vers l’extérieur ;
  • l’environnement chroot peut être mal préparé ;
  • des erreurs de configuration peuvent permettre une fuite.
Exemple dangereux :

Code: Select all

int fd = open("/", O_RDONLY | O_DIRECTORY);
chroot("/srv/jail");
fchdir(fd);
Si un programme garde un descripteur vers l’ancien système de fichiers, la jail peut être contournée dans certaines conditions.

Une isolation moderne utilise plutôt une combinaison de mécanismes :
  • namespaces ;
  • capabilities ;
  • seccomp ;
  • AppArmor / SELinux ;
  • containers ;
  • pivot_root ;
  • réduction des privilèges.
38. realpath() — résoudre un chemin canonique

Prototype :

Code: Select all

#include <stdlib.h>

char *realpath(const char *pathname, char *resolved_path);
realpath() transforme un chemin en chemin absolu canonique.

Elle résout :
  • les chemins relatifs ;
  • . ;
  • .. ;
  • les liens symboliques.
Exemple :

Code: Select all

./demo/../test/lien
peut devenir :

Code: Select all

/home/user/test/original.txt
si lien est un lien symbolique vers original.txt.

Exemple de code :

Code: Select all

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

int main(void)
{
    char buffer[PATH_MAX];

    if (realpath(".", buffer) == NULL) {
        perror("realpath");
        return 1;
    }

    printf("Chemin canonique : %s\n", buffer);
    return 0;
}
39. Prudence avec realpath()

realpath() est utile pour afficher ou normaliser un chemin.

Mais en sécurité, il faut rester prudent.

Exemple de problème :

Code: Select all

1. realpath() vérifie un chemin.
2. Un autre processus modifie le chemin.
3. Le programme ouvre ensuite le chemin.
Entre la vérification et l’ouverture, la cible peut avoir changé.

C’est encore un problème de type TOCTOU.

Donc realpath() ne remplace pas une conception sécurisée avec des descripteurs ouverts et des fonctions *at().

40. dirname() et basename()

Ces fonctions découpent un chemin.

Prototypes :

Code: Select all

#include <libgen.h>

char *dirname(char *path);
char *basename(char *path);
dirname() renvoie la partie dossier.

basename() renvoie le dernier composant.

Exemple :

Code: Select all

/home/user/test.txt
Donne :

Code: Select all

dirname  -> /home/user
basename -> test.txt
Exemple de code :

Code: Select all

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <libgen.h>

int main(void)
{
    const char *path = "/home/user/test.txt";
    char *copy1 = strdup(path);
    char *copy2 = strdup(path);

    if (copy1 == NULL || copy2 == NULL) {
        perror("strdup");
        free(copy1);
        free(copy2);
        return 1;
    }

    printf("dirname  : %s\n", dirname(copy1));
    printf("basename : %s\n", basename(copy2));

    free(copy1);
    free(copy2);
    return 0;
}
41. Pièges de dirname() et basename()

Ces fonctions peuvent modifier la chaîne passée en argument.

Donc il ne faut pas faire :

Code: Select all

dirname("/home/user/test.txt");
car c’est une chaîne littérale, potentiellement non modifiable.

Il faut utiliser une chaîne modifiable :

Code: Select all

char path[] = "/home/user/test.txt";
dirname(path);
Ou dupliquer la chaîne avec strdup().

Cas particuliers :

Code: Select all

chemin              dirname        basename
/home/user/test.txt /home/user     test.txt
/home/user/         /home          user
test.txt            .              test.txt
/                   /              /
""                  .              .
42. Tableau récapitulatif des fonctions

Code: Select all

Fonction      Rôle principal                          Piège principal

link()        Créer un lien physique                  Ne copie pas le fichier
unlink()      Supprimer un nom                        Ne détruit pas forcément les données immédiatement
symlink()     Créer un lien symbolique                La cible peut ne pas exister
readlink()    Lire la cible d'un symlink              N'ajoute pas automatiquement \0
stat()        Infos sur la cible                      Suit les liens symboliques
lstat()       Infos sur le lien lui-même              Ne suit pas le symlink
rename()      Renommer/déplacer                       Très rapide sur le même FS car pas de recopie
mkdir()       Créer un répertoire                     Le mode est filtré par umask
rmdir()       Supprimer un répertoire vide            Échoue si non vide
remove()      Supprimer fichier ou dossier vide       Pas récursif
opendir()     Ouvrir un flux de répertoire            À fermer avec closedir()
readdir()     Lire les entrées                        Ne donne pas toutes les métadonnées
closedir()    Fermer un flux de répertoire            Libère les ressources
rewinddir()   Revenir au début du flux                Relit depuis le début
dirfd()       Obtenir le fd d'un DIR *                Utile avec les fonctions *at()
nftw()        Parcourir une arborescence              Attention aux symlinks
getcwd()      Lire le répertoire courant              Peut échouer si buffer trop petit
chdir()       Changer le répertoire courant           Impact global sur le processus
fchdir()      Changer via un fd                       Utile pour restaurer un ancien dossier
openat()      Ouvrir relativement à un dirfd          Plus robuste que chdir()+open()
chroot()      Changer la racine visible               Pas une sécurité parfaite seul
realpath()    Résoudre un chemin canonique            Peut créer un TOCTOU si mal utilisé
dirname()     Obtenir le dossier parent               Peut modifier la chaîne
basename()    Obtenir le dernier composant            Peut modifier la chaîne
43. Erreurs fréquentes du débutant
  • Croire que le nom du fichier est le fichier lui-même.
  • Croire qu’un hard link est un raccourci.
  • Croire qu’un symlink pointe directement vers l’inode.
  • Croire que unlink() détruit toujours les données immédiatement.
  • Oublier qu’un fichier supprimé peut rester vivant s’il est encore ouvert.
  • Confondre stat() et lstat().
  • Utiliser readlink() sans ajouter \0.
  • Penser que remove() supprime récursivement un dossier.
  • Oublier que rmdir() ne supprime que les répertoires vides.
  • Utiliser chdir() sans réfléchir dans un programme multi-thread.
  • Croire que chroot() est une vraie sandbox complète.
  • Utiliser realpath() comme solution magique de sécurité.
44. Mini-projets pour s’entraîner

Projet 1 : ls simplifié
  • Utiliser opendir().
  • Lire les entrées avec readdir().
  • Afficher d_name.
  • Ignorer ou non . et ...
Projet 2 : détecteur de liens symboliques
  • Parcourir un dossier.
  • Appeler lstat() sur chaque entrée.
  • Tester S_ISLNK(st.st_mode).
  • Lire la cible avec readlink().
Projet 3 : détecteur de symlinks cassés
  • Trouver les liens symboliques avec lstat().
  • Tester si stat() échoue sur la cible.
  • Afficher les liens cassés.
Projet 4 : tree simplifié
  • Utiliser opendir() / readdir().
  • Faire une récursion sur les sous-répertoires.
  • Afficher l’indentation selon la profondeur.
Projet 5 : calculateur de taille de dossier
  • Utiliser nftw().
  • Additionner st_size pour les fichiers normaux.
  • Ne pas suivre les symlinks avec FTW_PHYS.
Projet 6 : suppression récursive contrôlée
  • Parcourir avec nftw() et FTW_DEPTH.
  • Supprimer les fichiers avec unlink().
  • Supprimer les dossiers avec rmdir().
  • Ajouter une option “dry-run” avant de supprimer réellement.
45. Schéma mental final

Le modèle final à retenir est :

Code: Select all

chemin
  -> résolution dans les répertoires
      -> entrée de répertoire
          -> inode
              -> métadonnées
              -> blocs de données
Un hard link ajoute un nouveau nom vers le même inode :

Code: Select all

a.txt -> inode 100
b.txt -> inode 100
Un symlink contient un chemin :

Code: Select all

lien.txt -> "a.txt"
a.txt    -> inode 100
unlink() retire un nom :

Code: Select all

a.txt -> inode 100
b.txt -> inode 100

unlink("a.txt")

b.txt -> inode 100
Un fichier n’est réellement libéré que quand :

Code: Select all

nombre de liens physiques == 0
et
aucun descripteur ouvert ne le référence
46. Conclusion

Ce chapitre est essentiel pour comprendre la logique du système de fichiers Linux.

Il faut surtout retenir que Linux ne pense pas seulement en “noms de fichiers”.

Il pense en :
  • chemins ;
  • répertoires ;
  • entrées de répertoire ;
  • inodes ;
  • liens physiques ;
  • liens symboliques ;
  • descripteurs ouverts ;
  • racine visible du processus ;
  • répertoire courant du processus.
Quand on comprend cela, beaucoup de comportements deviennent logiques :
  • pourquoi rename() est rapide ;
  • pourquoi unlink() ne détruit pas toujours immédiatement les données ;
  • pourquoi un fichier supprimé peut rester ouvert ;
  • pourquoi stat() et lstat() ne donnent pas la même chose ;
  • pourquoi un symlink peut être cassé ;
  • pourquoi les fonctions *at() existent ;
  • pourquoi chroot() ne suffit pas à faire une sécurité complète.
Le point central du chapitre peut se résumer ainsi :

Code: Select all

Un nom n'est pas le fichier.
Un chemin n'est pas le fichier.
Un répertoire associe des noms à des inodes.
L'inode représente réellement le fichier.
C’est une base indispensable pour écrire des programmes Linux solides, comprendre les outils système, manipuler les fichiers proprement, et plus tard aborder des sujets plus avancés comme les namespaces, les containers, les systèmes de fichiers virtuels et la sécurité bas niveau.

Who is online

Users browsing this forum: No registered users and 1 guest