http://www.comparat.de
HOME
http://www.comparat.com
INTERNATIONAL HOME
PROFIL BACKUP-SERVER INDIVIDUELLE SOFTWARE PROJEKTE IT-GLOSSAR JOBS IMPRESSUM AGB
WHITE PAPERS cplApp Framework
FACHARTIKEL Speicherverwaltung
mit pools

 



08.09.2008

Speicherverwaltung mit Pools

Manfred Rebentisch

May 26, 2006

Kurzbeschreibung

Der Apache-Webserver basiert auf ihr und inzwischen viele andere Software: die Apache Portable Runtime Library, kurz APR genannt. Diese Library verwendet konsequent ein Speichermanagement auf der Basis von Memory-Pools. Die APR-Pools sind dafür optimiert, HTTP-Anfragen zu behandeln. Eine solche Anfrage wird durch einen eigenen Prozess oder Thread behandelt und sein ganzer Speicherbereich kann nach Erledigung verworfen werden.

Die C-Klasse PTPool wurde dafür entworfen, mit den APR-Pools zuarbeiten, mit Shared Memory und mit dem üblichen Heap-Speicher von malloc() und Co.

Mit dem Einsatz der PTPool-Klasse gewinnt man beträchtliche Vorteile beim Schreiben von Source-Code und zusätzlich die Integration einer großartigen portablen Bibliothek: die Apache Portable Runtime Library.

Inhalt

 

Einleitung

Ich habe, zusammen mit den Studenten Thomas Ciesiecki und Valdis Pornieks, eine C-Klasse zur Speicherverwaltung entwickelt. Die Besonderheiten dieser C-Klasse (ja, keine C++-Klasse!) sind:

  • Wahlweise Verwendung von:
    • Standard-Speicher (malloc)
    • Apache Pools (apr_pool_t)
    • Shared Memory
  • automatische Freigabe
  • statistische Angaben
  • einfaches Debugging
  • einfache Erweiterbarkeit

Angefangen hat es damit, daß ich meine vorhandenen C-Bibliotheken für mein Internet Application Framework cpIApp mit der APR-Library (Apache Portable Runtime Library) verheiraten wollten. Die APR sollte mir vor allem Vorteile in Hinblick auf die Plattformunabhängigkeit und bei der Integration in den Apache-Server bringen.

Die APR wird auf der Website http://apr.apache.org unter der Apache License 2.0 zur Verfügung gestellt. Bei einer Installation des Apache Server ist immer schon eine APR-Library mit installiert. Für die Entwicklung muß dann das passende Entwicklerpaket installiert werden. Unter SuSE ist das zum Beispiel die libapr0-2.0.50-7.12 und apache2-devel-2.0.50-7.12. Unter Debian kann man mit apt-get install libapr0 libapr0-dev die Libraries installieren. Hierbei fällt natürlich auf, daß immer noch eine alte Version verwendet wird, meist 0.9.6 oder 0.9.7. Dabei ist die Version 1.2.7 aktuell. Wenn man diese Version verwenden will, kann man sie selbst mit dem üblichen Dreiklang configure; make; make install konfigurieren, compilieren und installieren. Diese Version kann neben der 0-er Version bestehen. Wenn man jedoch ein Apache-Modul schreiben will, muß man die gleiche Version nehmen, die auch der Apache-Server verwendet.

Nun arbeiten die APR Funktionen konsequent mit Pools, eben den APR-Pools. Alle Funktionen, die Speicher anfordern müssen, bekommen als Parameter einen Zeiger auf ein apr_pool_t Objekt geliefert (das ist eine Datenstruktur, deren Aufbau der Anwendung verborgen bleibt). Mit Hilfe eines Satzes von apr_pool*- Funktionen wird Speicher angefordert oder freigegeben.

APR-Pool Beispiel

apr_pool_t  pmain;
char*       mystr;
char*       otherstr;
pmain       = apr_pool_create( &pmain, NULL );
mystr       = apr_palloc( pmain, 250 );
otherstr    = apr_psprintf( pmain, "Das geht einfach %s
", "so" );

Der für mystr angeforderte Speicher wird jedoch nicht mehr einzeln freigegeben. Stattdessen wird es im Programmablauf einen Zeitpunkt X geben, zu dem der ganze Pool pmain freigegeben wird:

APR-Pool freigeben

apr_pool_destroy(pmain);

Dieses Verfahren ist natürlich ideal für die Request-Behandlung. Wenn eine Anfrage vom Client abgearbeitet ist, kann alles verworfen werden, was dazu nötig war. Nicht ideal ist dieses Verhalten wenn man sich nicht so konsequent und durchgängig auf die APR-Bibliothek einlassen will oder kann.

Zunächst könnte man sich fragen, warum PTPool so geschrieben wurde, wie sie ist. Wenn man eine Bibliothek wie die APR nutzt, könnte man sich doch ganz und gar auf sie einlassen und den Quellcode darauf abstimmen. Sicherlich kommt niemand auf die Idee, die Qt-Library von Trolltech in eigene Klassen zu kapseln (Wrapper), aber ich hatte Software vor Augen, die unabhängig von den APR-Libraries sein sollte, ja sein mußte. Und trotzdem wollte ich meine Library, die libstd3000c, verwenden können. Daß die Library dann irgendwann C-Klassen und Funktionen enthält, die ohne APR nicht bestehen können, war mir klar. Eine langfristige Bindung an die APR wollte ich in Kauf nehmen, da der Quellcode zur Verfügung steht und von hoher Qualität ist.

Ich habe eine Konstruktion entwickelt, bei der alle Requests (zu einem VirtualHost) aller Prozesse des Apache Webservers für bestimmte Zwecke ein und denselben gemeinsamen Speicher verwenden. Sprachentexte und Templates liegen dort zum Lesen bereit, aber auch gleich die vollständigen Strukturen und Objektdaten. Zur Verwaltung verwenden wir Maps mit AVL-Bäumen, die auf dem Algorithmus von Ben Pfaff basieren. Ich habe für Maps und AVL-Bäume eigene C-Klassen geschaffen, die sehr einfach zu verwenden sind. Diese Klassen wollte ich für die Verwendung mit Shared Memory nicht neu schreiben. Die Einführung von Pool-Objekten für die Speicherallozierung schien jedoch sehr einfach.

Der Aufbau von PTPool

Um das ganze besser verstehen zu können, erkläre ich kurz das Interface von PTPool - also die Schnittstelle, die sich dem Programmierer anbietet. Dann schauen wir uns die Interna an.

PTPool ist eine Struktur, die auch Zeiger auf Funktionen enthält. In C++ nennt man das eine Klasse mit Methoden. Die Struktur enthält auch Daten, allerdings nur einen void*-Zeiger auf ein internes Objekt, auf das der Programmierer nie direkt zugreifen darf. Ähnlich wie in C++ kann man so in C eine Kapselung von Funktionen, die mit Daten arbeiten, erreichen. Diese Technik wird übrigens auch von der Berkeley-DB und teilweise auch von der APR-Bibliothek verwendet (ja, und viele andere machen es auch so). Anders als in C++ bekommt eine solche Methode jedoch keinen automatischen this-Zeiger und die Methode könnte auf das eigene Objekt nicht zugreifen. Deshalb habe ich mich dazu entschieden, daß jede Methode einer C-Klasse als ersten Parameter einen Zeiger auf das "eigene" Objekt erhält, also ein manueller Nachbau für den this-Zeiger.

mystr = pool->alloc(pool, 250);

Hier wird die Methode alloc des Objektes pool aufgerufen und das Objekt als erster Parameter übergeben.

Durch dieses Vorgehen ist sichergestellt, daß die Funktionen über das Objekt aufgerufen werden und daß sie Zugriff auf das eigene Objekt haben. Dadurch wiederrum, kann bei Bedarf (also im Regelfall) stets die Validität des Objektes überprüft werden. Ein problematischer Fehler, der dem Programmierer dabei unterlaufen kann, ist, daß dabei zwei Objekte durcheinander gebracht werden können:

mystr = poolA->alloc(poolB, 250);

Mir selbst passiert sowas natürlich nie. Naja, jedenfalls kennt die C-Klasse auch eine Methode freemem(PTPool, void*) zum freigeben von Speicher. Dabei darf der Zeiger auf das freizugebende Objekt auch NULL sein und es kann sein, daß die Funktion gar nichts tut. Warum das so ist, erkläre ich am besten mit der create-Funktion. Mit

PTPool CPPoolCreate( PTPool parent, uint flags );

wird ein PTPool Objekt erzeugt. Für den Parameter parent kann man NULL angeben oder ein anderes Pool-Objekt. Für den Parameter flags können folgende Werte gesetzt werden:

CPPOOL_USEALLOC
Mit diesem Wert wird bestimmt, daß die Standard Funktionen malloc, calloc, realloc und free verwendet werden. Die Methode freemem() führt dazu, daß tatsächlich Speicher freigegeben wird. Man kann jedoch den Aufruf unterlassen, kann auf freemem() verzichten - außer man hat zusätzlich das Flag CPPOOL_NOQUEUE gesetzt.
CPPOOL_USEAPR
Dieser Wert bestimmt, daß intern die APR-Pools verwendet werden. Die Methode freemem() macht tatsächlich nichts.
CPPOOL_USESHM
Dieser Wert bestimmt, daß intern Shared Memory verwendet wird, wiederum aber auch mit Funktionen aus der APR-Library.
CPPOOL_NOQUEUE
Dieser Wert zusammen mit CPPOOL_USEALLOC angegeben bewirkt, daß es keine interne Liste mit allozierten Zeigern geben wird. Alle angeforderten Speicherblöcke müssen dann direkt mit freemem() freigegeben werden.
CPPOOL_PARENT
Mit diesem Wert, zusammen mit einem gültigen Zeiger auf parent, wird bestimmt, daß die Flags vom Parent-Objekt übernommen werden.

Eine besondere Eigenschaft des mit PTPool allozierten Speichers ist, daß man die Größe des Speicherblocks abfragen kann:

Beispiel für PTPool->memsize()

const char*     myptr;
myptr       = pool->alloc( pool, 513 );
printf("Allocated size of 'myptr': %Zu
", pool->memsize(pool, myptr) );
pool->free(pool);

Diese Eigenschaft bietet die APR-Library nicht. Auch die Methode getsum() ist eine Eigenheit meiner PTPool-Klasse: es wird ermittelt, wievele Speicherobjekte insgesamt alloziert wurden und wie groß die Gesamtmenge des allozierten Speichers war:

Beispiel für PTPool->getsum()

const char*     myptr;
size_t          ncount;
size_t          bsum;
myptr       = pool->alloc( pool, 513 );
myptr       = pool->alloc( pool, 2500 );
myptr       = pool->alloc( pool, 51 );
bsum        = pool->getsum(pool, &ncount);
printf("Sum of allocation sizes: %Zu with %Zu nodes
", bsum, ncount);
pool->free(pool);

Die mit getsum() ermittelte Zahl enthält nicht die mit freemem() freigegebenen Bereiche. Dadurch kann man ermitteln, wieviel Speicher mit den APR-Pools verbraten wird, wenn zwischendurch keine Pool-Objekte aufgelöst werden.

Ruft man die Methode pool->free(pool) auf, dann wird der ganze Speicher freigeben, der über dieses Objekt angefordert wurde.

Für die Verwendung von PTPool zusammen mit der APR-Library ist es notwendig, gleich zu Beginn in main() die Code-Schnipsel:

CPPoolInitialize();
atexit(CPPoolTerminate);

aufzurufen. Dadurch werden alle PTPool-Objekte bei Programmende automatisch freigegeben, auch wenn man kein pool->free(pool) aufgerufen hat.


Die drei Varianten für PTPool

Nun möchte ich Ihnen kurz erklären, wie die drei Speichertypen verwendet werden können.


Die herkömmliche Allokierung mit malloc

Das PTPool-Objekt wird mit einer create-Funktion erzeugt. Die Funktion

PTPool pool = CPPoolCreate( NULL, CPPOOL_USEALLOC );

erzeugt das Objekt. Für den Parameter flags wird für die Verwendung von malloc der Wert CPPOOL_USEALLOC (0x10) verwendet. Die Liste der internen Methoden (wie PTPool->alloc) wird dann auf statisch verborgene Funktionen gesetzt, die sich mit malloc abmühen. Wenn man gleichzeitig das Flag CPPOOL_NOQUEUE verwendet, wird intern keine free-Liste geführt. Der Programmierer muß dann mit PTPool->freemem jeden Speicherblock selbst freigeben. Andernfalls hat man den Luxus, auf free-Aufrufe vollständig verzichten zu können. Der über das PTPool-Objekt angeforderte Speicher wird auf einen Schlag freigegeben, wenn man entweder PTPool->freeall aufruft oder das Objekt auflöst mit PTPool->free (oder konkret: pool->free(pool);.

Wenn man Apache-Module schreibt und ohnehin mit APR-Pools befaßt ist, kann es in manchen Fällen dennoch sinnvoll sein, diesen PTPool-Typ CPPOOL_USEALLOC zu verwenden. Denn ein pool->realloc(pool, ptr, oldsize, newsize) oder ein pool->freemem(pool, ptr) gibt wirklich Speicher frei. In manchen Verarbeitungsschleifen (Template-Verarbeitung) kommen immer wieder Kopieraktionen mit Speichererweiterungen zum tragen.


Die Arbeit mit Apache-Pools

Das PTPool-Objekt wird wieder mit der gleichen Funktion, aber einem anderen Flag erzeugt:

PTPool pool = CPPoolCreate( NULL, CPPOOL_USEAPR );

Für den Parameter flags wird für die Verwendung von APR-Pools der Wert CPPOOL_USEAPR eingesetzt. Die Objekt-Methoden werden auf interne Funktionen gesetzt, die sich mit den APR-Funktionen beschäftigen.

Wenn man ein Apache-Modul schreibt, oder aus anderen Gründen ein apr_pool_t-Objekt zur Verfügung hat, kann man auch die Funktion

PTPool pool = CPPoolCreateAPR( aprpool, CPPOOL_USEAPR );

verwenden (aprpool ist dann vom Typ apr_pool_t*). Damit wird der Parentpool gleich auf den APR-Pool gesetzt und man könnte am Ende auch auf das pool->free(pool) verzichten, weil die Tochter-Objekte automatisch mit gelöscht werden.

Wenn man mit diesen APR-Pools arbeitet, wird es wichtig, darauf zu achten, daß man wiederholte String-Concenationen oder ähnliches vermeidet. Folgendes Beispiel-Fragment aus dem Programmierer-Alltag:

Beispiel für eine Speicherverwendung

char* beispiel( PTPool apool, const char* row_tmplate, PTRecord record)
{
    PTTemplate      ptrow       = NULL;
    PTPool          apool       = NULL;
    ptrow       = CPTemplateCreate('{', '}', 0, apool);
    ptrow->loadBuffer(ptrow, row_template);
    while(record) {
        ptrow->assign(ptrow, "EVENTZEIT", 
                     CPPStrDupSize(record->getContentIdx(record, 4), 
                                   5, apool));
        ptrow->parse(ptrow, TPP_APPEND);
        record = record->next;
    }
    return ptrow->fetchStr(ptrow);
}

Natürlich ist das Beispiel arg verkürzt, aber es wird gezeigt, daß ein Objekt in der Schleife durch die parse()-Funktion mit dem TPP_APPEND - Parameter immer mehr Speicher braucht. Mit APR wird einfach neuer Speicher angefordert, der den alten Inhalt und den neuen Inhalt aufnehmen kann. Der alte Inhalt bleibt einfach unbenutzt stehen. Wenn apool in diesem Beispiel mit dem Flag CPPOOL_USEALLOC erzeugt wurde, dann wird der alte Inhalt jeweils freigegeben, so daß der Gesamtspeicherbedarf kleiner bleibt. Ich habe ein Beispiel in der Request-Verarbeitung gehabt, wo die Benutzung von APR-Pools eine solche Schleife unakzeptabel langsam gemacht hat. Die schlichte Umstellung auf CPPOOL_USEALLOC brachte eine (gefühlsmäßig) 50-fache Beschleunigung des Programms.

Daraus resultiert, daß die APR-Pools nicht in jeder Situation die richtige Wahl sind. Aber sie sind eben Bedingung, wenn man mit der APR-Library programmiert.

Die PTPool-Klasse kennt die Methode PTPool->getAprPool(). Damit wird ein Zeiger auf die APR-Struktur apr_pool_t zurückgeliefert (NULL, wenn nicht CPPOOL_USEAPR angegeben worden war). Dadurch ist es leicht, Sourcecode mit der APR-Library zu mischen.


Der Einsatz von Shared Memory mit PTPool

Wenn man Shared Memory benutzen möchte, hat man normalerweise eine ganze Menge zu berücksichtigen. Die direkte Arbeit damit ist umständlich und fehleranfällig und noch dazu oft nicht portabel.

Die APR-Library bietet mit den Shared Memory Routines und den Relocatable Memory Management Routines eine ausgezeichnete Grundlage, auf portable Weise und sehr bequem mit Shared Memory zu arbeiten.

Man könnte mit den apr_shm_* und den apr_rmm_* Funktionen einfach loslegen, wären da nicht die Pools, mit denen man ohnehin arbeitet. Man kann sich eine Allocator Funktion schreiben und mit apr_pool_create_ex() verwenden. Dieser Allocator könnte den Speicher über die apr_shm_* und apr_rmm_* Funktionen bereitstellen.

Ich bin den eigenen Weg gegangen und habe diese Funktionalität in die PTPool-Klasse integriert.

Ein PTPool-Objekt, das mit Shared-Memory arbeitet, wird so erzeugt:

Pool für Shared Memory erzeugen

PTPool      pool;
pool        = CPPoolCreateAPRSHM( aprpool, CPPOOL_USESHM, 
                                  "/tmp/sharedname.mem", 20000 );

Mit diesem Aufruf bekommt man einen Pool, der über 20 kB Platz im Shared Memory bietet. Der Parameter aprpool stammt direkt von apr_pool_create() oder von PTPool->getAprPool(). Die Shared Memory Funktionalität von PTPool geht nur zusammen mit der APR-Library, weil ich darauf verzichtet habe, den portablen Code nochmal zu erfinden.

Je nach Plattform wird der angegebene Dateiname "/tmp/sharedname.mem" verwendet oder auch nicht. Eine erste Testfunktion könnte so aussehen:

Shared Memory Testprogramm

#include <cpgen.h>  /* generic macros and switches for Std3000C */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <time.h>

#include <cppool.h>
#include <cplog.h>


int main(int argc, char** argv)
{
    PTPool          pool        = NULL;
    PTPool          shmpool     = NULL;
    const char*     sharedname  = NULL;
    size_t          shmsize     = 300;
    size_t          getsize     = 0;
    char*           tmp         = NULL;

    if(argc < 2 ) {
        printf("Zuwenig Parameter: bitte den Namen eines "
               "Shared-Memory-Bereichs angeben. 
"
               "Beispiel: /tmp/shared_test12345.mem
");
        return EXIT_FAILURE;
    }
    sharedname = argv[1];

    CPPoolInitialize();
    atexit(CPPoolTerminate);

    CPLogInit(LOGTYPE_STDERR | /* write to stderr*/
            LOGKIND_ALL,       /* do this for all kind of error,*/
            LOGKIND_ALL,       /* log-level: log all kind of erros*/
            NULL);             /* non own log file*/

    pool = CPPoolCreate(NULL, CPPOOL_USEAPR);
    if(! pool ) {
        LOGERR(("CPPoolCreate with USEAPR failed!
"));
        return EXIT_FAILURE;
    }
    shmpool = CPPoolCreateAPRSHM( pool->getAprPool(pool),
                                CPPOOL_USESHM,
                                sharedname, shmsize );
    if(TESTBIT(shmpool->options(shmpool), CPPOOL_ATTACHED)) {
        printf("Attached. ");
        tmp = shmpool->attach(shmpool, &getsize);
        printf("tmp: [%s], size: [%Zu]
", tmp, getsize);
    } else {
        tmp     = shmpool->alloc(shmpool, 250);
        strcpy(tmp, "Manfred Rebentisch");
        printf("Writing '%s' to memory
", tmp);
    }
    if(TRUE) {
        struct timespec     ts;
        ts.tv_sec   = 30;
        ts.tv_nsec  = 0;
        nanosleep(&ts, NULL);
    }

    shmpool->free(shmpool);
    pool->free(pool);
    return EXIT_SUCCESS;
}

Die beiden Zeilen mit

    shmpool->free(shmpool);
    pool->free(pool);

sind tatsächlich verzichtbar, weil die Speicherbereinigung durch die Installation von atexit(CPPoolTerminate); automatisch aufgerufen wird.

Der Shared Memory Bereich bleibt so lange erhalten, wie er benötigt wird. Um das genauer zu zeigen, habe ich vor der Freigabe des Shared-Memory eine 30-Sekunden-Pause eingelegt. Ich kann dann das Programm in zwei Konsolen nebeneinander starten und sehe in einem ein 'Writing' und im anderen ein 'Attached'.

Nach dieser Stufe kommt dann noch die Absicherung schreibender Zugriffe durch einen Mutex, damit nicht mehrere Threads oder Prozesse gleichzeitig die Map erweitern. Aber das lassen wir hier mal beiseite.

Schluss

Mit Hilfe der PTPool-Klasse kann ich in C so einfach programmieren, als hätte ich einen Garbage-Kollektor. Ich muss nur noch darauf achten, speicherintensiven Programmteilen eigene Pools zu geben und mich an einige wenige hier genannte Regeln halten.

Die libstd3000c Library kann man unter http://developer.berlios.de/projects/std3000c bekommen. Ich empfehle das CVS-Repository, da die Pakete noch nicht gepflegt werden.

Manfred Rebentisch, Mai 2006

COPYRIGHT BY COMPARAT SOFTWARE-ENTWICKLUNGS-GMBH, 2008