# 内存堆管理 ***Copyright © Quectel Wireless Solutions Co., Ltd. 2026. All rights reserved.*** --- 本文档旨在介绍UniRTOS堆内存管理的功能概述、相关API、示例代码以及常见问题,帮助用户正确理解并使用UniRTOS的内存管理功能。 # 功能概述 内存是程序运行时用于保存指令和数据的空间,通常由RAM提供。它支持读写操作,且掉电后数据会丢失。在嵌入式系统中,由于RAM容量有限且内容动态变化,内存管理的核心目标如下: 1. 在有限空间内提升利用率。 2. 确保内存分配与释放行为可控。 3. 降低内存碎片与耗尽风险。 UniRTOS的内存管理设计思想:将内核与具体的堆分配算法解耦。 内核仅定义统一接口,不强制具体的底层实现。这使得开发者能够根据实际产品场景灵活选择分配策略,在可靠性、实时性与内存利用率之间取得最佳平衡。 ```{note} 不同项目模块的物理内存大小存在差异。 ``` ## 静态内存 ### **概述** 静态存储空间(即静态存储期)是指在程序整个运行期间持续存在的数据区域。此类对象的生命周期贯穿程序的启动与结束,不会因函数的退出而释放。在嵌入式系统中,这类数据通常由链接脚本分配至固定的内存段(如 *.data*、*.bss*、*.rodata*),其物理地址一般在链接阶段确定。 ### **全局变量** 全局变量是定义在函数外部、可被多个函数访问的变量。根据其初始化情况,可分为以下两类: 1. 已初始化全局变量 此类变量通常存放于 *.data* 段。在常见的MCU/SoC架构中,其初始值保存在非易失性存储(Flash/ROM)中。系统上电启动时,启动代码会将这些初值拷贝至RAM,以供运行时读写。 2. 未初始化全局变量(或显式初始化为0) 此类变量通常存放于 *.bss* 段。由于它们无需在Flash中保存完整的初值镜像,启动时由启动代码统一进行清零操作。 ### **静态变量** 静态变量是使用 *static* 修饰的变量,生命周期同样贯穿程序整个运行期。根据定义位置不同,含义略有区别: 1. 文件作用域 *static* 变量 定义在函数外部,仅在当前源文件内可见,常用于实现模块级的私有数据。 2. 函数内部 *static* 变量 定义在函数内部,仅在该函数的作用域内可见。此类变量不会随函数的返回而销毁,在多次调用该函数时,能够保留上一次调用结束时的值。 ## 动态内存 ### **概述** 动态存储空间是指在程序运行期间按需分配与释放的内存区域,其生命周期并不固定。常见的动态内存来源包括栈分配和堆分配。需要强调的是,“地址是否变化”并非动态内存的本质特征;其核心本质在于,内存的分配时机与生命周期由程序的运行状态决定,而非在编译阶段静态确定。 ### **栈** 栈主要用于保存函数调用的现场信息与自动对象,典型内容包括函数参数、返回地址、部分寄存器上下文以及普通局部变量等。其主要特点如下: 1. **自动管理** 内存在进入作用域时自动分配,在退出作用域时自动回收,该过程通常由编译器和运行时环境自动完成。 2. **线程私有** 在UniRTOS及多线程系统中,每个线程均拥有独立的栈空间。当线程结束时,其占用的栈资源会被系统自动回收。 3. **高效但容量受限** 栈内存通常按顺序推进与回退,分配开销小且速度快。但由于其空间有限,定义过大的局部对象极易导致栈溢出。 4. **可配置而非“完全不可控”** 虽然开发者通常无法手动释放单个栈变量,但可以灵活配置线程栈的大小、栈布局以及溢出检测策略。 5. **线程栈来源** 在许多系统中,线程栈通常从堆或内存池中动态申请;但在部分平台上,也可以进行静态预留,因此线程栈并非总是来源于堆内存。 ### **堆** 堆是用于在运行期间进行动态分配的大块内存区域,通常由内存分配器统一管理(如 *malloc/free*、*UniRTOS heap_x*、内存池等)。其主要特点如下: 1. **手动或半自动管理** 其典型模式是内存的申请与释放必须成对出现。若管理不当,极易引发内存泄漏、悬垂指针(野指针)以及重复释放等问题。 2. **生命周期灵活** 堆上分配的对象可以跨函数、跨模块甚至跨任务存在,非常适合用于存储大小和存活时间在编译期难以确定的数据。 3. **分配开销高于栈** 内存分配器需要维护额外的元数据并遍历查找可用内存块,因此其分配速度通常慢于栈内存分配。 4. **碎片风险** 长期的随机分配与释放操作容易产生外部碎片,导致“总空闲内存充足,但缺乏足够大的连续内存块”的现象。 5. **管理结构不唯一** “基于链表”仅仅是堆分配的一种常见实现方式。在实际的底层设计中,还可能采用分离链表、位图、伙伴系统、TLSF算法或固定块内存池等多种数据结构。 ## RTOS内存管理算法 ### 概述 本节所指的内存管理,仅针对堆内存空间。模块在开机运行后,会预留一块较大的内存区域作为堆内存。该堆的实际可用空间会随着应用程序的启动与停止而动态变化。鉴于不同RTOS的内存管理机制存在差异,UniRTOS作为多RTOS抽象层进行了统一适配。目前,该抽象层底层主要支持ThreadX和FreeRTOS两种内核。 ### 常用算法 1. **首次适应算法(First Fit)** 该算法要求空闲分区链表按照地址从小到大的顺序进行链接。在分配内存时,系统从链表的第一个空闲分区开始顺序查找,并将第一个大小能够满足要求的空闲分区分配给进程。 2. **循环首次适应算法(Next Fit)** Next Fit由First Fit算法演变而来。分配内存时,从上一次刚分配过的空闲分区的下一个开始查找,直至找到能满足要求的空闲分区。 3. **最佳适应算法(Best Fit)** 该算法旨在从所有空闲分区中,找出能满足要求且大小最小的空闲分区。为了加快查找速度,Best Fit算法会将所有空闲分区按其容量从小到大的顺序链接成链表。这样,系统第一次找到的满足大小要求的空闲分区必然是最优的。 4. **最坏适应算法(Worst Fit)** 该算法旨在从所有空闲分区中,找出能满足要求且大小最大的空闲分区。为了加快查找速度,Worst Fit算法会将所有空闲分区按其容量从大到小的顺序链接成链表。 5. **两级隔离适应算法(TLSF)** 该算法使用两层链表结构来管理空闲内存。第一级链表按大小范围对空闲分区进行粗略分类,每一类对应一个空闲链表,其中的分区大小均在特定范围内。由于存在多个空闲链表,系统引入了第二级索引链表来统一管理这些链表,索引表的每一项都对应一种特定的空闲链表,并记录其表头指针。 6. **伙伴算法(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*。该块作为内存池的边界哨兵,在逻辑上始终处于已分配状态,用于防止内存操作越界。 ```{image} images/image_EqcObVPSsohRZqx9Ho3cUnWcnnf.webp :width: 6328px :height: 662px :align: center ``` 下图展示了首次内存分配后的状态。最初的第一块内存被拆分为两块:新分配的第一块和剩余的第二块。其中,第一块被返回给应用程序。由于每个内存块的前8个字节为控制字段(Header),因此返回给应用程序的内存起始指针(*memory_ptr*)会向后偏移8个字节,直接指向实际可用的数据区域。对于新分配的第一块内存,其控制字段被更新为已分配状态:前4字节存储第二块内存的起始地址;后4字节则指向内存池管理结构体。这一状态标识表明该块内存已被占用,不再是空闲块。对于剩余的第二块内存,其前4字节更新为指向第三块内存(即初始化时的最后一块边界内存),从而继续保持单向链表的连贯性;其后4字节则存储 *TX_BYTE_BLOCK_FREE* 标志值,明确标识该块仍为空闲内存块。 ```{image} images/image_CMjRbs5Y3oJ1zjxv7Bocr1XGnue.webp :width: 5312px :height: 789px :align: center ``` #### 释放 内存释放时,将释放内存的首地址减8,得到内存块控制字段的首地址,再通过前4字节找到内存池管理结构体的起始地址,即可执行内存释放操作。释放后,控制字段的后4字节(偏移4~7)存储 *TX_BYTE_BLOCK_FREE*,标记该块为空闲内存块。 释放后,即使相邻的两个内存块地址连续,系统也不会立即进行合并或内存整理。等到申请内存时,若请求大小大于当前块的大小,系统会继续查找下一块。如果发现当前块与下一块地址连续,则将两者合并为一块。 释放内存后,系统会检查挂起链表中是否有线程等待。若有,则尝试分配内存并恢复线程执行。线程按FIFO顺序恢复,而非按照线程优先级高低。如需改变此行为,可在释放内存前调用 *tx_byte_pool_prioritize()*,将最高优先级线程移动到挂起链表最前面,使其优先被恢复。 #### 碎片处理 内存在多次分配和释放后,可能会出现大量小的内存块,这种现象称为内存碎片化。 当需要分配较大的内存块时,每次可能需要先遍历大量的小内存块,这会导致查找开销增加,算法性能下降。由于每个内存块都占用8个字节的控制字段,大量小内存会导致内存的浪费。在查找过程中,若发现两个相邻内存块的地址连续,则将其合并为一个内存块,这个过程称为内存整理。 下图为多次内存分配和释放后的结构。其中,第一块已被占用,第二块、第三块、第四块为空闲内存块。第二块大小为64字节,第三块大小为64字节,第四块内存大小为256字节。假设现在申请分配的内存大小为128字节。在分配查找过程中,发现第二块太小无法满足,但第二块与第三块地址连续,于是将两者合并为一块。合并后的块大小正好满足请求(64+64=128),系统将其返回给应用程序,如下面第二个图所示。 ```{image} images/image_InMRbDFouoqFQlxaFDJc2QSTnAg.webp :width: 3885px :height: 1079px :align: center ``` ### FreeRTOS FreeRTOS提供5种内存分配实现,开发者可按需求选择其一,或采用自定义方案。这5种实现分别对应源码中的heap_1.c、heap_2.c、heap_3.c、heap_4.c和heap_5.c。各实现的适用场景见下文。为便于理解其核心机制,文中给出的伪代码仅用于说明实现思路,不等同于完整工程代码且不可运行。 heap_1.c: ```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; } ``` ```{image} images/image_Kla9bIp1eoLmSbxiJOZcrow1nM5.webp :width: 1789px :height: 822px :align: center ``` heap_2 ```c // 空闲块链表节点 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() ``` ```{image} images/image_HaBpbtkA2opHh1xr15Wca7s5nfP.webp :width: 1781px :height: 844px :align: center ``` heap_3:其实就pvPortMalloc定向标准库已经做好的malloc和free函数,底层就不用像heap_1和heap_2一样进行实现。 ```c 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函数。 ```c 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)算法,每次从头开始查找空闲内存块,找到大小合适的内存块后,返回成功。 通过一个链表维护未分配的内存。链表节点定义如下: ```c 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中申请指定大小的内存。当前工程中的适配实现会对成功申请到的内存做清零初始化。 - **函数原型** ```c void* qosa_malloc(qosa_size_t nbytes) ``` - **参数说明** | **参数名** | **输入/输出** | **类型** | **说明** | | --- | --- | --- | --- | | *nbytes* | 输入 | qosa_size_t | 需要申请的内存大小。单位:字节 | - **返回值说明** 非空指针:申请成功,返回可用内存首地址 *QOSA_NULL*:申请失败,通常表示heap空间不足 ```{note} 1. 当前工程里的 *qosa_malloc()* 行为更接近“malloc + memset(0)”。 2. 申请成功后,必须在不再使用时调用 *qosa_free()* 释放。 3. 不应使用 *qosa_free()* 释放静态内存、栈内存或其他模块内部管理的内存。 ``` ### qosa_free - **功能描述** 释放通过 *qosa_malloc()*、*qosa_calloc()*、*qosa_realloc()*、*qosa_strdup()* 等接口获得的heap内存。 - **函数原型** ```c void qosa_free(void* ptr) ``` - **参数说明** | **参数名** | **输入/输出** | **类型** | **说明** | | --- | --- | --- | --- | | *ptr* | 输入 | void* | 待释放的内存指针 | - **返回值说明** 无 ### qosa_realloc - **功能描述** 调整一块已申请heap内存的大小,可用于扩容或缩容。 - **函数原型** ```c 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内存,并将申请到的内存全部清零。 - **函数原型** ```c 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中生成新的字符串副本。 - **函数原型** ```c char* qosa_strdup(const char* s) ``` - **参数说明** | **参数名** | **输入/输出** | **类型** | **说明** | | --- | --- | --- | --- | | *s* | 输入 | const char* | 源字符串指针 | - **返回值说明** 非空指针:复制成功,返回新字符串地址 *QOSA_NULL*:输入为空或内存申请失败 # 示例代码 完整示例代码请查看https://github.com/UniRTOS/UniRTOS-Doc-Examples/blob/main/system/memory/memory.c # 常见问题 ## 如何避免内存碎片? 内存管理算法可在一定程度上减少内存碎片,但无法完全避免。建议采取以下措施: - 尽量使用栈空间,减少动态内存分配函数的调用。 - 分配内存和释放内存尽量在同一个函数中完成。 - 尽量一次性申请较大的内存块,避免反复申请小内存,以减少内存分割。 - 自行设计内存池来管理内存。 ## 堆安全剩余量 虽然堆空间是动态分配和释放的,但仍需保证有足够的剩余空间,以满足复杂业务场景下的内存申请需求。堆空间的总量由底层决定,固件生成后该值即确定。 ## 内存泄露 常见内存泄漏的场景有: - 分配和释放内存的操作不成对。例如,业务开始时申请了内存,但在业务结束时未释放,下次业务运行时又重新申请。如此反复,导致可用内存不断减少,最终可能耗尽整个内存空间,使程序无法申请到内存而崩溃。 - 重复创建线程。底层或应用层创建线程和删除线程的操作不成对,导致相同业务的线程被重复创建。每创建一个线程都需要从堆中分配栈空间,最终将耗尽整个堆空间。 ## 内存越界 常见内存越界的场景有: - 数组越界:操作数组时,索引超过了实际分配的数组大小,导致访问越界到其他内存空间,可能破坏这些空间中的数据,从而引发系统异常。 - 栈溢出:创建线程时传入的栈空间大小小于业务实际运行所需空间,导致运行过程中数据操作越界到其他内存空间,可能破坏这些空间中的数据,从而引发系统异常。 ## 内存申请失败异常信息 底层内存申请失败通常会触发系统崩溃异常。