I/O asynchrones (épisode 1)

Mardi, juillet 5, 2011
By David Baffaleuf in Operating System (dbaffaleuf@capdata-osmozium.com) [71 article(s)]

Ce post est le premier d’une série sur les API systèmes impliquées au niveau de SQL Server:

  1. Gestion des entrées/sorties: alignement, I/Os asynchrones, alternate streams, scatter gather, I/O completion ports, etc…
  2. Le multithreading:  synchronisation, events, mutexes, spinlocks, sections critiques…
  3. La mémoire: allocation, fichiers mappés, AWE, large pages…

Cette série plutôt barbare va dévier un peu des sujets SGBD traditionnels. Elle s’adresse à toute personne souhaitant approfondir les mécanismes intrinsèques de SQL Server, pour en comprendre la logique et les contraintes. Ces articles seront classés sous les sujets Operating System et SQL Server. Leur lecture nécessite que vous ayez des connaissances minimum sur C/C++ et l’API win32.

Dans un post précédent, nous avions déjà effleuré la question de l’écriture à travers le cache, nous reprendrons le même exemple pour évoquer le sujet du jour: les I/Os asynchrones dans des fichiers. C’est une question large donc je propose d’éclater le sujet en trois épisodes:

  1. Ce premier pour expliquer le principe et montrer à travers un petit exemple simple  les avantages, mais aussi les contraintes de faire des I/Os asynchrones.
  2. Un second pour montrer comment gérer plus efficacement  la synchronisation des I/Os asynchrones entre plusieurs threads, notamment avec l’utilisation d’I/O completion ports.
  3. Un dernier pour montrer l’utilisation qui en est faite par SQL Server, ainsi que certaines problématiques particulières.

SYNCHRONE vs ASYNCHRONE:

Quand on écrit un programme sous windows, on doit tôt ou tard accéder à un device en lecture / écriture. La plupart des applications que l’on utilise n’ont pas besoin d’écrire très souvent des  données durables. Souvent elles misent sur la bufferisation des entrées / sorties, c’est à dire l’écriture ou la lecture de données en mémoire. Parfois cependant, elles devront lire des données sur disque, par exemple lorsqu’elles démarrent, ou bien écrire des informations dans un fichier de configuration lorsqu’elles s’arrêtent. Le moyen de plus simple de lire ou d’écrire dans un fichier est alors de lancer une entrée/sortie synchrone en utilisant ReadFile() ou WriteFile().

On utilise le terme synchrone parce que le thread qui a initié cette écriture ou cette lecture doit attendre l’acquittement ou le retour de l’entrée/sortie avant de pouvoir continuer son exécution. Dès que la demande de lecture ou d’écriture est lancée, il entre immédiatement dans un état d’attente, il ne peut rien faire d’autre. De la sorte, s’il doit lancer de nombreuses opérations d’entrées sorties, il va devoir attendre que chacune soit terminée avant de pouvoir exécuter la suivante, ce qui va limiter en quelque sorte sa capacité à traiter un grand nombre d’opérations par seconde.

L’alternative est d’indiquer au thread de ne pas attendre le retour de l’I/O pour continuer son exécution, en postant des I/Os asynchrones (ou overlapped IO). Il existe plusieurs contraintes associées à l’utilisation des IO asynchrones, qui rendent la chose nettement plus compliquée à gérer:

  • Lors d’une IO synchrone, le device object maintient un pointeur d’offset de sorte que si on lance deux lectures de 8K à la suite par exemple, le kernel sait toujours là où il s’est arrêté et là où il doit reprendre. D’ailleurs, si on regarde le prototype de ReadFile() ou WriteFile(), il n’y a pas d’information d’offset passées lors de l’appel. Avec une IO asynchrone, le pointeur d’offset est ignoré par le driver donc il faut le gérer dans le code. C’est une des différences les plus caractéristiques entre ces deux modes, et un vrai casse tête comme on va le voir dans un instant.
  • Il y a de surcroît forcément un point dans le code où on ne  peut plus avancer sans avoir le retour de ces IO. Il faut donc trouver un moyen de se synchroniser avec la fin des opérations.

Dans la pratique, chaque thread  peut se synchroniser de 4 manières différentes:

  1. Sur le fichier lui-même, car le fichier est un handle comme un autre. Ce serait le plus simple, il n’y a pas d’autre objet à créer. Le problème est que s’il y a plusieurs I/Os en cours sur le même fichier, on ne pourra pas déterminer laquelle vient de se terminer car le handle est signalé dès que la première I/O est retournée.
  2. Sur un event associé à l’I/O. Comme nous allons le voir dans la section suivante, on utilise une structure comme traceur pour suivre chaque entrée/sortie asynchrone et cette structure contient par défaut un event associé, sur lequel le thread pourra se synchroniser. C’est la méthode que l’on va aborder dans cet épisode.
  3. En utilisant une routine APC. A chaque thread créé avec beginthreadex() est associé une APC queue qui lui permet d’exécuter une routine de complétion lorsque sa tache est terminée. On peut utiliser ces routines avec les primitives WriteFileEx() et ReadFileEx(). L’avantage principal de cette méthode est que l’exécution de l’APC se fait en user mode, donc le programme conserve le contrôle pendant toute l’opération. Malgré ces quelques atouts, elle a une réputation calamiteuse [1] et reste compliquée à gérer. Le simple fait de gérer la completion dans une fonction séparée rend les choses beaucoup moins maintenables et débogables.
  4. Sur un outil de synchronisation plus perfectionné, comme un I/O completion port. SQL Server utilise un I/O completion port par CPU-node pour gérer les entrées / sorties réseau. Nous verrons l’utilisation d’un tel outil dans l’épisode 2 pour gérer des I/O dans des fichiers.

Principes d’utilisation dans le code:

La structure OVERLAPPED

Que l’on utilise des entrées/sorties synchrones ou asynchrones, les primitives utilisées sont les mêmes [2], on leur passera simplement des paramètres différents. Pour pouvoir pister nos entrées / sorties asynchrones, on va utiliser un genre de traceur sous la forme d’une structure C appelée  Overlapped. Si on regarde sa composition de plus près:

typedef struct _OVERLAPPED {
  ULONG_PTR Internal;
  ULONG_PTR InternalHigh;
  union {
    struct {
      DWORD Offset;
      DWORD OffsetHigh;
    };
    PVOID  Pointer;
  };
  HANDLE    hEvent;
} OVERLAPPED, *LPOVERLAPPED;

Parmi les propriétés importantes pour le code utilisateur, Offset et OffsetHigh délimitent une portion de 64 bits de données et indiquent à quel endroit on va commencer d’écrire dans le fichier. Offset gère les 32 premiers bits pour les fichiers < 4Gb (souvenez-vous FAT 32…), et OffsetHigh les 32 suivants pour les fichiers de plus de 4Gb. Si on exécutait deux IO synchrones, elles s’exécuteraient l’une derrière l’autre et le kernel s’occupperait d’indiquer à la seconde écriture où elle doit commencer, parce que la première s’est terminée et donc on sait où on en est. Mais dans le cas d’écritures asynchrones, on n’a pas cette information, donc si on exécute une seconde entrée/sortie avant que la première ne soit terminée, il faut pouvoir gérer les offsets soi-même. C’est le rôle de ces deux propriétés, mais c’est à nous de les maintenir. Lors d’une première exécution, ces deux valeurs doivent être initalisées (souvent à 0 pour marquer le premier offset en début de fichier):

OVERLAPPED ov; 
ov.Offset = 0; 
ov.OffsetHigh = 0;

De son côté, hEvent est l’event  que l’on va pouvoir utiliser pour synchroniser le thread sur l’I/O asynchrone. Si on utilise cette technique, alors il va falloir l’initialiser avec CreateEvent() en lui passant un event à reset manuel et dans l’état non – signalé (second et troisième paramètres, cf MSDN CreateEvent())

Une structure OVERLAPPED est associée à chaque I/O asynchrone que l’on va effectuer. Si on en lance 8 en même temps, il faudra créer et initialiser 8 structures. On peut utiliser un OVERLAPPED [] pour stocker toutes nos instances de structure par exemple. La chose la plus importante au sujet d’OVERLAPPED, est qu’elle doit toujours être accessible pendant toute la durée de vie de l’I/O, donc pas question d’allouer le tableau d’OVERLAPPED[] sur la stack. Le cas d’erreur le plus courant est d’exécuter un appel à ReadFile() ou WriteFile() dans une fonction et gérer la complétion dans le main ou une autre fonction, par exemple:

VOID ReadMyFile(HANDLE h)
{
    OVERLAPPED ov;
    BYTE b[255];
    ov.Offset=0;
    ov.OffsetHigh=0;
    ReadFile(h,b,255,NULL,&ov);
}

Lorsque la fonction sort la stack est vidée alors que le driver pointe encore vers &ov qui n’est déjà plus valide.

Pour indiquer à Windows que l’on souhaite faire des I/O asynchrones sur un fichier, on utilise un flag spécifique FILE_FLAG_OVERLAPPED, que l’on passe à CreateFile(), tel que:

// Ouverture du fichier Writethrough + No Buffering + Overlapped ----------------------------
HANDLE hOutputFile=CreateFile(argv[1]
       ,GENERIC_READ
       | GENERIC_WRITE
       ,0
       ,NULL
       ,CREATE_ALWAYS
       ,FILE_ATTRIBUTE_NORMAL
       | FILE_FLAG_WRITE_THROUGH
       | FILE_FLAG_NO_BUFFERING
       | FILE_FLAG_OVERLAPPED
       ,NULL);

 if (INVALID_HANDLE_VALUE==hOutputFile)
 {
       printf("Unable to open file %s.  Last error=%d\n",argv[1],GetLastError());
       return 1;
 }

A noter que cet exemple utilise aussi des I/Os non bufferisées comme nous l’avons déjà vu dans l’article sur SATA. SQL Server utilise précisément tous ces flags pour ouvrir les fichiers MDF, NDF et LDF. Il ne reste plus qu’à appeler ReadFile() ou WriteFile() en passant la structure Overlapped initialisée:

WriteFile(hOutput,csBuffer,dg.BytesPerSector,&dwBytesWritten,&ov)

Gestion de Offset et OffsetHigh

Si on exécute au moins deux appels à la suite à ReadFile() ou WriteFile() , Windows ne sait pas où le premier en est et où le second peut commencer à lire ou à écrire. C’est donc au programme utilisateur de comptabiliser les offsets exacts où chaque IO va devoir s’appliquer. On utilise pour cela les deux propriétés Offset et OffsetHigh de la structure Overlapped.

Historiquement, le compilateur C qui supporte l’API win32 ne comporte pas de type natif sur 64 bits (ce n’est pas le cas du compilateur VC++). Donc on ne peut pas tracer des offsets d’une valeur supérieure à 4Gb. Et encore aujourd’hui, il faut jongler avec une construction de type LARGE_INTEGER pour arriver à jouer avec des offsets au delà de 2e32:

typedef union LARGE_INTEGER {
    struct {
        DWORD LowPart;
        LONG HighPart;
       };
    LONGLONG QuadPart;
};

En fait le type LARGE_INTEGER est une union d’un DWORD et d’un LONG pour former un LONGLONG de 64 bits. Si on considère le schéma ci-dessous:

On utilisera donc QuadPart pour allouer de nouvelles valeurs d’offsets sur 64 bits puis LowPart et HighPart pour les passer respectivement à Offset et OffsetHigh, quelque chose comme :

OffsethOutput.QuadPart=0;
while(!EOF)
{
    ov.Offset        = OffsethOutput.LowPart;
    ov.OffsetHigh    = OffsethOutput.HighPart;
    BOOL b = Readfile (..., &ov);
    OffsethOutput.QuadPart += (LONGLONG) dg.BytesPerSector;
}

Gérer le retour de ReadFile() / WriteFile():

Voici un exemple d’appel à WriteFile() avec une structure overlapped tel que codé dans l’exemple:

if (!WriteFile(hOutput,csBuffer,dg.BytesPerSector,&dwBytesWritten,&ov))
{
     DWORD dwLastErr=GetLastError();            
     if (ERROR_IO_PENDING == dwLastErr)
     {    
         isASYNCWRITE=TRUE;    // On a une IO asynchrone
         OffsethOutput.QuadPart += (LONGLONG) dg.BytesPerSector;    // On décale Offset et OffsetHigh de la valeur de l'IO
     }            
     else
     {
          if (ERROR_SUCCESS == dwLastErr)        // C'est une IO synchrone
          isASYNCWRITE=FALSE;
          else
          {
              printf("Error when Writing to file %S. Last error=%d\n",argv[1],GetLastError());
              return -1;
          }
      }
}

Déjà quelque chose doit vous interpeller à ce niveau-là du code. Dans les faits, on n’a jamais la garantie à 100% d’obtenir du système qu’il poursuive pour nous une IO asynchrone, car l’IO Manager a toujours le dernier mot. Il peut décider pour une raison ou pour une autre de transformer cette demande en IO synchrone. Donc il faut toujours tester le code retour de WriteFile() quand on lui passe une structure OVERLAPPED:

  • ERROR_SUCCESS =>Le kernel décide de transformer l’appel en IO synchrone. Il peut y avoir plusieurs raisons, notamment lorsqu’on écrit dans un fichier encrypté NTFS, ou lorsque l’écriture déclenche une augmentation de taille du fichier, cela doit vous rappeler certaines choses côté sikouel…  Une autre condition dans le cas d’un ReadFile() est que le kernel est capable d’accommoder la lecture immédiatement parce que la page demandée se trouve déjà dans le cache NTFS, ce qui ne sera jamais notre cas puisqu’on utilise FILE_FLAG_NO_BUFFERING.
  • ERROR_IO_PENDING => dans ce cas l’IO est asynchrone. Ça ne sert donc à rien  d’attendre de WriteFile() qu’il nous renvoie une valeur d’octets écrits puisque lorsqu’il retourne au caller, il ne le sait pas lui-même. L’IO va se terminer quelque part dans le futur, donc pour l’instant on n’aura rien dans &dwBytesWritten. Dans ce cas on indique la taille de cluster comme incrément au QuadPart pour la prochaine valeur d’Offset à lire.
  • ERROR_HANDLE_EOF  => peut être retournée lorsqu’on a atteint la fin du fichier par exemple dans le cas d’une lecture, mais comme on écrit on ne le teste pas dans notre cas.
  • AUTRE => auquel cas c’est une erreur et il faut la traiter.

Récupérer le résultat de l’IO une fois terminée avec GetOverlappedResult()

Utiliser des IO asynchrones, c’est un peu comme jongler avec plusieurs balles à la fois: au bout d’un moment il va falloir les rattraper. Il faut donc choisir un point dans le code où on va devoir se synchroniser pour attendre le retour de ces IO avant de pouvoir continuer. On peut utiliser les primitives standard de synchronisation telles que WaitForSingleObject(), et dans notre cas puisqu’on attend le retour de plusieurs IO, WaitForMultipleObjects().On bloque en passant à la primitive le ou les events qui attendent d’être signalés:

dwEventSignPos = WaitForMultipleObjects(MAXOUTSTANDINGIO, hEvents, FALSE, INFINITE) - WAIT_OBJECT_0;

Lorsqu’un premier event est signalé parce que son IO vient de se terminer, WaitForMultipleObjects() retourne WAIT_OBJECT_0, et le code peut se poursuivre. WAIT_OBJECT_0 représente le premier event qui vient d’être signalé, on va s’en servir pour déterminer l’indice de cet event dans un tableau, comme dans notre exemple commenté ci-dessous. On va ensuite devoir récupérer le nombre d’octets écrits, et si l’on souhaite réutiliser la structure Overlapped, réinitialiser son event, puis rebloquer en attendant le retour d’une autre IO:

ResetEvent (hEvents[dwEventSignPos]);

Il existe plusieurs primitives win32 pour gérer la fin d’une IO asynchrone:

  1. Utiliser GetOverlappedResults(): permet de récupérer la quantité d’octets écrits ou lus, ainsi que l’event qui a été signalé.
  2. Utiliser HasOverlappedIOCompleted(): renvoie vrai ou faux en fonction de la completion de l’IO. Les informations d’octets lus ou écrits sont disponibles dans la structure Overlapped (Internal contient le code retour et InternalHigh le nombre d’octets lus ou écrits). Dans ce cas on n’aurait pas besoin de bloquer avec WaitForMultipleObjects(), mais il faut prévoir plusieurs points dans le code où faire régulièrement cette vérification. C’est de cette manière que SQL Server procède, comme on le verra dans l’épisode 3. Dans l’exemple:
if (isASYNCWRITE)
{
    if (!GetOverlappedResult(hOutput,&olwrite[dwEventSignPos],&dwBytesWritten[dwEventSignPos],TRUE))
    {
        printf("Error getting pending write IO.  Last error=%d\n",GetLastError());
        break;
    }
    else
    {
    // Action principale: on totalise chaque fin d'écriture async
    llTotalBytesWritten+=dwBytesWritten[dwEventSignPos];
    }
}

Il est important de n’exécuter GetOverlappedResults() que si l’IO est bien asynchrone, c’est pourquoi on teste avec un booléen d’abord.

Voilà, on a fait le tour, place à l’exemple commenté !

Exemple commenté d’utilisation des I/Os asynchrones:

Cet exemple reprend le principe de ce qui avait été utilisé dans l’article sur SATA. Il utilise simplement des écritures asynchrones en plus. Évidemment ça n’a pas grand intérêt de faire des écritures asynchrones dans ce contexte là (mono-thread, on lance un nombre d’IO puis on bloque sur la completion de l’ensemble), mais il a l’avantage d’être simple et de ne pas nous embarquer dans des notions hors cadre de synchronisation de threads et compagnie… Encore une fois, il s’agit juste de comprendre le principe de mise en place.

On définit une quantité de données à écrire dans un fichier (FileSizeInBytes, par défaut 10Mb) ainsi qu’un certain nombre d’outstanding IOs (MAXOUTSTANDINGIO), c’est à dire d’IO simultanées. On ne peut envoyer plusieurs IO simultanées que si on effectue des IO asynchrones, c’est pourquoi on dit que des compteurs tels que ‘Current Disk Queue Length’ par exemple peuvent être normalement élevés avec SQL Server, car on envoie plusieurs IOs en même temps.

// writethrougasync.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#include
#include
#include

#define MAXOUTSTANDINGIO 4            // Definit le nb d'IO max en cours
#define FileSizeInBytes 10485760    // Definit la taille max du fichier

int _tmain(int argc, _TCHAR* argv[])
{
    if (argc!=2)
    {
        printf("Usage is: writethrougasync\n");
        printf("Example:  writethrougasync destfile.dat \n");
        return -1;
    }

    // Initialisation des différents objets nécessaires: OLs, events, HANDLES, etc... ------------------------------
    OVERLAPPED olwrite[MAXOUTSTANDINGIO];    // Ol IO pour l'écriture
    HANDLE hEvents[MAXOUTSTANDINGIO];        // tableau pour stocker les events écritures

    HANDLE hOutput;                             // HANDLE fichier dest
    LARGE_INTEGER OffsethOutput;                // (Offsets,OffsetHigh) olwrite 

    DWORD dwBytesWritten[MAXOUTSTANDINGIO];     // Nb octets lus par olwrite
    LONGLONG llTotalBytesWritten=0;             // Quantité totale d'octets écrits
    BOOL isASYNCWRITE=FALSE;                    // Flag de contrôle IO async / sync pour les écritures
    int NBIOCOMPLETED=0;                        // Compteur d'IOs terminées
    DWORD dwEventSignPos=0;                     // Index de l'event signalé retourné par WaitForMultipleObjects()

    // Récupération de la taille du secteur disque avec un IOCTL ---------------------------------------------------
    DISK_GEOMETRY dg;
    HANDLE hDrive;
    DWORD ioctlJnk; 

    hDrive=CreateFile(TEXT("\\\\.\\PhysicalDrive0")
                           ,0
                           ,FILE_SHARE_READ
                          | FILE_SHARE_WRITE
                          ,NULL
                          ,OPEN_EXISTING
                          ,0
                          ,NULL);

    if (INVALID_HANDLE_VALUE==hDrive)
    {
        printf("Unable to open drive PhysicalDrive0. Last error=%d\n",GetLastError());
        return 1;
    }

    if (! DeviceIoControl(hDrive
                          ,IOCTL_DISK_GET_DRIVE_GEOMETRY
                          ,NULL
                          ,0
                          ,&dg
                          ,sizeof(dg)
                          ,&ioctlJnk
                          ,NULL) )
    {
        printf("Error on IOCTL (IOCTL_DISK_GET_DRIVE_GEOMETRY) to %s.  Last error=%d\n",argv[1],GetLastError());
        return 1;
    }

    CloseHandle(hDrive);

    // Ouverture du fichier cible en write-though / no buffering / Overlapped -------------------------------------
    hOutput = CreateFile(argv[1]
                         ,GENERIC_READ
                         | GENERIC_WRITE
                          ,0
                          ,NULL
                          ,CREATE_ALWAYS
                          ,FILE_ATTRIBUTE_NORMAL
                          | FILE_FLAG_NO_BUFFERING
                          | FILE_FLAG_WRITE_THROUGH
                          | FILE_FLAG_OVERLAPPED
                          ,NULL);

    if (INVALID_HANDLE_VALUE==hOutput)
    {
        printf("Unable to open output file %S.  Last error=%d\n",argv[1],GetLastError());
        return -1;
    }

    // Initialisation du tampon --------------------------------------------------------------------------------
    wchar_t *csBuffer = (wchar_t *)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,dg.BytesPerSector * sizeof(wchar_t));
    int len = swprintf_s(csBuffer,dg.BytesPerSector,L"abcdefghijklmnopqrstuvwxyz");    

    // On initialise les events
    for(int i=0;i
  1. Comme on veut envoyer 4 IO en même temps, on est obligés d’avoir 4 structures Overlapped et 4 events. Donc on utilise un tableau de dimension [MAXOUTSTANDINGIO] de chaque pour stocker nos objets. Les autres variables locales sont plutôt évidentes: un HANDLE pour le fichier, un LARGE_INTEGER pour gérer nos offsets, deux variables pour comptabiliser les octets écrits, etc…
  2. Comme dans l’article précédent, l’utilisation conjointe de FILE_FLAG_WRITE_THROUGH et FILE_FLAG_NO_BUFFERING impose de faire des écritures alignées sur la taille du secteur disque, donc on va le récupérer avec un IOCTL. Sur mon laptop, il est de 512 octets.
  3. On ouvre le fichier en lui passant les deux flags pré-cités plus FILE_FLAG_OVERLAPPED qui indique que l’on souhaite effectuer des IO asynchrones.
  4. On initialise le buffer à écrire avec swprintf_s()
  5. On initialise les 4 Events avec CreateEvent() en passant bien bManualReset à TRUE (on resette manuellement l’event, prerequis pour se synchroniser dessus) et bInitialState à FALSE (non signalé, il sera signalé lors de la fin de l’IO correspondant à la structure overlapped).
  6. On initialise à 0 les Offsets en utilisant le QuadPart, on commence à écrire en tout début de fichier.
  7. On boucle tant qu’on n’a pas écrit la quantité de données attendue (10mb):
  8. On met à jour Offset et OffsetHigh puis on envoie les 4 premières IO simultanées: si GetLastError() renvoie 997 (ERROR_IO_PENDING), ASYNCWRITE passe à true et on a bien une IO asynchrone. Sinon si le code retour de WriteFile() est ERROR_IO_SUCCESS, alors on a une IO synchrone, et sinon on a une erreur.
  9. On incrémente les valeurs d’offsets en ajoutant la quantité d’octets envoyés avec le QuadPart.
  10. Lorsqu’on a envoyé notre premier paquet d’IOs, on bloque avec WaitForMultipleObjects sur le tableau d’events. Lorsqu’un premier event est signalé, on exécute un GetOverlappedResult () sur la structure Overlapped correspondante.
  11. On réinitialise l’event pour cette IO (ResetEvent() repasse bInitialState à FALSE).
  12. Et on incrémente la quantité totale d’octets écrits jusqu’à écrire les 10Mb, etc…

Résumé:

  • Par défaut, une opération d’entrée/sortie est synchrone, et le thread qui l’a initiée doit bloquer et attendre que chaque opération soit terminée avant de poursuivre. Pour ne pas bloquer, on utilise des IO asynchrones.
  • On indique le flag FILE_FLAG_OVERLAPPED à la primitive d’ouverture pour indiquer que l’on souhaite faire des IO asynchrones.
  • La structure Overlapped contient des informations sur chaque IO asynchrone. Elle doit être initialisée et maintenue dans le code utilisateur.
  • Il existe plusieurs façons de se synchroniser sur le retour des IOs, la plus simple étant d’utiliser un event associé à chaque structure overlapped. Cet event doit être à reset manuel et initialisé dans l’état non-signalé.
  • Il faut maintenir les pointeurs d’offsets manuellement dans le cas d’une IO asynchrone. On utilise les propriétés Offset et OffsetHigh pour cela.
  • Rien ne garantit que l’IO sera bien asynchrone, il existe des cas où il ne sera pas possible de le faire, donc il faut pouvoir tester le code retour de ReadFile() ou WriteFile() et agir en conséquence.
  • On peut récupérer la fin d’une IO asynchrone en utilisant GetOverlappedResult() ou HasOverlappedIOCompleted().

La prochaine fois, on verra comment faire pour utiliser les IO completion ports pour gérer les IO asynchrones.

A+. David B.

Biblio:

Windows via C++ (Jeffrey Richter / Christope Navarre)
Floundercraft: a Joseph M. Newcomer resource
http://download.microsoft.com/download/6/E/8/6E882A06-B71B-4642-9EB4-D1EA0D6223C8/SQL%20Server%20IO%20Reliability%20Program%20Requirements%20Document.docx : SQL Server I/O Reliability Program Review.

Notes:

[1] Dave Cutler, le papa de VMS et du premier kernel windows NT avait été tellement déprimé par l’implémentation des APC dans VMS qu’il aurait déclaré plus tard lorsqu’il travaillait sur le kernel NT : « Asynchronous callback I/O will go into Windows over my dead body! »
[2] en dehors de ReadFileEx et WriteFileEx qui sont réservées aux I/Os asynchrones.

Continuez votre lecture sur le blog :




Cliquer pour partager cet article sur Viadeo
Cliquer sur "CAPTURER" pour sauvegarder cet article dans Evernote Clip to Evernote

Tags:

Leave a Reply