Cours sur le système de fichier et les I/O

Développement système natif en c/c++ avec win32 ...

Moderator: Rick

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

Cours sur le système de fichier et les I/O

Post by Hydraxx »

Salut, :D

on continue avec un autre gros morceau du développement Win32 : le système de fichiers et les I/O.

C’est un sujet qu’on sous-estime souvent au début, parce que lire un fichier avec ReadFile parait simple. En réalité, dès qu’on sort du cas trivial, on tombe très vite sur des notions importantes : handles, partage, mode d’ouverture, pointeur de fichier, I/O synchrones ou asynchrones, cache système, attributs, tailles, chemins, énumération de répertoires, informations détaillées, notifications, verrous, et parfois meme I/O sur des objets qui ne sont pas de simples fichiers.

Pourquoi les I/O sont un sujet important

Quand on comprend mal les entrées/sorties, on écrit souvent du code qui fonctionne “sur sa machine”, puis qui devient mauvais ou fragile dans un vrai contexte. Typiquement, cela donne :
  • un programme lent parce qu’il fait trop d’I/O inutiles ou mal structurées
  • un thread principal qui se bloque
  • des erreurs aléatoires sur certains fichiers ou certains chemins
  • des comportements différents selon le disque, le réseau, le système de fichiers ou la charge de la machine
  • des handles oubliés qui deviennent des fuites de ressources
Le but de ce cours est donc de poser une vision propre du modèle Win32, puis de faire le tour des API et structures les plus utiles.

Tout passe par des handles

Sous Windows, une très grande partie du modèle d’I/O repose sur les handles. Un handle n’est pas un pointeur vers une structure user-mode. C’est une référence opaque gérée par le noyau. Quand ton programme ouvre un fichier, un pipe, un event, un mutex, un device, ou bien d’autres objets encore, il reçoit le plus souvent un HANDLE.

Il faut bien comprendre ce point, parce qu’il conditionne toute la suite. Le HANDLE représente une ressource ouverte, sur laquelle on va effectuer des opérations, puis qu’il faudra libérer correctement.

En pratique, le schéma général est presque toujours le meme :
  • ouvrir ou créer une ressource
  • récupérer un HANDLE
  • utiliser ce HANDLE avec d’autres API
  • fermer la ressource quand on a terminé
Oublier CloseHandle ne provoque pas toujours un crash immédiat, mais cela provoque une fuite de ressources. Dans un programme long, ou dans un service, cela finit par devenir un vrai problème.

Ouvrir un fichier avec CreateFile

L’API centrale pour ouvrir un fichier en Win32 est CreateFileW. Malgré son nom, elle ne sert pas seulement à “créer” un fichier. Elle sert surtout à ouvrir ou créer un objet fichier selon le mode demandé.

Exemple :

Code: Select all

HANDLE hFile = CreateFileW(
    L"test.txt",
    GENERIC_READ | GENERIC_WRITE,
    FILE_SHARE_READ,
    NULL,
    OPEN_ALWAYS,
    FILE_ATTRIBUTE_NORMAL,
    NULL
);

if (hFile == INVALID_HANDLE_VALUE)
{
    DWORD err = GetLastError();
    return 1;
}
Il faut déjà savoir lire les paramètres principaux :
  • le chemin de l’objet
  • les droits demandés, par exemple GENERIC_READ ou GENERIC_WRITE
  • le mode de partage, via FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE
  • les attributs de sécurité éventuels
  • le mode de création, par exemple OPEN_EXISTING, CREATE_ALWAYS, OPEN_ALWAYS
  • les flags et attributs du fichier
  • un éventuel handle de modèle
Les modes de création les plus fréquents sont :
  • CREATE_NEW : crée seulement si le fichier n’existe pas
  • CREATE_ALWAYS : recrée et écrase si nécessaire
  • OPEN_EXISTING : ouvre seulement si le fichier existe
  • OPEN_ALWAYS : ouvre s’il existe, sinon crée
  • TRUNCATE_EXISTING : vide un fichier existant
Les flags et attributs utiles à connaitre sont nombreux. Parmi les plus fréquents :
  • FILE_ATTRIBUTE_NORMAL
  • FILE_ATTRIBUTE_HIDDEN
  • FILE_ATTRIBUTE_READONLY
  • FILE_FLAG_OVERLAPPED
  • FILE_FLAG_SEQUENTIAL_SCAN
  • FILE_FLAG_RANDOM_ACCESS
  • FILE_FLAG_WRITE_THROUGH
  • FILE_FLAG_NO_BUFFERING
  • FILE_FLAG_BACKUP_SEMANTICS
  • FILE_FLAG_DELETE_ON_CLOSE
Déjà ici, on voit que CreateFile est plus riche qu’un simple fopen. Le choix de ces paramètres change réellement le comportement du système.

Les droits, le partage et les erreurs d’ouverture

Beaucoup d’erreurs lors d’un CreateFile ne viennent pas d’un “fichier introuvable”, mais d’un conflit de droits ou de partage. Si un autre processus a ouvert le fichier sans permettre certains partages, ton ouverture peut échouer.

Il faut donc distinguer deux choses :
  • les accès que ton programme demande
  • les accès qu’il autorise aux autres pendant que le handle reste ouvert
Par exemple, demander GENERIC_READ n’est pas la meme chose qu’autoriser FILE_SHARE_READ. La première notion décrit ce que ton code veut faire. La seconde décrit ce qu’il laisse faire aux autres.

C’est une distinction fondamentale pour comprendre les erreurs de type sharing violation.

Lire et écrire : ReadFile et WriteFile

Une fois le handle obtenu, les fonctions de base sont ReadFile et WriteFile.

Exemple de lecture :

Code: Select all

DWORD bytesRead = 0;
char buffer[128] = { 0 };

BOOL ok = ReadFile(
    hFile,
    buffer,
    sizeof(buffer) - 1,
    &bytesRead,
    NULL
);

if (!ok)
{
    DWORD err = GetLastError();
    return 1;
}
Exemple d’écriture :

Code: Select all

DWORD bytesWritten = 0;
const char* msg = "Hello from Win32\r\n";

BOOL ok = WriteFile(
    hFile,
    msg,
    (DWORD)strlen(msg),
    &bytesWritten,
    NULL
);

if (!ok)
{
    DWORD err = GetLastError();
    return 1;
}
Deux points sont essentiels ici.

Premièrement, il faut toujours vérifier le retour BOOL. Une opération d’I/O peut échouer, etre partielle, ou demander un traitement particulier.

Deuxièmement, bytesRead et bytesWritten indiquent ce qui a réellement été traité. Il ne faut jamais supposer qu’une lecture ou une écriture a concerné exactement la taille demandée sans vérifier.

Le pointeur de fichier

Lorsqu’on travaille avec un fichier classique ouvert de manière synchrone, le handle possède une position courante, souvent appelée file pointer. Chaque lecture ou écriture avance cette position.

Pour la déplacer explicitement, on utilise en général SetFilePointerEx :

Code: Select all

LARGE_INTEGER off;
off.QuadPart = 0;

if (!SetFilePointerEx(hFile, off, NULL, FILE_BEGIN))
    return 1;
Les origines possibles sont notamment :
  • FILE_BEGIN
  • FILE_CURRENT
  • FILE_END
Pour récupérer la taille d’un fichier, GetFileSizeEx est l’API la plus pratique :

Code: Select all

LARGE_INTEGER size;

if (!GetFileSizeEx(hFile, &size))
    return 1;
Le recours à LARGE_INTEGER est important, car les tailles de fichier dépassent très largement 32 bits dans beaucoup de cas réels.

Tronquer, étendre et positionner correctement un fichier

Autour du pointeur de fichier, quelques API sont bonnes à connaitre :
  • SetEndOfFile, pour tronquer ou étendre le fichier à la position courante
  • SetFilePointerEx, pour se placer précisément
  • GetFileSizeEx, pour connaitre la taille totale
Exemple de troncature :

Code: Select all

LARGE_INTEGER off;
off.QuadPart = 100;

if (!SetFilePointerEx(hFile, off, NULL, FILE_BEGIN))
    return 1;

if (!SetEndOfFile(hFile))
    return 1;
Cela ramène le fichier à 100 octets si tout se passe bien.

I/O synchrones : le modèle par défaut

Par défaut, ReadFile et WriteFile sont synchrones. Cela signifie que le thread appelant reste bloqué jusqu’à ce que l’opération soit terminée, ou qu’elle échoue.

Ce modèle est simple à comprendre, mais il a une conséquence immédiate : si tu effectues une I/O lente dans le mauvais thread, par exemple le thread principal d’une interface ou d’une boucle réactive, ton programme peut se figer.

C’est pour cela qu’en pratique, il faut toujours réfléchir à la question suivante : est-ce que je peux me permettre de bloquer ce thread ?

I/O overlapped : le modèle asynchrone Win32

Pour ne pas bloquer le thread appelant, Windows propose les I/O asynchrones dites overlapped. Le principe repose sur deux éléments :
  • ouvrir l’objet avec FILE_FLAG_OVERLAPPED
  • fournir une structure OVERLAPPED
Exemple d’ouverture :

Code: Select all

HANDLE hFile2 = CreateFileW(
    L"test.txt",
    GENERIC_READ | GENERIC_WRITE,
    FILE_SHARE_READ,
    NULL,
    OPEN_ALWAYS,
    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
    NULL
);

if (hFile2 == INVALID_HANDLE_VALUE)
    return 1;
Exemple d’écriture overlapped :

Code: Select all

OVERLAPPED ov = { 0 };

BOOL ok = WriteFile(
    hFile2,
    msg,
    (DWORD)strlen(msg),
    NULL,
    &ov
);

if (!ok)
{
    DWORD err = GetLastError();

    if (err != ERROR_IO_PENDING)
        return 1;
}
Si GetLastError renvoie ERROR_IO_PENDING, cela signifie que l’opération a bien été lancée mais qu’elle n’est pas encore terminée.

Le résultat se récupère ensuite via GetOverlappedResult, ou bien en attendant un event associé à la structure OVERLAPPED.

La structure OVERLAPPED

OVERLAPPED est une structure qu’il faut connaitre dès qu’on fait des I/O asynchrones.

Les champs les plus importants sont :
  • Offset
  • OffsetHigh
  • hEvent
Offset et OffsetHigh servent à préciser la position de l’opération dans le fichier. hEvent peut désigner un event signalé lorsque l’opération est terminée.

Exemple :

Code: Select all

OVERLAPPED ov = { 0 };
ov.Offset = 0;
ov.OffsetHigh = 0;
ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
Puis :

Code: Select all

DWORD transferred = 0;

if (!GetOverlappedResult(hFile2, &ov, &transferred, TRUE))
    return 1;
Le paramètre TRUE indique ici qu’on accepte d’attendre la fin de l’opération.

Annuler une I/O asynchrone

Quand on travaille avec des I/O asynchrones, il faut aussi connaitre les API d’annulation :
  • CancelIo
  • CancelIoEx
  • CancelSynchronousIo
Elles sont importantes quand un thread doit s’arreter proprement, quand une opération prend trop de temps, ou quand une logique d’arret doit interrompre des I/O en cours.

Attendre une I/O et raisonner en termes d’événements

Une I/O overlapped s’intègre naturellement au modèle de synchronisation Win32. Si ov.hEvent est renseigné, on peut attendre la fin de l’opération avec WaitForSingleObject ou WaitForMultipleObjects.

C’est un point important, parce qu’il relie directement le modèle I/O au modèle de synchronisation vu dans le cours précédent.

Verrouillage de zones dans un fichier

Il existe aussi des API pour verrouiller une région d’un fichier :
  • LockFile
  • LockFileEx
  • UnlockFile
  • UnlockFileEx
Ce type de verrou ne protège pas les structures mémoire de ton processus, mais certaines portions du fichier lui-meme contre des accès concurrents incompatibles. C’est utile dans certains scénarios multi-processus ou pour des formats nécessitant un accès coordonné.

Le cache système et FlushFileBuffers

Sous Windows, les I/O sur fichiers passent souvent par le cache système. Cela veut dire qu’un WriteFile réussi ne signifie pas forcément que les données ont été physiquement écrites sur le disque à cet instant précis.

Pour forcer le flush des buffers du fichier, on utilise :

Code: Select all

if (!FlushFileBuffers(hFile))
    return 1;
Cela a un cout. Il ne faut donc pas appeler FlushFileBuffers partout par réflexe, mais seulement quand la garantie d’écriture compte réellement.

Les flags CreateFile comme FILE_FLAG_WRITE_THROUGH et FILE_FLAG_NO_BUFFERING influencent aussi fortement le comportement, mais ils doivent etre utilisés avec discernement.

Le mode non buffered et ses pièges

FILE_FLAG_NO_BUFFERING permet de contourner une partie du cache système, mais il impose des contraintes importantes sur les tailles, les alignements et les offsets. Ce n’est pas un mode à utiliser “pour aller plus vite” sans comprendre précisément les règles du système de stockage.

De meme, FILE_FLAG_WRITE_THROUGH peut changer le compromis entre performance et persistance. Ce sont des options de niveau plus avancé, utiles, mais à manier proprement.

Lister des fichiers et parcourir un répertoire

Pour lister les entrées d’un dossier, on utilise généralement FindFirstFileW et FindNextFileW, avec la structure WIN32_FIND_DATAW.

Exemple :

Code: Select all

WIN32_FIND_DATAW data;
HANDLE hFind = FindFirstFileW(L"*.*", &data);

if (hFind != INVALID_HANDLE_VALUE)
{
    do
    {
        // data.cFileName
    } while (FindNextFileW(hFind, &data));

    FindClose(hFind);
}
WIN32_FIND_DATAW contient notamment :
  • cFileName
  • dwFileAttributes
  • nFileSizeHigh
  • nFileSizeLow
  • ftCreationTime
  • ftLastAccessTime
  • ftLastWriteTime
C’est donc une structure très utile pour l’énumération simple.

Autour des répertoires et fichiers, il faut aussi connaitre :
  • CreateDirectoryW
  • CreateDirectoryExW
  • RemoveDirectoryW
  • DeleteFileW
  • MoveFileW
  • MoveFileExW
  • CopyFileW
  • CopyFileExW
  • ReplaceFileW
Les chemins et leurs pièges

Le sujet des chemins sous Windows est plus subtil qu’il n’en a l’air. Il faut connaitre au moins :
  • MAX_PATH et ses limites historiques
  • le préfixe \\?\ pour les chemins étendus
  • la différence entre chemins relatifs et absolus
  • les problèmes de normalisation
  • les chemins UNC pour le réseau
Quelques API utiles ici :
  • GetFullPathNameW
  • GetFinalPathNameByHandleW
  • GetTempPathW
  • GetTempFileNameW
  • SetCurrentDirectoryW
  • GetCurrentDirectoryW
Quand on manipule des fichiers réels sur des machines différentes, les erreurs de chemin sont très fréquentes.

Attributs et informations générales

Pour récupérer les attributs d’un chemin :

Code: Select all

DWORD attr = GetFileAttributesW(L"test.txt");
if (attr == INVALID_FILE_ATTRIBUTES)
    return 1;
Pour les modifier :
  • SetFileAttributesW
  • GetFileAttributesW
Pour récupérer des informations plus complètes à partir d’un handle, on peut utiliser :

Code: Select all

BY_HANDLE_FILE_INFORMATION info;

if (!GetFileInformationByHandle(hFile, &info))
    return 1;
Cette structure contient notamment :
  • dwFileAttributes
  • ftCreationTime
  • ftLastAccessTime
  • ftLastWriteTime
  • dwVolumeSerialNumber
  • nFileSizeHigh
  • nFileSizeLow
  • nNumberOfLinks
  • nFileIndexHigh
  • nFileIndexLow
Elle est utile pour récupérer la taille, des timestamps, le nombre de liens, ou encore un identifiant de fichier lié au volume.

GetFileInformationByHandleEx et les structures modernes

Au-delà de BY_HANDLE_FILE_INFORMATION, il est important de connaitre GetFileInformationByHandleEx, qui permet d’obtenir des informations plus ciblées selon une classe demandée.

Parmi les structures utiles de cette famille :
  • FILE_BASIC_INFO
  • FILE_STANDARD_INFO
  • FILE_NAME_INFO
  • FILE_ATTRIBUTE_TAG_INFO
  • FILE_ID_INFO
  • FILE_COMPRESSION_INFO
  • FILE_ALIGNMENT_INFO
  • FILE_STORAGE_INFO
Par exemple, FILE_BASIC_INFO donne accès à :
  • CreationTime
  • LastAccessTime
  • LastWriteTime
  • ChangeTime
  • FileAttributes
FILE_STANDARD_INFO fournit notamment :
  • AllocationSize
  • EndOfFile
  • NumberOfLinks
  • DeletePending
  • Directory
Ce sont des structures très intéressantes pour du code plus sérieux d’inspection ou de diagnostic.

Timestamps et structure FILETIME

Dès qu’on manipule les dates de fichiers, il faut connaitre FILETIME. Cette structure encode un temps sous forme de deux DWORD représentant un entier 64 bits comptant des intervalles de 100 nanosecondes depuis une époque définie par Windows.

On la rencontre dans beaucoup de structures, par exemple :
  • WIN32_FIND_DATAW
  • BY_HANDLE_FILE_INFORMATION
  • FILE_BASIC_INFO
Les API utiles autour du temps sont notamment :
  • FileTimeToSystemTime
  • SystemTimeToFileTime
  • CompareFileTime
  • GetSystemTimeAsFileTime
  • SetFileTime
Le type d’objet derrière un handle

Tous les HANDLE ne représentent pas la meme chose. Il peut donc etre utile de connaitre :
  • GetFileType
Cette API permet de savoir si un handle correspond à un disque, un pipe, une console, ou autre.

Les valeurs classiques sont :
  • FILE_TYPE_DISK
  • FILE_TYPE_CHAR
  • FILE_TYPE_PIPE
  • FILE_TYPE_UNKNOWN
C’est une petite API, mais elle aide beaucoup quand on écrit du code plus générique.

DeviceIoControl : au-delà des simples fichiers

Le modèle I/O de Windows ne se limite pas aux fichiers de données. Beaucoup d’objets du système sont exposés via des handles et des requetes d’I/O plus générales. C’est là qu’intervient DeviceIoControl.

Exemple schématique :

Code: Select all

DWORD returned = 0;
BYTE outBuffer[256] = { 0 };

BOOL ok = DeviceIoControl(
    hDevice,
    ioControlCode,
    NULL,
    0,
    outBuffer,
    sizeof(outBuffer),
    &returned,
    NULL
);
Cette API est centrale dès qu’on parle :
  • de communication avec des drivers
  • d’accès à des périphériques
  • de certaines opérations spécialisées sur volumes ou systèmes de fichiers
Elle repose sur les IOCTL, ce qui en fait une passerelle très importante entre user-mode et kernel-mode.

Notifications de changement dans les répertoires

Pour surveiller les changements dans un répertoire, une API très utile est ReadDirectoryChangesW.

Elle permet de recevoir des notifications quand des fichiers sont ajoutés, supprimés, renommés ou modifiés dans un dossier donné.

La structure centrale ici est FILE_NOTIFY_INFORMATION.

On la rencontre quand on veut construire :
  • un watcher de dossier
  • un outil de synchronisation
  • un indexeur
  • une surveillance temps réel
Les pipes et les I/O génériques

Même si ce cours est centré sur le filesystem, il faut garder à l’esprit que ReadFile et WriteFile ne servent pas uniquement aux fichiers disque. On les utilise aussi avec :
  • des pipes anonymes
  • des named pipes
  • certaines consoles
  • divers objets exposant un modèle d’I/O compatible
C’est important, parce que cela montre que Win32 adopte un modèle d’I/O relativement unifié autour des handles.

Lecture et écriture positionnées

Avec les I/O overlapped, Offset et OffsetHigh permettent d’effectuer des opérations à une position précise sans dépendre du pointeur de fichier partagé par le handle dans le modèle synchrone classique. C’est un point important pour le multithreading et pour les I/O parallèles.

Autrement dit, la structure OVERLAPPED peut aussi servir à exprimer “où” lire ou écrire.

Les structures qu’il faut connaitre

Pour avoir déjà une base propre, il faut au minimum connaitre les structures suivantes :
  • OVERLAPPED
  • WIN32_FIND_DATAW
  • BY_HANDLE_FILE_INFORMATION
  • FILETIME
  • FILE_BASIC_INFO
  • FILE_STANDARD_INFO
  • FILE_NAME_INFO
  • FILE_ID_INFO
  • FILE_NOTIFY_INFORMATION
  • SECURITY_ATTRIBUTES
Tu n’utiliseras pas toutes ces structures dans chaque programme, mais les reconnaitre te fera gagner énormément de temps quand tu liras du code Win32 plus avancé.

Les API à connaitre absolument

Pour l’ouverture, la fermeture et les opérations de base :
  • CreateFileW
  • ReadFile
  • WriteFile
  • CloseHandle
  • FlushFileBuffers
  • GetLastError
Pour le positionnement et la taille :
  • SetFilePointerEx
  • GetFileSizeEx
  • SetEndOfFile
Pour les I/O asynchrones :
  • GetOverlappedResult
  • CancelIo
  • CancelIoEx
  • CancelSynchronousIo
Pour les fichiers et répertoires :
  • FindFirstFileW
  • FindNextFileW
  • FindClose
  • CreateDirectoryW
  • RemoveDirectoryW
  • DeleteFileW
  • MoveFileExW
  • CopyFileExW
  • ReplaceFileW
Pour les attributs et métadonnées :
  • GetFileAttributesW
  • SetFileAttributesW
  • GetFileInformationByHandle
  • GetFileInformationByHandleEx
  • SetFileTime
  • GetFinalPathNameByHandleW
Pour les verrous et notifications :
  • LockFileEx
  • UnlockFileEx
  • ReadDirectoryChangesW
Pour les I/O spécialisées :
  • DeviceIoControl
  • GetFileType
Erreurs classiques

Comme souvent en Win32, les erreurs les plus pénibles ne viennent pas d’une API exotique, mais d’un mauvais usage des bases.

Parmi les fautes très fréquentes :
  • oublier CloseHandle
  • ne pas vérifier les codes de retour
  • ignorer GetLastError quand une API échoue
  • bloquer un thread important avec des I/O synchrones
  • supposer que bytesRead ou bytesWritten valent toujours la taille demandée
  • mal gérer les chemins, les partages ou les modes d’ouverture
  • confondre objet fichier, handle de fichier et chemin
  • utiliser FILE_FLAG_NO_BUFFERING sans respecter les contraintes d’alignement
Conclusion

Le système de fichiers et les I/O sous Windows reposent sur quelques idées qu’il faut vraiment maitriser : presque tout passe par des handles, CreateFile est l’API d’entrée majeure, ReadFile et WriteFile forment la base des opérations synchrones, OVERLAPPED ouvre la porte aux I/O asynchrones, et tout le reste gravite autour de la bonne gestion du modèle de partage, des chemins, des métadonnées et du cycle de vie des ressources.

Si tu maitrises déjà correctement :
  • CreateFileW
  • ReadFile et WriteFile
  • SetFilePointerEx et GetFileSizeEx
  • OVERLAPPED et GetOverlappedResult
  • WIN32_FIND_DATAW et BY_HANDLE_FILE_INFORMATION
  • FlushFileBuffers, LockFileEx et ReadDirectoryChangesW
  • GetFileInformationByHandleEx
  • DeviceIoControl
alors tu possèdes déjà une base solide en I/O Win32.

Et surtout, tu commences à voir que le “filesystem” sous Windows n’est pas juste un ensemble de fonctions pour lire des octets, mais un vrai modèle d’objets, d’handles, d’opérations synchrones ou asynchrones, et d’abstractions noyau que l’on retrouve partout dans l’architecture système.

A bientot pour le prochain cours :D

Who is online

Users browsing this forum: No registered users and 1 guest