Memory Mapped Files

Martin Kompf

Wenn verschiedene Prozesse Daten untereinander austauschen sollen, so kann dazu auf Memory Mapped Files zurückgegriffen werden. Dies sind Dateien, die sich per Systemaufruf in den Arbeitsspeicher eines oder mehrerer Prozesse einblenden lassen.

Hinweis: Der in diesem Artikel gezeigte Sourcecode kann als Zipfile heruntergeladen werden.

Der Datenaustausch zwischen verschiedenen Prozessen ist eine immer wiederkehrende Aufgabe für den Entwickler von Client-Server-Programmen. Es gibt dazu eine Vielzahl von Möglichkeiten, die unter dem Begriff »Inter Prozess Communication« (IPC) zusammengefasst werden. In den Artikeln zur Netzwerkprogrammierung (Teil 1, Teil 2) wurde bereits eine IPC-Methode vorgestellt: Sockets. Diese eignet sich insbesondere für die Kommunikation über Rechnergrenzen hinweg.

Hier soll nun mit der Verwendung von Memory Mapped Files eine weitere IPC-Methode beschrieben werden. Diese eignet sich ausschließlich für Prozesse, die auf einem System laufen (kann also nicht über das Netzwerk arbeiten), bietet gegenüber Sockets jedoch den Vorteil der höheren Performance. Außerdem müssen die Daten nicht wie bei Sockets serialisiert werden. Last but not least sind die in Memory Mapped Files stehenden Daten automatisch persistent, nach Beenden und Neustart des Programms stehen sie diesem wieder unverändert zur Verfügung.

Leider ist die Art der Anwendung im C-Programm zwischen den Unix- und Windows-Welten völlig unterschiedlich. Die Idee ist jedoch die gleiche: Der Inhalt einer Datei wird per Systemfunktion mmap (Unix) bzw. CreateFileMapping und MapViewOfFile (Windows) in den Adressbereich des jeweiligen Prozesses projiziert. Wenn mehrere Prozesse dies mit derselben Datei tun, so können sie auf diese Art und Weise Daten miteinander austauschen.

Es empfiehlt sich, die zur Verwendung von Memory Mapped Files notwendigen Routinen inklusive Fehlerbehandlung, Zugriffsschutz und Portabilität auf Unix und Windows in ein eigenes Modul memmap auszulagern. Zunächst sei hier die Headerdatei memmap.h angegeben:

#ifndef memmap_h
#define memmap_h 1

struct mappedRegionS;
typedef struct mappedRegionS* mappedRegion;

/* Map a file into the memory of this process.
 * Input parameters:
 *    path:   The name of the file to be mapped.
 *            The file is created if it doesn't exist.
 *    id:     A system wide unique identifier of the mapped region
 *    length: The length of the mapped region.
 *            This parameter is used only if the file doesn't exist, 
 *            otherwise the length of the existing file is used.
 * Output parameter:
 *    hReg:   A handle that describes the mapped region.
 * Return value:
 *    0: Successfull completion.
 *   -1: An error occured. The cause is specified in the variable errno.  
 *   -2: Not enough memory available.
 *   -3: An error occured. 
 *       The cause may be determined by GetLastError (Windows only).
 */
int memmap( const char* path, int id, int length, mappedRegion *hReg);

/* Unmap a file from the memory 
 * Input parameter:
 *    hReg:   The handle of the mapped region created by memmap
 */
void memunmap( mappedRegion *hReg);

/* Get the (local) address of a mapped region
 * Input parameter:
 *    hReg:   The handle of the mapped region created by memmap
 * Return value:
 *    The address
 */
char* memGetAddr( mappedRegion *hReg);

/* Get the length of a mapped region
 * Input parameter:
 *    hReg:   The handle of the mapped region created by memmap
 * Return value:
 *    The length in bytes
 */
int memGetLength( mappedRegion *hReg);

#endif

Die Implementierung (memmap.c) sieht folgendermaßen aus:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
#include <process.h>
#else
#include <sys/mman.h>
#include <unistd.h>
#endif

#include "memmap.h"

struct mappedRegionS {
    void * addr;
    int length;
};

int memmap( const char *path, int id, int length, mappedRegion *hReg)
{
    struct stat stat_buf;
    int res;
    unsigned int i;
    int exists = 1;
    char buffer[1024];
#ifdef _WIN32
    PSECURITY_DESCRIPTOR pSD;
    HANDLE hFile, hMem;
    SECURITY_ATTRIBUTES  sa;
    DWORD sz;
#else
    int fdes;
#endif

    mappedRegion reg = (mappedRegion)malloc( sizeof( struct mappedRegionS));
    if (NULL == reg) return -3;
    *hReg = reg;

    /* check if file already exists and determine its length */
    res = stat( path, &stat_buf);
    if (res < 0) {
        if (errno == ENOENT) exists = 0;
        else return -1;
    }
    if (exists) reg->length = stat_buf.st_size;
    else reg->length = length;

#ifdef _WIN32
    /* create security descriptor (needed for Windows NT) */
    pSD = (PSECURITY_DESCRIPTOR) malloc( SECURITY_DESCRIPTOR_MIN_LENGTH );
    if( pSD == NULL ) return -2;

    InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION);
    SetSecurityDescriptorDacl(pSD, TRUE, (PACL) NULL, FALSE);

    sa.nLength = sizeof(sa);
    sa.lpSecurityDescriptor = pSD;
    sa.bInheritHandle = TRUE;

    /* create or open file */
    if (exists) {
        hFile = CreateFile ( path, GENERIC_READ | GENERIC_WRITE,
            FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL, NULL);
    }
    else {
        hFile = CreateFile ( path, GENERIC_READ | GENERIC_WRITE,
            FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_ALWAYS,
            FILE_ATTRIBUTE_NORMAL, NULL);
    }
    if (hFile == INVALID_HANDLE_VALUE) {
        free( pSD);
        return -3;
    }
    if (! exists) {
        /* ensure that file is long enough and filled with zero */
        memset( buffer, 0, sizeof(buffer));
        for (i = 0; i < reg->length/sizeof(buffer); ++i) {
            if (! WriteFile( hFile, buffer, sizeof(buffer), &sz, NULL)) {
                return -3;
            }
        }
        if (! WriteFile( hFile, buffer, reg->length, &sz, NULL)) {
            return -3;
        }
    }
        
    /* create file mapping */
    sprintf( buffer, "%d", id);
    hMem = CreateFileMapping( hFile, &sa, PAGE_READWRITE, 0,
            reg->length, buffer);
    free( pSD);
    if (NULL == hMem) return -3;

    /* map the file to memory */
    reg->addr = MapViewOfFile( hMem, FILE_MAP_ALL_ACCESS, 0, 0, 0);
    if (NULL == reg->addr) return -3;

    CloseHandle( hFile);
    CloseHandle( hMem);

#else
    /* UNIX */
    if (exists) {
        /* open mapped file */
        fdes = open( path, O_RDWR, S_IRUSR | S_IWUSR);
        if (fdes < -1) return -1;
    }
    else /* not exists */ {
        /* create mapped file */
        fdes = open( path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
        if (fdes < -1) return -1;
        /* ensure that file is long enough and filled with zero */
        memset( buffer, 0, sizeof(buffer));
        for (i = 0; i < reg->length/sizeof(buffer); ++i) {
            if (write( fdes, buffer, sizeof(buffer)) != sizeof(buffer)) {
                return -1;
            }
        }
        if (write( fdes, buffer, reg->length) != reg->length) {
            return -1;
        }
    }

    /* map the file to memory */
    reg->addr = mmap( NULL, reg->length,
        PROT_READ | PROT_WRITE, MAP_SHARED, fdes, 0);
    close( fdes);
    if (reg->addr == (void *)-1) return -1;
#endif

    return 0;
}

void memunmap( mappedRegion *hReg)
{
    mappedRegion reg = *hReg;
    if (reg) {
#ifdef _WIN32
        if (reg->addr) {
            UnmapViewOfFile( reg->addr);
        }
#else
        if (reg->addr) {
            munmap( reg->addr, reg->length);
        }
#endif
        free( reg);
    }
    *hReg = 0;
}

char* memGetAddr( mappedRegion *hReg)
{
    return (char *)((*hReg)->addr);
}

int memGetLength( mappedRegion *hReg)
{
    return (*hReg)->length;
}

Die Verwendung dieser Funktionen gestaltet sich relativ einfach. Als erstes sei ein kleines Programm mapt vorgestellt, welches eine Liste von Zahlen erzeugt und verändert. Diese Liste soll mit weiteren Programmen ausgetauscht werden und wird daher in ein Memory Mapped File gelegt. Dies erfolgt nach Inkludieren der notwendigen Header durch Aufruf der Funktion memmap(), welche die Datei xxxmapfilexxx mit der Größe 200 * 4 bytes erzeugt und in den Arbeitsspeicher projiziert:

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

#include "memmap.h"

#ifdef _WIN32
#include <windows.h>

void sleep( int x) { Sleep( 1000*x); }
#endif

/* get integer random number in range a <= x <= e */
int irand( int a, int e)
{
    double r = e - a + 1;
    return a + (int)(r * rand()/(RAND_MAX+1.0));
}

int main( int argc, char **argv)
{
    int rc, i;
    int *a;
    mappedRegion reg;

    rc = memmap( "xxxmapfilexxx", 12345, 200*sizeof(int), &reg);

Mittels memGetAddr() wird die Adresse der Projektion in den Arbeitsspeicher der Variablen a zugewiesen:

    a = (int *)memGetAddr( &reg);

Von jetzt an kann mit a genauso wie mit jeder anderen Variablen auch gearbeitet werden; die Tatsache, dass die Daten in eine Datei gespiegelt werden, wird vollkommen vom Betriebssystem vor dem Programmierer versteckt. Zur Demonstration werden im Sekundenrhytmus Daten in das Array a geschrieben (die Funktion irand() stammt aus dem Artikel über Zufallszahlen):

    srand( time(0));
    for (;;) {
        a[irand(0,199)] = irand(-1000, 1000);
        sleep(1);
    }
}

Jetzt ist die Programmierung eines zweiten Programmes maps, welches die von mapt erzeugten Daten ausgibt und eine Summe berechnet, relativ einfach. Es wird einfach die gleiche Datei xxxmapfilexxx per memmap() in den Arbeitsspeicher projiziert:

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

#include "memmap.h"

int main( int argc, char **argv)
{
    int rc, i;
    int *a;
    int sum = 0;
    mappedRegion reg;

    rc = memmap( "xxxmapfilexxx", 12345, 200*sizeof(int), &reg);
    if (rc != 0) {
        printf( "memmap failed, rc = %d\n", rc);
        exit(1);
    }
    
    a = (int *)memGetAddr( &reg);
    if (200 > memGetLength( &reg)) {
        printf( "mapped region too small\n");
        exit(1);
    }

    for (i=0; i<200; ++i) {
        if (i%10 == 0) printf("\n");
        printf( "%6d", a[i]);
        sum += a[i];
    }
    printf("\n Summe = %d\n", sum);
    memunmap( &reg);
}

Die beschriebenen Programme wurden mit dem Borland Compiler 5.5 unter Windows NT/98 und dem GNU-Compiler unter Linux und Solaris getestet. Ein Problem bleibt jedoch vorerst offen: Während das Programm maps die Summe der Liste berechnet, kann es durchaus vorkommen, dass mapt irgendeine Zahl neu einträgt oder verändert, da beide Prozesse ja voneinander unabhängig asynchron arbeiten. Dann ist die Ausgabe von maps falsch. Lösen lässt sich dies durch Synchronisation der Zugriffe auf den gemeinsam genutzten Speicher, zum Beispiel mittels Semaphoren.