zoukankan      html  css  js  c++  java
  • 简述动态存储分配及malloc(),free()函数

    (本文基于linux系统。)

    首先说一下程序运行是的存储分配:

    存储分配
    存储分配

    这张是典型的C语言的存储分配图。动态存储分配主要涉及图中的堆区。堆是无结构的连续的存储区域。当调用malloc()函数时,存储分配器从堆中找一块合适大小的连续的内存空间返回给程序。

    malloc和free函数的原型如下:

    void * malloc(size_t size)

    void free(void* ptr)

    这两个函数的使用就不在赘述了。

    其实说白了,malloc和free函数就是负责对堆区的存储空间进行管理。既然要对堆区进行管理,而堆区又是无结构的,因此需要一个(运行库)自定义的结构来对堆区进行管理。

    既然是对存储空间的管理,那么必然要面对两个问题--------存储空间的利用率和运行的效率。

    首先是存储空间的利用率,这里面牵扯到一个概念--------碎片(fragmentation)。碎片又分为两种:内碎片和外碎片。

    1. 内碎片是指分配给程序的存储空间没有用完,有一部分是闲着的,这部分空间成为内碎片。
    2. 外碎片是虽然还有空间可以分配,但是这些空间都是不连续的小块,不能满足程序对连续存储的要求,因此无法分配给程序使用。这些不能满足程序要求的小的可以分配的存储空间成为外碎片。

    对内碎片的处理很容易,只要保证分配的空间和使用的空间一样大小就行了。这主要是靠写程序的程序员们估算好自己所需要的空 间。(为了保证分配的空间是块对齐,也就是双字对齐,分配器可能返回比实际申请的空间多出一个字节的空间。但相对于目前庞大的内存空间,这一个字节已经微 不足道了。但是如果程序申请大量的小块内存,这个问题还是要考虑的。。。比如:大量申请3个字节的空间,为了对齐,返回四个字节。这就导致四分之一的空间 浪费。)

    对于外碎片,处理的方法说起来很简单--------合并。但说归说,真正合并的时候就很困难。如果空闲的碎片是连续的, 那合并很简答。如果碎片不连续,这就很麻烦了。可以对内存进行紧缩,但这样效率很难保证。操作系统对内存进行管理是,大都使用分页,分段或段页式的管理来 减少外碎片。如果在程序中对堆区的管理再引入复杂的外碎片处理措施,不但运行效率低,而且多此一举。。。所以,当在处理合并的时候,基本都只考虑相邻的空 闲块。

    关于效率的问题,后面会详细说明。

    如果空间无限大,那么malloc就可以不断的分配新的空间给程序,而free函数则什么都不要做。因此,使用过的空间将直接丢弃而不再重用。这样 malloc和free函数即简单,效率也高。但是!存储空间是有限的。理论上在 32-位 x86 系统上,进程可用的最大空间是4G(由地址宽度决定)。但是不可能直接给程序分配4G的空间。。。因此,程序一开始运行的时候,堆区都有一个默认的大小。 这样,在程序运行的过程中,可能出现堆区中的空间都耗尽的情况(也可能没有耗尽,但都是小的碎片)。因此,就要给程序的堆区扩容,也就是让操 作系统给程序分配更多的空间,术语叫把空间映射到程序的堆区中。在这里有一个概念,叫系统中断点或当前中断点,也就是被映射的内存的边界(最后一个有效地 址),也就是堆顶。

    对堆区进行扩容,可调用下面的两个系统调用:

    • brk: int brk(void *end_data_segment) 是一个非常简单的系统调用。  brk() 只是简单地将系统中断点设置为end_data_segment,向进程添加内存或者从进程取走内存。
    • mmap: mmap(),或者说是“内存映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存, 而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将 它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。 munmap() 所做的事情与mmap() 相反。

    还有一个叫sbrk的系统调用void *sbrk(intptr_t increment)。sbrk()增加increment字节的内存到进程的堆区。而其返回的是旧的系统中断点,而不是新的。

    下面说一下一种叫做隐式空闲链表的管理堆区的方法。堆块的格式如下:

    -------------------------------------------------------------------

    |         int         available        /*是否空闲*/                                |

    |         int         size                   /*此块的大小*/                             |

    ------------------------------------------------------------------

    |                                                                                                 | <----malloc返回的指针指向此处

    |                      实际空闲内存区                                                     |

    |                                                                                            |

    ------------------------------------------------------------------

    |                       填充区(可选)                                                   |

    ------------------------------------------------------------------

    available 表示此块是否可用(被分配)。为1时表示可以分配,为0时表示被占用。

    size表示此块内存的大小。

    填充区用来进行块对齐(双字对齐)。

    当调用malloc进行内存分配的时候,需要从空闲的内存块中选择一块合适的。选择的方法常用的有三种:首次匹配,下一次匹配,最佳匹配。

    首次匹配:从头开始搜索表,选择第一个合适的块。

    下一次匹配:和首次匹配类似,但是不是每次都从头搜索,而是从上一次搜索结束的位置开始搜索。

    最佳匹配:选择所有合适的空闲块中最小的一块。

    下一次匹配的运行速度要比首次匹配快,但空间利用率没有首次匹配高。最佳匹配的空间利用率最高,但运行也是最慢的。

    当搜索到合适的空闲块后,这个空闲块往往不会整好合适,基本上都会大一下。多余的部分如果也分配给程序,就造成了内碎片。如果不分配,就要对空闲块进行分割。将多余的部分重新组成一个空闲块。这就有可能造成外碎片。因此,到底怎样处理,要看运行库的实现了。

    若两个空闲块整好是连着的,那么把这两个块合并成一个块就可以提高空间的利用率。如果当前块的后一个块是空闲块,那么根据当前块的指针和其长度,很容易得到下一个空闲块的位置指针。但当空闲块在当前块的前面时,就很难获得其指针了。Knuth提出了一种聪明而有效的方法,叫做边界标记。就是复制块头到空闲块的尾部,形成一个块尾。这样,只需将当前块的指针向前移动一个块头大小的距离,就可以得到指向前一个块的块尾的指针,这样,前一个块的所有信息就都得到了。

    在实际的实现中,往往引入两个特殊的块------序言块和结尾块。

    序言块是程度为零且一直标记为不可分配的块,在整个堆区的最下面(堆区的开始位置)。序言块有块头和块尾。

    结尾块在堆区的最上面(堆区的结束位置),也是长度为零且一直为不可分配。结尾块只有块头。

    这两个块的引入可以减少很多边界的处理,提高程序的简洁性和效率。

    下面是一个简单的实现的例子:

    采用首次匹配。

      1 /* 
      2  * File:   my_alloc.h
      3  */
      4 
      5 #ifndef _MY_ALLOC_H
      6 #define    _MY_ALLOC_H
      7 
      8 #ifdef    __cplusplus
      9 extern "C"
     10 {
     11 #endif
     12 
     13 #include <unistd.h>
     14 #define INIT_S 10240 //堆区的默认大小
     15     int is_initialized = 0//标记是否已经初始化
     16     void *memory_start; //堆区的开始地址
     17     void *memory_end; //系统中断点
     18 
     19     /*
     20      * 内存控制块
     21      * 块头和块尾的结构
     22      */
     23     typedef struct __mem_control_block
     24     {
     25         int available; //是否可用
     26         int size; //大小
     27     } mem_control_block;
     28 
     29     /*
     30       * 初始化堆区。
     31       * 建立堆区的初始结构,设置序言块,结尾块等。
     32       */
     33     void my_malloc_init()
     34     {
     35         /*
     36          * 获得系统中断点,也即堆区的堆顶
     37          */
     38         memory_end = sbrk(0);
     39         /*
     40          * 堆区的开始位置
     41          */
     42         memory_start = memory_end;
     43         /*
     44          * 为堆区预先分配INIT_S个字节。
     45          */
     46         if (sbrk(INIT_S) <= 0)
     47         {
     48             printf("Init Error!\n");
     49             return;
     50         }
     51         memory_end += INIT_S;
     52 
     53         mem_control_block *mcb;
     54         /*序言块*/
     55         /*块头*/
     56         mcb = memory_start;
     57         mcb -> available = 0;
     58         mcb -> size = 0;
     59         /*块尾*/
     60         mcb = (void*)mcb+ sizeof(mem_control_block);
     61         mcb -> available = 0;
     62         mcb -> size = 0;
     63         
     64         /*设置控制块, 块头*/
     65         mcb = memory_start + 2 * sizeof(mem_control_block);
     66         mcb -> available = 1;
     67         /*这个块的长度为整个堆区(除去控制块的长度)*/
     68         mcb -> size = INIT_S - 5 * sizeof (mem_control_block);
     69         /*设置边界标记,块尾*/
     70         mcb = memory_end - 2 * sizeof (mem_control_block);
     71         mcb -> available = 1;
     72         mcb -> size = INIT_S - 5 * sizeof (mem_control_block);
     73 
     74 
     75         /*结尾块*/
     76         mcb = memory_end - sizeof (mem_control_block);
     77         mcb -> available = 0;
     78         mcb -> size = 0;
     79 
     80         /*
     81          * 已经进行了初始化
     82          */
     83         is_initialized = 1;
     84     }
     85 
     86     /*
     87       * 模拟malloc函数
     88       */
     89     void * my_malloc(int size)
     90     {
     91         if (!is_initialized)
     92         {
     93             my_malloc_init();
     94         }
     95 
     96         /*分配的内存块的地址*/
     97         void * mem_location = 0;
     98 
     99         /*内存控制块的指针*/
    100         mem_control_block *curr_mcb, *tail;
    101 
    102         /*从头开始遍历*/
    103         curr_mcb = memory_start;
    104 
    105         while ((void*)curr_mcb < memory_end)
    106         {
    107             if (curr_mcb -> available)/*找到一个空闲块*/
    108             {
    109                 if (curr_mcb -> size >= size && curr_mcb -> size < size + 2 * sizeof (mem_control_block))
    110                     /*大小合适,多余的空间不够进行分割的。也就是剩余的空间无法满足块头和块尾所需要的空间。*/
    111                 {
    112                     /*获得返回的内存地址*/
    113                     /*
    114                           *     此处必须把curr_mcb转成void*的格式!!
    115                           *  否则,在加的时候是以sizeof(mem_control_block)的倍数增加,而不是仅仅加一!!
    116                           */
    117                     mem_location = (void *) curr_mcb + sizeof (mem_control_block);
    118                     /*标记已经占用*/
    119                     curr_mcb -> available = 0;
    120 
    121                     /*获得边界标记的指针,块尾*/
    122                     tail = (void *) curr_mcb + sizeof (mem_control_block) + curr_mcb -> size;
    123 
    124                     /*标记已经占用*/
    125                     tail -> available = 0;
    126                     break;
    127                 }
    128                 else if (curr_mcb -> size > size + 2 * sizeof (mem_control_block))
    129                     /*进行分割*/
    130                 {
    131                     int old_size = curr_mcb -> size;
    132                     /*获得分配的内存块的地址*/
    133                     mem_location = (void *) curr_mcb + sizeof (mem_control_block);
    134                     /*标记已经占用*/
    135                     curr_mcb -> available = 0;
    136                     curr_mcb -> size = size;
    137                     /*获得边界标记的指针,块尾*/
    138                     tail = (void *) curr_mcb + sizeof (mem_control_block) + size;
    139                     /*标记已经占用*/
    140                     tail -> available = 0;
    141                     tail -> size = size;
    142 
    143                     /*将余下的部分分割成新的空闲块*/
    144                     mem_control_block *hd, *tl; /*新块的块头和块尾*/
    145                     /*块头*/
    146                     hd = (void *) tail + sizeof (mem_control_block);
    147                     hd -> available = 1;
    148                     hd -> size = old_size - size - 2 * sizeof (mem_control_block);
    149                     /*块尾*/
    150                     tl = (void *) hd + hd -> size + sizeof (mem_control_block);
    151                     tl -> available = 1;
    152                     tl -> size = hd -> size;
    153 
    154                     break;
    155                 }
    156             }
    157 
    158             /*指向下一个块*/
    159             curr_mcb = (void*) curr_mcb + curr_mcb -> size + 2 * sizeof (mem_control_block);
    160         }
    161 
    162         /*没有找到合适的块,则扩展堆区,分配合适大小的内存加到堆区*/
    163         if (!mem_location)
    164         {
    165             /*申请空间*/
    166             if (sbrk(size + 2 * sizeof (mem_control_block)) <= 0)
    167             {
    168                 printf("Sbrk Error!\n");
    169                 return 0;
    170             }
    171             /*设置控制块的信息*/
    172             curr_mcb = (void*)memory_end - sizeof(mem_control_block);
    173             curr_mcb -> available = 0;
    174             curr_mcb -> size = size;
    175             /*设置边界标记,块尾的信息*/
    176             tail = (void*) curr_mcb + curr_mcb -> size + sizeof (mem_control_block);
    177             tail -> available = 0;
    178             tail -> size = size;
    179 
    180             /*获得分配的内存块的地址*/
    181             mem_location = (void*)curr_mcb + sizeof (mem_control_block);
    182 
    183             memory_end =  memory_end + size + 2 * sizeof (mem_control_block);
    184              /*设置结尾块*/
    185             tail = (void*)memory_end - sizeof(mem_control_block);
    186             tail -> available = 0;
    187             tail -> size = 0;
    188         }
    189 
    190         return mem_location;
    191     }
    192     /*
    193      * 模拟free函数
    194      */
    195     void my_free(void *ptr)
    196     {
    197         if (ptr <= 0)
    198         {
    199             return;
    200         }
    201 
    202         mem_control_block *curr;
    203         /*指向控制块的地址*/
    204         curr = ptr - sizeof (mem_control_block);
    205 
    206         
    207 
    208         /*标记为空闲,可用*/
    209         curr->available = 1;
    210         /*
    211          * 合并
    212          */
    213         mem_control_block *pre, *next, *tmp;
    214         
    215         /*获得前一个块的块头地址*/
    216         pre = ptr - 2 * sizeof (mem_control_block);/*这条语句获得了前一个块的块尾的地址*/
    217         pre = (void *) pre - pre -> size - sizeof (mem_control_block);/*进一步计算块头的地址*/
    218 
    219         /*获得后一个块的块头地址*/
    220         next = ptr + curr -> size + sizeof (mem_control_block);
    221 
    222         if (!pre -> available && next -> available)/*只有后一个块空闲*/
    223         {
    224             curr -> size += (next -> size + 2 * sizeof (mem_control_block));
    225             /*设置块尾*/
    226             tmp = (void *) curr + curr -> size + sizeof (mem_control_block);
    227             tmp -> available = 1;
    228             tmp -> size = curr -> size;
    229         }
    230         else if (pre -> available && !next -> available)/*只有前一个块空闲*/
    231         {
    232             pre -> size += (curr -> size + 2 * sizeof (mem_control_block));
    233             /*设置块尾*/
    234             tmp = (void *) pre + pre -> size + sizeof (mem_control_block);
    235             tmp -> available = 1;
    236             tmp -> size = pre -> size;
    237         }
    238         else if (pre -> available && next -> available)/*前后都块空闲*/
    239         {
    240             pre -> size += (curr -> size + 4 * sizeof (mem_control_block) + next -> size);
    241             /*设置块尾*/
    242             tmp = (void *) pre + pre -> size + sizeof (mem_control_block);
    243             tmp -> available = 1;
    244             tmp -> size = pre -> size;
    245         }
    246 
    247         return;
    248 
    249     }
    250 
    251     void print_info()
    252     {
    253         printf("printf_info:\n");
    254         mem_control_block *curr = memory_start;
    255         while ((void*)curr < memory_end)
    256         {
    257 
    258             printf("a? %d s: %d %d %d\n", curr ->available, curr ->size, memory_end, curr);
    259             curr = (void*) curr + curr -> size + 2 * sizeof (mem_control_block);
    260         }
    261     }
    262 
    263 
    264 #ifdef    __cplusplus
    265 }
    266 #endif
    267 
    268 #endif    /* _MY_ALLOC_H */
    269 
    270 

    在真正的实现中,不可能使用上面程序中的结构体来表示控制块(块头,块尾)。这样虽然简单但很浪费空间。实际中一般都是位来表示,这样节省空间,但是对编程要求细致入微,来不得半点马虎。上面的例子仅仅是模拟一下。有兴趣的读者可以看看gun libc的代码。

    隐式空闲链表的实现简单,但效率较低。因为运行的时候需要很多顺寻查找。实际的运用中可以使用分离的空闲链表或显示空闲链表等方法。有兴趣的读者可以参考《深入理解计算机系统》Randal E.Bryant David O'Hallaron  中国电力出版社的汉译版 第十章 10.9节动态存储分配。

    好了,就写这么多。如有错误还请各位读者指正。


    参考资料:《深入理解计算机系统》Randal E.Bryant David O'Hallaron 著 中国电力出版社


  • 相关阅读:
    ubuntu 安装 redis desktop manager
    ubuntu 升级内核
    Ubuntu 内核升级,导致无法正常启动
    spring mvc 上传文件,但是接收到文件后发现文件变大,且文件打不开(multipartfile)
    angular5 open modal
    POJ 1426 Find the Multiple(二维DP)
    POJ 3093 Margritas
    POJ 3260 The Fewest Coins
    POJ 1837 Balance(二维DP)
    POJ 1337 A Lazy Worker
  • 原文地址:https://www.cnblogs.com/kernel_hcy/p/1545977.html
Copyright © 2011-2022 走看看