The Slab Allocator is an allocator for SunOS 5.4. The allocator notes that initialization and destruction of objects are generally expensive, and so caching should be a high priority of the system.
Object Caching is a technique for dealing with objects that are
frequently allocated and freed. Take a mutex – mutex_init()
needs to be applied once, and then the object can be freed and
reallocated without incurring the cost of mutex_destroy()
and mutex_init()
.
The Design of the cache is straightforward:
if (there's an object in the cache)
(no construction required);
take it else {
;
allocate memory;
construct the object}
(no destruction required); Return it to the cache
;
take objects from the cache;
destroy the objects; free the underlying memory
An object’s constructed state must be initialized only once.
The interface follows from two observations:
Thus, the interface to construct an object cache looks like this:
struct kmem_cache *kmem_cache_create(
char *name,
size_t size,
int align,
void (*constructor)(void*, size_t),
void (*destructor)(void*, size_t)
);
This creates a cache of objects, each with a size size
,
aligned on an align
boundary. The name
is for
debugging, and constructor
is a function that constructs
objects in the cache, and destructor
undoes that.
Next, clients should need just two simple functions to allocate and free objects:
To get an object from the cache:
void *kmem_cache_alloc(
struct kmem_cache *cp,
int flags);
The first parameter is the cache to retrieve items from, and flags
can be either KM_SLEEP
or KM_NOSLEEP
, which
indicates whether its acceptable to wait for memory if none is currently
available.
To return an item to the cache:
void kmem_cache_free(
struct kmem_cache *cp,
void* buf);
If a cache is no longer needed, the client can destroy it:
void kmem_cache_destroy(struct kmem_cache *cp);
This interface allows us to build a flexible allocator that is ideally suited to the needs of its clients. It also doesn’t require compile time knowledge of custom allocators, nor does it have to guess what the ideal cache size is as in adaptive-fit methods.
Taking this struct:
struct foo {
;
kmutex_t foo_lock;
kcondvar_t foo_cvstruct bar *foo_barlist;
int foo_refcnt;
};
We would create a constructor like this:
void foo_constructor(void *buf, int size) {
struct foo *foo = buf;
(&foo->foo_lock, ...);
mutex_init(&foo->foo_cv, ...);
cv_init->foo_refcnt = 0;
foo->foo_barlist = NULL;
foo}
And a destructor like this:
void foo_destructor(void *buf, int size) {
struct foo *foo = buf;
(foo->foo_barlist == NULL);
ASSERT(foo->foo_refcnt == 0);
ASSERT(&foo->foo_cv);
cv_destroy(&foo->foo_lock);
mutex_destroy}
And then to create the cache:
= kmem_cache_create("foo_cache",
foo_cache sizeof (struct foo), 0,
, foo_destructor); foo_constructor
And then to free:
= kmem_cache_alloc(foo_cache, KM_SLEEP);
foo ;
use foo(foo_cache, foo); kmem_cache_free
Thus, allocation becomes fast, because the allocator will usually do nothing more than fetch an already-constructed foo from the cache.
We’ve discussed the frontend of the cache, but there’s also a backend:
Backend | Frontend |
---|---|
kmem_cache_grow | kmem_cache_alloc |
kmem_cache_reap | kmem_cache_free |
The grow call gets memory from the VM system, makes objects, and feeds those items to the cache.
The reap call frees memory from the cache and returns it to the VM.
The slab allocator isn’t a monolithic entity, but is rather a loose confederation of independent caches with no shared state. This allows them to have per-cache locking instead of the entire heap.
The slab is the unit of currency in the slab allocator. When the allocator needs to grow, it acquires an entire slab of objects at once. When the allocator needs more memory, a slab is freed.
This leads to the following benefits:
The contents of each slab are managed by a kmem_slab
data structure, that maintains the slab’s linkage in the cache.