内存堆管理¶
Copyright © Quectel Wireless Solutions Co., Ltd. 2026. All rights reserved.
本文档旨在介绍UniRTOS堆内存管理的功能概述、相关API、示例代码以及常见问题,帮助用户正确理解并使用UniRTOS的内存管理功能。
功能概述¶
内存是程序运行时用于保存指令和数据的空间,通常由RAM提供。它支持读写操作,且掉电后数据会丢失。在嵌入式系统中,由于RAM容量有限且内容动态变化,内存管理的核心目标如下:
在有限空间内提升利用率。
确保内存分配与释放行为可控。
降低内存碎片与耗尽风险。
UniRTOS的内存管理设计思想:将内核与具体的堆分配算法解耦。
内核仅定义统一接口,不强制具体的底层实现。这使得开发者能够根据实际产品场景灵活选择分配策略,在可靠性、实时性与内存利用率之间取得最佳平衡。
备注
不同项目模块的物理内存大小存在差异。
静态内存¶
概述¶
静态存储空间(即静态存储期)是指在程序整个运行期间持续存在的数据区域。此类对象的生命周期贯穿程序的启动与结束,不会因函数的退出而释放。在嵌入式系统中,这类数据通常由链接脚本分配至固定的内存段(如 .data、.bss、.rodata),其物理地址一般在链接阶段确定。
全局变量¶
全局变量是定义在函数外部、可被多个函数访问的变量。根据其初始化情况,可分为以下两类:
已初始化全局变量
此类变量通常存放于 .data 段。在常见的MCU/SoC架构中,其初始值保存在非易失性存储(Flash/ROM)中。系统上电启动时,启动代码会将这些初值拷贝至RAM,以供运行时读写。
未初始化全局变量(或显式初始化为0)
此类变量通常存放于 .bss 段。由于它们无需在Flash中保存完整的初值镜像,启动时由启动代码统一进行清零操作。
静态变量¶
静态变量是使用 static 修饰的变量,生命周期同样贯穿程序整个运行期。根据定义位置不同,含义略有区别:
文件作用域 static 变量
定义在函数外部,仅在当前源文件内可见,常用于实现模块级的私有数据。函数内部 static 变量
定义在函数内部,仅在该函数的作用域内可见。此类变量不会随函数的返回而销毁,在多次调用该函数时,能够保留上一次调用结束时的值。
动态内存¶
概述¶
动态存储空间是指在程序运行期间按需分配与释放的内存区域,其生命周期并不固定。常见的动态内存来源包括栈分配和堆分配。需要强调的是,“地址是否变化”并非动态内存的本质特征;其核心本质在于,内存的分配时机与生命周期由程序的运行状态决定,而非在编译阶段静态确定。
栈¶
栈主要用于保存函数调用的现场信息与自动对象,典型内容包括函数参数、返回地址、部分寄存器上下文以及普通局部变量等。其主要特点如下:
自动管理
内存在进入作用域时自动分配,在退出作用域时自动回收,该过程通常由编译器和运行时环境自动完成。线程私有
在UniRTOS及多线程系统中,每个线程均拥有独立的栈空间。当线程结束时,其占用的栈资源会被系统自动回收。高效但容量受限
栈内存通常按顺序推进与回退,分配开销小且速度快。但由于其空间有限,定义过大的局部对象极易导致栈溢出。可配置而非“完全不可控”
虽然开发者通常无法手动释放单个栈变量,但可以灵活配置线程栈的大小、栈布局以及溢出检测策略。线程栈来源
在许多系统中,线程栈通常从堆或内存池中动态申请;但在部分平台上,也可以进行静态预留,因此线程栈并非总是来源于堆内存。
堆¶
堆是用于在运行期间进行动态分配的大块内存区域,通常由内存分配器统一管理(如 malloc/free、UniRTOS heap_x、内存池等)。其主要特点如下:
手动或半自动管理
其典型模式是内存的申请与释放必须成对出现。若管理不当,极易引发内存泄漏、悬垂指针(野指针)以及重复释放等问题。生命周期灵活
堆上分配的对象可以跨函数、跨模块甚至跨任务存在,非常适合用于存储大小和存活时间在编译期难以确定的数据。分配开销高于栈
内存分配器需要维护额外的元数据并遍历查找可用内存块,因此其分配速度通常慢于栈内存分配。碎片风险
长期的随机分配与释放操作容易产生外部碎片,导致“总空闲内存充足,但缺乏足够大的连续内存块”的现象。管理结构不唯一
“基于链表”仅仅是堆分配的一种常见实现方式。在实际的底层设计中,还可能采用分离链表、位图、伙伴系统、TLSF算法或固定块内存池等多种数据结构。
RTOS内存管理算法¶
概述¶
本节所指的内存管理,仅针对堆内存空间。模块在开机运行后,会预留一块较大的内存区域作为堆内存。该堆的实际可用空间会随着应用程序的启动与停止而动态变化。鉴于不同RTOS的内存管理机制存在差异,UniRTOS作为多RTOS抽象层进行了统一适配。目前,该抽象层底层主要支持ThreadX和FreeRTOS两种内核。
常用算法¶
首次适应算法(First Fit)
该算法要求空闲分区链表按照地址从小到大的顺序进行链接。在分配内存时,系统从链表的第一个空闲分区开始顺序查找,并将第一个大小能够满足要求的空闲分区分配给进程。
循环首次适应算法(Next Fit)
Next Fit由First Fit算法演变而来。分配内存时,从上一次刚分配过的空闲分区的下一个开始查找,直至找到能满足要求的空闲分区。
最佳适应算法(Best Fit)
该算法旨在从所有空闲分区中,找出能满足要求且大小最小的空闲分区。为了加快查找速度,Best Fit算法会将所有空闲分区按其容量从小到大的顺序链接成链表。这样,系统第一次找到的满足大小要求的空闲分区必然是最优的。
最坏适应算法(Worst Fit)
该算法旨在从所有空闲分区中,找出能满足要求且大小最大的空闲分区。为了加快查找速度,Worst Fit算法会将所有空闲分区按其容量从大到小的顺序链接成链表。
两级隔离适应算法(TLSF)
该算法使用两层链表结构来管理空闲内存。第一级链表按大小范围对空闲分区进行粗略分类,每一类对应一个空闲链表,其中的分区大小均在特定范围内。由于存在多个空闲链表,系统引入了第二级索引链表来统一管理这些链表,索引表的每一项都对应一种特定的空闲链表,并记录其表头指针。
伙伴算法(Buddy systems)
该算法是Segregated Fit算法的一种变种,具有更高效的内存拆分与回收合并机制。伙伴算法包含多种实现形式,如Binary Buddies和Fibonacci Buddies等。其中,Binary Buddies是最简单且最流行的一种。它将所有空闲分区按大小进行分类,每一类包含大小完全相同的空闲分区集合,并使用双向链表进行管理。在Binary Buddies算法中,所有分区的大小均严格为2的幂次方。
内存算法 |
优点 |
缺点 |
|---|---|---|
First Fit |
高地址空间的大空闲块得以保留 |
低地址空间被不断拆分,产生碎片;每次都从第一个空闲分区开始查找,系统开销增加 |
Next Fit |
空闲分区分布比较均匀,算法开销小 |
缺乏大内存空闲块 |
Best Fit |
用最小内存满足要求,保留大内存空闲块 |
每次分配后拆分的剩余空闲内存最小,易产生大量小碎片,算法开销大 |
Worst Fit |
每次分配后拆分的剩余空闲内存较大,可减小碎片产生 |
缺乏大内存空闲块,算法开销大 |
TLSF |
查找效率高,时间复杂度低,碎片问题表现良好 |
内存回收时算法复杂,系统开销大 |
Buddy systems |
分配和回收速度快,合并高效 |
内部碎片比较严重 |
ThreadX¶
分配¶
ThreadX的内存分配采用首次适应算法。系统在分配内存时,会从第一个空闲内存块开始顺序查找,一旦找到大小满足要求的空闲块,便立即完成分配并返回成功。
空闲内存块采用链表方式进行管理,且各内存块在物理地址上按从低到高的顺序排列。每个内存块的前8个字节被用作控制字段(Header),其具体结构如下:
前4字节:存储下一块相邻内存的起始地址。
后4字节:用于标识当前内存块的状态。若为空闲内存,则存储特定标志值 TX_BYTE_BLOCK_FREE;若为已分配内存,则存储所属内存池管理结构体的起始地址。
在内存池初始化时,整个内存空间首先被划分为两个特殊的内存块:位于起始位置的第一块和位于末尾的最后一块。
第一块(初始空闲区):其前4字节存储最后一块的起始地址,后4字节被设置为 TX_BYTE_BLOCK_FREE,标记为尚未使用的空闲块。
最后一块(边界哨兵):其前4字节存放内存池管理结构体的起始地址,后4字节被标记为 TX_BYTE_BLOCK_ALLOC。该块作为内存池的边界哨兵,在逻辑上始终处于已分配状态,用于防止内存操作越界。
下图展示了首次内存分配后的状态。最初的第一块内存被拆分为两块:新分配的第一块和剩余的第二块。其中,第一块被返回给应用程序。由于每个内存块的前8个字节为控制字段(Header),因此返回给应用程序的内存起始指针(memory_ptr)会向后偏移8个字节,直接指向实际可用的数据区域。对于新分配的第一块内存,其控制字段被更新为已分配状态:前4字节存储第二块内存的起始地址;后4字节则指向内存池管理结构体。这一状态标识表明该块内存已被占用,不再是空闲块。对于剩余的第二块内存,其前4字节更新为指向第三块内存(即初始化时的最后一块边界内存),从而继续保持单向链表的连贯性;其后4字节则存储 TX_BYTE_BLOCK_FREE 标志值,明确标识该块仍为空闲内存块。
释放¶
内存释放时,将释放内存的首地址减8,得到内存块控制字段的首地址,再通过前4字节找到内存池管理结构体的起始地址,即可执行内存释放操作。释放后,控制字段的后4字节(偏移4~7)存储 TX_BYTE_BLOCK_FREE,标记该块为空闲内存块。
释放后,即使相邻的两个内存块地址连续,系统也不会立即进行合并或内存整理。等到申请内存时,若请求大小大于当前块的大小,系统会继续查找下一块。如果发现当前块与下一块地址连续,则将两者合并为一块。
释放内存后,系统会检查挂起链表中是否有线程等待。若有,则尝试分配内存并恢复线程执行。线程按FIFO顺序恢复,而非按照线程优先级高低。如需改变此行为,可在释放内存前调用 tx_byte_pool_prioritize(),将最高优先级线程移动到挂起链表最前面,使其优先被恢复。
碎片处理¶
内存在多次分配和释放后,可能会出现大量小的内存块,这种现象称为内存碎片化。
当需要分配较大的内存块时,每次可能需要先遍历大量的小内存块,这会导致查找开销增加,算法性能下降。由于每个内存块都占用8个字节的控制字段,大量小内存会导致内存的浪费。在查找过程中,若发现两个相邻内存块的地址连续,则将其合并为一个内存块,这个过程称为内存整理。
下图为多次内存分配和释放后的结构。其中,第一块已被占用,第二块、第三块、第四块为空闲内存块。第二块大小为64字节,第三块大小为64字节,第四块内存大小为256字节。假设现在申请分配的内存大小为128字节。在分配查找过程中,发现第二块太小无法满足,但第二块与第三块地址连续,于是将两者合并为一块。合并后的块大小正好满足请求(64+64=128),系统将其返回给应用程序,如下面第二个图所示。
FreeRTOS¶
FreeRTOS提供5种内存分配实现,开发者可按需求选择其一,或采用自定义方案。这5种实现分别对应源码中的heap_1.c、heap_2.c、heap_3.c、heap_4.c和heap_5.c。各实现的适用场景见下文。为便于理解其核心机制,文中给出的伪代码仅用于说明实现思路,不等同于完整工程代码且不可运行。
heap_1.c:
// 全局静态堆指针,大小由 configTOTAL_HEAP_SIZE 决定
static uint8_t ucHeap[HEAP_SIZE];
// 下一个可分配位置(偏移)
static size_t xNextFreeByte = 0;
// 已分配总量(可选统计)
static size_t xFreeBytesRemaining = HEAP_SIZE;
void *pvPortMalloc(size_t xWantedSize)
{
void *pvReturn = NULL;
// 1) 对齐到字节边界
xWantedSize = align_up(xWantedSize, PORT_BYTE_ALIGNMENT);
// 2) 进入临界区(线程安全)
suspend_scheduler_or_enter_critical();
// 3) 检查是否还能分配
if ((xWantedSize > 0) &&
(xNextFreeByte + xWantedSize <= HEAP_SIZE))
{
pvReturn = &ucHeap[xNextFreeByte];
xNextFreeByte += xWantedSize;
xFreeBytesRemaining -= xWantedSize;
}
else
{
// 分配失败,返回 NULL
pvReturn = NULL;
}
// 4) 退出临界区
resume_scheduler_or_exit_critical();
// 5) 可选:malloc failed hook
if (pvReturn == NULL) {
call_malloc_failed_hook_if_enabled();
}
return pvReturn;
}
void vPortFree(void *pv)
{
// heap_1 不支持释放
(void)pv;
}
heap_2
// 空闲块链表节点
typedef struct BlockLink_t {
struct BlockLink_t *pxNextFreeBlock; // 指向下一个空闲块
size_t xBlockSize; // 当前块大小(包括节点头)
} BlockLink_t;
// 全局变量
static BlockLink_t *pxEnd = NULL;
static BlockLink_t xStart; // 哨兵节点
static uint8_t ucHeap[HEAP_SIZE];
function prvHeapInit():
// 将整个堆组织为一个大空闲块
pxFirstFreeBlock = &ucHeap[0]
pxFirstFreeBlock->xBlockSize = HEAP_SIZE
pxFirstFreeBlock->pxNextFreeBlock = NULL
// xStart 和 xEnd 作为哨兵
xStart.pxNextFreeBlock = pxFirstFreeBlock
xStart.xBlockSize = 0
pxEnd = NULL // 标记堆末尾
function pvPortMalloc(xWantedSize):
pxBlockToAllocate = NULL
pxPreviousBlock = &xStart
pxBlock = xStart.pxNextFreeBlock
xWantedSize = xWantedSize + sizeof(BlockLink_t) // 加上节点头
enter_critical()
// 遍历找最小满足块(Best Fit)
while pxBlock != NULL:
if pxBlock->xBlockSize >= xWantedSize:
// 如果这是第一个满足的块 或 比前一个找到的块更小
if (pxBlockToAllocate == NULL) or (pxBlock->xBlockSize < pxBlockToAllocate->xBlockSize):
pxBlockToAllocate = pxBlock
pxPreviousBlockForAlloc = pxPreviousBlock
pxPreviousBlock = pxBlock
pxBlock = pxBlock->pxNextFreeBlock
// 如果找到了合适的块
if pxBlockToAllocate != NULL:
// 检查是否需要拆分
if pxBlockToAllocate->xBlockSize > xWantedSize + heapMINIMUM_BLOCK_SIZE:
// 拆分:剩余部分作为新空闲块
pxNewBlockLink = (char *)pxBlockToAllocate + xWantedSize
pxNewBlockLink->xBlockSize = pxBlockToAllocate->xBlockSize - xWantedSize
pxNewBlockLink->pxNextFreeBlock = pxBlockToAllocate->pxNextFreeBlock
// 原块变小
pxBlockToAllocate->xBlockSize = xWantedSize
pxBlockToAllocate->pxNextFreeBlock = pxNewBlockLink
// 从链表移除已分配块
pxPreviousBlockForAlloc->pxNextFreeBlock = pxBlockToAllocate->pxNextFreeBlock
pvReturn = (char *)pxBlockToAllocate + sizeof(BlockLink_t)
else:
pvReturn = NULL
exit_critical()
if pvReturn == NULL:
call_malloc_failed_hook_if_enabled()
return pvReturn
function vPortFree(pv):
if pv == NULL:
return
enter_critical()
// 倒退找到块头
pxBlockToFree = (BlockLink_t *)((char *)pv - sizeof(BlockLink_t))
// 重新插入空闲链表(按地址有序)
pxBlock = &xStart
while pxBlock->pxNextFreeBlock != NULL:
if (char *)pxBlock->pxNextFreeBlock > (char *)pxBlockToFree:
break
pxBlock = pxBlock->pxNextFreeBlock
// 注意:heap_2 不合并相邻块!
pxBlockToFree->pxNextFreeBlock = pxBlock->pxNextFreeBlock
pxBlock->pxNextFreeBlock = pxBlockToFree
exit_critical()
heap_3:其实就pvPortMalloc定向标准库已经做好的malloc和free函数,底层就不用像heap_1和heap_2一样进行实现。
function pvPortMalloc(xWantedSize):
pvReturn = NULL
// 挂起调度器(保证线程安全)
vTaskSuspendAll()
{
// 直接调用标准库 malloc
pvReturn = malloc(xWantedSize)
// 可选:记录 trace 信息
traceMALLOC(pvReturn, xWantedSize)
}
// 恢复调度器
xTaskResumeAll()
// 分配失败处理
if pvReturn == NULL:
if CONFIG_USE_MALLOC_FAILED_HOOK == 1:
// 回调失败处理钩子
vApplicationMallocFailedHook()
return pvReturn
function vPortFree(pv):
if pv == NULL:
return
// 挂起调度器
vTaskSuspendAll()
{
// 直接调用标准库 free
free(pv)
// 可选:记录 trace 信息
traceFREE(pv, 0)
}
// 恢复调度器
xTaskResumeAll()
heap_4: 直接在heap_2的基础上增加了合并相邻节点的功能,可以减少碎片化,下面伪代码只展示与heap_2差异之处,vPortFree函数。
function vPortFree(pv):
pxBlockToFree = NULL
pxIterator = NULL
if pv == NULL:
return
enter_critical()
// 倒退找到块头
pxBlockToFree = (BlockLink_t *)((char *)pv - sizeof(BlockLink_t))
// 更新统计
xFreeBytesRemaining = xFreeBytesRemaining + pxBlockToFree->xBlockSize
// 找到要插入的位置(按地址有序)
pxIterator = &xStart
while (pxIterator->pxNextFreeBlock != NULL) and
((char *)pxIterator->pxNextFreeBlock < (char *)pxBlockToFree):
pxIterator = pxIterator->pxNextFreeBlock
// ===== 关键:合并逻辑开始 =====
// 1) 检查与后面的块合并
pxBlockToFree->pxNextFreeBlock = pxIterator->pxNextFreeBlock
if pxBlockToFree->pxNextFreeBlock != NULL:
if ((char *)pxBlockToFree + pxBlockToFree->xBlockSize ==
(char *)pxBlockToFree->pxNextFreeBlock:
// 与后面的块地址连续,合并
pxBlockToFree->xBlockSize = pxBlockToFree->xBlockSize +
pxBlockToFree->pxNextFreeBlock->xBlockSize
pxBlockToFree->pxNextFreeBlock = pxBlockToFree->pxNextFreeBlock->pxNextFreeBlock
// 2) 检查与前面的块合并
if pxIterator != &xStart:
if ((char *)pxIterator + pxIterator->xBlockSize ==
(char *)pxBlockToFree:
// 与前面的块地址连续,合并
pxIterator->xBlockSize = pxIterator->xBlockSize + pxBlockToFree->xBlockSize
pxIterator->pxNextFreeBlock = pxBlockToFree->pxNextFreeBlock
else:
// 不连续,正常插入
pxIterator->pxNextFreeBlock = pxBlockToFree
else:
// 插在链表最前面
pxIterator->pxNextFreeBlock = pxBlockToFree
// ===== 合并逻辑结束 =====
exit_critical()
heap_5:
在保留heap_4主要优势(支持分配、释放和碎片合并)的基础上进一步增强了堆来源管理能力。与前几种实现通常只使用单一堆数组不同,heap_5可将多块彼此不连续的内存区域统一组织为一个逻辑堆,并通过同一个pvPortMalloc接口对外提供服务。对上层业务而言,无需感知底层内存来自几块区域,只需按统一接口申请和释放内存;对底层实现而言,可同时管理两块、三块甚至更多内存来源。这些内存来源既可以是同一RAM中相邻或不相邻的区域,也可以来自不同RAM/存储体。
方法 |
适用场景 |
|---|---|
heap_1 |
支持内存分配但不支持回收。适用于资源受限的小型嵌入式设备,系统启动时申请内存,在整个程序生命周期内通常无需释放 |
heap_2 |
支持内存回收,但不支持碎片合并。使用Best Fit算法。适用于每次申请内存大小相对固定的场景 |
heap_3 |
在标准库的malloc和free接口基础上增加了线程安全保护 |
heap_4 |
支持内存分配、回收及碎片合并。适用于频繁分配和释放大小不确定的内存的场景 |
heap_5 |
支持将多个不连续的内存区域组成堆,适用于内存分布不连续的嵌入式设备 |
当前模块使用的是heap_4方法,本小节主要介绍heap_4的算法实现。
分配¶
内存分配使用首次适应(First Fit)算法,每次从头开始查找空闲内存块,找到大小合适的内存块后,返回成功。
通过一个链表维护未分配的内存。链表节点定义如下:
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock;/*<< The next free block in the list. */
size_t xBlockSize;/*<< The size of the free block. */
} BlockLink_t;
两个变量分别为:指向下一块内存的地址指针 pxNextFreeBlock,以及当前块的内存大小 xBlockSize。申请内存时,系统会在申请大小 xWantedSize 的基础上加上 heapSTRUCT_SIZE(链表节点大小),作为最终申请的内存大小。然后从链表头开始遍历未分配内存链表,查找符合大小的内存块。同时判断当前内存块是否有剩余(即大于所申请的大小)。若有剩余,则将剩余部分新建为一个未分配内存块节点,插入到未分配链表中,供下次分配使用。整体算法与前述ThreadX类似。
释放¶
内存释放时,根据释放的内存地址向前定位到对应的链表节点,获取该内存块的信息,然后调用链表插入函数将节点归还。
碎片处理¶
由上述分配过程可知,内存在多次分配和释放后同样会产生内存碎片。因此需要将内存碎片合并成大块内存,合并算法与前述ThreadX类似。为实现该合并算法,空闲内存链表按内存地址顺序(从小到大)进行存储。例如,对于待插入的内存块P,系统找到其地址前面的相邻内存块A,判断A与P之间是否存在已分配的内存块。若不存在,则直接将两者合并。接着,再检查P与其后面的相邻内存块C的位置关系。若P与C之间没有其他已分配的内存块,则同样进行合并。
应用场景 - 动态内存¶
底层动态内存申请和释放¶
底层程序在运行过程中,当需要内存空间存储数据时,会调用malloc接口从堆内存中申请一定大小的内存。当业务执行结束、不再需要该内存块时,会调用free接口释放内存并将其归还到堆内存中。
创建线程¶
程序运行过程中创建新线程时,需为该线程分配栈空间,该栈空间同样从堆内存中申请。当线程执行结束或被删除时,会释放其栈空间并将其归还到堆内存中。底层和应用层的程序都有可能创建线程。
内存管理API¶
头文件¶
qosa_def.h
函数概览¶
函数 |
说明 |
|---|---|
qosa_malloc() |
从系统heap中申请指定大小的内存 |
qosa_free() |
释放获得的内存 |
qosa_realloc() |
调整一块已申请heap内存的大小 |
qosa_calloc() |
申请heap内存 |
qosa_strdup() |
复制一个字符串,并在heap中生成新的字符串副本 |
函数详解¶
qosa_malloc¶
功能描述
从系统heap中申请指定大小的内存。当前工程中的适配实现会对成功申请到的内存做清零初始化。函数原型
void* qosa_malloc(qosa_size_t nbytes)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
nbytes |
输入 |
qosa_size_t |
需要申请的内存大小。单位:字节 |
返回值说明
非空指针:申请成功,返回可用内存首地址
QOSA_NULL:申请失败,通常表示heap空间不足
备注
当前工程里的 qosa_malloc() 行为更接近“malloc + memset(0)”。
申请成功后,必须在不再使用时调用 qosa_free() 释放。
不应使用 qosa_free() 释放静态内存、栈内存或其他模块内部管理的内存。
qosa_free¶
功能描述
释放通过 qosa_malloc()、qosa_calloc()、qosa_realloc()、qosa_strdup() 等接口获得的heap内存。函数原型
void qosa_free(void* ptr)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
ptr |
输入 |
void* |
待释放的内存指针 |
返回值说明
无
qosa_realloc¶
功能描述
调整一块已申请heap内存的大小,可用于扩容或缩容。函数原型
void* qosa_realloc(void* ptr, qosa_size_t nbytes)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
ptr |
输入 |
void* |
原内存指针。通常应来自 qosa_malloc()、qosa_calloc() 或 qosa_realloc() |
nbytes |
输入 |
qosa_size_t |
调整后的目标大小。单位:字节 |
返回值说明
非空指针:调整成功,返回新的内存首地址
QOSA_NULL:调整失败,原指针通常仍然有效,调用方不应直接丢失原指针
qosa_calloc¶
功能描述
按“元素个数 × 单个元素大小”的方式申请heap内存,并将申请到的内存全部清零。函数原型
void* qosa_calloc(qosa_size_t count, qosa_size_t size)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
count |
输入 |
qosa_size_t |
元素个数 |
size |
输入 |
qosa_size_t |
单个元素大小。单位:字节 |
返回值说明
非空指针:申请成功
QOSA_NULL:申请失败,通常表示heap空间不足
qosa_strdup¶
功能描述
复制一个字符串,并在heap中生成新的字符串副本。函数原型
char* qosa_strdup(const char* s)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
s |
输入 |
const char* |
源字符串指针 |
返回值说明
非空指针:复制成功,返回新字符串地址
QOSA_NULL:输入为空或内存申请失败
示例代码¶
完整示例代码请查看https://github.com/UniRTOS/UniRTOS-Doc-Examples/blob/main/system/memory/memory.c
常见问题¶
如何避免内存碎片?¶
内存管理算法可在一定程度上减少内存碎片,但无法完全避免。建议采取以下措施:
尽量使用栈空间,减少动态内存分配函数的调用。
分配内存和释放内存尽量在同一个函数中完成。
尽量一次性申请较大的内存块,避免反复申请小内存,以减少内存分割。
自行设计内存池来管理内存。
堆安全剩余量¶
虽然堆空间是动态分配和释放的,但仍需保证有足够的剩余空间,以满足复杂业务场景下的内存申请需求。堆空间的总量由底层决定,固件生成后该值即确定。
内存泄露¶
常见内存泄漏的场景有:
分配和释放内存的操作不成对。例如,业务开始时申请了内存,但在业务结束时未释放,下次业务运行时又重新申请。如此反复,导致可用内存不断减少,最终可能耗尽整个内存空间,使程序无法申请到内存而崩溃。
重复创建线程。底层或应用层创建线程和删除线程的操作不成对,导致相同业务的线程被重复创建。每创建一个线程都需要从堆中分配栈空间,最终将耗尽整个堆空间。
内存越界¶
常见内存越界的场景有:
数组越界:操作数组时,索引超过了实际分配的数组大小,导致访问越界到其他内存空间,可能破坏这些空间中的数据,从而引发系统异常。
栈溢出:创建线程时传入的栈空间大小小于业务实际运行所需空间,导致运行过程中数据操作越界到其他内存空间,可能破坏这些空间中的数据,从而引发系统异常。
内存申请失败异常信息¶
底层内存申请失败通常会触发系统崩溃异常。