原文:nedmalloc结构分析

nedmalloc是一个跨平台的高性能多线程内存分配库,很多库都使用它,例如:OGRE.现在我们来看看nedmalloc的实现 (以WIN32部分为例)
    
位操作小技巧;
i.获取最低位的出现位置的掩码;x&(-x)
ii.判断值为2的幂:x & (x-1) == 0
iii.获取从最低的值为1的位开始到左边MSB的掩码: x | (-x)
iv.字节对齐;(x + 2^m) &( 2^m -1)

nedmalloc设计的数据结构和使用方法有几个有趣的地方:
1.从操作系统得到的内存后分了3层,内存块=>简单内存描述结构(数据节点)=>内存数据节点链(面向开发者)
2.内存块处理流程:
  创建线程共享内存池(多个线程通过这个”池”来向系统申请/复用内存,这里需要互斥)
       |-->释放内存时将内存放到各线程自己的数据结构中(TLS),对于小块内存用简单数组链表来保存,
           对于大块的内存就用过“树”来保存(设计上应该是考虑小块的内存使用频率较高,简单链表访问
           时间相对快)  
  线程请求内存时,首先从线程自己的维护的空闲内存查找,然后再从线程内存池中查找。

3.内存是按照"块"对齐的形式分配的,而且用户得到的可用内存是真正内存块的"一部分",由于块的大小是对齐的,
  表示块大小字节的最低3位用于表示块的使用标志。

现在我们具体看看Win32平台下nedmalloc
1.分配流程
   nedpmalloc(nedpool *p, size_t size)
      |
   从线程的独立数据中查询空闲的内存
   GetThreadCache(&p, &threadcache, &mymspace, &size)
      |    |------>检查申请大小,如果小于sizeof(threadcacheblk),用sizeof(threadcacheblk)代替
      |    |        因为在该块内存"释放"时需要放到空闲链表中,注意threadcacheblk的内存布局和malloc_chunk
      |    |        是一样的,虽然它们的含义有区别。
      |    |                |
      |    |        对于首次调用来说,需要初始化全局内存池
      |    |        InitPool(&syspool, 0, -1)
      |    |             |--------->检查全局参数并初始化ensure_initialization
      |    |                        初始化内存池的锁和设置TLS
      |    |                        INITIAL_LOCK(&p->mutex)
      |    |                        TlsAlloc(syspool->mycache)
      |    |                              |
      |    |                        创建线程池的空间
      |    |                        create_mspace(capacity, 1)
      |    |                              |
      |    |                     计算"实际"分配的大小:
      |    |      capacity + pad_request(sizeof(struct malloc_state)) + { align_offset(chunk2mem(0))
      |    |     pad_request(sizeof(struct malloc_segment)) + ((MCHUNK_SIZE + CHUNK_ALIGN_MASK) &
      |    |       ~CHUNK_ALIGN_MASK)}
      |    |                        
      |    |  从大小的计算可以知道,在没有被"外部接口"使用时,至少会包含malloc_state结构和malloc_segment结构
      |    | 这里多个数据结构都是分别计算块对齐的(这里分结构对齐的目的一方面为了访问结构的时候可以从块对
      |    | 齐的位置开始,这样在存储的时候会快一些,但最主要的是为了使地址低位的bit"空闲",用于表示其他的含义)
      |    |                              |
      |    |                   初始化malloc_state结构
      |    |                   init_user_mstate(tbase, tsize)
      |    |                              ||
      |    |                  这个函数中需要注意几个细节:
      |    |             1.指针计算:
      |    |                   mchunkptr msp = align_as_chunk(tbase); 计算对齐偏移,align_as_chunk中会有chunk2mem的调用
      |    |                   mstate m = (mstate)(chunk2mem(msp));malloc_state的位置比对块其后的malloc_chunk偏移8个字
      |    |                   节(32Bit计算)
      |    |              2.大小计算:
      |    |                  msp->head = (pad_request(sizeof(struct malloc_state))|PINUSE_BIT|CINUSE_BIT)
      |    |                  malloc_chunk的大小只计算了malloc_state,而不是可用空间的大小,可用空间大小是在
      |    |                  malloc_state中设置的
      |    |                          
      |    |                  m->seg.base = m->least_addr = tbase;
      |    |                  m->seg.size = m->footprint = m->max_footprint = tsize;
      |    |                       
      |    |              3. 内存块链表的初始化
      |    |                   init_bins(m);
      |    |                   malloc_state结构中smallbins是一个malloc_chunk的指针数组,特别需要注意它的定义和使用
      |    |                   smallbins[(NSMALLBINS+1)*2]
      |    |                   这里一共分配了 sizeof(malloc_chunk*) * (NSMALLBINS+1)*2 个字节
      |    |                   在使用的时候是通过smallbin_at来获取对应的指针,这个宏返回的地址实际上是smallbins中
      |    |                  元素的地址,并将这个地址强制转换为malloc_chunk类型变量,也就是说如果通过这个
      |    |                  指针访问/修改变量,实际上修改的是smallbins的内容,而且
      |    |                   p = smallbin_at(m, i)得到的p和smallbins对应关系是:
      |    |                   p->prev_foot <==> smallbins[i*2]
      |    |                   p->head <==> smallbins[i*2 + 1]
      |    |                  p->fd   <==> smallbins[i*2 + 1 + 1] == smallbins[(i+1)*2], smallbin_at(i+1)的返回值
      |    |             4. 计算下一个malloc_chunk的位置,这个malloc_chunk才是用于维护后面连续的内存块的
      |    |                 next_chunk(mem2chunk(m))
      |    |                 初始化空闲内存块的信息
      |    |                 init_top(m, mn, (size_t)((tbase + tsize) - (char*)mn) - TOP_FOOT_SIZE)    
      |    |                 这个函数做两件事:
      |    |                 i.设置malloc_state真正的开始分配位置和可用到小;
      |    |                ii.设置"最后"一个malloc_chunk(末尾的块,不用特殊用途)的大小,这个块的是没有使用的标志位的
      |    |---->分配线程缓存AllocCache(nedpool* p)
      |               |-->分配新的threadcache结构
      |               tc=p->caches[n]=(threadcache *) mspace_calloc(p->m[0], 1, sizeof(threadcache))
      |               mspace_malloc(这个函数中的逻辑分支比较多,需要结合内存释放来分析,后面我们再详细看),由于
      |               threadcache比较小,
      |               所以这里大小会调整为MIN_CHUNK_SIZE,并且内存会在malloc_state的top中分配
      |                     size_t rsize = ms->topsize -= nb;
      |                      mchunkptr p = ms->top;
      |                      mchunkptr r = ms->top = chunk_plus_offset(p, nb);
      |                      r->head = rsize | PINUSE_BIT;
      |                           |
      |                           |
      |              初始化thread_cache,并设置线程的TLS
      |          TlsSetValue(p->mycache, (void *)(size_t)(n+1))  
      |          需要注意的是:这里用的值是n+1, 另外tc->mymspace=tc->threadid % end(这里求余实际上是为了减少碰撞)
      |           (事实上thread_cache结构是用于维护内存申请者内存块信息的中间结构)
如果返回的内存缓存thread_cache结构有效,而且调整后的大小,那么请求的内存从cache中分配threadcache_malloc(p, tc, &size)
      |    |-->分配内存的时候是按照块对齐的(8,16,20...)字节,所以这里首先计算能满足用户需求的最小的块大小
      |      (threadcache有一个在使用上和malloc_state的smallbins非常相似的结构设计成员bins,它是threadcacheblk的指
      |       针数组,但是在使用上,它们有所不同)
      |            |
      |      获取对应块大小对齐的指针binsptr=&tc->bins[idx*2](这里是对指针数组成员取了地址),如果当前的链表信息为
      |     空,或者空间不足,那么检查下一个块,这里只检查下一个大小的块,是为了减少损失的内存使用。如果得
      |      到的块非空,那么将当前的块从链表中分离出来返回(这里每个链包含两个指针,应该是首,尾指针)
      |
  如果从线程各自的缓存中分配失败,那么就从malloc_state中分配
  mstate pms = GetMSpace(p, &tc, mymspace, size)
      |            |->根据myspace获取malloc_state(注意,获取的malloc_state并不一定是当前线程创建的),
      |               如果该malloc_state不能锁定,那么历遍其他的malloc_state看能否锁定,如果失败,只要没有超过内
      |               存池允许的上限, 那么创建一个。这里有个细节
      |               if(tc)
      |                  tc->mymspace=n;
      |              else
      |              {
      |                  if(TLSSET(p->mycache, (void *)(size_t)(-(n+1)))) abort();
      |              }
      |              如果在首次初始化线程缓存thread_cache的时候失败,TLS的值将会是-1, 而后面会到达GetMSpace,假
      |              设这时候创建成功,那么TLS会变成-2,这样在下次GetThreadCache的时候会重新myspace为1,这样它
      |              不会进行Allocache的调用;如果这时创建失败,那么会等待上一次使用的malloc_state空闲,这是TLS会
      |              保持-1, 最后会将myspace设置为0。
  在获取的malloc_state上分配空间mspace_malloc(pms, size)
  
  
2.内存释放流程
  nedpfree(nedpool *p, void* mem)
     |
  计算当前线程使用的cache信息
  GetThreadCache(&p, &tc, &mymspace, 0)(线程对应的cache确定(分配成功)后是不会改变的)
     |
  如果内存块比较小,而且thread_cache成功,那么将内存块放到cache中
  threadcache_free(p, tc, mymspace, mem, memsize)
         |->将mem转变为threadcacheblk*,并根据mem对应内存块的实际大小(申请者使用的部分只是
            真实内存块的一部分)链入到thread_cache的bin成员中(有需要的话调整首尾指针)
            如果cache中的内存块总大小超过特定上限时将cache中的内存返回到malloc_state中
            ReleaseFreeInCache->根据加入到cache的先后顺序将threadcacheblk释放到malloc_state
                 |
            RemoveCacheEntries->从cache的尾指针开始历遍threadcacheblk链,将"时间"过长的块释放
            mspace_free(0, f)
                 |--->获取malloc_state 
                   这里需要先描述一下malloc_trunk结构的含义:
                   struct malloc_chunk {
                      size_t               prev_foot;  /* Size of previous chunk (if free).  */
                      size_t               head;       /* Size and inuse bits. */
                      struct malloc_chunk* fd;         /* double links -- used only if free. */
                      struct malloc_chunk* bk;
                    };
                   如果当前malloc_chunk被使用,那么下一个malloc_chunk的head的pinuse位会被置位,而且它(下一个
                   chunk)的prev_foot = malloc_state ^ mparams.magic. 如果当前块未被使用,那么它(当前块)的
                   prev_foot是上一个块的大小,而且下一个块的pinuse不会被置位,而且它(下一个chunk)的prev_foot表
                   示上一块的大小。                    
                    (prev_foot的最低位是用于表示该块是否为操作系统的真正分配的内存)
                    
                    根据malloc_chunk的状态进行不同的"释放"处理:
                    i.如果当前块是从操作系统中分配,那么返还给操作系统HeapFree
                    ii.[向前合并]如果当前块的前一块空闲,那么将这两块(不可能同时出现3块同时空闲,而且但前块在最
                       后一块"FFN")一起处理
                       a.如果首块地址不同于malloc_state的dv(它的作用是保存连续空间中中间释放的连续块,对于先申请
                          先释放的应用来说,这种处理方式会有好处,因为在分配的时候会先检查dv是否能满足需求),那么
                          根据块的大小分别是"存放"到不同的地方等待复用unlink_chunk
                       b.如果下一块正在被使用,那么修改下一块的prev_foot和pinuse标志位
                    iii.[向后合并]如果下一个块空闲,那么将当前块和下一块合并处理
                       a.如果下一个块已经是top(末尾的空闲块),那么更新top的指向(扩容)
                       b.如果下一个块是dv,那么将当前块合并到dv中
                       c.如果都不是,那么简单地释放下一个块,并修改下一块地prev_foot,pinuse
                         unlink_chunk(fm, next, nsize);
                         set_size_and_pinuse_of_free_chunk(p, psize);
                    iv.如果下一块正在使用,那么简单修改下一块的标志set_free_with_pinuse(p, psize, next)
                    
                    对于没有合并到"空闲"空间中的块,根据块的大小,挂接到不同的链表(树)中
                    if (is_small(psize)) {
                       insert_small_chunk(fm, p, psize);
                    }
                    else {
                       tchunkptr tp = (tchunkptr)p;
                       insert_large_chunk(fm, tp, psize);
                    }
                    
                    这里需要补充一下malloc_state的smallbins成员的使用:
                    所有"释放"到malloc_state的空闲内存块会连成双向链表,而smallbins中pref_foot和head是不直接使用
                    的,smallbins的大小是为了访问fd和bk两个指针而设计的。smallbins实际上是将链表中按照内存块的的
                    大小分段保存链表的指针,方便在分配时查找。
                    (理解了这个,那么insert_small_chunk的处理流程就比较简单了)
                    
                    现在看看比较大的内存块的处理思想:
                    "大块"内存的"释放"是放到"树中的,树的结构根据内存块的大小(256为单位)按照类似"哈夫曼"编码的的
              形式划分到二叉树中,树的每个节点是一个双向链表,保存了大小相同的块的指针。(思路清楚了,加
              入、删除节点的代码比较容易理解,这里不再展开)需要注意的是这里malloc_stat的treebins成员保存的是
              树(块区域大小)的开始指针(很简单的使用方式),它的用法和smallbins的"似结构体非结构体"的特殊用
              法不同。                    
    
3.扩展分配nedprealloc函数
     这个函数是nedpmalloc -> memcpy -> nedpfree的组合,这里不展开了,需要注意的是,如果新申请的空间比原来的空间小,那么是直接返回原来的空间的。

现在,我们再看看内存分配最终的入口mspace_malloc的实现(对着mspace_free来看,比较容易理解)
4.内存分配逻辑
    mspace_malloc
        |
    i.如果请求的块小于MAX_SMALL_REQUEST,首先尝试在smallbins中分配
      b = smallbin_at(ms, idx);
      p = b->fd;
      unlink_first_small_chunk(ms, b, p, idx);
      set_inuse_and_pinuse(ms, p, small_index2size(idx));
      注意,为了提高重用成功率,这里允许使用使用比实际请求大小大一阶(下一个块对齐大小)的块
          |(如果分配不成功)
      如果请求的块大于malloc_state的dvsize(上一个空洞块留下的空隙):
        i.smallbin非空,那么在smallbin中分配后检查是否可以替换原来的dv块
            if (SIZE_T_SIZE != 4 && rsize < MIN_CHUNK_SIZE){...}
            else{... replace_dv(ms, r, rsize);}
        ii.从treebin中分配tmalloc_small()
                               |
                     根据请求大小计算树的根(开始查找最小匹配块的根)
                     compute_bit2idx(leastbit, i);
                     v = t = *treebin_at(m, i);
                               |
                     查找最小的匹配块while ((t = leftmost_child(t)) != 0){...}
                     并将分配后留下的空闲块设置到dv中
                     unlink_large_chunk(m, v);
                     replace_dv(m, r, rsize);
   ii.如果申请大小大于MAX_REQUEST,实际上会失败
   iii.计算块对齐大小pad_request(bytes),并从树中分配
        tmalloc_large(ms, nb)
           |
        tmalloc_large和tmalloc_small的主要不同是:
        a.tmalloc_large首先根据大小计算"最接近"的节点,并从该节点开始计算"最小的"满足需求的节点
        b.如果"最接近节点"为空,tmalloc_large允许扩展一阶大小来寻找"最小的"满足需求大的节点
        (结合malloc_tree_chunk和块的组织方式,代码比较容易理解)
        
   iv.如果请求大小小于dvsize,那么从dv中分配
      mchunkptr p = ms->dv;
      mchunkptr r = ms->dv = chunk_plus_offset(p, nb);
      ms->dvsize = rsize;
   v.如果请求大小小于topsize,那么从malloc_state的top块中分配
   vi.从系统空间中分配sys_alloc(ms, nb);
      sys_alloc兼容了多个平台分配机制,通过不同宏来开关对应的代码段,对于Win32来说,最终会调用HeapAlloc
      sys_alloc流程:
      按照块对齐和附加内存管理结构(如malloc_state)计算内存块的大小
      -->不同平台下使用不同的系统函数分配"物理内存"(系统内存),并将得到的内存
             |(这里主要不同宏控制的代码,比较简单,不展开了)
             |
      如果malloc_state不含有真正的可用内存(top为空),那么初始化它init_bins,init_top
      如果malloc_state已经初始化,那么检查是否可以将top中剩下的空间合并到新分配的空间中(只有在可连续分配扩展的
      情况下才有效),并重新初始化init_top, 这里合并分了两种情况:
        a.新分配的块在某个分快后,并和前一个分块在地址空间上相连,而且前一分块空间包含top
          while (sp != 0 && tbase != sp->base + sp->size)
            sp = (NO_SEGMENT_TRAVERSAL) ? 0 : sp->next;
          segment_holds(sp, m->top)
        b.某一个现有的分快紧接着新分配的块,这时需要将原来的块合并到新分配的块prepend_alloc
        c.a和b都不满足的情况下,将新块加入到块链表add_segment(m, tbase, tsize, mmap_flag),并重新设置top
          |
          |
       从top中分配内存
       
  
   好了,现在我们对nedmalloc的处理思想和算法实现都比较清楚了(在*nix平台下还有一些细节这里没有列处理,可以查看代码),下面概括一下:
    1.使用连续的内存分段思想管理大片的连续内存
    2.从1的内存块中以块对齐方式分配内存,小的内存分块放到线程的TLS指定的cache双向链表中,大的分块放到树结构中
    3.树结构是以类似哈夫曼编码的方式组织的(以块大小编码),每个内部节点是一个双向链表
    4.外部内存申请:thread cache->线程公用内存池; 释放:线程cache链表->内存节点树

【转载】nedmalloc结构分析的更多相关文章

  1. Facebook的体系结构分析---外文转载

    Facebook的体系结构分析---外文转载 From various readings and conversations I had, my understanding of Facebook's ...

  2. 转载,crtmpserver文件夹结构分析

    1. 顶层(crtmpserver/) 下的文件夹结构 3rdparty/             : 对lua, xml进行解析的源代码文件夹 sources/              : 项目的 ...

  3. 【转载】Quick-Cocos2d-x文件结构分析

    在上一章我们讲过了Quick-Cocos2d-x中的环境搭建,这章我们分析下quick中的文件结构吧!打开quick的文件夹,可以看到如下的这些目录和文件: bin:存放各种与引擎相关的脚本 comp ...

  4. 转载:java集合类数据结构分析

    数组是 最常用的数据结构.数组的特点是长度固定,可以用下标索引,并且所有的元素的类型都是一致的.数组常用的场景有把:从数据库里读取雇员的信息存储为 EmployeeDetail[],把一个字符串转换并 ...

  5. laravel5-目录结构分析

    laravel5-目录结构分析 (2016-01-21 11:24:03) 转载▼     一.环境配置: $ lsb_release -a No LSB modules are available. ...

  6. apache kafka技术分享系列(目录索引)--转载

    原文地址:http://blog.csdn.net/lizhitao/article/details/39499283 kafka开发与管理: 1)apache kafka消息服务 2)kafak安装 ...

  7. (转载)Google的PageRank算法

    本文由张洋(敲代码的张洋)投稿于伯乐在线. 本文转载于:http://blog.jobbole.com/23286/ 很早就对Google的PageRank算法很感兴趣,但一直没有深究,只有个轮廓性的 ...

  8. [转载] Redis资料汇总专题

    转载自http://www.cnblogs.com/tommyli/archive/2011/12/14/2287614.html 1.Redis是什么? 十五分钟介绍 Redis数据结构 Redis ...

  9. python爬虫——爬取小说 | 探索白子画和花千骨的爱恨情仇(转载)

    转载出处:药少敏   ,感谢原作者清晰的讲解思路! 下述代码是我通过自己互联网搜索和拜读完此篇文章之后写出的具有同样效果的爬虫代码: from bs4 import BeautifulSoup imp ...

随机推荐

  1. 前端框架与UI搭配

    如果是 Angular 那就选 Ionic (一对好 CP)如果是 Vue 那就选 Vux (基于 WeUI)如果是 jQuery 那就选 Framework7 (iOS 和 Android 双皮肤) ...

  2. maven相关

    1. 创建/导入maven项目时 eclipse默认jdk版本配置:http://blog.csdn.net/lzj0470/article/details/42292021 2. eclipse 使 ...

  3. 启动管理软件服务器时,提示midas.dll错误

    首先确认系统以及管理软件目录内是否有midas.dll文件,如果没有,请复制或下载midas.dll到相应目录.系统默认路径为:'c:\windows\system32\' 然后依次打开“开始菜单”内 ...

  4. centos7.0 没有netstat 和 ifconfig命令问题

    yum install wget 运行  yum install net-tools  就OK了 默认CentOS已经安装了OpenSSH,即使你是最小化安装也是如此.所以这里就不介绍OpenSSH的 ...

  5. A Mysql backup script

    UseCentOS can help IT managers to get rid of the boring learning methods, quick grasp Linux technolo ...

  6. Linux下查看用户列表

    cat /etc/passwd 可以查看所有用户的列表 w 可以查看当前活跃的用户列表 cat /etc/group 查看用户组

  7. MySQL之对数据库库表的字符集的更改

    数据字符集修改步骤: 对于已有的数据库想修改字符集不能直接通过 "alter database character set *"或 "alter table tablen ...

  8. centos6.5 64bit 实现root开机自动登录X桌面

    vim /etc/gdm/custom.conf 修改daemon节点,添加如下内容: AutomaticLoginEnable=true AutomaticLogin=root 重启计算机. 完.

  9. ecshop微信支付(0923更新)商户支付密钥key的生成与设置

    ECSHOP 微信支付(0923更新)商户支付密钥key的生成与设置 说明:新版微信支付,用户必须授权登录才能支付.需要商家自己设置商户号支付密钥. 申请微信支付手机版部分时需要填写的配置接口地址: ...

  10. 删除ecshop云服务及授权关于官方等信息

    一.删除[云服务中心] 删除/admin/cloud.php 删除/admin/templates/menu.htm中以下代码 Ajax.call('cloud.php?is_ajax=1>ac ...