Memory Management
Overview
Gantry’s memory model is based around referenced counted blocks of memory. Memory blocks are allocated from a set of fixed-size pools. Each allocation comes from the smallest pool that can satisfy the requested size. Blocks are reference-counted and freed automatically when the count reaches zero.
Usage
Memory can be allocated using MEM_ALLOC. A pointer must be provided that will be populated with a block pointer assuming one is available. The provided pointer must be NULL to prevent the caller from accidentally overwriting data.
When successful, the caller is provided a block with a reference count of one. To increment the reference count use MEM_REF. Likewise, MEM_UNREF decrements the reference count. Once the reference count reaches zero, the block is freed and available for future allocations. When a block is freed, the pointer provided by the caller is set to NULL. It is okay to call MEM_UNREF on a block that has already been freed (the provided block pointer points to NULL).
void* buf = NULL;
MEM_ALLOC(64, &buf); // allocate; ref count starts at 1
MEM_REF(buf); // share with another owner; ref count = 2
MEM_UNREF(&buf); // release; ref count = 1, buf unchanged
MEM_UNREF(&buf); // release; ref count = 0, buf freed and set to NULL
While the direct functions like mem_alloc() can be used, the macros are recommended as they allow for tracing the source of memory operations when memory tracing is enabled. If you are writing a wrapper around the memory manager, you may want to use the direct function calls instead. See Memory Tracing for more information.
Memory Leak Detection
To avoid memory leaks, the general memory model is as follows: the function that allocates memory should free it. If the memory block is passed to another function, as is often the case, that function should reference count the memory to claim ownership, then free it when done.
This model allows the Resource Checker to automatically detect memory leaks. It also keeps memory management consistent and clear. If a function allocates or references memory, it must also free it.
There will be exceptions to this rule. For example, when making a helper function that allocates memory and provides it to the caller. In these cases, the PASS_OWNERSHIP macro can be used to indicate to the resource checker that the memory is intentionally not freed. Additionally, when exiting a function early due to a failed allocation, the resource checker must be informed that the memory was not allocated and therefore does not need to be freed. For this, use the NOT_REFERENCED macro.
int custom_alloc(void** block)
{
void* new_block = NULL;
int ret = mem_alloc(42, &new_block);
if (ret != SUCCESS)
{
// Alloc didn't succeed so there is nothing to free
NOT_REFERENCED(new_block);
return ret;
}
*block = new_block;
// The intent is to give ownership to the caller
PASS_OWNERSHIP(new_block);
return SUCCESS;
}
Note
You must not provide pointers as complex expressions to the memory API. This allows the Resource Checker to identify memory leaks. The resource checker will warn you when complex expressions are used.
// Do this
void* buf = buffers[i];
MEM_REF(buf);
// NOT this
MEM_REF(buffers[i]); // The resource checker will warn you
The Resource Checker is not a substitute for proper memory management. It will catch common cases within a single function scope, but it will not detect more complex leaks. Regardless, it should provide a safety net and some peace of mind. Additionally, if you create helper functions that allocate memory, you can add them to the resource checker, see Resource Checker for more information.
Configuration
The memory module allows you to define up to six pools of memory blocks. You can set the number of pools as well as their block size and count via the following Kconfig options:
CONFIG_GANTRY_MEMORY=y
CONFIG_MEM_POOL_COUNT=3
CONFIG_MEM_POOL1_BLOCK_SIZE=16
CONFIG_MEM_POOL1_BLOCK_COUNT=128
CONFIG_MEM_POOL2_BLOCK_SIZE=128
CONFIG_MEM_POOL2_BLOCK_COUNT=64
CONFIG_MEM_POOL3_BLOCK_SIZE=1024
CONFIG_MEM_POOL3_BLOCK_COUNT=16
Important
Block sizes must increase as the pool number rises. This ensures the memory manager can allocate using the smallest pool possible.
Debugging
Debugging memory issues is no fun, so Gantry provides some tools to help!
Memory Tracing
When memory tracing is enabled via MEM_TRACE, each memory block tracks which function originally allocated it, the caller of the MEM_ALLOC macro.
If you are writing a helper function to wrap memory allocation, you might want to track who called your function as the original creator of the memory. In this case, use the mem_* functions directly in your helper and wrap calls to your helper in the TRACE_WRAP (or TRACE_WRAP_VOID for void return values) macro. This will result in the caller of your API being considered the original allocator of the block.
int alloc_helper(void** block) {
return mem_alloc(42, block);
}
void foo() {
void* block = NULL;
TRACE_WRAP(alloc_helper(&block));
// `foo` is tracked as the allocator of `block`
}
This same tracing logic applies to referencing and dereferencing memory blocks. To view this trace information raise the MEM_LOG_LEVEL to Debug. This will log all memory operations including: what operation is occurring, what function triggered the operation, and what function allocated the block being operated on.
Watermarks
To keep your memory usage in check, and potentially identify leaks, you can use watermarks. Enable watermarks using the MEM_WATERMARK Kconfig option. When enabled you can register a one-shot callback to fire when a pool crosses a usage threshold. The callback fires once; call mem_set_watermark() again to re-arm it.
void on_watermark(uint8_t pool_index, uint8_t percent)
{
LOG_WRN("Pool %d reached %d%% usage", pool_index, percent);
}
// Fire `on_watermark` when the first pool (0-indexed) reaches 80 percent
mem_set_watermark(0, 80, on_watermark);
Additionally, you can manually check the current usage of each pool using mem_get_pool_usage(). This is available regardless of whether watermarks are enabled.
API Reference
-
typedef void (*mem_watermark_cb_t)(uint8_t pool_index, uint8_t percent)
Callback type for pool watermark notifications.
- Param pool_index:
The pool index that hit the watermark
- Param percent:
The watermark percentage that was hit
-
int mem_alloc(size_t size, void **block_ptr)
Allocate a block of memory.
The block will be allocated from a memory pool based on the requested size. The block will be provided from the smallest possible memory pool capable of holding the requested size.
Note
It is recommended that you always use the MEM_ALLOC macro which calls this function with the debug information filled in.
- Parameters:
size – [in] The requested buffer size in bytes
block_ptr – [out] Pointer to be populated with the address of memory block. This pointer must point to a NULL pointer when this function is called. It is only populated when the allocation succeeds.
- Returns:
SUCCESS when the allocation is successful.
- Returns:
-EINVAL the provided pointer was NULL, a size of 0 was requested, or the requested size was larger than the maximum block size.
- Returns:
-ENOTEMPTY the block pointer was not pointing to a NULL pointer.
- Returns:
-ENOMEM No blocks were available that could fit the requested size.
-
void mem_ref(void *block)
Increment the reference count of a memory block.
Note
It is recommended that you always use the MEM_REF macro which calls this function with the debug information filled in.
- Parameters:
block – [in] The memory block pointer to be reference counted
-
void mem_unref(void **block_ptr)
Dencrement the reference count of a memory block.
Note
It is recommended that you always use the MEM_UNREF macro which calls this function with the debug information filled in.
Note
If block_ptr already points to a NULL pointer, this function will assume that the block was already freed and do nothing.
- Parameters:
block_ptr – [inout] A pointer to the memory block pointer to be dereferenced. If the reference count reaches 0, the block pointer will be set to NULL.
-
uint32_t mem_get_ref_count(void *block)
Get the current reference count for a memory block.
- Parameters:
block – The memory block to get the reference count of
- Returns:
The current reference count of the provided memory block
-
uint8_t mem_get_pool_count(void)
Get the number of active memory pools.
- Returns:
The number of configured memory pools
-
int mem_get_pool_usage(uint8_t pool_index, uint32_t *used_out, uint32_t *total_out)
Get the usage of a memory pool.
- Parameters:
pool_index – [in] The pool index (e.g. POOL1 = 0)
used_out – [out] Number of currently allocated blocks
total_out – [out] Total number of blocks in the pool
- Returns:
SUCCESS on success
- Returns:
-EINVAL if pool_index is out of range or either output pointer is NULL
-
int mem_set_watermark(uint8_t pool_index, uint8_t percent, mem_watermark_cb_t callback)
Register a watermark callback for a memory pool.
The callback is invoked the first time pool usage reaches or exceeds the given percentage. It will not fire again unless mem_set_watermark is called again.
- Parameters:
pool_index – [in] The pool index (e.g. POOL1 = 0)
percent – [in] Usage percentage threshold (0-100)
callback – [in] Function to call when the watermark is first reached
- Returns:
SUCCESS on success
- Returns:
-EINVAL if pool_index is out of range, percent > 100, or callback is NULL
-
TRACE_WRAP(func_call)
Call a function with the current location as the memory trace.
This macro sets and restores the trace in a threadsafe manner. It can be used in ISR contexts. When tracing is disabled this wrapper has no effect.
-
TRACE_WRAP_VOID(func_call)
Call a function with the current location as the memory trace.
Variant of TRACE_WRAP for functions that have no return value.
-
PASS_OWNERSHIP(data)
Indicates to static analysis that ownership over the memory will be the responsibility of the caller.
-
NOT_REFERENCED(data)
Indicates to static analysis that the memory was not allocated or referenced.