单片机内存管理剖析
一、概述
在单片机系统中,内存资源通常是有限的,因此高效的内存管理至关重要。合理地分配和使用内存可以提高系统的性能和稳定性,避免内存泄漏和碎片化问题。单片机的内存主要包括程序存储器(如 Flash)和数据存储器(如 RAM),其中数据存储器又可进一步分为静态数据区、栈区和堆区。动态内存分配主要发生在堆区,而 sbrk
、malloc
和 free
这三个函数 / 系统调用在堆内存管理中起着关键作用。
二、sbrk
:底层的内存边界调整
2.1 原理
sbrk
是一个底层的系统调用(在某些单片机库中也有对应的实现),其核心功能是调整进程数据段的结束地址,也就是 break
指针。通过改变 break
指针的位置,可以实现堆内存的扩展和收缩。当传入一个正的增量值时,break
指针向后移动,堆内存得到扩展;当传入一个负的增量值时,break
指针向前移动,堆内存被收缩。
2.2 源码示例与解释
#include <stdint.h>
#include <errno.h>
// 假设这是链接脚本定义的堆起始和结束地址
extern char _end[];
extern char _heap_end[];
// 当前堆指针
static char *curbrk = _end;
// sbrk 函数实现
void *_sbrk(int incr) {
char *old_brk = curbrk;
char *new_brk = curbrk + incr;
// 边界检查
if (new_brk < _end || new_brk > _heap_end) {
errno = ENOMEM; // 设置错误号表示内存不足
return (void *)-1;
}
curbrk = new_brk;
return (void *)old_brk;
}
- 全局变量
_end
:由链接脚本确定,代表堆的起始地址。_heap_end
:同样由链接脚本确定,代表堆的最大可用地址。curbrk
:静态变量,记录当前堆的结束地址,初始化为_end
。
- 函数逻辑
- 保存当前的
curbrk
到old_brk
中,这将作为函数的返回值。 - 根据传入的
incr
计算新的堆结束地址new_brk
。 - 进行边界检查,确保
new_brk
在合法范围内(不小于_end
且不大于_heap_end
)。如果超出范围,设置errno
为ENOMEM
并返回(void *)-1
表示内存分配失败。 - 如果边界检查通过,更新
curbrk
为new_brk
,并返回old_brk
,它指向新分配内存的起始位置。
- 保存当前的
2.3 使用场景和注意事项
- 使用场景:
sbrk
通常作为底层的内存分配原语,为更高级的内存分配函数(如malloc
)提供支持。在一些简单的单片机应用中,如果只需要简单的内存扩展和收缩操作,也可以直接使用sbrk
。 - 注意事项
- 由于
sbrk
直接操作堆的边界,使用不当可能会导致内存越界访问,破坏其他重要的数据。 sbrk
分配的内存是连续的,频繁的扩展和收缩操作可能会导致内存碎片化,降低内存的利用率。
- 由于
三、malloc
:用户级的动态内存分配
3.1 原理
malloc
是 C 标准库中提供的用于动态内存分配的函数,它建立在 sbrk
的基础之上。malloc
函数的主要任务是根据用户请求的内存大小,在堆中找到合适的空闲内存块并返回其起始地址。为了管理堆中的空闲内存,malloc
通常会维护一个空闲块链表,使用不同的分配策略(如首次适配、最佳适配等)来查找合适的空闲块。
3.2 源码示例与解释
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
// 内存块结构体
typedef struct mem_block {
size_t size;
int is_free;
struct mem_block *next;
} MemBlock;
// 空闲链表头指针
static MemBlock *free_list = NULL;
// 分配内存
void *malloc(size_t size) {
MemBlock *current = free_list;
MemBlock *prev = NULL;
// 查找合适的空闲块
while (current != NULL) {
if (current->is_free && current->size >= size) {
current->is_free = 0;
// 如果空闲块比需求大,分割空闲块
if (current->size > size + sizeof(MemBlock)) {
MemBlock *new_free_block = (MemBlock *)((char *)current + sizeof(MemBlock) + size);
new_free_block->size = current->size - size - sizeof(MemBlock);
new_free_block->is_free = 1;
new_free_block->next = current->next;
current->size = size;
current->next = new_free_block;
}
return (void *)(current + 1);
}
prev = current;
current = current->next;
}
// 没有合适的空闲块,调用 sbrk 扩展堆
size_t total_size = size + sizeof(MemBlock);
MemBlock *new_block = (MemBlock *)_sbrk(total_size);
if (new_block == (MemBlock *)-1) {
return NULL;
}
new_block->size = size;
new_block->is_free = 0;
new_block->next = NULL;
if (prev != NULL) {
prev->next = new_block;
} else {
free_list = new_block;
}
return (void *)(new_block + 1);
}
-
数据结构
-
MemBlock
结构体:用于表示堆中的内存块,包含三个成员:
size
:记录内存块的大小。is_free
:标记该内存块是否空闲。next
:指向下一个内存块的指针,用于构建空闲块链表。
-
free_list
:指向空闲块链表的头指针,初始化为NULL
。
-
-
函数逻辑
- 查找空闲块:遍历空闲块链表
free_list
,使用首次适配策略查找第一个大小足够的空闲块。 - 分割空闲块:如果找到的空闲块比请求的大小大,将其分割为两部分:一部分用于满足当前请求,另一部分作为新的空闲块插入到链表中。
- 扩展堆:如果在空闲块链表中没有找到合适的空闲块,调用
sbrk
函数扩展堆空间,分配一块新的内存,并将其初始化为一个新的内存块。 - 返回内存地址:返回分配的内存块的起始地址(跳过
MemBlock
结构体部分)。
- 查找空闲块:遍历空闲块链表
四、free
:动态内存的释放
4.1 原理
free
函数用于释放 malloc
、calloc
或 realloc
分配的内存块。当调用 free
时,它会将指定的内存块标记为空闲,并尝试合并相邻的空闲块,以减少内存碎片化。
4.2 源码示例与解释
// 释放内存
void free(void *ptr) {
if (ptr == NULL) return;
// 获取内存块头部
MemBlock *block = (MemBlock *)ptr - 1;
block->is_free = 1;
// 合并相邻的空闲块
MemBlock *current = free_list;
MemBlock *prev = NULL;
// 找到合适的插入位置
while (current != NULL && current < block) {
prev = current;
current = current->next;
}
// 合并前一个空闲块
if (prev != NULL && prev->is_free) {
prev->size += block->size + sizeof(MemBlock);
prev->next = block->next;
block = prev;
}
// 合并后一个空闲块
if (current != NULL && current->is_free) {
block->size += current->size + sizeof(MemBlock);
block->next = current->next;
}
// 如果没有前一个块,更新空闲链表头
if (prev == NULL) {
free_list = block;
} else {
prev->next = block;
}
block->next = current;
}
- 函数逻辑
- 空指针检查:如果传入的指针
ptr
为NULL
,直接返回,不进行任何操作。 - 标记为空闲:通过指针计算得到内存块的头部信息(
MemBlock
结构体),将其is_free
标记设置为 1,表示该内存块已空闲。 - 合并相邻空闲块
- 遍历空闲块链表,找到合适的位置插入该空闲块。
- 检查前一个和后一个内存块是否空闲,如果是,则将它们合并成一个更大的空闲块。
- 更新空闲链表:根据合并结果更新空闲块链表的指针,确保链表的正确性。
- 空指针检查:如果传入的指针
4.3 使用场景和注意事项
- 使用场景:在不再需要使用动态分配的内存时,必须调用
free
函数释放内存,以避免内存泄漏。 - 注意事项
- 只能释放由
malloc
、calloc
或realloc
分配的内存,释放其他内存可能会导致未定义行为。 - 不要多次释放同一块内存,这会导致双重释放错误,可能会破坏内存管理数据结构。
- 只能释放由
sbrk
、malloc
和 free
是单片机内存管理中重要的工具,它们相互协作,实现了堆内存的动态分配和释放。sbrk
作为底层的系统调用,提供了基本的内存扩展和收缩功能;malloc
基于 sbrk
实现了用户级的动态内存分配接口,方便程序员在运行时分配所需的内存;free
则负责释放不再使用的内存,避免内存泄漏和碎片化。在实际应用中,需要合理使用这些函数,注意内存的分配和释放规则,以确保系统的稳定性和性能。