大家好,我是隔壁小王,前面我给大家讲了数据结构中的线性表,因为是第一次写作,可能很多地方描述得不清楚,真是惭愧至极啊!这两天我花了不少时间来学习如何写作,也看了不少公众号文章,今天我来讲讲线性表的应用,加深大家对线性表的应用。
不知道大家在电脑卡顿时会不会有这样的情况,突然变得很卡,一般解决办法就是关闭一些不必要的软件或者更换更大的内存条来改善。关闭软件我们不讨论,我们来讨论下这个内存。大家有没有想过内存插上之后,OS(Operatin System)是怎么管理内存的,要知道,内存条就是一个大仓库,要是没有仓库管理人员,以及管理方法仓库会有多么的混乱。
由此引出我们今天的主题——“内存管理”,什么是内存管理,即内存管理是指软件运行时对计算机内存资源的分配和使用的技术。内存管理有很多方法,我们讨论比较简单的也适合移植到单片机系统上的分块式内存管理。
不难发现,分块式内存管理由两部分组成,内存池——储存数据的地方,内存池被分成若干个块。内存管理表——用于管理内存的分配释放,管理表也被分成若干项,每一项指像分好块的内存块的首地址,讲到这里,有没有发现和我们上次讲的线性表很相识啦,内存块就是线性表中的数据项,管理表就是线性表中的索引,唯一的区别就在于内存管理的数据是指针,指向内存池。我们通过管理表来访问内存池。
假设内存池大小为32Byte,现在我们需要65KiloByte的内存,首先我们用65 / 32 = 2余1,需要两块内存,但是余数也是需要内存的,所以还需要多分配一块内存,所以总共需要3块内存,然后算法会从末尾往前面找3块连续的内存出来,接下来,是重要的一步,总得让其他应用知道,这3块内存是我的,谁也不能抢对吧!
所以我们要对这3块内存对应的管理表做标记,并且每一块都要标记
所以,就变成了下面这个亚子
这样,应用就分配到了3块内存,如果这个应用不用了,要释放内存怎么办,简单!!直接修改管理表就可以了,真是渣男!在内存池上留下这么多足迹,结果改一下管理表就不管不顾了!
现在原理基本讲完了,大家应该很清楚了吧!下面就是code time!(代码时间)
#define MEM_BLOCK_SIZE 32 //内存块大小(Byte)#define MEM_MAX_SIZE 1 * 1024 //内存池大小(KiloByte)#define MEM_TABLE_SIZE MEM_MAX_SIZE / MEM_BLOCK_SIZE //内存管理表大小(Byte)static uint8_t membase[MEM_MAX_SIZE];static uint16_t memmap[MEM_TABLE_SIZE];typedef struct {void (*init)(void); //内存初始化uint32_t (*perused)(void); //内存使用量uint8_t* base; //内存池uint16_t* map; //内存管理表uint8_t ready; //内存就绪}malloc_t;
先是对定义内存池的相关参数,为了方便后面观察,这里我们管理1kb的内存,大家不要小看这1kb,在单片机系统里面1kb能储存1024b数据呢,哈哈哈!
我们定义了一个内存池和内存管理表,并且定义了内存管理的结构体。
void mem_init(void); void Memset(void* dst, uint8_t c, uint32_t size); void* Memcpy(void* dst, void* src, uint32_t size); uint8_t mem_free(uint32_t ptr); uint32_t get_mem_perused(void); uint32_t mem_malloc(uint32_t count);void Free(void* ptr); void* Malloc(uint32_t size); void* Realloc(void* ptr, uint32_t size)
然后是相关的函数,其实用户可以用到的就只有3个Malloc,Free,Realloc。
在使用内存之前我们要先初始化内存池和管理表
void mem_init(void) { Memset(membase, 0, MEM_MAX_SIZE); Memset(memmap, 0, MEM_TABLE_SIZE * 2); malloc_dev.ready = 1;}
这样,内存池被打扫得干干净净,管理表也准备开始工作
当我们调用Malloc的时候,会放生什么呢?
程序会去访问内存管理表,并且找出连续可用的内存块出来,这里我并没有直接返回对应内存池的地址,而是返回一个相对地址偏移量,这是为什么呢?
因为,这个偏移量是相对于地址0x00000000开始的,但是我们的内存是的起始地址可能不是0x00000000,有可能是0x05201314,所以真实地址还得加上内存池的起始地址,从程序的模块化来说,我把这两个操作分开了。
uint32_t mem_malloc(uint32_t size){ uint32_t offset; //地址偏移量 uint32_t mem_block; //需要的内存块数 uint32_t mem_find = 0,i; //以找到的内存块数 if (size == 0) return 0xFFFFFFFF; mem_block = size / MEM_BLOCK_SIZE; if (size % MEM_BLOCK_SIZE) mem_block++; for (offset = MEM_BLOCK_SIZE - 1; offset > 0; offset--) { if(!malloc_dev.map[offset]) mem_find++; else mem_find = 0; if (mem_find == mem_block){ for (i = 0; i < mem_find; i++){ malloc_dev.map[offset + i] = (uint16_t)mem_find; //标记内存 } return offset * MEM_BLOCK_SIZE; }}return 0xFFFFFFFF;}
当我们获取到了相对偏移地址之后,咱们加上起始地址,是不是就得到了对应在内存池中的地址啦?很简单吧!
void* Malloc(uint32_t size) { uint32_t offset; offset = mem_malloc(size); if (offset == 0xFFFFFFFF) return NULL; return (void*)(malloc_dev.base + offset);}
申请内存是标记内存管理表,那释放内存不就是申请的逆过程嘛,所以我们只要找到需要释放的内存对应的管理表,将标记取消,拍拍屁股走人,就对了,因为没有清空内存池中的数据,所以,大家在使用指针申请内存的时候有没有发现,如果打印的话,会有随机值,虽然在一开始我们初始化了内存池,它特别干净,但是经过一段时间得运行,应用A使用一点,应用B也使用一点,释放的时候也没有去清空内存池,导致内存池中的数据是随机的,这就是上一个应用不负责任的后果,让下一个应用当老实人,要是不注意的话,这盘,哼哼哼哼。。。。
uint8_t mem_free(uint32_t ptr) { uint32_t i; if (!malloc_dev.ready) { malloc_dev.init(); return 1; } if (ptr < MEM_MAX_SIZE) { uint32_t index = ptr / MEM_BLOCK_SIZE; uint32_t mem_block = malloc_dev.map[index]; for (i = 0; i < mem_block; i++) { malloc_dev.map[index + i] = 0; } return 0;}return 2;}
代码很简单,就是从管理表找到内存块的数量,然后依次清零,它的父级函数
void Free(void* ptr) {uint32_t offset;if (ptr == NULL) return; offset = (uint32_t)ptr - (uint32_t)malloc_dev.base; mem_free(offset);}
我们从当前地址去找到管理表的偏移量,然后在调用子级函数做其他处理
好啦,现在内存管理的重要函数我们已经讲完了,接下来我们就来测试下吧
int main () { malloc_dev.init(); char* a = (char*)Malloc(sizeof(char)*1); printf("a address:0x%p", a); int i; printf("Memory manahement table(dir--->)(size:%d):", MEM_TABLE_SIZE); for (i = 0; i < 32; i++) { printf("%d ", malloc_dev.map[i]); } printf(""); printf("a content:"); strcpy(a, "I Love You"); printf("%s", a); printf("used:%.2f", malloc_dev.perused()/1000.0); Free(a); printf("used:%.2f", malloc_dev.perused()/1000.0); return 0;}
下面是运行结果
从图中可以看到,变量a获取到的内存地址,与我们在malloc里面通过相对偏移加上起始地址之后的地址是一致的,并且通过内存管理表可以看到占用的内存块的个数,以及使用率。
这里不知道大家有没有发现一个问题,我们申请一个char类型的内存,也就是一个字节,但是系统却返回给我们的是一个内存块!而这里我们内存块的单位是32字节,也就是说我们还有31字节没有使用到,资源浪费太多,但是如果我们缩小内存块的单位,势必管理表的数量又会增加,在每次分配或者释放的时候,照成时间延时比较多,所以如何权衡内存块的最小值,是一个难题。
但是我们还有其他更优秀的算法来弥补这个分块管理的缺陷,这里我就不做过多的讨论了,在单片机系统里面,我们对内存没有很严格的要求,所以这个算法也就被经常使用了,因为单片机系统往往主频小,运行速度比不上PC,像红黑树那些算法跑起来需要较多的时间。
同时,我也在公众号:GeEes
关注并回复:002
或者关注头条号并私信:002
都可以获得本文的参考资料和全部内容!
非常感谢你的关注!