retour




Memory mapped file

Notice

Le présent document est publié sous la licence « Verbatim Copying » telle que définie par la Free Software Fondation dans le cadre du projet GNU.


Toute copie ou diffusion de ce document dans son intégralité est permise (et même encouragée), quel qu'en soit le support à la seule condition que cette notice, incluant les copyrights, soit préservée.


Copyright (C) 2007 Yann LANGLAIS, ilay.


La plus récente version en date de ce document est sise à l'url http://ilay.org/yann/articles/mmap/.


Toute remarque, suggestion ou correction est bienvenue et peut être adressée à Yann LANGLAIS, ilay.org.

Introduction

Présentation

mmap

munmap

Opérations sur les fichiers

La première des utilisations qui vient à l'esprit quand on parle de « fichiers cartographiés en mémoire » est justement de faire en sorte que le contenu d'un fichier soit représenté en mémoire.

Lire un fichier

Dans un premier temps, regardons ce que peut nous apporter mmap sur un exemple simple : la lecture d'un fichier, ou plus exactement, le passage en revue de tous les octets d'un fichier. Afin d'avoir des points de comparaison, nous allons traiter le problème de trois façons différentes :

Utilisation des fonctions sur les flux (stdio)

Nous allons nous baser sur les fonctions de haut niveau fopen(), fgetc() et fclose().

Le code va se découper en 4 étapes :

  1. la détermination de la taille du fichier,
  2. l'ouverture du fichier
  3. le parcours de chaque octet du ficher
  4. la fermeture du fichier.

L'étape 1 vise à connaître a priori le nombre d'octet du fichier. Il est possible d'ignorer cette taille et de se baser sur la fonction feof(). Cependant, cette dernière prend pas mal de temps et elle est utilisée à toutes les itérations. une autre alternative serait de comparer le retour de fgetc avec le caractère EOF (Ctrl-D).

Le parcourt est simulé par une simple affectation à une variable de type char. On pourrait faire une sortie écran ou fichier pour vérifier le bon fonctionnement. Cependant, dans l'optique de l'évaluation des performances respectives de chaque versions, il est préférable d'éviter les fonctions de formattages qui sont à ma fois très couteuses et dont la durée d'exécution dépend fortement de la charge machine et des éventuels blocages d'entrées-sorties.

Une boucle est ajoutée pour pouvoir traiter plusieurs fichiers.

Voici donc le code source « stdio_read_file.c »:

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

int
main(int n, char *a[]) {
    int i, j;
    FILE *pf;
    struct stat stats;
    char c;

    /* if (n < 2) return 1; */

    for (j = 1; j < n; j++) {

        /* 1 Get file size: */
        if (stat(a[j], &stats)) {
            perror("Cannot stat file: ");
            return 2;
        }

        /* 2 Open the file: */
        if (!(pf = fopen(a[j], "r"))) {
            perror("Cannot open file: ");
            return 3;
        }

        /* 3 Acces the file: */
        for (i = 0; i < stats.st_size; i++) {
            c = getc(pf);
        }

        /* 4 Fermeture du fichier: */
        fclose(pf);
    }
    return 0;
}

Pour le compiler, on tapera :

cc stdio_read_file.c -o stdio_read_file

ou

gcc stdio_read_file.c -o stdio_read_file

Utilisation de fonctions de plus bas niveau (unistd)

Après les fonctions de haut niveau, voyons les fonctions de plus bas niveau open(), read() et close().
Le code va se découper en 7 étapes :

  1. la détermination de la taille du fichier,
  2. l'ouverture du fichier
  3. l'allocation de la mémoire,
  4. le chargement de l'ensemble du fichier en mémoire,
  5. le parcours de chaque octet,
  6. la désallocation de la mémoire,
  7. la fermeture du fichier.

Les étapes 3 et 4 visent à éviter la lecture octet par octet par la fonction read(). En effet, cette fonction n'est pas optimale pour des octets. En allouant tout l'espace nécessaire pour le chargement en mémoire, on accélère beaucoup les choses... et l'on obtiendrait presque l'équivalent d'un « mapping » en mémoire. En fait, ce n'est pas du tout un mapping, mais une duplication des données.

Voici donc le code source « io_read_file.c »:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int
main(int n, char *a[]) {
    int i, j;
    int  fid;
    struct stat stats;
    char *p, c;

    for (j = 1; j < n; j++) {

        /* 1 Get file size: */
        if (stat(a[j], &stats)) {
            perror("Cannot stat file: ");
            return 2;
        }

        /* 2 Open the file: */
        if ((fid = open(a[j], O_RDONLY)) < 1) {
            perror("Cannot open file: ");
            return 3;
        }

        /* 3 Allocate memory: */
        if (!(p = (char *) malloc(stats.st_size))) {
            perror("Cannot allocate memory: ");
            close(fid);
            return 4;
        }

        /* 4 Read the whole file : */
        read(fid, p, stats.st_size);

        /* 5 Access the data : */
        for (i = 0; i < stats.st_size; i++) c = p[i];

		/* 6 Deallocate memory: */
		free(p);

        /* 7 Close the file: */
        close(fid);
    }
    return 0;
}

Pour le compiler, on tapera :

cc io_read_file.c -o io_read_file

ou

gcc io_read_file.c -o io_read_file

Utilisation de mmap

Et maintenant, passons à mmap

Le code va se découper en 6 étapes :

  1. la détermination de la taille du fichier,
  2. l'ouverture du fichier,
  3. le mapping du fichier en mémoire,
  4. le parcours de chaque octet,
  5. l'unmapping du fichier,
  6. la fermeture du fichier

L'algorithme reste a peu près le même, sauf que l'opération de lecture est remplacée par une opération de mappage et sa contrepartie.

Voici donc le code source « map_read_file.c »:

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

int
main(int n, char *a[]) {
    int i, j;
    int fid;
    struct stat stats;
    char *p, c;

    for (j = 1; j < n; j++) {

        /* 1 Get file size: */
        if (stat(a[j], &stats)) {
            perror("Cannot stat file: ");
            return 2;
        }

        /* 2 Open the file: */
        if ((fid = open(a[j], O_RDONLY)) < 1) {
            perror("Cannot open file: ");
            return 3;
        }

        /* 3 Map the file to process memory: */
        if (!(p = (char *)mmap(NULL, stats.st_size, PROT_READ, MAP_PRIVATE, fid, 0))) {
            perror("Cannot map file: ");
            close(fid);
            return 4;
        }

        /* 4 Access the file: */
        for (i = 0; i < stats.st_size; i++)
            c = p[i];

        /* 5 Unmap the file */
        munmap(p, stats.st_size);

        /* 6 Close the file: */
        close(fid);
    }

    return 0;
}

Pour le compiler, on tapera :

cc map_read_file.c -o map_read_file

ou

gcc map_read_file.c -o map_read_file

Comparaisons

On peut envisager différentes comparaisons. La première, du point de vue intuitif, porte sur le code. La seconde, déjà moins intuitive, est celle des appels systèmes. Enfin, on peut aussi se pencher sur les différences de performances des 3 solutions.

Complexité du code

Dans l'ensemble, la complexité des trois solutions reste comparable. L'utilisation de mmap() est moins intuitive que notre traditionnel triptique ouverture/lecture/fermeture. Mais, à part le nombre et le sens des arguments utilisés, le mapping nous permet de n'avoir à manipuler qu'un simple pointeur.

Les autres différences portent sur la façon de gérer les données du fichier. Avec les fonctions de flux comme avec mmap, nous n'avons pas besoin de réserver de mémoire. Par contre, avec le read, il est nécessaire de stocker les données quelque part. Nous devons donc faire un malloc;.. et un free.

Complexité des appels systèmes

Pour vérifier la complexité des appels systèmes, on ne lit qu'un seul fichier. Par ailleurs, on crée une version modifiée de la vesion « stdio » afin de n'effectuer qu'un seul fgetc().

Sous Solaris 8, on lance les différents programmes de la manière suivante :

truss nom_du_programme 10Mo

Et on obtient :

stdio
stat("10Mo", 0xFFBEEC1C)                                                     = 0
open("10Mo", O_RDONLY)                                                       = 4
fstat64(4, 0xFFBEEAA8)                                                       = 0
brk(0x00020BF8)                                                              = 0
brk(0x00024BF8)                                                              = 0
ioctl(4, TCGETA, 0xFFBEEA34)            Err#25 ENOTTY
read(4, " y\n y\n y\n y\n y\n y\n".., 8192)                                  = 8192
llseek(4, 0xFFFFFFFFFFFFE001, SEEK_CUR)                                      = 1
close(4)                                                                     = 0
llseek(0, 0, SEEK_CUR)                                                       = 990736
_exit(0)
io
stat("10Mo", 0xFFBEEC24)                                                     = 0
open("10Mo", O_RDONLY)                                                       = 4
brk(0x00020C68)                                                              = 0
brk(0x00A22C68)                                                              = 0
read(4, " y\n y\n y\n y\n y\n y\n".., 10485760)                              = 10485760
close(4)                                                                     = 0
llseek(0, 0, SEEK_CUR)                                                       = 1001947
_exit(0)
mmap
stat("10Mo", 0xFFBEEC24)                                                     = 0
open("10Mo", O_RDONLY)                                                       = 4
mmap(0x00000000, 10485760, PROT_READ, MAP_PRIVATE, 4, 0)                     = 0xFE800000
munmap(0xFE800000, 10485760)                                                 = 0
close(4)                                                                     = 0
llseek(0, 0, SEEK_CUR)                                                       = 994638
_exit(0)

Sous GNU/Linux, on lance la commande suivante :

strace nom_du_programme 10Mo

Et on obtient :

stdio
stat("10Mo", {st_mode=S_IFREG|0644, st_size=10485760, ...})                  = 0
brk(0)                                                                       = 0x501000
brk(0x522000)                                                                = 0x522000
open("10Mo", O_RDONLY)                                                       = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=10485760, ...})                      = 0
mmap(NULL, 32768, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)    = 0x2aaaaaaac000
read(3, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"..., 32768)        = 32768
close(3)                                                                     = 0
munmap(0x2aaaaaaac000, 32768)                                                = 0
exit_group(0)                                                                = ?
Process 16478 detached
io
stat("10Mo", {st_mode=S_IFREG|0644, st_size=10485760, ...})                  = 0
open("10Mo", O_RDONLY)                                                       = 3
mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x2aaaaaada000
read(3, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"..., 10485760)     = 10485760
close(3)                                                                     = 0
exit_group(0)                                                                = ?
Process 16540 detached
mmap
stat("10Mo", {st_mode=S_IFREG|0644, st_size=10485760, ...})                  = 0
open("10Mo", O_RDONLY)                                                       = 3
mmap(NULL, 10485760, PROT_READ, MAP_PRIVATE, 3, 0)                           = 0x2aaaaaada000
munmap(0x2aaaaaada000, 10485760)                                             = 0
close(3)                                                                     = 0
exit_group(0)                                                                = ?
Process 16548 detached

On voit clairement que les deux systèmes n'implémentent pas la lecture des fichiers de la même manière. Sous Solaris, les différents types de lecture sont implémentés de façons différentes. A contrario, sous GNU/Linux, on fait toujours du mmap sans le savoir. On voit bien que sous GNU/Linux, la méthode io effectue une copie inutile. En effet, le fichier étant mappé, la lecture en mémoire de l'ensemble du fichier est une étape inutile d'un point de vue système. Sous GNU/Linux toujours, il n'est pas non plus intéressant de passer par stdio dans le mesure ou la lecture totale du fichier implique plusieurs fgetc(). De ce fait, il est plus rationnel de passer par mmap, même si la syntaxe est un peu plus contraignante.

Sous Solaris, il est plus difficile de se prononcer. On foit que les méthodes stdio et io réservent de la mémoire (appels à la fonction brk()) alors que la solution mmap n'en réserve pas. En nombre d'appels système, la méthode mmap est la plus concise.

Performances

Pour analyser les performances, on peut se baser sur des fichiers de différentes tailles, allant de 1ko à 10Mo, que l'on pourra générer de la manière suiavante :

yes | head -c 10485760 > 10Mo

On relie 10 fois le même fichier pour mieux mettre en évidence les différences, et on réïtére 10 fois les mesures afin de s'affranchir de l'effet de la charge du système :

for i in `seq 10`; do time map_read_file 10Mo 10Mo 10Mo 10Mo 10Mo 10Mo 10Mo 10Mo 10Mo 10Mo ; done

Une fois la moyenne de ces 10 exécutions calculées, on obtient les valeurs suivant:

SolarisGNU/Linux
stdioiommap stdioiommap
1 Ko 0.02820.02320.0295
10 Ko 0.03190.03970.0324
100 Ko0.11010.07610.0637
1 Mo 0.62980.50740.3365
10 Mo 6.43864.99603.06851.68201.07600.9800

Ces résultats confirment l'intérêt de mmap par rapport aux autres méthodes, même sous Solaris, en particulier pour les fichiers de taille importante.

Ecrire un fichier

L'écriture dans un fichier avec mmap peut s'avérer un peu plus difficile. En effet, comme nous avons notre fichier en mémoire, nous devons sa taille. On distingue deux cas. Dans le premier, on ne fait que remplacer des octets existants. Dans le deuxième, on ajoute des octets à la fin d'un fichier.

Emballer le tout

Parler des différences de mémoires partagées pour les libs (flags lors du truss/strace pendant le chargement des bibliothèques.

Gérer la mémoire

Partager la mémoire entre applications