内存管理整体结构
内存管理其实是对一片连续的内存资源进行分配、释放、查找、回收,让多个任务按需使用内存。
堆(heap):链接脚本划定一块连续的RAM,用来动态分配内存。
块(block):堆被切分为一个个独立的单元,每个单元为一个块。
整体内存布局如下图:
每块都是8字节块头与用户空间,物理上相邻无间隙,块头8字节是元数据,用户空间是实际载荷。
已分配块布局如下:
next闲置4字节,空闲时是next指针指向free_list链表,已分配时候也是占位不用做链表。
链接脚本分配堆大小:
._user_heap_stack :
{
. = ALIGN(8);
_sheap = .;
PROVIDE ( end = . ); /* libc 用 end 符号表示堆起点 */
PROVIDE ( _end = . );
. = ORIGIN(RAM) + LENGTH(RAM) - 0x400; /* 留 1KB 给 MSP */
_eheap = .;
. = ALIGN(8);
} >RAM
初始化堆空间,堆大小即链接脚本中定义的_eheap - _sheap字节的头。初始化时整个堆空间作为一个空闲块。
块头中包含next指针与size块大小,其中32位机器下size_t是32位的,使用0-30位用于表示块大小,第31位表示是否已经分配,为1则已分配,为0则未分配,此块头大小为8字节,后面的有效载荷也需要8字节对齐,定义如下:
typedef struct block { struct block *next; /* 空闲链表 next (已分配时被用户数据覆盖) */ size_t size; /* 整块大小(含头), bit31=1 表示已分配 */ } block_t;定义单个实例堆的全部状态,定义如下:
typedef struct { block_t *free_list; // 空闲链表头 size_t total; // 堆总大小 size_t used; // 当前已分配 size_t min_free; // 历史最低剩余 uint8_t inited; // 初始化门控 } heap_t;其中:
(1)free_list:空闲块挂载到空闲链表。
(2)total:堆的总大小,即_eheap - _sheap。
(3)used:当前已经分配的堆大小。
(4)min_free:历史最低剩余堆大小。
(5)inited:初始化开关,防止重复初始化。
kmalloc
kmalloc通过size大小进行分配内存,必须要求8字节对齐,这里是因为32位机需要4字节对齐,64位机需要8字节对齐,因此为了兼容全部要求8字节对齐,否则会触发HardFault导致系统崩溃。
这里要求的8字节对齐,头结构已经8字节对齐,因此用户空间分配的大小也应该8字节对齐,并且需要向上对齐。
对齐方法如下:
#define MM_ALIGN (8U) #define ALIGN_MASK (MM_ALIGN - 1) #define ALIGN_UP(n) ((size_t)(n) + ALIGN_MASK) & ~(size_t)ALIGN_MASK)将传入的n即size大小加上对齐字节减1(这是为了保证向上对齐,否则内存会不够),并将对应的低三位清零。
例如:16 16 + (8 - 1) = 23 = 0001 0111 0001 0111 & 1111 1000 = 0010 0000 = 16
18 18 + (8 - 1) = 25 = 0001 1001 0001 1001 & 1111 1000 = 0001 1000 = 24
因此申请的堆空间大小为当前向上对齐8字节的size + 头的总大小。同时为了防止多线程申请,因此申请堆空间过程中需要关闭中断。
分配内存时分为以下两种情况:
(1)按照当前大小分配后块的剩余空间小于16字节,不足以在分配下一个内存,因此直接将当前内存全部给此次分配。
由于使用的单向链表,因此判断该分配块的前一块是否存在,若存在则直接通过前一空闲块链接到下一空闲块,若不存在,则证明当前就是链表头,直接将后面的空闲区域链接到空闲链表。并置31位为1。
(2)若分配后大于或等于16字节,就将当前分割后剩下的块添加到空闲链表等待下一次分配使用。
计算分割后剩余的大小以及位置,同样按照上述判断进行合并空闲区域。并置31位为1。
例如:目前存在一块内存48字节,头部占8字节,用户申请字节32字节,图示
由于后面的空闲在切割后还剩下16字节,因此将其添加到空闲链表等待下一次使用,申请后的内存布局如下:
若块大小还是48字节,若need=40字节,那么会导致剩余空闲空间不足16字节,因此需要全部分配给当前用户,如下:
这里要求大小必须大于16字节是因为块头8字节对齐,用户数据8字节对齐,加在一起至少需要16字节。
kfree
kfree通过传入申请的内存地址进行释放内存。
此处做了三重防护:
(1)保证传入的指针在有效范围,即_sheep到_eheap
(2)必须8字节对齐,块头地址 & 0x7
(3)判断是否已经清理,防止二次释放,通过bit31是否为0判断
实现步骤:
(1)关中断,计算实际块大小,清除标志
(2)找到第一个比用户空间块大的块,这个就是用户block的后继,这个用户空间的前驱指向prev,prev -> block -> cur
(3)判断block的尾部恰好是cur的头部,则这两块内存可合并,向后合并
(4)判断prev的尾部是否为block的头部,若是则两块可合并,向前合并。目的是为了解决碎片化内存
注:这里必须先进行后合并,在进行前合并