2

前面已经讲了一些C中基本的内存管理的方式,Fist-fit, Best-fit都是基于对象的大小来分配或者找到合适的大小的。当涉及到大量的malloc,free的时候频繁的系统调用肯定会影响到系统的性能,这里有一种更有效的内存管理方式就是基于块的内存分配方式也就是我们经常说的内存池(在软件的世界中你可以发现很多类似的池化设计),下面就来实现一个简单的内存池。

我们使用如下的数据结构来管理内存池:

struct T {
  T prev;
  char *avail;
  char *limit;
};

prev指向存储块的头, avail指向第一个可用地址,limit指向存储块的末尾地址,其中从avail到limit的空间都是我们可以使用空间。下图就是一个有三个内存块的内存池的状态,其中阴影部分表示这部分内存已经被使用了。
图片描述

下面我们就来看看具体的代码怎么去实现。我们可以从头开始看,首先有的肯定是这个数据结构的头了,其实这就是我们在数据结构书中经常看到的有头链表的实现方式。不同点在于我们在练习的时候每个里面就是简单的存储了一些数据而这里每个块都有一个很大的内存, 当有一个alloc请求的时候就从list里找到一个合适的块,然后在这上面进行分配。

T Arena_new() {
  T arena = malloc(sizeof(*arena));

  if (arena == NULL) {
    fprintf(stderr, "ALLOC MEM FAILED\n");
    return NULL;
  }
  arena->prev = NULL;
  arena->limit = arena->avail = NULL;
  return arena;
}

等有了header之后我们接下来要做的就是分配内存块并对他进行管理了,下面还是直接上代码:

void *Arena_alloc(T arena, long nbytes,const char *file,
                  int line){
  assert(arena);
  assert(nbytes > 0);

  // for memory align                                                                                                                                                                                       
  size_t ldsize = sizeof(long double);
  nbytes = (((nbytes + ldsize - 1) / ldsize) * ldsize);

  while(nbytes > arena->limit - arena->avail) {

    T ptr;
    char *limit;
    if ((ptr = freechunks) != NULL) {
      freechunks = freechunks->prev;
      nfree--;
      limit = ptr->limit;
    } else {
      long m = sizeof(struct T) + nbytes + 10 * 1024;
      ptr = (T)malloc(m);
      if (ptr == NULL) {
        fprintf(stderr, "ALLOC MEM FAILED %s:%d\n", file, line);
      }
      limit = (char *)ptr + m;
    }
    *ptr = *arena;
    arena->avail= (char *)((struct T *)ptr + 1);
    arena->limit = limit;
    arena->prev = ptr;
  }
  arena->avail += nbytes;
  return arena->avail - nbytes;
}

基于我们这里每个块是10k+的大小,所以大多的内存分配都是直接的从header->prev上面分配一块合适的内存,while体应该是很少有机会能执行到。如果header->prev上的空间无法满足我们的请求的时候我们才去寻找一个更合适的空间(代码中while部分),这里我们也有一个类似缓存的设计,当free一块内存块的时候我们并不是立马的归还给操作系统,而是把他放到本地的freechuncks中保存起来,当有新的请求的时候我们首先从freechunks里的第一个块加入到第一个内存块上,然后再去check这块内存是否满足request,如果合适就直接在该块上分配,否则循环直到freechunks里的块全部都加入进来,如果将全部的freechunks都加入到了linklist中仍然无法满足请求则通过malloc分配一个内存给他。下面是一个简单的图例,其中左边为从freechunks取出一块内存之前,虚线表示讲内存块加入到arena指向的linkList中。
图片描述

下面我们来看看对应的free函数。有了之前的freechunks的概念之后理解free函数起来就相对简单很多了,这里只是简单的遍历整个arena,如果freechunks的大小小于threshold那么就把整个块放入freechunks里,负责就直接调用系统free释放之。

void Arena_free(T arena) {
  assert(arena);
  while(arena->prev) {
    struct T tmp = *arena->prev;
    if (nfree < THRESHOLD) {
      arena->prev->prev = freechunks;
      freechunks = arena->prev;
      nfree++;
      freechunks->limit = arena->limit;
    } else
      free(arena->prev);
    *arena = tmp;
  }

当然啦这里有很多问题,就如练习6.1 我们只查找了arena描述的内存块,如果没有足够的空间就分配一块新的内存块,即使后面的内存块有足够的空间。我的理解是这里我们使用了freechuncks来缓存了一些内存块,这些内存块的大小都是10k+,我觉得10k+是足够新的请求的,如果一次请求的数据块大于10k的话我觉的我们的设计是需要重新思考的。

这里就是一个简答的内存池的实现。全部代码可以参考github.
-END-


lzh2nix
52 声望19 粉丝

2019年目标: