一、概述
1、malloc简介
函数所在头文件:<stdlib.h>
函数原型是:void *malloc (size_t n)
函数功能:在内存的动态存储区中分配一个长度为size的连续空间。其参数是一个无符号整形数,返回值是一个指向所分配的连续存储域的起始地址的指针。
2、malloc函数使用注意事项
- 申请了内存空间后,必须检查是否分配成功。
- 当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针指向NULL,防止程序后面不小心使用了它。
- malloc和free函数应该配对使用。如果申请后不释放,就是内存泄露;如果无故释放那就是什么也没有做。释放只能一次,如果释放两次及两次以上会出现错误。
- 越界使用动态分配的存储块,尤其是越界赋值,可能引起非常严重的后果,通常会破坏程序的运行系统,可能造成本程序或者整个计算机系统垮台。
3、本文的目的
一直很想亲手实现一个malloc函数,不仅仅是锻炼自己的编程能力,对指针的驾驭能力,而且也是为了解除自己对malloc函数的疑惑--为什么使用malloc函数有上述的注意事项?要想知道原因,只有自己查阅源码,或者自己编写功能类似的源码,而笔者选用了后者。
二、本程序实现机制
1、块控制头部
块控制头部结构体定义如下所示:
typedef struct{ unsigned char is_available; /* whether blcok is avaiable */ unsigned int prior_blocksize; /* size of prior block */ unsigned int current_blocksize; /* block size */ }mem_control_block;
每申请一个内存空间,无论大小(char、short、long)都将其命名为"Block"。在分配一个Block的时候,在堆区(heap)首先存放的是Block的控制块,然后才是有效的数据区。可以想象,假设我们申请一个char型大小的空间,实际上在堆区占据了“sizeof(char)+sizeof(mem_control_block)”。就是通过Block的头部控制块将堆区连成一个链表,通过这个链表可以遍历整个堆区的每一个块。
is_avilable标记内存块是否被分配,是否可用。prior_blocksize保存了前一个块的大小,current_blocksize保存了当前块的实际有效数据空间大小。有了current_blocksize,我们可以从前向后遍历整个堆区;而有了prior_blocksize,我们可以从后向前遍历整个堆区。也就是说,这个控制块头部使我们为堆区建立了一个双向链表,以便于管理。
2、malloc
调用malloc函数时,它沿连接表从前向后,寻找一个没有被占用的,而且大到足以满足用户请求所需要的内存块。在寻找过程中,分三种情况。
第一种情况,倘若可用而且大小刚好合适,那么直接将此块返回就完成了分配。
第二种情况,倘若可用而且超过了所需要的内存块并且还能容纳一个新块的控制头部,这个时候就将此块一分为二。将分配给用户的那块内存的is_available置0(不再可用),然后将此块的有效数据区首地址作为函数值返回。而新块的控制头部要进行赋值(只有含有头部的块才能称的上一个块),没有控制头部的块不能进行管理。
第三种情况,倘若可用内存块的空间小于需要的空间,或者虽然超过了但是不能腾出一个控制头部,最终的处理都是跳过改块,移动到链表的下一块进行新一轮的比较。
3、free
free函数用于内存块的回收,当不用的内存块调用此函数释放空间的时候,将is_available置1(可以重新分配)。接下来要做的就是整合内存碎片。由于可能多次malloc函数,产生了很多的内存碎片,在释放一个块的时候,不仅要标记此块为“可用”。另外,还需要试着将此块前后的块进行合并,这样才能为将来申请更大的空间做好准备。
4、malloc_init
初始化函数,主要是确定堆区的起始位置、大小和结束地址,也就是确定堆区的边界。我们申请的内存块必须在堆区内,不得超出边界,否则会修改其他的数据。
另外,还需要对堆区进行初始化,建立第一个内存块(初始化头部控制结构体)。显然,这个块是一个完整的块,也是一次能分配最大内存空间的块。之所以进行初始化,原因还是没有头部控制结构体的块无法进行管理。
三、代码及解释
1、malloc_init
1 /** 2 * @brief Initializes malloc's gloabl varialbe and head of heap memory 3 * @param None 4 * @retval None 5 */ 6 void malloc_init(void) 7 { 8 mem_control_block * mcb = NULL; 9 10 /* confirm heap's start address */ 11 managed_memory_start = (unsigned int)malloc_addr; 12 /* confirm heap's size */ 13 managed_memory_size = malloc_size; 14 /*confirm heap's end address */ 15 managed_memory_end = managed_memory_start + managed_memory_size; 16 17 /* make mcb point to heap's start address */ 18 mcb = (mem_control_block *)managed_memory_start; 19 /*the first blcok is avaialbe */ 20 mcb->is_available = 1; 21 /*there is no block before the first block */ 22 mcb->prior_blocksize = 0; 23 /*the first block's block size is difference of between heap's size and control block */ 24 mcb->current_blocksize = managed_memory_size - sizeof(mem_control_block); 25 /* Initialize done */ 26 has_initialized = 1; 27 }
2、malloc
1 /** 2 * @brief Dynamic distribute memory function 3 * @param numbytes: what size you need 4 * @retval a void pointer to the distribute first address 5 */ 6 void * malloc(unsigned int numbytes) 7 { 8 unsigned int current_location,otherbck_location; 9 /* This is the same as current_location, but cast to a memory_control_block */ 10 mem_control_block * current_location_mcb = NULL,* other_location_mcb = NULL; 11 /* varialbe for saving return value and be set to 0 until we find something suitable */ 12 void * memory_location = NULL; 13 /* current dividing block size */ 14 unsigned int process_blocksize; 15 16 /* Initialize if we haven't already done so */ 17 if(! has_initialized) { 18 malloc_init(); 19 } 20 21 /* Begin searching at the start of managed memory */ 22 current_location = managed_memory_start; 23 /* Keep going until we have searched all allocated space */ 24 while(current_location != managed_memory_end){ 25 /* current_location and current_location_mcb point to the same address. However, 26 * current_location_mcb is of the correct type, so we can use it as a struct. current_location 27 * is a void pointer so we can use it to calculate addresses. 28 */ 29 current_location_mcb = (mem_control_block *)current_location; 30 /* judge whether current block is avaiable */ 31 if(current_location_mcb->is_available){ 32 /* judge whether current block size exactly fit for the need */ 33 if((current_location_mcb->current_blocksize == numbytes)){ 34 /* It is no longer available */ 35 current_location_mcb->is_available = 0; 36 /* We own it */ 37 memory_location = (void *)(current_location + sizeof(mem_control_block)); 38 /* Leave the loop */ 39 break; 40 /* judge whether current block size is enough for dividing a new block */ 41 }else if(current_location_mcb->current_blocksize >= numbytes + sizeof(mem_control_block)){ 42 /* It is no longer available */ 43 current_location_mcb->is_available = 0; 44 /* because we will divide current blcok,before we changed current block size,we should 45 * save the integral size. 46 */ 47 process_blocksize = current_location_mcb->current_blocksize; 48 /* Now blcok size could be changed */ 49 current_location_mcb->current_blocksize = numbytes; 50 51 /* find the memory_control_block's head of remaining block and set parameter,block of no 52 * parameter can't be managed. 53 */ 54 other_location_mcb = (mem_control_block *)(current_location + numbytes \ 55 + sizeof(mem_control_block)); 56 /* the remaining block is still avaiable */ 57 other_location_mcb->is_available = 1; 58 /* of course,its prior block size is numbytes */ 59 other_location_mcb->prior_blocksize = numbytes; 60 /* its size should get small */ 61 other_location_mcb->current_blocksize = process_blocksize - numbytes \ 62 - sizeof(mem_control_block); 63 64 /* find the memory_control_block's head of block after current block and \ 65 * set parameter--prior_blocksize. 66 */ 67 otherbck_location = current_location + process_blocksize \ 68 + sizeof(mem_control_block); 69 /* We need check wehter this block is on the edge of managed memeory! */ 70 if(otherbck_location != managed_memory_end){ 71 other_location_mcb = (mem_control_block *)(otherbck_location); 72 /* its prior block size has changed! */ 73 other_location_mcb->prior_blocksize = process_blocksize\ 74 - numbytes - sizeof(mem_control_block); 75 } 76 /*We own the occupied block ,not remaining block */ 77 memory_location = (void *)(current_location + sizeof(mem_control_block)); 78 /* Leave the loop */ 79 break; 80 } 81 } 82 /* current block is unavaiable or block size is too small and move to next block*/ 83 current_location += current_location_mcb->current_blocksize \ 84 + sizeof(mem_control_block); 85 } 86 /* if we still don't have a valid location,we'll have to return NULL */ 87 if(memory_location == NULL) { 88 return NULL; 89 } 90 /* return the pointer */ 91 return memory_location; 92 }
此函数流程图
3、free
1 /** 2 * @brief free your unused block 3 * @param firstbyte: a pointer to first address of your unused block 4 * @retval None 5 */ 6 void free(void *firstbyte) 7 { 8 unsigned int current_location,otherbck_location; 9 mem_control_block * current_mcb = NULL,* next_mcb = NULL,* prior_mcb \ 10 = NULL,* other_mcb = NULL; 11 /* Backup from the given pointer to find the current block */ 12 current_location = (unsigned int)firstbyte - sizeof(mem_control_block); 13 current_mcb = (mem_control_block *)current_location; 14 /* Mark the block as being avaiable */ 15 current_mcb->is_available = 1; 16 17 /* find next block location */ 18 otherbck_location = current_location + sizeof(mem_control_block) \ 19 + current_mcb->current_blocksize; 20 /* We need check wehter this block is on the edge of managed memeory! */ 21 if(otherbck_location != managed_memory_end){ 22 /* point to next block */ 23 next_mcb = (mem_control_block *)otherbck_location; 24 /* We need check whether its next block is avaiable */ 25 if(next_mcb->is_available){ 26 /* Because its next block is also avaiable,we should merge blocks */ 27 current_mcb->current_blocksize = current_mcb->current_blocksize \ 28 + sizeof(mem_control_block) + next_mcb->current_blocksize; 29 30 /* We have merge two blocks,so we need change prior_blocksize of 31 * block after the two blocks,just find next block location. 32 */ 33 otherbck_location = current_location + sizeof(mem_control_block) \ 34 + current_mcb->current_blocksize; 35 /* We need check wehter this block is on the edge of managed memeory! */ 36 if(otherbck_location != managed_memory_end){ 37 other_mcb = (mem_control_block *)otherbck_location; 38 /* its prior block size has changed! */ 39 other_mcb->prior_blocksize = current_mcb->current_blocksize; 40 } 41 } 42 } 43 44 /* We need check wehter this block is on the edge of managed memeory! */ 45 if(current_location != managed_memory_start){ 46 /* point to prior block */ 47 prior_mcb = (mem_control_block *)(current_location - sizeof(mem_control_block)\ 48 - current_mcb->prior_blocksize); 49 /* We need check whether its prior block is avaiable */ 50 if(prior_mcb->is_available){ 51 /* Because its prior block is also avaiable,we should merge blocks */ 52 prior_mcb->current_blocksize = prior_mcb->current_blocksize \ 53 + sizeof(mem_control_block) + current_mcb->current_blocksize; 54 55 /* We have merge two blocks,so we need change prior_blocksize of 56 * block after the two blocks,just find next block location. 57 */ 58 otherbck_location = current_location + sizeof(mem_control_block) \ 59 + current_mcb->current_blocksize; 60 /* We need check wehter this block is on the edge of managed memeory! */ 61 if(otherbck_location != managed_memory_end){ 62 other_mcb = (mem_control_block *)otherbck_location; 63 /* its prior block size has changed! */ 64 other_mcb->prior_blocksize = prior_mcb->current_blocksize; 65 } 66 } 67 } 68 }
知乎:
释放一个已分配的数据块,只是对此数据块以及其前后数据块的控制块头部进行调整,并没有对实际有效数据区域(用户可见的区域)进行初始化操作,也就是说数据区域中的数据保持不变。
四、实现过程中易出现的Bug
我们管理堆,靠的就是头部控制块。不管是分配还是释放,都是通过修改头部控制块的信息来实现堆链表的改变,从而达到从堆区中分配一块内存空间给用户,或者将一个内存块释放为重新可用。分配过程中的将大块一分为二,其实就是增加一个控制块头部;释放过程中的将两个连续可用块合并到一起,实际上就是减少一个控制块头部。
堆栈的分配和释放,在我们改变堆栈控制头部的信息之前,一定要检查块是否是已经位于堆区之外(如果你确定这个块一定在堆内,可以省去检查代码)。倘若不去核查,而直接更改堆内存数据,有可能会更改堆区之外的数据,这是非常危险的。
五、测试结果
1、初始化建立第一个块成功
2、支持在第一个块上分割出一个用户块
3、用户块释放之后,可以和前后的可用块合并
4、释放后的块可以重新用于分配
5、支持在一个大的可用块中分出一个较小的用户块,伴随着一个新的较小块的产生
6、找不到合适的块分配给用户了,返回空地址
六、目前本程序的应用以及展望
1、目前本程序的应用
操作系统的动态分配内存函数malloc的实现方法与本文差别还是很大的。目前本程序不支持操作系统的动态内存分配,但是可以在单片机的动态内存管理上使用。
使用方法就是一开始为单片机定义一个固定位置、固定大小的堆区,然后将堆区的起始地址和大小作为宏参数传递给Rewrite_malloc_init函数中,并进行初始化。接下来,就可以调用malloc和free对内存进行动态分配和管理。
2、展望
本程序的malloc函数,在没有足够的堆空间分配给用户时,就直接以NULL返回给用户。如果可能的话,在操作系统中,可以继续从操作系统中申请堆空间(sbrk函数),然后再进行分配。
七、探秘malloc函数使用注意事项
1、使用malloc函数,注意对不用的空间进行释放
显然,每申请一个用户空间都会引起可用堆区的减少。如果只申请不释放,就会导致可用堆区不断减少,到最后有可能就没有空间分配给新的用户需要。
2、释放只能一次,重复释放有可能产生错误
多次释放同一用户空间,有可能不会出现错误;但是,也有可能会出现错误。也就是说,多次释放产生的结果是难以预测的。下面展示一个错误的释放例程。
/** * @brief Test malloc function * @param None * @retval int */ int main(void) { int * p1,* p2,* p3; memset((void *)heap,0,HEAPSIZE); malloc_init(); p1 = (int *)malloc(sizeof(int)); p2 = (int *)malloc(sizeof(int)); p3 = (int *)malloc(sizeof(int)); free((void *)p2); free((void *)p1); free((void *)p2); while(1); return 0; }
解释:
可以肯定的是,p1、p2、p3这三个块对应的块大小都是相同的。一个块的大小,包含一个头部(sizeof(mem_control_block) = 12Byts)和有效数据区(4 Bytes),所以总共16个bytes。
当第一次释放p2的时候,p2控制块的is_avaialbe变为1(重新可用)。当释放p1的时候,会将p1和p2整合到一起,这个时候p2对应的块就被注销了(注意块头部的信息还在内存中,只不过p1的块大小变大,将来沿着链表遍历时,会将这个块跳过)。当再次释放p2的时候,由于控制头部信息还在,所以还会再进行一次p1和p2块的整合,结果将p1块的大小设置成更大的,而超出了p1块实际拥有的空间。
因此重复释放可能导致“块头部控制结构体”的错误修改,而导致块管理信息错误,丧失堆区管理功能。
参考资料:浅析malloc()的几种实现方式
源码及VC6.0工程:malloc.zip