zoukankan      html  css  js  c++  java
  • 内存分配malloc浅析 -SIGSEGV

    malloc和free都是库函数,调用系统函数sbrk()来分配内存。除了分配可使用的内存以外,还分配了”控制“信息,这有点像内存池常用的手段。

    凡是涉及到内存的地方都有相应的内存分配算法。内核空间有内核空间的内存分配算法,比如,Buddy-System、slab分配器等;用户进程空间也有相应的内存分配算法,比如,First-Fit、Best-Fit、Nest-Fit、Worst-Fit等;而应用程序也可能根据自身内存使用特点,采用自己实现的内存分配算法,比如,memcached、nginx、lighttpd会使用splay tree之类的内存分配算法。可见没有绝对好的算法,只有最适用的算法。

    今天简单了解一下我们平时最常用的用户进程空间的内存分配函数:malloc(3)。这个内存分配函数其实有很多种实现,常用的一种内存分配算法是First-Fit,因为分配速度快。一般做法是,维护一个空闲链表,记录所有可用的内存块。当遇到内存请求的时候,遍历该链表,找到第一个满足条件的内存块即返回。当然这样可能很容易导致内存碎片,所以对碎片的处理是各种malloc算法的关键。相应的free函数一般比较简单,直接在空闲链表里标记该内存可用即可,也有些做法是在free的时候做相邻碎片的合并。

    大致算法描述如下:

    malloc(待分配内存大小n)
    Begin
        For 空闲链表的每个空闲块记录 Do
            If 空闲块大于n Then
                切分空闲块,剩下的重新挂到空闲链表上
                Return 大小为n的空闲块
            End If
        End For
        合并空闲链表上相邻的空闲块
        If 合并后的空闲块大于n Then
                切分空闲块,剩下的重新挂到空闲链表上
                Return 大小为n的空闲块
        End If
        调用sbrk(n)像系统请求内存,即扩展堆大小
        If 调用成功 Then
            Return 请求到的内存块
        End If
        Return NULL
    End
    
    free(内存地址)
    Begin
        在空闲链表上标记该内存可用
    End
    

    下边按照如上算法描述实现一套简陋的内存分配方法。其中一个技巧是,使用自定义的内存控制块结构mem_control_block来保存当前内存块的相关信息,同时可以在一个连续线性地址上当作空闲链表节点来使用。

    /* 内存分配程序的初始化标识 */
    int has_initialized = 0;
    /* 内存分配的起始地址 */
    void *managed_memory_start;
    /* 内存分配的结束地址 */
    void *last_valid_address;
    
    /* 内存分配程序的初始化函数 */
    void malloc_init()
    {
        /* 获取堆的边界地址 */
        last_valid_address = sbrk(0);
        /* 还没有分配内存,所以初始化为边界地址 */
        managed_memory_start = last_valid_address;
        /* 标记初始化完毕 */
        has_initialized = 1;
    }
    
    /* 内存控制块结构定义 */
    struct mem_control_block
    {
        int is_available;
        int size;
    };
    
    void free(void *firstbyte)
    {
        struct mem_control_block *mcb;
        /* 首先寻找mem_control_block结构 */
        mcb = firstbyte - sizeof(struct mem_control_block);
        /* 修改标识,表明该段内存重新可用了 */
        mcb->is_available = 1;
        return;
    }
    
    void *malloc(long numbytes)
    {
        /* 从这个地址开始寻找空闲内存 */
        void *current_location;
        /* 同上,但被cast成 struct mem_control_block* */
        struct mem_control_block *current_location_mcb;
        /* 待返回的分配地址 */
        void *memory_location;
        if(! has_initialized) {
            malloc_init();
        }
        /* 实际分配的地址还要包括控制信息,但这对用户是透明的 */
        numbytes = numbytes + sizeof(struct mem_control_block);
        memory_location = 0;
        current_location = managed_memory_start;
        while(current_location != last_valid_address) {
            current_location_mcb = (struct mem_control_block *)current_location;
            if(current_location_mcb->is_available) {
                if(current_location_mcb->size >= numbytes) {
                    /* 标记找到了合适的内存块 */
                    current_location_mcb->is_available = 0;
                    memory_location = current_location;
                    break;
                }
            }
            /* 没找到合适的内存块,跳到链表的下一元素 */
            current_location = current_location + current_location_mcb->size;
        }
        /* 如果当前链表中没有足够的内存块分配,则向操作系统请求 */
        if(! memory_location) {
            /* 著名的sbrk()函数,扩大堆边界 */
            sbrk(numbytes);
            memory_location = last_valid_address;
            last_valid_address = last_valid_address + numbytes;
            /* 保存信息到控制节点 */
            current_location_mcb = memory_location;
            current_location_mcb->is_available = 0;
            current_location_mcb->size = numbytes;
        }
        memory_location = memory_location + sizeof(struct mem_control_block);
        return memory_location;
    }
    

    Linux系统里glibc的malloc函数使用ptmalloc实现,是来自dlmalloc的改进。dlmalloc把类似大小的内存块组成bins以便改进速度和降低总体的碎片数,dlmalloc算法大致如下。

    • 对小于256字节的请求(smallbin),使用了一个简单的两倍Best-Fit分配器。并且利用CPU指令(__builtin_clz())来快速寻找合适的bin。如果在一个bin里面没有空闲块,就接着寻找下一个bin并切分。
    • 对大于等于256字节但小于mmap阈值的(largebin),使用了一个in-place的bitwise trie算法。
    • 对大于等于mmap阈值的,使用mmap系统调用分配内存,它总是按页分配,即4KB的倍数。mmap阈值默认是256KB(ptmalloc是1MB),可以使用mallopt()函数更改。
    • 如果小于mmap阈值,又没有足够的空闲空间,dlmalloc会通过brk()增加堆的大小。

    因为空闲空间的碎片合并会导致刷新TLB,所以dlmalloc只有一个很弱的碎片合并算法,每4096次free()调用会执行一次碎片合并。

    http://blog.jqian.net/post/malloc.html

    ------------------越是喧嚣的世界,越需要宁静的思考------------------ 合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。 积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步;驽马十驾,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也。蟹六跪而二螯,非蛇鳝之穴无可寄托者,用心躁也。
  • 相关阅读:
    HAOI2015 树上染色
    HAOI2010 软件安装
    T2 Func<in T1,out T2>(T1 arg)
    事无巨细
    LitJson JavaScriptSerializer
    数据库操作
    jQuery:总体掌握
    sql一个题的解法分析讲解
    Javascript系列:总体理解
    c#
  • 原文地址:https://www.cnblogs.com/feng9exe/p/14372317.html
Copyright © 2011-2022 走看看