伙伴系统的一种简单实现

什么是伙伴系统

伙伴系统是一种将资源分配出去,再回收回来的一种方法,典型的使用场景是内存池。

伙伴系统的基本原理

当要实现内存管理时,通常会面对分配粒度的问题。要对内存进行分配,如果固定粒度的话,一定会造成内部碎片,而且根据放置算法的不同,还表现出不同的特点。

伙伴分配器可以实现这样的作用:将内存将2的幂进行划分:

  1. 需要分配内存时如果没有合适的内存块,会对半切分内存块直到分离出合适大小的内存块为止,最后再将其返回。
  2. 需要释放内存时,会寻找相邻的块,如果其已经释放了,就将这俩合并,再递归这个过程,直到无法再合并为止

伙伴系统的实现

伙伴分配器的数据结构在逻辑上的表示就像是一个完全二叉树。大概像这样:

img

当然实际编码过程中并不会使用一个struct TreeNode的形式去把二叉树的各个节点用指针连起来,因为是这个树一定是完全二叉树,所以可以使用一个数组来表示树的结构。这里分析一个COOLSHELL网站上提出极简实现。

首先数据结构长这样:

struct buddy {
  unsigned size;
  unsigned longest[0];
};

在C中,这就是一个变长数组的定义。这个实现中,默认要管理的内存是2的幂次。这里的size是指buffer的长度,longest数组的长度是2*size-1,因为这是满二叉树的结点数,所以整个buddy的内存占用大小就是2*size-1,longest数组每个元素都对应一个二叉树的结点,元素的值代表对应内存块的空闲容量。据此,初始化的代码如下:

inline bool is_power_of_2(unsigned x) {
    return !(x & (x - 1));
}

struct buddy* buddy_new(int size) {
    struct buddy* self;
    unsigned node_size;
    int i;

    if (size < 1 || !is_power_of_2(size)) {
        return NULL;
    }

    self = (struct buddy*)malloc(2 * size * sizeof(unsigned));
    self->size = size;
    node_size = size * 2;

    // 遍历每个二叉树结点,为其赋值
    for (i = 0; i < 2 * size - 1; ++i) {
		// 注意:1也是2的幂
        if (is_power_of_2(i + 1)) {
            node_size /= 2;
        }

        // 第一层的结点的值是2*size,第二层是size,第三次是size/2,以此类推
        // 每个值代表的是空闲内存块的数量
        self->longest[i] = node_size;
    }

    return self;
}

可以看到,整个管理其所占的内存块大小是需要管理内存大小的两倍。

在完成了初始化后,下面实现从伙伴分配器中分配内存的过程。基本思路是:

  1. 首先需要将要分配的size向上调整为2的幂次,比如33只能调整到64,并检测是否超过最大限度
  2. 进行深度优先搜索,搜到一个最合适的块,将longest的元素值标记为0
  3. 最后将这个内存块的偏移返回
unsigned fixsize(unsigned size) {
  size |= size >> 1;
  size |= size >> 2;
  size |= size >> 4;
  size |= size >> 8;
  size |= size >> 16;
  return size + 1;
}

inline unsigned left_leaf(unsigned index) {
    return index * 2 + 1;
}

inline unsigned right_leaf(unsigned index) {
    return index * 2 + 2;
}

inline unsigned parent(unsigned index) {
    return (index + 1) / 2 - 1;
}

int buddy_alloc(struct buddy *self, int size) {
    unsigned index = 0;
    unsigned node_size;
    unsigned offset = 0;
    
    if(self == NULL) {
        return -1;
    }
    
    if(size <= 0) {
        size = 1;
    } else if(!IS_POWER_OF_2(size)) {
        size = fixsize(size); // 向上调整到2的幂次
    }
    
    // 如果根节点下挂的size都不够分配,就返回失败
    if(self->longest[index] < size) {
        return -1;
    }
    
    // 由大到小搜索最符合size的结点
    // 并在搜索的过程中,更新index,优先使用左孩子
    for(node_size = self->size; node_size != size; node_size /= 2) {
        if(self->longest[left_leaf(index)] >= size) {
            index = left_leaf(index);
        } else {
            index = right_leaf(index);
        }
    }
    
    // 找到对应的结点了,就将其管理的空闲内存块数量标记为0
    self->longest[index] = 0;
    
    // 这里的node_size是对应层的结点所管理的内存的大小,而index是结点的编
    // 根据这个算法,offset恰好是分配内存的起始/索引,从offset往后数size个字节的内存都是可用的
    offset = (index + 1) * node_size - self->size;
    
    // 因为更新了longest[index]的标记,所以需要更新它上层所有父节点的标记
    // 其中的原理是,如果小块内存被占用,那么大块内存就不满足原来的可用状态了
    while(index) {
        index = parent(index);
        self->longest[index] = std::max(self->longest[left_leaf(index)],
                                       self->longest[right_leaf(index)]);
    }
    
    return offset;
}

释放的接口实现的基本思路是:

  1. 函数传入之前buddy_alloc返回的offset,也就是地址索引,确保是有效值
  2. 和buddy_alloc的方向相反,做回溯,恢复结点的值
  3. 检查需要合并的内存块
void buddy_free(struct buddy *self, int offset) {
    unsigned node_size, index = 0;
    unsigned left_longest, right_longest;
    
    assert(self && offset >= 0 && offset < size);
    
    node_size = 1;
    index = offset + self->size - 1;
    
    // 找到被占用的那个内存块,并更新node_size,即那层的结点所管理的内存块的大小
    for(; self->longest[index] != 0; index = parent(index)) {
        node_size *= 2;
        
        // 如果根节点的都被占用了,则直接返回
        if(index == 0) {
            return;
        }
    }
    
    // 归还内存,将longest恢复到原来结点的值
    self->longest[index] = node_size;
    
    // 接下来恢复被占用结点的所有父节点
    while(index) {
        index = parent(index);
        node_size *= 2;
        
        // 在向上回溯的过程中,如果发现左右孩子的元素加起来等于自己,则说明需要合并
        // 否则取他们中大的那个
        left_longest = self->longest[left_leaf(index)];
        right_longest = self->longest[right_leaf(index)];
        
        if(left_longest + right_longest == node_size) {
             self->longest[index] = node_size;
        } else {
            self->longest[index] = std::max(left_longest, right_longest);
        }
    }
}

怎么用?

说到底的是伙伴系统只是个辅助系统,buddy的longest数组并不能当作内存池来用,而是要自己额外分配一段和buddy的size大小符合的内存与buddy搭配使用。

比如拿到了buddy_alloc返回的offset,就取实际内存池的[offset, offset + size]这段内存使用。

posted @ 2020-10-12 18:02  joeyzzz  阅读(1501)  评论(0编辑  收藏  举报