[ GD32开发日记 ] 开辟基于SDRAM的内存分配系统
目录
最近在开发GD32开发板的时候出现了一个问题:内存分配问题;
由于我最近在嵌入式的GD32开发板上布署深度学习的环境,因此内存是十分宝贵的资源。
众所周知,GD32开发板的内存有两个部分组成:
- CPU片内内存;
- SDRAM静态缓存;
其中,CPU片内内存少的离谱,最多不超过4KB,这部分的内存主要是存储全局变量等基本内容。如果指望这部分内存来保存深度学习的中间参数,是极其不现实的。
malloc 函数问题😿😿
然而,悲伤的是:GD32的C语言环境中,原生的malloc函数位于<stdio.h>库函数,这个函数默认分配的是片内内存!
这时候,我把目光投向 SDRAM, 这部分的内存可是有足足256KB大小,保存一张图片的中间参数的内容还是绰绰有余的。
GD32 环境问题😿😿
然而,更悲伤的是:GD32 原生库中,存在对于SDRAM的访问函数,在exmc_sdram.h中有定义(如下所示):
void sdram_writebuffer_8
(uint32_t sdram_device, uint8_t *pbuffer, uint32_t writeaddr, uint32_t numbytetowrite)
{
uint32_t temp_addr;
/* select the base address according to EXMC_Bank */
if(sdram_device == EXMC_SDRAM_DEVICE0) {
temp_addr = SDRAM_DEVICE0_ADDR;
} else {
temp_addr = SDRAM_DEVICE1_ADDR;
}
/* while there is data to write */
for(; numbytetowrite != 0; numbytetowrite--) {
/* transfer data to the memory */
*(uint8_t *)(temp_addr + writeaddr) = *pbuffer++;
/* increment the address */
writeaddr += 1;
}
}
void sdram_readbuffer_8(uint32_t sdram_device, uint8_t *pbuffer, uint32_t readaddr, uint32_t numbytetoread)
{
uint32_t temp_addr;
/* select the base address according to EXMC_Bank */
if(sdram_device == EXMC_SDRAM_DEVICE0) {
temp_addr = SDRAM_DEVICE0_ADDR;
} else {
temp_addr = SDRAM_DEVICE1_ADDR;
}
/* while there is data to read */
for(; numbytetoread != 0; numbytetoread--) {
/* read a byte from the memory */
*pbuffer++ = *(uint8_t *)(temp_addr + readaddr);
/* increment the address */
readaddr += 1;
}
}
这个函数基本无法直接使用,实在太简陋了。
但是上述代码提供了一些十分关键的信息:
GD32 示例代码的关键信息👌👌
1. 访问SDRAM也是通过一个具体地址;
2. 访问SDRAM也是通过指针来进行交互;
3. 最小单位为byte!
4. SDRAM的起始地址存储在SDRAM_DEVICE0_ADDR中;
查询SDRAM_DEVICE0_ADDR后得知起始地址为如下所示:
#define SDRAM_DEVICE0_ADDR ((uint32_t)0xC0000000)
#define SDRAM_DEVICE1_ADDR ((uint32_t)0xD0000000)
SDRAM_DEVICE1_ADDR 不能用,没有这个设备
得到这些关键信息,就可以开始设计自己的malloc函数了;
基于SDRAM的内存分配的基本思想和细节😼😼
基于SDRAM的内存分配的基本思想和细节😼😼
勘误:此办法设计的内存系统存在重大bug(后续文章已改正)👌
这个系统的本质是建立一个管理内存的双向链表结构。
解释:
~~1. 采用链表,是为了方便插入和删除节点。2. 采用双向链表,为了方便进行内存碎片的合并。
勘误:
按照上述思想设计的系统,可能导致以下几个问题:
- 链表头结点链表过长:
- 链表头结点大小过大。
- 由于内存的大小并不是一个固定的量,因此可能存在很小的内存碎片也需要分配一个头结点这种情况存在,因此会导致链表头结点撑爆内存。
- 回收链表操作十分复杂:
- 双向链表回收节点时本身操作的复杂度就很高。
- 如果按照原文中的代码来实现所需操作时,节点融合的时候,头结点只是逻辑上被删除,但是实际上还是用不了,占用了空间,这些空间无法释放,导致僵尸空间了。
SDRAM应该分配为两块空间:
1. 链表头节点;
2. 具体数据;
实际解决问题的内存结构:
头+身子结构。
即需要分配内存时,分配一整块内存,同时把内存中前几个字节作为头结点,该部分利用链表结构链接起来,每次需要访问时,都访问该头结点地址+offset的位置,利用该办法,极大简化了分配和销毁操作,同时节省了头结点的内存空间。
malloc函数的链表节点结构:
typedef struct s_block
{
t_block next ; /* 指向下个块的指针 */
uint32_t free; // 标记位置
uint32_t dsize; // 块大小,最大为256KB
}s_block ,* t_block;
每个头节点,记录了
- 下一个块指针;
- 数据块的大小;
- 空闲位;
malloc操作:
初始化,分配,释放,分裂,合并,读取,写入
初始化
void init_block(){ // 初始化
uint32_t limit_index , start_index;
uint32_t Cur_block_index;
Cur_block_index = MALLOC_LISTDATA_START_INDEX;
first_block = (t_block)(Cur_block_index);
first_block->next = NULL;
first_block->dsize = (uint32_t)0x00040000 - (uint32_t)sizeof(s_block); // 256KB - 头节点
first_block->free = 1;
}
初始化工作:
- 初始化一个first_block的指针,指向一个具体地址;
- dsize大小由于未分配内存,因此为总的内存空间;
- 后向指针为null
- 是否空闲为1空闲
分配函数:my_malloc函数
分配一块空间包括以下操作:
- 访问first_block,遍历链表;
- 找到第一个大小符合要求的块;
- 把这个块的大小改为所需size大小,如果还有剩余空间,则分裂这个块为两个块;
- 返回地址指针;
如果没有适配地块,则返回error,空间不足。
my_malloc函数
void * my_malloc ( uint32_t size ) { //byte 为单位
t_block b , last ;
uint32_t s = size ;// byte为单位
if ( first_block ) { // 判断first_block是否被初始化
/* 查找合适的block */
last = first_block ;
b = find_block ( &last , s ) ;
if ( b ) {
/* 如果可以,则分裂 */
if ( ( b -> dsize - s ) > 0 ) // 分配完成后有剩余空间
split_block ( b , s ) ;
b -> free = 0 ;
} else {
/* 没有合适的block,错误 */
return NULL;
}
}else{ // 没有初始化
init_block();
return my_malloc(s);
}
return (void *)( (uint32_t) b + (uint32_t)sizeof(s_block)) ; // 返回头节点 + 头节点大小的地址
}
分裂操作split_block函数
生成一个block指针,把剩余的大小转移到这个block中,把链表连起来,放到分裂的块的后面。
void split_block ( t_block b , uint32_t s ) {
t_block new ;
new = (t_block)((uint32_t) b + s + sizeof(s_block)); // 将指针指向新地址
// Cur_block_index += (sizeof(s_block) + s); // 新的地址为 t_block 大小 + 块大小
new->next = b->next;
b->next = new;
new -> dsize = (b -> dsize) - s - sizeof(s_block) ; // new的大小 = 原块大小 - 已分配大小 - new头节点大小
new -> free = 1 ;
// printf( "new size is %x , b->dsize %x\n" ,new -> dsize , b -> dsize);
b -> dsize = s ; // 更新b的大小
}
查找目标块的操作函数
t_block find_block ( t_block * last , uint32_t size ) { // 寻找目标模块和目标模块前一个模块
t_block b = first_block ;
while ( b && ! ( b -> free && b -> dsize >= size ) ) {
*last = b ;
b = b -> next ;
}
return b ;
}
释放空间操作myfree
根据提供的指针,查到链表中具体的块,把这个块前后的情况分析一下:
有free就合并;无free就保持不变;
无 free 就把这个块的free标记为置为1;
void my_free ( void * p ) {
t_block b , pre , next;
if ( valid_addr ( p ) ) {
b = (t_block)( ( (uint32_t)p ) - (uint32_t)sizeof(s_block) ) ;
if(!b){
printf("free error\n");
}
b -> free = 1 ;
pre = get_block(b);
next = b -> next;
while( (next && next->free) || (pre && pre->free) ){
if (pre && pre->free){
// printf("left free success\n");
b = left_fusion(pre);
}
if (next && next->free){
b -> dsize += next -> dsize + sizeof(s_block) ;
b -> next = b -> next -> next ;
// printf("right free success\n ");
}
next = b->next;
pre = get_block(b);
}
}
}
合并函数
t_block left_fusion ( t_block b ) {
if ( b -> next && b -> next -> free ) {
b -> dsize += b -> next -> dsize + sizeof(s_block); // 加上下一个块大小 以及一个头节点
b -> next = b -> next -> next ;
}
return b ;
}
访问/写入
本质上利用指针的访问方法就可以直接访问,例如下述代码:
int * a;
a = (int *) my_malloc ( sizeof ( int ));
*a = 1;
结语
在现代计算机系统中,内存分配系统是一个非常重要的组成部分,负责管理计算机内存的分配和释放。本文讨论了开辟内存分配系统的过程,并介绍了一些流行的内存分配算法和数据结构。
我从内存分配系统的需求出发,设计了一个简单的内存分配器。我还讨论了内存分配系统中的一些挑战,如内存碎片和并发访问问题,并提出了一些解决方案。在实践中,内存分配系统的设计和实现需要综合考虑多方面因素,包括性能、可扩展性、可靠性和安全性等。
总之,内存分配系统是计算机系统中的一个至关重要的部分,其质量和性能直接影响到整个系统的表现。通过本文所提供的信息和工具,希望读者能够更好地了解内存分配系统的工作原理,并在实践中设计和实现高效的内存分配器。
到此,整个内存分配系统就已经完成了。可以正常在GD32开发板上运行。点个赞把👍👍👍
勘误完成