File I/O Linux avancé : open, read, write, dup, atomicité et file descriptors

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:

File I/O Linux avancé : open, read, write, dup, atomicité et file descriptors

Post by Hydraxx »

File I/O : détails avancés, atomicité, descriptors, duplication et I/O modernes


Objectif du chapitre

Ce chapitre ne parle pas simplement de “lire et écrire dans un fichier”.

Il explique surtout comment Linux pense réellement les fichiers au niveau système :
  • un fichier ouvert n’est pas seulement un chemin ;
  • un file descriptor n’est pas le fichier lui-même ;
  • plusieurs descriptors peuvent partager le même état kernel ;
  • certaines opérations sont atomiques ;
  • certaines opérations peuvent provoquer des race conditions ;
  • les offsets peuvent être partagés ou indépendants ;
  • les I/O peuvent être bloquantes, non bloquantes, vectorisées ou positionnelles ;
  • les fichiers temporaires doivent être créés avec des fonctions sûres.
Ce chapitre est important parce qu’il pose les bases de beaucoup de domaines :
  • programmation système Linux ;
  • shells ;
  • serveurs ;
  • pipes ;
  • redirections ;
  • sockets ;
  • bases de données ;
  • sandboxing ;
  • sécurité système ;
  • programmation concurrente.

1. Rappel : open(), read(), write(), close()

Avant d’entrer dans les détails avancés, il faut rappeler la base.

Sous Linux, l’ouverture d’un fichier se fait généralement avec :

Code: Select all

int fd = open("fichier.txt", O_RDWR);
Si l’appel réussit, Linux retourne un entier :

Code: Select all

3
4
5
...
Cet entier est le file descriptor.

Ce n’est PAS le fichier lui-même.

C’est seulement un index dans une table de descriptors appartenant au processus.

Exemple simple :

Code: Select all

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

int main(void)
{
    int fd = open("test.txt", O_CREAT | O_WRONLY, 0644);

    if (fd == -1) {
        perror("open");
        return 1;
    }

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

    close(fd);

    return 0;
}
Ici :
  • open() demande au kernel d’ouvrir ou créer le fichier ;
  • write() écrit des octets ;
  • close() libère le descriptor dans le processus.

2. Atomicité

Une opération atomique est une opération qui est vue comme indivisible.

Cela veut dire :
  • elle s’exécute entièrement ;
  • aucun autre thread/processus ne voit un état intermédiaire ;
  • elle réussit complètement ou échoue complètement.
En programmation système, l’atomicité est fondamentale.

Pourquoi ?

Parce que sous Linux, plusieurs processus peuvent accéder en même temps aux mêmes ressources :
  • un même fichier ;
  • un même répertoire ;
  • une même socket ;
  • un même pipe ;
  • une même zone mémoire ;
  • un même device.
Sans atomicité, on peut avoir des bugs très subtils.


2.1 Exemple de bug sans atomicité

Imaginons ce code :

Code: Select all

if (access("lockfile", F_OK) == -1) {
    fd = open("lockfile", O_CREAT | O_WRONLY, 0644);
}
Le programme veut dire :
  • si le fichier n’existe pas ;
  • alors je le crée.
Mais ce code est dangereux.

Pourquoi ?

Parce qu’entre :

Code: Select all

access("lockfile", F_OK)
et :

Code: Select all

open("lockfile", O_CREAT | O_WRONLY, 0644)
un autre processus peut créer le fichier.

Chronologie possible :

Code: Select all

Processus A : vérifie si lockfile existe
Processus A : lockfile n’existe pas

Le scheduler interrompt A

Processus B : crée lockfile

Processus A reprend
Processus A : crée ou ouvre lockfile en croyant être seul
C’est une race condition.


3. Race Condition

Une race condition apparaît quand le résultat dépend de l’ordre exact d’exécution entre plusieurs threads/processus.

C’est un problème classique en système.

Forme typique :

Code: Select all

check
puis
use
Exemple :

Code: Select all

if (file_exists(path)) {
    open(path);
}
Entre la vérification et l’utilisation, le monde peut changer.

Ce type de bug est souvent appelé :

Code: Select all

TOCTOU
Time Of Check To Time Of Use.

En français :
  • moment où on vérifie ;
  • moment où on utilise.
Si quelque chose change entre les deux, le programme peut devenir vulnérable.


4. O_EXCL : création exclusive atomique

La bonne solution pour créer un fichier uniquement s’il n’existe pas est :

Code: Select all

int fd = open("lockfile", O_CREAT | O_EXCL | O_WRONLY, 0644);
Ici :
  • O_CREAT demande la création si le fichier n’existe pas ;
  • O_EXCL demande l’échec si le fichier existe déjà.
Le point important :

la vérification et la création sont atomiques côté kernel.

Donc il n’y a pas de fenêtre entre le check et l’usage.

Si le fichier existe déjà :

Code: Select all

open() échoue avec EEXIST
Exemple robuste :

Code: Select all

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

int main(void)
{
    int fd = open("lockfile",
                  O_CREAT | O_EXCL | O_WRONLY,
                  0644);

    if (fd == -1) {
        if (errno == EEXIST) {
            printf("Le fichier existe déjà\n");
        } else {
            perror("open");
        }
        return 1;
    }

    write(fd, "lock\n", 5);
    close(fd);

    return 0;
}
À retenir :

Quand on veut créer un fichier de manière sûre, on évite le couple access() + open(). On utilise open() avec O_CREAT | O_EXCL.


5. O_APPEND : écriture atomique en fin de fichier

O_APPEND est un flag très important.

Exemple :

Code: Select all

int fd = open("log.txt", O_WRONLY | O_APPEND);
Avec O_APPEND, chaque appel à write() écrit à la fin du fichier.

Même si le file offset courant est ailleurs.

Exemple :

Code: Select all

lseek(fd, 0, SEEK_SET);
write(fd, "ABC", 3);
Si le fichier a été ouvert avec O_APPEND, l’écriture sera faite à la fin du fichier, pas au début.

Le plus important :

le déplacement à la fin du fichier et l’écriture sont faits atomiquement par le kernel.

Sans O_APPEND, un programme pourrait faire :

Code: Select all

lseek(fd, 0, SEEK_END);
write(fd, buffer, size);
Mais ce n’est pas atomique.

Chronologie dangereuse :

Code: Select all

Processus A : lseek fin fichier
Processus B : lseek fin fichier
Processus A : write
Processus B : write
Les écritures peuvent se chevaucher ou produire un résultat incorrect.

Avec O_APPEND :

Code: Select all

Processus A : write atomique en fin de fichier
Processus B : write atomique en fin de fichier
Le kernel sérialise correctement la position de fin.


5.1 Exemple mental avec logs

Un serveur web peut avoir plusieurs processus qui écrivent dans le même fichier de log.

Si chaque processus écrit avec O_APPEND :

Code: Select all

open("server.log", O_WRONLY | O_APPEND);
Alors chaque ligne est ajoutée à la fin.

C’est indispensable pour éviter que deux processus écrivent au même offset.


6. fcntl() : fonction de contrôle des file descriptors

fcntl() est une fonction très générale.

Prototype :

Code: Select all

#include <fcntl.h>

int fcntl(int fd, int cmd, ...);
Elle sert à manipuler un file descriptor.

Selon la commande cmd, elle peut :
  • dupliquer un descriptor ;
  • lire les flags ;
  • modifier certains flags ;
  • poser des verrous ;
  • configurer le comportement non bloquant ;
  • manipuler des propriétés avancées.

6.1 Lire les flags : F_GETFL

Exemple :

Code: Select all

int flags = fcntl(fd, F_GETFL);

if (flags == -1) {
    perror("fcntl F_GETFL");
}
F_GETFL retourne les file status flags associés à l’open file description.

On peut tester certains flags :

Code: Select all

if (flags & O_APPEND) {
    printf("O_APPEND est activé\n");
}

6.2 Modifier les flags : F_SETFL

Pour ajouter O_NONBLOCK :

Code: Select all

int flags = fcntl(fd, F_GETFL);

if (flags == -1) {
    perror("fcntl F_GETFL");
    return 1;
}

if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
    perror("fcntl F_SETFL");
    return 1;
}
Important :
  • on lit d’abord les flags actuels ;
  • on ajoute le flag voulu ;
  • on remet le tout.
Il ne faut pas faire naïvement :

Code: Select all

fcntl(fd, F_SETFL, O_NONBLOCK);
car cela risquerait d’écraser d’autres flags.


7. File descriptor, open file description et inode

C’est probablement la partie la plus importante du chapitre.

Sous Linux, il faut distinguer trois niveaux :

Code: Select all

file descriptor
        ↓
open file description
        ↓
inode

7.1 File descriptor

Le file descriptor est un entier local au processus.

Exemple :

Code: Select all

0 = stdin
1 = stdout
2 = stderr
3 = premier fichier ouvert
4 = deuxième fichier ouvert
Le fd est une entrée dans la table des descriptors du processus.

Chaque processus a sa propre table.

Donc le fd 3 dans un processus A n’est pas forcément le même fichier que le fd 3 dans un processus B.


7.2 Open file description

L’open file description est une structure kernel créée lorsqu’un fichier est ouvert.

Elle contient notamment :
  • le file offset courant ;
  • les file status flags ;
  • le mode d’ouverture ;
  • une référence vers le fichier réel.
Le file offset, c’est la position actuelle dans le fichier.

Exemple :

Code: Select all

read(fd, buffer, 10);
Après cette lecture, le file offset avance de 10 octets.


7.3 Inode

L’inode représente le fichier au niveau filesystem.

Il contient des métadonnées :
  • taille du fichier ;
  • permissions ;
  • owner ;
  • timestamps ;
  • nombre de liens ;
  • emplacement des blocs disque ;
  • type de fichier.
L’inode ne contient pas le nom du fichier.

Le nom est dans le répertoire.

Un même inode peut avoir plusieurs noms si on utilise des hard links.


7.4 Schéma simplifié

Code: Select all

Processus A

fd table :
    0 -> stdin
    1 -> stdout
    2 -> stderr
    3 ----------------------+
                            |
                            v
                    open file description
                    offset = 128
                    flags = O_RDWR
                            |
                            v
                          inode
                       fichier.txt

7.5 Pourquoi c’est important ?

Parce que plusieurs file descriptors peuvent pointer vers la même open file description.

Donc ils peuvent partager :
  • le même offset ;
  • les mêmes file status flags ;
  • le même état d’ouverture.
C’est exactement ce qui se passe avec :
  • dup() ;
  • dup2() ;
  • fork() ;
  • certains mécanismes de transmission de fd.

8. dup(), dup2() et F_DUPFD

dup() duplique un file descriptor.

Prototype :

Code: Select all

#include <unistd.h>

int dup(int oldfd);
Exemple :

Code: Select all

int fd2 = dup(fd1);
Le kernel crée un nouveau file descriptor dans le même processus.

Mais ce nouveau fd pointe vers la même open file description.

Code: Select all

fd1 ----+
        |
        v
   open file description
        ^
        |
fd2 ----+
Conséquence :

fd1 et fd2 partagent le même offset.


8.1 Exemple avec offset partagé

Code: Select all

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

int main(void)
{
    int fd1 = open("test.txt", O_CREAT | O_TRUNC | O_RDWR, 0644);
    int fd2 = dup(fd1);

    write(fd1, "ABC", 3);
    write(fd2, "DEF", 3);

    close(fd1);
    close(fd2);

    return 0;
}
Résultat dans le fichier :

Code: Select all

ABCDEF
Pourquoi ?

Parce que :
  • fd1 écrit ABC ;
  • l’offset partagé avance à 3 ;
  • fd2 écrit DEF à partir de l’offset 3.

8.2 dup2()

Prototype :

Code: Select all

int dup2(int oldfd, int newfd);
dup2() force le nouveau descriptor à utiliser le numéro newfd.

Exemple :

Code: Select all

dup2(fd, STDOUT_FILENO);
Ici, stdout devient une copie de fd.

Tous les printf() iront vers fd.

Exemple complet :

Code: Select all

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

int main(void)
{
    int fd = open("sortie.txt", O_CREAT | O_TRUNC | O_WRONLY, 0644);

    if (fd == -1) {
        perror("open");
        return 1;
    }

    if (dup2(fd, STDOUT_FILENO) == -1) {
        perror("dup2");
        return 1;
    }

    close(fd);

    printf("Cette ligne part dans sortie.txt\n");

    return 0;
}
C’est la base des redirections de shell.

Quand on écrit :

Code: Select all

programme > sortie.txt
le shell fait essentiellement :
  • open("sortie.txt") ;
  • dup2(fd, STDOUT_FILENO) ;
  • execve(programme).

8.3 Redirection stderr vers stdout

En shell :

Code: Select all

commande > out.txt 2>&1
Cela signifie :
  • stdout est redirigé vers out.txt ;
  • stderr devient une copie de stdout.
Conceptuellement :

Code: Select all

dup2(fd, STDOUT_FILENO);
dup2(STDOUT_FILENO, STDERR_FILENO);
C’est pour ça que dup2() est fondamental pour comprendre les shells.


8.4 fcntl() avec F_DUPFD

fcntl() peut aussi dupliquer un fd :

Code: Select all

int newfd = fcntl(oldfd, F_DUPFD, 10);
Cela demande au kernel de retourner le plus petit fd disponible supérieur ou égal à 10.

Différence mentale :
  • dup() choisit le plus petit fd disponible ;
  • dup2() force exactement un numéro ;
  • F_DUPFD choisit le plus petit numéro disponible à partir d’une valeur minimale.

9. fork() et partage des descriptors

Lorsqu’un processus fait fork(), le fils hérite des file descriptors du parent.

Exemple :

Code: Select all

int fd = open("test.txt", O_RDWR);

pid_t pid = fork();
Après fork() :
  • le parent a fd ;
  • le fils a fd ;
  • les deux descriptors pointent vers la même open file description.
Donc :

l’offset est partagé entre parent et fils.

Schéma :

Code: Select all

Parent fd 3 ----+
                |
                v
        open file description
                ^
                |
Child fd 3 -----+
C’est très important pour comprendre :
  • les redirections ;
  • les pipes ;
  • les shells ;
  • les serveurs multi-process ;
  • les interactions parent/enfant.

10. Transmission de file descriptors entre processus

Un file descriptor est local à un processus.

Le nombre 4 n’a pas de sens universel.

Dans le processus A :

Code: Select all

fd 4 -> fichier secret.txt
Dans le processus B :

Code: Select all

fd 4 -> socket réseau
Donc envoyer seulement le nombre 4 à un autre processus ne suffit pas.


10.1 Pipe classique

Une pipe classique transmet des octets.

Exemple :

Code: Select all

pipe(pipefd);
Elle donne :
  • pipefd[0] pour lire ;
  • pipefd[1] pour écrire.
Mais une pipe ne transmet pas directement un objet file descriptor.

Elle transmet des bytes.

Donc on peut envoyer le texte :

Code: Select all

"4"
mais cela ne donnera pas automatiquement accès au fd 4 dans l’autre processus.


10.2 Vrai fd passing : UNIX domain socket + SCM_RIGHTS

Pour transmettre réellement un file descriptor à un autre processus, Linux utilise généralement :

Code: Select all

UNIX domain socket + sendmsg()/recvmsg() + SCM_RIGHTS
Le kernel crée alors un nouveau fd dans le processus receveur.

Ce nouveau fd pointe vers la même open file description.

Schéma :

Code: Select all

Processus A
fd 5 ----+
         |
         v
 open file description
         ^
         |
fd 8 ----+
Processus B
Le processus B ne reçoit pas juste un entier.

Il reçoit une nouvelle entrée valide dans sa propre table de descriptors.

Ce mécanisme est utilisé dans :
  • systemd ;
  • Docker ;
  • Chromium ;
  • Wayland ;
  • PulseAudio ;
  • QEMU ;
  • sandboxing ;
  • privilege separation.

11. pread() et pwrite()

read() et write() utilisent le file offset courant.

Exemple :

Code: Select all

read(fd, buf, 100);
Lit 100 octets à partir de la position actuelle, puis avance l’offset.

Mais parfois on veut lire à une position précise sans toucher à l’offset partagé.

C’est le rôle de pread().

Prototype :

Code: Select all

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
Exemple :

Code: Select all

pread(fd, buffer, 100, 4096);
Cela lit 100 octets à partir de l’offset 4096.

Mais le file offset global n’est pas modifié.

Même idée pour pwrite() :

Code: Select all

ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

11.1 Pourquoi c’est important ?

pread()/pwrite() sont très utiles quand plusieurs threads utilisent le même fichier.

Avec read()/write(), ils partagent l’offset.

Cela peut provoquer des conflits.

Avec pread()/pwrite(), chaque opération indique explicitement son offset.

Donc pas besoin de faire :

Code: Select all

lseek(fd, offset, SEEK_SET);
read(fd, buf, size);
Ce couple est dangereux en multithread, car un autre thread peut modifier l’offset entre lseek() et read().

pread() résout ça.


11.2 Équivalent mental Windows

Pour un développeur Win32, pread()/pwrite() ressemblent beaucoup à l’idée de :

Code: Select all

OVERLAPPED
Avec Windows :
  • on indique un offset dans la structure OVERLAPPED ;
  • l’opération peut se faire sans dépendre du pointeur de fichier global.
Sous Linux :
  • pread() et pwrite() font directement ça avec un paramètre offset.

12. read() et write() peuvent être partiels

C’est un point très important en programmation système.

Un appel à write() peut écrire moins d’octets que demandé.

Exemple :

Code: Select all

ssize_t n = write(fd, buffer, 4096);
n peut valoir :

Code: Select all

4096
mais aussi :

Code: Select all

1024
ou :

Code: Select all

-1
Même chose pour read().

Il faut toujours vérifier la valeur de retour.


12.1 Fonction writen()

Pour écrire exactement N octets, on écrit souvent une fonction qui boucle.

Code: Select all

#include <unistd.h>
#include <errno.h>

ssize_t writen(int fd, const void *buf, size_t count)
{
    const char *p = buf;
    size_t left = count;

    while (left > 0) {
        ssize_t n = write(fd, p, left);

        if (n == -1) {
            if (errno == EINTR)
                continue;

            return -1;
        }

        if (n == 0)
            break;

        p += n;
        left -= n;
    }

    return count - left;
}
Pourquoi gérer EINTR ?

Parce qu’un signal peut interrompre un appel système.


12.2 Fonction readn()

Même logique pour lire exactement N octets :

Code: Select all

#include <unistd.h>
#include <errno.h>

ssize_t readn(int fd, void *buf, size_t count)
{
    char *p = buf;
    size_t left = count;

    while (left > 0) {
        ssize_t n = read(fd, p, left);

        if (n == -1) {
            if (errno == EINTR)
                continue;

            return -1;
        }

        if (n == 0)
            break;

        p += n;
        left -= n;
    }

    return count - left;
}
Ces fonctions sont très importantes pour :
  • sockets ;
  • pipes ;
  • protocoles binaires ;
  • fichiers spéciaux ;
  • I/O robustes.

13. Scatter/Gather I/O : readv() et writev()

Le scatter/gather I/O permet de lire ou écrire plusieurs buffers avec une seule syscall.

Fonctions :

Code: Select all

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
Structure :

Code: Select all

struct iovec {
    void  *iov_base;
    size_t iov_len;
};

13.1 writev()

Exemple :

Code: Select all

#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
    int fd = open("message.txt", O_CREAT | O_TRUNC | O_WRONLY, 0644);

    struct iovec iov[3];

    char header[] = "HEADER\n";
    char body[]   = "BODY\n";
    char footer[] = "FOOTER\n";

    iov[0].iov_base = header;
    iov[0].iov_len  = strlen(header);

    iov[1].iov_base = body;
    iov[1].iov_len  = strlen(body);

    iov[2].iov_base = footer;
    iov[2].iov_len  = strlen(footer);

    writev(fd, iov, 3);

    close(fd);

    return 0;
}
Résultat :

Code: Select all

HEADER
BODY
FOOTER
Avantage :
  • une seule syscall ;
  • pas besoin de concaténer les buffers ;
  • meilleure performance ;
  • moins de copies mémoire.

13.2 Où c’est utilisé ?

Très utilisé dans :
  • serveurs HTTP ;
  • réseau ;
  • protocoles ;
  • bases de données ;
  • kernels ;
  • drivers ;
  • systèmes haute performance.
Exemple mental :

Code: Select all

header HTTP
+
body
+
footer
Au lieu de tout copier dans un gros buffer, on envoie plusieurs buffers séparés en une seule opération.


14. truncate() et ftruncate()

truncate() modifie la taille d’un fichier à partir de son chemin.

Prototype :

Code: Select all

int truncate(const char *path, off_t length);
ftruncate() fait la même chose avec un fd :

Code: Select all

int ftruncate(int fd, off_t length);
Exemple :

Code: Select all

ftruncate(fd, 0);
Cela vide le fichier.

Si la nouvelle taille est plus petite :
  • les données après cette taille sont supprimées.
Si la nouvelle taille est plus grande :
  • le fichier est agrandi ;
  • les nouvelles zones lisibles valent zéro.

14.1 Exemple

Code: Select all

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

int main(void)
{
    int fd = open("data.bin", O_CREAT | O_RDWR, 0644);

    write(fd, "ABCDEFGH", 8);

    ftruncate(fd, 4);

    close(fd);

    return 0;
}
Contenu final :

Code: Select all

ABCD

15. Nonblocking I/O

Par défaut, beaucoup d’opérations I/O sont bloquantes.

Exemple avec un pipe :

Code: Select all

read(fd, buffer, size);
Si aucune donnée n’est disponible, read() peut attendre.

Avec O_NONBLOCK :

Code: Select all

fcntl(fd, F_SETFL, flags | O_NONBLOCK);
l’appel retourne immédiatement.

Si aucune donnée n’est disponible :

Code: Select all

read() retourne -1
errno = EAGAIN ou EWOULDBLOCK

15.1 Exemple

Code: Select all

int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

ssize_t n = read(fd, buffer, sizeof(buffer));

if (n == -1) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        printf("Aucune donnée disponible pour le moment\n");
    } else {
        perror("read");
    }
}

15.2 Utilité

Le nonblocking est essentiel pour :
  • sockets ;
  • serveurs réseau ;
  • event loops ;
  • select() ;
  • poll() ;
  • epoll() ;
  • applications haute performance.
Sous Linux, beaucoup de serveurs fonctionnent avec :

Code: Select all

O_NONBLOCK + epoll
Le modèle est souvent :
  • ne jamais bloquer sur une seule connexion ;
  • surveiller beaucoup de descriptors ;
  • réagir quand un fd devient prêt.

15.3 Équivalent mental Windows

Sous Windows, on pense souvent à :
  • OVERLAPPED I/O ;
  • IOCP ;
  • WSARecv()/WSASend() asynchrones.
Différence importante :
  • Linux avec epoll est souvent un modèle de readiness : “ce fd est prêt”.
  • Windows IOCP est plutôt un modèle de completion : “cette opération est terminée”.
Ce n’est pas identique, mais l’objectif est proche :

ne pas bloquer un thread pour chaque opération I/O.


16. Large File Support

Historiquement, sur les systèmes 32 bits, off_t pouvait être limité.

Cela posait problème pour les fichiers supérieurs à environ 2 Go.

Linux a donc introduit le Large File Support.

La manière classique d’activer les offsets 64 bits :

Code: Select all

#define _FILE_OFFSET_BITS 64
À placer avant les includes :

Code: Select all

#define _FILE_OFFSET_BITS 64

#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
Cela permet d’utiliser off_t en 64 bits.

Donc les fonctions comme :
  • open()
  • lseek()
  • truncate()
  • stat()
utilisent automatiquement les versions compatibles grands fichiers.


17. /dev/fd

Linux expose les file descriptors via :

Code: Select all

/dev/fd/
Exemples :

Code: Select all

/dev/fd/0
/dev/fd/1
/dev/fd/2
Correspondent à :
  • stdin ;
  • stdout ;
  • stderr.
Souvent, /dev/fd est lié à :

Code: Select all

/proc/self/fd
On peut observer les descriptors ouverts :

Code: Select all

ls -l /proc/self/fd
Exemple possible :

Code: Select all

0 -> /dev/pts/0
1 -> /dev/pts/0
2 -> /dev/pts/0
3 -> /home/user/file.txt
C’est très utile pour comprendre ce qu’un processus a ouvert.


18. Fichiers temporaires

Beaucoup de programmes ont besoin de fichiers temporaires.

Exemples :
  • compilateurs ;
  • éditeurs ;
  • outils de compression ;
  • navigateurs ;
  • serveurs ;
  • scripts ;
  • programmes de traitement de données.
Mais créer un fichier temporaire est sensible en sécurité.

Mauvaise idée :

Code: Select all

char *name = tmpnam(NULL);
int fd = open(name, O_CREAT | O_WRONLY, 0644);
Pourquoi ?

Parce qu’entre la génération du nom et la création du fichier, un autre processus peut créer ce fichier.

C’est encore une faille TOCTOU.


18.1 mkstemp()

La fonction recommandée est :

Code: Select all

#include <stdlib.h>

int mkstemp(char *template);
Le template doit finir par :

Code: Select all

XXXXXX
Exemple :

Code: Select all

char template[] = "/tmp/myprogXXXXXX";

int fd = mkstemp(template);
mkstemp() :
  • remplace les X par une chaîne unique ;
  • crée le fichier ;
  • l’ouvre ;
  • retourne un fd ;
  • fait tout ça de manière sûre.
Exemple complet :

Code: Select all

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

int main(void)
{
    char template[] = "/tmp/demoXXXXXX";

    int fd = mkstemp(template);

    if (fd == -1) {
        perror("mkstemp");
        return 1;
    }

    printf("Fichier temporaire : %s\n", template);

    write(fd, "temporary data\n", 15);

    close(fd);

    return 0;
}
Important :

template doit être modifiable.

Donc ceci est incorrect :

Code: Select all

char *template = "/tmp/demoXXXXXX";
Car une chaîne littérale peut être en mémoire read-only.

Il faut faire :

Code: Select all

char template[] = "/tmp/demoXXXXXX";

18.2 Supprimer le nom après création

Technique Unix classique :

Code: Select all

int fd = mkstemp(template);
unlink(template);
Le fichier n’a plus de nom dans le filesystem.

Mais tant que fd est ouvert, les données restent accessibles.

Quand fd est fermé, le kernel libère le fichier.

Exemple :

Code: Select all

char template[] = "/tmp/secretXXXXXX";

int fd = mkstemp(template);

unlink(template);

/*
    Le fichier existe encore tant que fd est ouvert,
    mais il n'est plus accessible par son nom.
*/
Très utile pour créer un fichier temporaire privé.


18.3 tmpfile()

tmpfile() utilise l’interface stdio :

Code: Select all

#include <stdio.h>

FILE *tmpfile(void);
Exemple :

Code: Select all

FILE *f = tmpfile();

fprintf(f, "hello\n");
Le fichier est automatiquement supprimé à la fermeture.

Avantage :
  • simple ;
  • pas besoin de choisir un nom ;
  • nettoyage automatique.
Limite :
  • on travaille avec FILE* ;
  • pas directement avec un fd brut sauf conversion avec fileno().

19. Liens avec Windows / Win32

Pour quelqu’un qui connaît Win32, les concepts Linux peuvent se comprendre avec des équivalences mentales.

Attention : ce ne sont pas toujours des équivalences exactes, mais ça aide.


19.1 fd et HANDLE

Linux :

Code: Select all

file descriptor
Windows :

Code: Select all

HANDLE
Dans les deux cas, c’est un identifiant utilisé par le processus pour parler à un objet kernel.

Mais Windows a un modèle plus général avec l’Object Manager NT.


19.2 dup() et DuplicateHandle()

Linux :

Code: Select all

dup(fd);
dup2(oldfd, newfd);
Windows :

Code: Select all

DuplicateHandle(...);
Dans les deux cas, on obtient un nouvel identifiant vers le même objet kernel.


19.3 pread()/pwrite() et OVERLAPPED

Linux :

Code: Select all

pread(fd, buffer, size, offset);
Windows :

Code: Select all

ReadFile(handle, buffer, size, &bytesRead, &overlapped);
L’idée commune :
  • faire une I/O à un offset précis ;
  • ne pas dépendre seulement du pointeur global.

19.4 O_APPEND et FILE_APPEND_DATA

Linux :

Code: Select all

O_APPEND
Windows :

Code: Select all

FILE_APPEND_DATA
Idée :
  • écriture forcée en fin de fichier ;
  • utile pour logs ;
  • évite certains conflits de concurrence.

19.5 O_NONBLOCK + epoll et IOCP

Linux :

Code: Select all

O_NONBLOCK + epoll
Windows :

Code: Select all

OVERLAPPED I/O + IOCP
Objectif commun :
  • gérer beaucoup d’I/O sans bloquer un thread par opération.
Mais le modèle interne est différent.


20. Exercices importants du chapitre

Exercice : O_APPEND

Créer un fichier avec O_APPEND, faire un lseek() au début, puis écrire.

Question :

Code: Select all

Où les données sont-elles écrites ?
Réponse :

À la fin du fichier.

Parce que O_APPEND force le kernel à écrire en fin de fichier.


Exercice : dup() et offset partagé

Prédire le contenu final :

Code: Select all

int fd1 = open("test.txt", O_CREAT | O_TRUNC | O_RDWR, 0644);
int fd2 = dup(fd1);

write(fd1, "ABC", 3);
write(fd2, "DEF", 3);

lseek(fd1, 0, SEEK_SET);
write(fd2, "X", 1);
Analyse :
  • fd1 et fd2 partagent le même offset ;
  • ABC écrit offset 0-2 ;
  • DEF écrit offset 3-5 ;
  • lseek(fd1, 0) remet l’offset partagé à 0 ;
  • write(fd2, "X", 1) écrit au début.
Résultat :

Code: Select all

XBCDEF

Exercice : readn()/writen()

Implémenter des fonctions robustes qui bouclent jusqu’à lire ou écrire le nombre d’octets demandé.

C’est indispensable en réseau.


21. Résumé ultra important

À retenir absolument :
  • Un file descriptor est un entier local à un processus.
  • Le fd n’est pas le fichier.
  • Le vrai état d’ouverture est dans l’open file description.
  • L’open file description contient notamment l’offset et les flags.
  • L’inode représente le fichier côté filesystem.
  • dup() crée un nouveau fd vers la même open file description.
  • dup2() permet de forcer un numéro de fd.
  • fork() fait hériter les fd au processus fils.
  • Après dup() ou fork(), l’offset peut être partagé.
  • O_APPEND rend l’écriture en fin de fichier atomique.
  • O_EXCL permet une création exclusive atomique.
  • pread()/pwrite() lisent/écrivent à un offset précis sans modifier l’offset global.
  • read()/write() peuvent être partiels.
  • Il faut toujours vérifier les valeurs de retour.
  • readv()/writev() permettent plusieurs buffers en une syscall.
  • O_NONBLOCK permet de ne pas bloquer.
  • mkstemp() est la bonne méthode pour créer un fichier temporaire sécurisé.
  • Une pipe transmet des bytes, pas directement des fd.
  • Le vrai fd passing se fait avec UNIX domain sockets et SCM_RIGHTS.

22. Les 5 idées les plus importantes

1. fd != fichier

Le fd est seulement un index dans une table du processus.

Code: Select all

fd -> open file description -> inode

2. dup() partage l’offset

Après :

Code: Select all

fd2 = dup(fd1);
fd1 et fd2 pointent vers la même open file description.


3. O_APPEND est atomique

Avec O_APPEND, le kernel garantit :

Code: Select all

écriture à la fin du fichier
sans race condition sur le file offset.


4. pread()/pwrite() évitent les conflits d’offset

Très utile en multithreading.


5. Les fichiers temporaires doivent être créés proprement

Utiliser :

Code: Select all

mkstemp()
Éviter :

Code: Select all

tmpnam()
mktemp()

Conclusion

Ce chapitre est un chapitre central en programmation système Linux.

Il explique comment le kernel gère réellement les fichiers ouverts.

Ce n’est pas seulement une histoire de fichiers texte.

C’est la base de :
  • la redirection shell ;
  • les pipes ;
  • les processus ;
  • les serveurs ;
  • les sockets ;
  • la concurrence ;
  • les logs ;
  • les fichiers temporaires sécurisés ;
  • le fd passing ;
  • la compréhension du modèle UNIX.
Quand on maîtrise ce chapitre, on commence à comprendre Linux comme un système, pas seulement comme un environnement de programmation.

C’est exactement le genre de bases qu’il faut maîtriser pour aller ensuite vers :
  • processus ;
  • threads ;
  • signals ;
  • pipes ;
  • sockets ;
  • epoll ;
  • IPC ;
  • sandboxing ;
  • sécurité système ;
  • kernel internals.

Who is online

Users browsing this forum: No registered users and 1 guest