malloc内存分配

来源:互联网 发布:怎么在淘宝社区发帖 编辑:程序博客网 时间:2024/05/17 04:29

鉴于上次领导告诉一个解决方案,让我把它写成文档,结果自己脑子里知道如何操作和解决,但就是不知道如何用语言文字把它给描述出来。决定以后多写一些笔记和微博来锻炼自己的文字功底和培养逻辑思维,不然只会是一个敲代码的,永远到不了管理的层面。

        把《C程序设计语言》细读了一遍后,到第8章UNIX系统接口的最后两节——“目录列表”和“存储分配程序”,看了一遍都没看懂。智商不过高啊。把存储分配重新看了一遍,才有了眉头。这两天还要找时间把目录列表再看一遍,确保掌握。(前几章节有些地方看了很多遍也糊里糊涂,就找谷歌百度帮忙了)

        这里的存储分配程序,讲的就是标准库中malloc函数的实现原理。首先要了解针对malloc的内存存储结构。malloc不像全局变量一样,不是在编译器编译的时候就会分配内存空间,而是在调用到malloc函数时才会分配空间。有时还会中途调用free函数释放空间出来。所以:

        1、malloc在第一次被调用时,从系统中获取最小为一个单元的空闲空间(eg:最小单元为1024个最受限单元块。当x<=1024,获取1024个,否则获取x个),再进行分配;

        2、malloc所剩下的空闲空间一般都不是连续的,而是分散的。这样也提高了空间的利用率。

        为了管理malloc的空闲空间,每一个独立块的最前面都包含了一个“头部”信息:一个指向下一个空闲块的指针、一个本身独立块的长度(书上说还有一个指向自身存储空间的指针,但每个存储空间都有自身的指针,为什么还要这个呢。后看英语版原著,这么写的:Each block contains a size, a pointer to nextblock, and the space itself.)。下一个空闲块是按存储地址升序排列,离本空闲块最近的一个空闲块,若本空闲块在最后,则指向最前的空闲块。这样所有属于malloc的空闲空间都被串在了一起。如下图所示:


注:中文版的此图解释翻译搞错了,如下图:



        因为后面的free函数已经把相邻的空闲链表给整合成一块了,所以我的图没有出现相邻的空闲链表。

        每块由malloc控制的空间都包含一个“头部”信息,为了方便管理,每块的空间大小都是头部大小的整数倍。而头部长度=指向下一个空闲块的指针的长度+自身空间大小的unsigned长度。但为了确保由malloc函数返回的存储空间满足将要保存的对象的对齐要求。每个机器都有一个最受限的类型:如果最受限的类型可以存储在某一个特定的地址中,则其他所有的类型也可以存放在此地址中。有的是double型,有的是long型,甚至还有的是int型。因此,头部结构将与最受限的类型进行联合,来确保对齐。

        因为有头部信息,头部信息里的本块空间size也是包括头部的大小,所以每次申请malloc空闲块的时候,都要加上一个单元,最后返回给用户的时候,再去掉头部。

        每次调用malloc申请空间时,malloc有一个专门指向当前空闲块链表的静态指针freep。从当前开始扫描剩下的空闲块链表,直到扫到一个足够大的空闲块。此算法成为“首次适应”(first fit),与之相对的是“最佳适应”(best fit):它将扫描出满足条件最小的块。这里的代码是“首次适应”算法。结果将出现三种情况:

        1)找到一块刚好合适的空闲块,则此块空间从链表中移走并将此块的地址返回给用户,并把静态指针freep指向前一空闲块地址;

        2)找到一块比需求大的空闲块,则从此空闲块中的后部取一块与需求一样的空间给用户,前部改变空闲块大小便可;


注(直到写本博文才发现自己错了):

①一直都认为返回给用户地址前一单元的头部,应该把空间退还给前面的空闲块,不然就闲着了。其实,里面记录了一个重要信息:空间块大小(包括头部和返回给用户的单元大小),在free释放空间的时候,就必须用到此头部的信息;

②后面的free程序以为是专供系统申请空间后插入空闲块链表用的,其实它就是我们平常用malloc、realloc、或calloc申请空间后,再释放的程序。

        3)如果扫描了一遍,都没有找到足够大的空闲块,则向系统再申请一块新的空间。

        上面都是在malloc已经有了空闲块的前提下,但第一次申请的时候,malloc是没有空闲块空间的。因此,在预编译时,就建立了一个单元的空闲块链表base来当做空闲链表的入口。当第一次调用malloc时,空闲链表的静态指针freep为NULL,那将它指向base,大小设为0(这样这块base空间将一直存在,且不被申请,确保了之后freep一直指向有效的空闲块链表),且指向它自己,同时向系统申请空闲空间(每次向系统申请的空间都是一块连续的空闲块)。

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. typedef long Align; /*按照long类型的边界对齐,即以long作为最受限类型*/  
  2. union header{   /*头部信息*/  
  3.     struct {  
  4.         union header *ptr;  /*指向下一个空闲块*/  
  5.         unsigned size;      /*本空闲块大小*/  
  6.     }s;  
  7.     Align x;    /*强制对齐*/  
  8. };  
  9. typedef union header Header;  
  10.   
  11. static Header base; /*第一次调用malloc的空闲块链表入口,大小为0的空链表(按照上面逻辑的来说,这里的size应该为1)*/  
  12. static Header *freep = NULL;    /*静态的空闲块链表指针,初始化为NULL。第一次申请后才会指向base*/  
  13.   
  14. /*malloc函数:通用存储分配函数*/  
  15. void *malloc(unsigned nbytes)  
  16. {  
  17.     Header *p,*prevp;   /*定义一个当前空闲块指针变量,和前一个空闲块指针变量*/  
  18.     Header *morecore(unsigned); /*用于向系统申请空闲空间函数*/  
  19.     unsigned nunits;    /*需要申请的实际单元大小,即上面图中的z*/  
  20.   
  21.     nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;  /*与上图对应,把字节大小转换为单元大小,向上取整,并加上一个单元(头部)*/  
  22.     if((prevp = freep) == NULL){    /*没有空闲链表,第一次申请*/  
  23.         base.s.ptr = prevp = freep = &base; /*freep指向base,base的下一个空闲块指针指向自己*/  
  24.         base.s.size =0; /*设置大小为0*/  
  25.     }  
  26.     for(p = prevp->s.ptr;;prevp = p, p = p->s.ptr){  
  27.         if(p->s.size >= nunits){  
  28.             if(p->s.size == nunits)  /*大小刚好合适*/  
  29.                 prevp->s.ptr = p->s.ptr;  /*移走此块空闲区域*/  
  30.             else{                   /*比实际需求大,从空闲块尾部分配*/  
  31.                 p->s.size -= nunits; /*缩小空闲块大小*/  
  32.                 p += p->s.size;  /*指针指向被申请的空间的头部*/  
  33.                 p->s.size = nunits;      /*设置被申请的空闲块大小*/  
  34.             }  
  35.             freep =prevp;   /*当前静态指针指向前一空闲块,如果当前块还有空闲区域,下次将继续从此处开始扫描,节省时间*/  
  36.             return (void *)(p+1);   /*返回去头部单元的空闲空间*/  
  37.         }  
  38.         if(p == freep)  /*闭环的空闲链表,第一次调用malloc申请,或扫描一遍,未发现足够大的空间*/  
  39.             if((p = morecore(nunits)) == NULL)  /*向系统申请空间*/  
  40.                 return NULL;    /*未申请成功,*/  
  41.     }  
  42. }  

        通过下面的morecore()和free()函数的程序分析可知,在向系统成功申请空间后,p将指向有足够空间的空闲块。但在此代码中,进入下一此空闲块扫描前,p将指向下一块不足的空闲块,导致多扫描了一遍。个人觉得,如果空间足够,可以多申请一个静态指针beforefreep,指向freep的前一个空闲块。这样上面代码可添加一句,提高效率:

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. if(p == freep){ /*这样要添加大括号*/  
  2. if((p = morecore(nunits)) == NULL)  
  3.         return NULL;  
  4.     p = beforefreep;  
  5. }  
  6. 或  
  7. if(p == freep) /*这里可以不用加大括号,else与最近的if匹配*/  
  8. if((p = morecore(nunits)) == NULL)  
  9.         return NULL;  
  10.     else  
  11. p = beforefreep;  

        向系统申请空间的时,不是按需分配,而是有一个最小申请单元数。让您足够用,这次用不完可以留着下次用,不用每次都向系统申请,又不会系统浪费空间。

        真正向系统申请空间,还需调用系统调用sbrk(n)(UNIX下),若申请成功,该指针返回指向n个字节的存储空间;若申请失败,返回-1(不是NULL)。返回的指针类型是char *(应该是最小的存储空间单元)。

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. #define NALLOC 1024 /*最小申请单元数*/  
  2.   
  3. /*morecore函数:向系统申请更多的存储空间*/  
  4. static Header *morecore(unsigned nu)    /*返回的是静态空闲块链表指针*/  
  5. {  
  6.     char *cp, *sbrk(int);  
  7.     Header *up;  
  8.   
  9.     if(nu < NALLOC)  
  10.         nu =NALLOC;  
  11.     cp = sbrk(nu * sizeof(Header)); /*调用系统调用申请系统空间*/  
  12.     if(cp == (char *) -1)  
  13.         return NULL;    /*申请失败,没有空间*/  
  14.     up = (Header *)cp;  /*转换为Header*指针类型*/  
  15.     up->s.size = nu; /*设置此空间块的大小*/  
  16.     free((void *)(up +1));  /*释放空间*/  
  17.     return freep;     
  18. }  

        这里的返回的freep,在free中更新了,才返回的。当初也想过既然freep都是静态全局变量了,那这里为什么还要返回一个静态变量呢,直接在函数里赋值就好了。其实这里有成功与失败,所以程序来需要判断申请结果,而且返回的freep是与申请最相关东西。

        free(void *ap)函数就是释放指针ap所指的空间,具体要释放的大小在ap前一个指针,即头部信息里。释放主要就是为了把此空间插入到空闲块链表中。所以要找到此空间块两边的空闲块(也有可能只有一块空闲块,即入口base)。然后判断是否与前一块相连,与后一块相连,相连的话,合并成一块,否则直接在中间插入一个新的空闲块链表。

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. /*free函数:释放ap,将ap块放入空闲链表中*/  
  2. void free(void *ap)  
  3. {  
  4.     Header *p, *bp;  
  5.   
  6.     bp =(Header *)ap -1;    /*指向ap块的头部*/  
  7.     for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)    /*找到bp所在空闲链表中的位置*/  
  8.         if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))   /*判断是否在链表的开头或末尾*/  
  9.             break;  
  10.   
  11.     if(bp + bp->s.size == p->s.ptr){  /*先判断能否与高地址的空闲块合并,即与后一块合并*/  
  12.         bp->s.size += p->s.ptr->s.size;  
  13.         bp->s.ptr = p->s.ptr->s.ptr;  
  14.     }  
  15.     else  
  16.         bp->s.ptr = p->s.ptr; /*不能合并,bp指向后一块地址*/  
  17.   
  18.     if(p + p->s.size == bp){ /*再判断能否与地地址的空闲块合并,即与前一块合并*/  
  19.         p->s.size += bp->s.size;  
  20.         p->s.ptr = bp->s.ptr;  
  21.     }  
  22.     else  
  23.         p->s.ptr =bp;    /*不能合并,p指向bp地址*/  
  24.     freep =p;  
  25. }  

注:中文版翻译的又有歧义了,原著分别是“join to upper nbr”和“jointo lower nbr”。


        这个free程序,处理的太妙了。一般思维,先与前一块合并,再与下一块合并,如下面的程序(显然比我的好多了):

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. if(p + p->s.size == bp){ /*与前一块相连?*/  
  2.     if(bp + bp->s.size == p->s.ptr){  /*与后一块相连?*/  
  3.         p->s.size += bp->s.size + p->s.ptr->s.size;  
  4.         p->s.ptr = p->s.ptr->s.ptr;  
  5.     }else  
  6.         p->s.size += bp->s.size;  
  7. }else{  
  8.     if(bp + bp->s.size == p->s.ptr){  /*与后一块相连?*/  
  9.         bp->s.size += p->s.ptr->s.size;  
  10.         bp->s.ptr = p->s.ptr->s.ptr;  
  11.         p->s.ptr = bp;  
  12.     }else   /*不与任何一块相连*/  
  13.         bp->s.ptr = p->s.ptr;  
  14.         p->s.ptr = bp;  
  15. }  

         从这里可以看出,通过malloc申请后的空间,并没有初始化,所以在使用前记得初始化,不小心当做右值使用,出错的概率很大。

        《C程序设计语言》(第2版·新版)不愧是经典,值得细读和巩固。感谢作者!

        写了很久才写完,有错误或不好的地方,欢迎各位指正和批评!也感谢您花时间阅读!

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 贴纸贴反了怎么办 汽车贴纸刚贴好就下雨怎么办 白条贷款逾期了怎么办 联通充值卡过期了怎么办 购物卡消磁了怎么办 ie浏览器报错怎么办 db2导入大数据怎么办 拼团人数不够怎么办 耳机线外皮破了怎么办 耳机罩掉皮了怎么办 耳机皮掉了怎么办 债券逆回购买入怎么办 京东保价 发票怎么办 win7用户已锁定怎么办 电脑账户被锁定怎么办 win7状态不可用怎么办 电脑的本地连接不见了怎么办 win7电脑没有本地连接怎么办 win7字体换不了怎么办 电脑没系统了怎么办 电脑系统没有了怎么办 系统调用区域小怎么办 win8.1网络受限怎么办 电脑连wifi受限怎么办 鼠标不好用了怎么办 手机指纹不好使怎么办 手机录像不好使怎么办 手机1个卡不好使怎么办 手机收藏不好使怎么办 window10连不上网怎么办 电脑没有家庭组怎么办 usb共享网络出错怎么办 电脑共享有密码怎么办 win10连不上网怎么办 w10网络重置了怎么办 点击网络重置后怎么办? 电脑启动找不到硬盘怎么办 电脑开机找不到硬盘怎么办 电脑找不到宽带连接怎么办 电脑文件找不到了怎么办 电脑找不到手机热点怎么办