Lookaside-Caches

Ein Gerätetreiber muß oft immer wieder viele Objekte der gleichen Größe allozieren. Da der Kernel ohnehin schon eine Reihe von Speicher-Pools mit Objekten verwaltet, die alle die gleiche Größe haben, liegt es ja nahe, noch einige Spezial-Pools für diese in großer Zahl vorkommenden Objekte hinzuzufügen. Dies tut der Kernel durch Lookaside-Caches. Gerätetreiber haben normalerweise kein Verhalten, das die Verwendung eines Lookaside-Caches rechtfertigen würde, es gibt jedoch Ausnahmen: Die USB- und ISDN-Treiber in Linux 2.4.

Linux-Speicher-Caches sind vom Typ kmem_cache_t und werden durch das Aufrufen von kmem_cache_create erzeugt:


 kmem_cache_t * kmem_cache_create(const char *name, size_t size,
    size_t offset, unsigned long flags,
    void (*constructor)(void *, kmem_cache_t *,
       unsigned long flags),
    void (*destructor)(void *, kmem_cache_t *,
       unsigned long flags) );

Diese Funktion erzeugt ein neues Cache-Objekt, das eine beliebige Anzahl von Speicherbereichen der gleichen Größe (angegeben durch das Argument size) aufnehmen kann. Das Argument name wird mit diesem Cache verbunden und dient als Verwaltungsinformation, die man sich beim Aufspüren von Problemen zunutze machen kann; normalerweise wird hier der Name des Strukturtyps, der gecacht werden soll, verwendet. Der Name darf maximal zwanzig Zeichen lang sein (einschließlich des abschließenden Null-Bytes).

Der offset ist der Versatz des ersten Objekts in der Seite; dies kann dazu verwendet werden, um eine bestimmte Ausrichtung der allozierten Objekte zu erreichen; normalerweise werden Sie aber den Defaultwert 0 verwenden. flags steuert, wie die Allozierung vorgenommen wird, und ist eine Bitmaske aus den folgenden Flags:

SLAB_NO_REAP

Das Setzen dieses Flags schützt den Cache davor, verkleinert zu werden, wenn das System auf der Suche nach Speicher ist. Normalerweise brauchen Sie dieses Flag nicht.

SLAB_HWCACHE_ALIGN

Dieses Flag erzwingt, daß jedes Datenobjekt auf einer Cache-Zeile ausgerichtet wird; die tatsächliche Ausrichtung hängt vom Cache-Layout auf der Host-Plattform ab. Dieses Flag ist oft sehr nützlich.

SLAB_CACHE_DMA

Dieses Flag erzwingt, daß jedes Datenobjekt in DMA-fähigem Speicher alloziert wird.

Die Argumente constructor und destructor sind optionale Funktionen (es kann aber keinen Destruktor ohne einen Konstruktor geben); der Konstruktor kann dazu verwendet werden, frisch allozierte Objekte zu initialisieren, der Destruktor dazu, Objekte “aufzuräumen”, bevor ihr Speicher insgesamt an das System zurückgegeben wird.

Konstruktoren und Destruktoren können nützlich sein, aber es gibt einige Einschränkungen, die Sie im Hinterkopf behalten sollten. Ein Konstruktor wird aufgerufen, wenn der Speicher für einen Satz von Objekten alloziert wird; weil dieser Speicher mehrere Objekte enthalten kann, kann der Konstruktor auch mehrfach aufgerufen werden. Sie können nicht davon ausgehen, daß der Konstruktor als unmittelbare Folge der Objekt-Allozierung aufgerufen wird. Entsprechend können Destruktoren zu irgendeinem zukünftigen Zeitpunkt aufgerufen werden, nicht unbedingt unmittelbar nachdem ein Objekt freigegeben worden ist. Konstruktoren oder Destruktoren können schlafen gelegt werden oder nicht, je nachdem, ob das Flag SLAB_CTOR_ATOMIC gesetzt ist oder nicht (wobei CTOR eine Abkürzung für constructor ist).

Aus Gründen der Bequemlichkeit können Programmierer die gleiche Funktion für den Konstruktor und den Destruktor verwenden; der Allokator übergibt immer SLAB_CTOR_CONSTRUCTOR, wenn es sich um einen Konstruktor handelt.

Sobald ein Objekt-Cache erzeugt worden ist, können Sie darin mit kmem_cache_alloc Objekte allozieren.


 void *kmem_cache_alloc(kmem_cache_t *cache, int flags);

Das Argument cache gibt den vorher erzeugten Cache an; die Flags sind die gleichen wie die, die an kmalloc übergeben werden, und werden verwendet, wenn kmem_cache_alloc selbst mehr Speicher allozieren muß.

Um ein Objekt wieder freizugeben, verwenden Sie kmem_cache_free.


 void kmem_cache_free(kmem_cache_t *cache, const void *obj);

Wenn der Treiber-Code den Cache nicht mehr benötigt (was normalerweise der Fall ist, wenn das Modul entladen wird), dann sollte der Cache folgendermaßen freigegeben werden:


 int kmem_cache_destroy(kmem_cache_t *cache);

Dies funktioniert nur, wenn alle in diesem Cache allozierten Objekte auch wieder freigegeben worden sind. Module sollte also den Rückgabewert von kmem_cache_destroy abfragen; ein Fehler zeigt hier ein Speicherleck im Modul an (da manche der Objekte nicht wieder freigegeben worden sind).

Ein weiterer Vorteil der Verwendung von Lookaside-Caches besteht darin, daß der Kernel Statistiken über die Cache-Verwaltung unterhält. Es gibt sogar eine Kernel-Konfigurationsoption, die das Einsammeln zusätzlicher statischer Informationen ermöglicht, allerdings merklich auf Kosten der Performance. Die Cache-Statistiken finden Sie in /proc/slabinfo.

Ein scull mit Slab-Caches: scullc

Es ist Zeit für ein Beispiel. scullc ist eine kürzere Version des scull-Moduls, die nur das rohe Gerät implementiert, also den persistenten Speicherbereich. Im Gegensatz zu scull, das kmalloc verwendet, benutzt scullc Speicher-Caches. Die Größe des Quantums kann zur Compile- und zur Ladezeit geändert werden, aber nicht zur Laufzeit. Letzteres würde das Erzeugen eines neuen Speicher-Caches erfordern, ein Detail, mit dem wir uns hier auseinandersetzen wollten. Dieses Modul kann nicht mit 2.0-Kerneln kompiliert werden, weil es damals noch keine Speicher-Caches gab, wie wir in “the Section called Abwärtskompatibilität>” noch erläutern werden.

scullc ist ein vollständiges Beispiel, das zum Testen verwendet werden kann. Es unterscheidet sich von scull nur durch einige wenige Code-Zeilen. Hier ist der Code zum Allozieren von Speicher-Quanta:


 /* Ein Quantum in dem Speicher-Cache allozieren */
 if (!dptr->data[s_pos]) {
     dptr->data[s_pos] =
         kmem_cache_alloc(scullc_cache, GFP_KERNEL);
     if (!dptr->data[s_pos])
         goto nomem;
     memset(dptr->data[s_pos], 0, scullc_quantum);
 }

Und hier wird der Speicher wieder freigegeben:


for (i = 0; i < qset; i++)
    if (dptr->data[i])
        kmem_cache_free(scullc_cache, dptr->data[i]);
kfree(dptr->data);

scullc_cache benötigt noch einige weitere Zeilen, die an den entsprechenden Stellen in der Datei eingefügt sind:


/* einen Cache-Zeiger deklarieren; wird für alle Geraete verwendet */
kmem_cache_t *scullc_cache;

    /* init_module: einen Cache für unsere Quanten erzeugen */
    scullc_cache =
        kmem_cache_create("scullc", scullc_quantum,
                          0, SLAB_HWCACHE_ALIGN,
                          NULL, NULL); /* kein Ctor/Dtor */
    if (!scullc_cache) {
        result = -ENOMEM;
        goto fail_malloc2;
    }

    /* cleanup_module: den Cache für unsere Quanta wieder freigeben */
    kmem_cache_destroy(scullc_cache);

> > > > > Die wichtigsten Unterschiede zwischen scull und scullc sind eine geringefügige Verbesserung der Geschwindigkeit und eine bessere Ausnutzung des Speichers. Weil die Quanta aus einem Pool von Speicher-Fragmenten mit genau der richtigen Größe alloziert werden, liegen sie so dicht wie es nur geht im Speicher, im Gegensatz zu den Quanten in scull, die eine unvorhersagbare Fragmentierung des Speichers mit sich bringen können.