IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    [TCMalloc] 全局内存分配器

    dutor发表于 2013-10-19 11:41:44
    love 0

      TCMalloc 的全局分配器,处于 TCMalloc 的最底层,负责向操作系统申请和释放内存,接口有两个,定义在 src/system-alloc.h|.cc:

    1
    2
    3
    
    extern void* TCMalloc_SystemAlloc(size_t bytes, size_t *actual_bytes,
                                      size_t alignment = 0);
    extern bool TCMalloc_SystemRelease(void* start, size_t length);

      TCMalloc_SystemAlloc 的参数,除了要申请的大小 bytes,还有 actual_bytes 和 alignment,因为对齐的需求,该接口可能分配大于 bytes 的内存,实际大小保存在 actual_bytes。TCMalloc_SystemRelease 负责『释放』内存,至于释放为什么加引号,下面会提到。接下来,介绍下这两个接口的实现。

      malloc-extension.h 里面,定义了接口类 SysAllocator,该类只有一个接口:

    1
    2
    3
    4
    5
    6
    
    class SysAllocator {
     public:
      SysAllocator() {}
      virtual ~SysAllocator();
      virtual void* Alloc(size_t size, size_t *actual_size, size_t alignment) = 0;
    };

      system-alloc.cc 里面,对 SysAllocator 有三个实现:SbrkSysAllocator, MmapSysAllocator, DefaultSysAllocator。SbrkSysAllocator 和 MmapSysAllocator 分别使用 sbrk 和 mmap 向系统分配内存,这两个类没有任何数据成员。DefaultSysAllocator 是 前两种 Allocator 的委托:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    
    class SbrkSysAllocator : public SysAllocator {
    public:
      SbrkSysAllocator() : SysAllocator() {
      }
      void* Alloc(size_t size, size_t *actual_size, size_t alignment);
    };
     
    class MmapSysAllocator : public SysAllocator {
    public:
      MmapSysAllocator() : SysAllocator() {
      }
      void* Alloc(size_t size, size_t *actual_size, size_t alignment);
    };
     
    class DefaultSysAllocator : public SysAllocator {
     public:
      DefaultSysAllocator() : SysAllocator() {
        for (int i = 0; i < kMaxAllocators; i++) {
          failed_[i] = true;
          allocs_[i] = NULL;
          names_[i] = NULL;
        }
      }
      void SetChildAllocator(SysAllocator* alloc, unsigned int index,
                             const char* name) {
        if (index < kMaxAllocators && alloc != NULL) {
          allocs_[index] = alloc;
          failed_[index] = false;
          names_[index] = name;
        }
      }
      void* Alloc(size_t size, size_t *actual_size, size_t alignment);
     private:
      static const int kMaxAllocators = 2;
      bool failed_[kMaxAllocators];
      SysAllocator* allocs_[kMaxAllocators];
      const char* names_[kMaxAllocators];
    };
    static char sbrk_space[sizeof(SbrkSysAllocator)];
    static char mmap_space[sizeof(MmapSysAllocator)];
    static char default_space[sizeof(DefaultSysAllocator)];

      在 libtcmalloc 初始化的时候,使用 new 操作符的 placeholder 形式,分别在 sbrk_space, mmap_space, default_space 上面初始化这三个 Allocator。然后调用 DefaultSysAllocator 的 SetChildAllocator 接口,将 SbrkSysAllocator 和 MmapSysAllocator 设置成待用分配器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    void InitSystemAllocators(void) {
      MmapSysAllocator *mmap = new (mmap_space) MmapSysAllocator();
      SbrkSysAllocator *sbrk = new (sbrk_space) SbrkSysAllocator();
      DefaultSysAllocator *sdef = new (default_space) DefaultSysAllocator();
      if (kDebugMode && sizeof(void*) > 4) {
        sdef->SetChildAllocator(mmap, 0, mmap_name);
        sdef->SetChildAllocator(sbrk, 1, sbrk_name);
      } else {
        sdef->SetChildAllocator(sbrk, 0, sbrk_name);
        sdef->SetChildAllocator(mmap, 1, mmap_name);
      }
      sys_alloc = tc_get_sysalloc_override(sdef);
    }

      DefaultSysAllocator 的 Alloc 接口是这样实现的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    void* DefaultSysAllocator::Alloc(size_t size, size_t *actual_size,
                                     size_t alignment) {
      for (int i = 0; i < kMaxAllocators; i++) {
        if (!failed_[i] && allocs_[i] != NULL) {
          void* result = allocs_[i]->Alloc(size, actual_size, alignment);
          if (result != NULL) {
            return result;
          }
          failed_[i] = true;
        }
      }
      // After both failed, reset "failed_" to false so that a single failed
      // allocation won't make the allocator never work again.
      for (int i = 0; i < kMaxAllocators; i++) {
        failed_[i] = false;
      }
      return NULL;
    }

      先后尝试使用 SbrkSysAllocator 和 MmapSysAllocator 分配内存,如果某个分配器分配失败,则标记相应的 failed_ 域,下次便不从这个分配器分配内存。如果全部失败,则返回 NULL,同时清除 failed_ 域,以免无法恢复。TCMalloc_SystemAlloc 函数分配内存时候,调用全局的 sys_alloc->Alloc() 即可。
      关于全局内存的分配要介绍的就这么多,至于 SbrkSysAllocator 和 MmapSysAllocator 的 Alloc 实现细节,但凡对 sbrk 和 mmap 了解的人一猜就知道了。
      下面介绍下内存的释放。可以注意到,前面的 Allocator 只有 Alloc 接口,并没有一个 Release 或者 Free 接口,那么内存是怎么释放的呢?事实上,TCMalloc 从来不释放内存!
      那么,TCMalloc_SystemRelease 做些什么呢?我们知道,无论是 brk,还是 mmap,申请的都是虚拟内存,物理内存是由内核在缺页中断时按需分配的。TCMalloc_SystemRelease 并没有调用 sbrk 和 munmap 将虚拟内存『归还』给操作系统,而是调用 madvise 系统调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    #if !defined(MADV_FREE) && defined(MADV_DONTNEED)
    # define MADV_FREE  MADV_DONTNEED
    #endif
    bool TCMalloc_SystemRelease(void* start, size_t length) {
    #ifdef MADV_FREE
      const size_t pagemask = pagesize - 1;
     
      size_t new_start = reinterpret_cast<size_t>(start);
      size_t end = new_start + length;
      size_t new_end = end;
      // Round up the starting address and round down the ending address
      // to be page aligned:
      new_start = (new_start + pagesize - 1) & ~pagemask;
      new_end = new_end & ~pagemask;
     
      if (new_end > new_start) {
        int result;
        do {
          result = madvise(reinterpret_cast<char*>(new_start),
              new_end - new_start, MADV_FREE);
        } while (result == -1 && errno == EAGAIN);
     
        return result != -1;
      }
    #endif
      return false;
    }

      因为 Linux 不支持 MADV_FREE,所以使用了 MADV_DONTNEED。使用 MADV_DONTNEED 调用 madvise,告诉内核这段内存今后『很可能』用不到了,其映射的物理内存尽管拿去好了!
      因此,TCMalloc_SystemRelease 只是告诉内核,物理内存可以回收以做它用,但虚拟空间还留着。
      那么,是不是 TCMalloc 永远不会归还虚拟空间呢?一般来说,是的。但当虚拟空间耗尽,或者虚拟空间有碎片,无法满足内存需要时,TCMalloc 会试图合并,然后把所有空闲的虚拟空间归还,然后从新分配。



沪ICP备19023445号-2号
友情链接