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 of pa_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.

results matching ""

    No results matching ""