What are mempools?
Nice commends from src/pulsecore/memblock.h
:
/* A pa_memblock is a reference counted memory block. PulseAudio
* passes references to pa_memblocks around instead of copying
* data. See pa_memchunk for a structure that describes parts of
* memory blocks. */
A pa_mempool
is a collection/pool of the blocks above, as defined in src/pulsecore/memblock.c
:
struct pa_mempool {
pa_semaphore *semaphore;
pa_mutex *mutex;
pa_shm memory;
size_t block_size;
unsigned n_blocks;
bool is_remote_writable;
pa_atomic_t n_init;
PA_LLIST_HEAD(pa_memimport, imports);
PA_LLIST_HEAD(pa_memexport, exports);
/* A list of free slots that may be reused */
pa_flist *free_slots;
pa_mempool_stat stat;
};
And we can see that pa_mempool_new
does:
pa_mempool* pa_mempool_new(bool shared, size_t size) {
pa_mempool *p;
char t1[PA_BYTES_SNPRINT_MAX], t2[PA_BYTES_SNPRINT_MAX];
p = pa_xnew0(pa_mempool, 1);
p->block_size = PA_PAGE_ALIGN(PA_MEMPOOL_SLOT_SIZE);
if (p->block_size < PA_PAGE_SIZE)
p->block_size = PA_PAGE_SIZE;
if (size <= 0)
p->n_blocks = PA_MEMPOOL_SLOTS_MAX;
else {
p->n_blocks = (unsigned) (size / p->block_size);
if (p->n_blocks < 2)
p->n_blocks = 2;
}
if (pa_shm_create_rw(&p->memory, p->n_blocks * p->block_size, shared, 0700) < 0) {
pa_xfree(p);
return NULL;
}
pa_log_debug("Using %s memory pool with %u slots of size %s each, total size is %s, maximum usable slot size is %lu",
p->memory.shared ? "shared" : "private",
p->n_blocks,
pa_bytes_snprint(t1, sizeof(t1), (unsigned) p->block_size),
pa_bytes_snprint(t2, sizeof(t2), (unsigned) (p->n_blocks * p->block_size)),
(unsigned long) pa_mempool_block_size_max(p));
pa_atomic_store(&p->n_init, 0);
PA_LLIST_HEAD_INIT(pa_memimport, p->imports);
PA_LLIST_HEAD_INIT(pa_memexport, p->exports);
p->mutex = pa_mutex_new(true, true);
p->semaphore = pa_semaphore_new(0);
p->free_slots = pa_flist_new(p->n_blocks);
return p;
}
And as we can see, it's a big pool of memory blocks, where each memory block is a reference-counted memory area. The code basically calculations the block count and their sizes, do some necessary initializations and most importantly call pa_shm_create_rw()
, which we should inspect in next section.
How are mempools used
A mempool is used by allocating a memblock ("a reference-counted peace of memory) from such a mempool. This is done by the following function:
/* No lock necessary */
pa_memblock *pa_memblock_new_pool(pa_mempool *p, size_t length) {
pa_memblock *b = NULL;
struct mempool_slot *slot;
static int mempool_disable = 0;
pa_assert(p);
pa_assert(length);
/* If -1 is passed as length we choose the size for the caller: we
* take the largest size that fits in one of our slots. */
if (length == (size_t) -1)
length = pa_mempool_block_size_max(p);
// Pool block can takes meta-data + the data itself?
if (p->block_size >= PA_ALIGN(sizeof(pa_memblock)) + length) {
if (!(slot = mempool_allocate_slot(p)))
return NULL;
b = mempool_slot_data(slot);
b->type = PA_MEMBLOCK_POOL;
pa_atomic_ptr_store(&b->data, (uint8_t*) b + PA_ALIGN(sizeof(pa_memblock)));
// Pool block can only take the data itself without any meta?
} else if (p->block_size >= length) {
if (!(slot = mempool_allocate_slot(p)))
return NULL;
if (!(b = pa_flist_pop(PA_STATIC_FLIST_GET(unused_memblocks))))
b = pa_xnew(pa_memblock, 1);
b->type = PA_MEMBLOCK_POOL_EXTERNAL;
pa_atomic_ptr_store(&b->data, mempool_slot_data(slot));
} else {
pa_log_debug("Memory block too large for pool: %lu > %lu", (unsigned long) length, (unsigned long) p->block_size);
pa_atomic_inc(&p->stat.n_too_large_for_pool);
return NULL;
}
PA_REFCNT_INIT(b);
b->pool = p;
b->read_only = b->is_silence = false;
b->length = length;
pa_atomic_store(&b->n_acquired, 0);
pa_atomic_store(&b->please_signal, 0);
stat_add(b);
return b;
}
There are two important cases to note above:
- If the pool contains memory blocks that fits the required data size + the meta-data header pa_memblock, the header is embedded in the block itself and its
->data
pointer can point to the data section directly after the header - If the pool contains memory blocks that can only fit the data, without the headers themselves
pa_memblock
, the code chooses a middle solution by using the block entirely for data and allocating the meta in an external section. It re-uses ideas from the SLAB allocator, by re-using a cache ofpa_memblock
structures instead of using it every time
Different types of memblocks
We've seen above two types of memblocks, one that embeds the meta-data into the block itself, and one that only use the pool block for the naked data. There are even more types to tackle, and they are (src/pulsecore/memblock.h
):
/* The type of memory this block points to */
typedef enum pa_memblock_type {
PA_MEMBLOCK_POOL, /* Memory is part of the memory pool */
PA_MEMBLOCK_POOL_EXTERNAL, /* Data memory is part of the memory pool but the pa_memblock structure itself is not */
PA_MEMBLOCK_APPENDED, /* The data is appended to the memory block */
PA_MEMBLOCK_USER, /* User supplied memory, to be freed with free_cb */
PA_MEMBLOCK_FIXED, /* Data is a pointer to fixed memory that needs not to be freed */
PA_MEMBLOCK_IMPORTED, /* Memory is imported from another process via shm */
PA_MEMBLOCK_TYPE_MAX
} pa_memblock_type_t;
We've checked the first two types PA_MEMBLOCK_POOL
and PA_MEMBLOCK_POOL_EXTERNAL
. What are the rest? First, let's check the barebone pa_memblock_new()
below:
/* No lock necessary */
pa_memblock *pa_memblock_new(pa_mempool *p, size_t length) {
pa_memblock *b;
pa_assert(p);
pa_assert(length);
if (!(b = pa_memblock_new_pool(p, length)))
b = memblock_new_appended(p, length);
return b;
}
We note that the memblock_new() method first tries PA_MEMBLOCK_POOL
and PA_MEMBLOCK_EXTERNAL
. If both fails, it exclusively uses PA_MEMBLOCK_APPENDED
, which is implemented as follows:
/* No lock necessary */
static pa_memblock *memblock_new_appended(pa_mempool *p, size_t length) {
pa_memblock *b;
pa_assert(p);
pa_assert(length);
/* If -1 is passed as length we choose the size for the caller. */
if (length == (size_t) -1)
length = pa_mempool_block_size_max(p);
b = pa_xmalloc(PA_ALIGN(sizeof(pa_memblock)) + length);
PA_REFCNT_INIT(b);
b->pool = p;
b->type = PA_MEMBLOCK_APPENDED;
b->read_only = b->is_silence = false;
pa_atomic_ptr_store(&b->data, (uint8_t*) b + PA_ALIGN(sizeof(pa_memblock)));
b->length = length;
pa_atomic_store(&b->n_acquired, 0);
pa_atomic_store(&b->please_signal, 0);
stat_add(b);
return b;
}
And as you can see above, PA_MEMBLOCK_APPENDED
is just allocated using regular malloc()
and have almost no relation to the memory pool! The only relation that exists if the size passed was -1, the memory block area allocated is the maximum this pool can support. Is the name PA_MEMBLOCK_APPENDED
quite deceiving, since it's just regular dynamic memory?
Remaining types of memblocks
The remaining types include PA_MEMBLOCK_FIXED
where the data pointer is just given as a parameter that can be freed later:
/* No lock necessary */
pa_memblock *pa_memblock_new_fixed(pa_mempool *p, void *d, size_t length, bool read_only) {
pa_memblock *b;
pa_assert(p);
pa_assert(d);
pa_assert(length != (size_t) -1);
pa_assert(length);
if (!(b = pa_flist_pop(PA_STATIC_FLIST_GET(unused_memblocks))))
b = pa_xnew(pa_memblock, 1);
PA_REFCNT_INIT(b);
b->pool = p;
b->type = PA_MEMBLOCK_FIXED;
b->read_only = read_only;
b->is_silence = false;
pa_atomic_ptr_store(&b->data, d);
b->length = length;
pa_atomic_store(&b->n_acquired, 0);
pa_atomic_store(&b->please_signal, 0);
stat_add(b);
return b;
}
So the above truly wraps any peace of memory with reference-counted memory block! Note that like it was done with PA_MEMBLOCK_APPENDED
, the meta-data are freed and allocated from a SLAB-like cache of pa_memblock blocks using pa_flist_pop
.