伙伴算法及实现上的改进
可变内存管理,随着内存的不断分配和回收,即使系统中有1MB的内存,也可能因没法分配大小为100KB的连续内存块而造成分配失败。
伙伴系统,可以大大改善这一情况。
伙伴系统的缺点:
- 仅管大小为2K内存块回收时只需要搜索同样字节大小的块以判断是否需要合并,但是时间还是没法确定的。内存块回收时链表遍历的时间之所以无法确定,是因为链表只能顺序搜索,那么复杂度就是O(n),如果能实现一种O(1)复杂度的搜索算法就可以解决这个问题。
- 内存控制块的组织问题。一般的实现方式是在内存块的开始部分预留出一段内存来作为内存控制块。
这样做有一个缺点,如用户分配512B的内存块,内存管理系统经过计算后知道需要的内存块其实为522B,因此,系统会分配一个1024B大小的内存块,本来用户为了充分利用内存,分配512B,但由于多了一个内存控制块,反而浪费得更多:1024-522=502B。
问题二的产生主要是因为内存控制块本身也属于内存块的一部分,把控制块单独抽出来,而真正可用的内存块留给用户就可以解决问题。
aCoral伙伴算法的实现思路
首先真正的物理内存块被分成了两部分,一部分为内存控制结构所使用,内存初始化函数buddy_init()将逐个初始化这些结构。
剩下的内存是用户可用内存。
这些内存被划分为总多基本块,每个基本块的大小可以通过常量BLOCK_SIZE配置。(默认1<<7字节)
这样内存的分配和回收都是基于序列的了,换句话说,这些基本内存块是从0到n逐个标记的。
在逻辑上这些内存块被组织成了m层,最大层数m可以通过常量LEVEL(14)进行配置。
第0层每个内存块的大小为BLOCK_SIZE,第1层每个内存块的大小为2*BLOCK_SIZE,以此类推,第n层内存块的大小为BLOCKSIZE<<n。
如图所示,这里的内存共包括8个基本内存块,在逻辑上被组织成了3层。
当需要分配2i字节大小的内存块时,用公式log (2i/BLOCK_SIZE),可求出从哪一个逻辑层分配内存。
如果该层有空闲内存块,即可分配。
当这一层没有空闲的内存块时,就向上层申请,最终会得到两个空闲的2i字节大小的内存块。
为了最终对应到物理块,这些逻辑的内存块始终是有序号标记的。
以8个基本内存块大小的内存举例。
- 开始的时候,8个基本内存块全部空闲(其序号依次为0,…,7),内存初始化时,可能将这8个基本内存块注册在第3层(详见伙伴系统初始化buddy_init(),第0层的基本内存块可能不能直接使用,用户只感觉到第3层的内存块(该层的内存块大小就是BLOCK_SIZE<<8=3))。
- 当系统申请一个BLOCK_SIZE大小的内存块,根据公式可得,从第0层开始分配,而这时除了第3层,其余层的内存块都不可用(内存初始化时设定)。
- 那么第0层向第1层申请,第1层的内存块也不可以,再向第2层申请,直到第3层。
- 第3层将唯一的一个内存块分成两个,供第二层使用,第二层取出一个(通常是序列号小的,即由基本块0,1,2,3组成)分配给第1层,另外一个标记为空闲。
- 依次向下,第0层有了两个空闲块,即基本内存块0、1.
- 根据基本内存块的序号(这里是0)转换成相应的物理地址返回给调用函数。
- 内存回收的时候,传入的参数是地址,先把地址转换成序号,再做回收。
- 回收的同时如果发现伙伴也是空闲,则向上合并成一个大的空闲块(最高层除外),从而减少外碎片。
为了标记每个逻辑内存块的空闲状态和快速找到一个空闲块,每层需要一个空闲状态位图块、空闲内存块链表数组、空闲内存块链表头三个结构。同时为了回收的效率,还需要为每个逻辑内存块存储逻辑层信息,即原来从哪一个逻辑层分配。
typedef struct{
int *free_list[LEVEL]; //各层空闲内存块链表
unsigned int *bitmap[LEVEL];//各层空闲位图
int free_cur[LEVEL];//各层空闲内存块链表头
unsigned int num[LEVEL]; //各层内存块个数
char level;//伙伴系统的层数
unsigned int start_add; //伙伴系统管理的内存起始地址
unsigned int end_addr; //末尾地址
unsigned int block_num; //基本内存块的数量,等于num[0]
unsigned int free_num; //剩余基本内存块数量
unsigned int block_size; //基本内存块大小
}
内存控制块acoral_block_ctrl_t是aCoral进行内存分配和回收的关键数据结构,其中的一个重要数据结构unsigned int*bitmap[LEVEL]是描述内存块状态的状态位图块数组,每一层均有一个空闲状态位图块数组bitmap,bitmap实际上是一个二维数组bitmap[m][n],第一个下标代表位图块所在的层数m,第二个下标代表该层的第n个位图块。
状态位图块数组bitmap[m][n]的每个值都是int类型,32位的,每一位要么为1,要么为0.
为0表示相邻两块内存块没有空闲的,为1表示相邻两块内存块至少有一块是空闲的。
由于bitmap[m][n]的每个值是32位,而每一位代表相邻两块内存块,所以biitmap[m][n]的每个值可以表示64个内存块的分配情况。
0~(m-1)层中,相邻的两块内存块由空闲位图块中的一位来标识是否空闲,对于第i层,每个内存块由空闲位图块中的一位来标识是否空闲。
1位只有0和1两种状态,则两块内存块有4种状态:两块都空闲、没有空闲块、只有奇数块空闲、只有偶数块空闲。两个状态如何表示4种状态?
这得从伙伴系统思想说起了,当伙伴系统回收时,如果导致某一层相邻两个内存块都空闲时,就会向上一层回收,将两个伙伴合并成一个更大的内存块。因此,正常情况下不存在两块都空闲的情况,0~(m-1)层相邻的两块内存块只有两种状态:没有空闲块、有一块是空闲的。
虽然通过状态位图块数组解决了回收时复杂度O(n)的问题,但没有解决空闲内存块分配问题,即分配内存时如何查找某一层空闲的内存块。
大家可能说,直接查看该层的内存状态位图数组中哪一位为1就可以了,但是如果是这样的话,和遍历链表没有本质区别,复杂度也是O(n)。
所以,aCoral增加了空闲块链表数组int *free_list[LEVEL],实现了分配时O(1)的复杂度,同时还可以解决内存控制块(链表的实体)占用部分内存块导致的问题。
首先,定义了空闲位图块链表头数组int free_cur[LEVEL];该数组元素指向第一个空闲位图块的标号。然后free_list[LEVEL]的值指出了下一个空闲位图块。
例如,对于第0层,free_cur[0]=2,那么读取free_list[0][2],得到下一个空闲块,假设其值为4,则读取free_list[0][4],再得到下一个空闲位图块,依次往后,形成一个空闲位图块的表链。
注意,这里的2和4表示的是第0层内存状态位图块的标号,这样2表示此时标号为2的内存状态位图块中的32位中有非0位,即这个非零位所对应的相邻内存块由空闲。
根据前面的描述,只要根据第m层的free_cur[m]找出空闲位图块对应的标号i,然后读取bitmap[m][i]的值,再判断bitmap[m][i]首先出现“1”的那一位,并找到该位对应的内存块序号,便可确定该内存块对应的基本内存块标号,最后得到相应物理地址返回给用户,并将刚才的“1”置为0,如果此时空闲状态位图块变为了0(32位每一位都为0),更改free_cur[m]=free_list[m][i],由此可见,根据空闲内存块链表数组就能快速找到空闲内存块,而对链表的维护的复杂度也是O(1)。
还有一个问题就是,系统回收内存块的时候,传送的是地址,根据地址可以知道这个内存块开始地址对应的基本内存块的标号,但是如何知道这块内存块的大小呢?即从第几层分配呢?我们知道不同层分配的内存块包含的基本内存块是不一样的,0层包含1个基本内存块,1层包含2个基本内存块。
因此,不同层分配出去的内存块的起始地址可能相同(对应的基本内存块编号相同),这就需要一个数据结构来保存基本内存块i(i=1,2,3)的起始地址所对应的逻辑内存块大小。
typedef struct{
char level;
}acoral_block_t;
由于标号为奇数的基本内存块肯定是从第0层分配出去(因为偶数的基本内存块由非0层分配出去),因此不用保存其是从第几层分配出去的。
如果某个基本内存块尚未分配出去,则level的值为-1。
level的值同时也可用来区分内存块位图管理时的两块兄弟内存块的状态。
根据前面的描述,在1~m-1层,状态位图块bitmap[m][n]的某一位为1时说明该位图管理的奇数块或偶数基本内存块在使用,但是如果偶数块使用了的话,其对应的acoral_block_t的level值大于0,否则为-1,因此可区分这两种状态。
伙伴算法的初始化
aCoral伙伴算法能正常工作以前,需要通过buddy_init()进行初始化。
buddy_init()传入参数是start_addr和end_addr,分别是系统可用物理内存的起始和终止地址。
#defiine acoral_mem_init(start,end) buddy_init(start,end)
acoral_mem_init((unsigned int)&head_start, (unsigned int)*heap_end);
acoral_block_ctrl_t *acoral_mem_ctrl;
acoral_block_t *acoral_mem_blocks;
unsigned int buddy_init(unsigned int start_adr, unsigned int end_adr)
{
int i, k;
unsigned int resize_size;
unsigned int save_adr;
unsigned int index;
unsigned int num = 1;
unsigned int adjust_level = 1;
int level = 0;
unsigned int max_num, o_num;
unsigned int cur;
start_adr += 3;
start_adr &= ~(4-1);
end_adr &= ~(4-1);
resize_size = BASIC_BLOCK_SIZE;
end_adr = end_adr - sizeof(acoral_block_ctrl_t); //减去内存控制块的大小,剩下的才是可分配内存
end_adr &= ~(4-1);
acoral_mem_ctrl = (acoral_block_ctr_t *)end_adr; //内存控制块的地址
//内存少,不分配
if(start_adr > end_adr || end_adr - start_adr < BASIC_BLOCK_SZE)
{
acoral_mem_ctrl->state = MEM_NO_ALLOC;
return -1;
}
acoral_mem_ctrl->state = MEM_OK;
/*根据基本内存块的值和堆的大小获得最大层数,如基本内存块BLOCK_SIZE=4B,而内存的大小为18B,则最大层数为3,第0层是4B,第1层是8B,第二层16B,num记录系统总内存可以分成多少个BLOCK_SIZE大小的基本内存。*/
while(1)
{
if(end_adr <= start_adr + resize_size)
break;
resize_size = resize_size << 1;
num = num << 1;
adjust_level++;
}
//根据num为最小内存控制块(第0层)acoral_block_t分配空间(每一个最小内存控制块均有一个acoral_block_t),因为一个acoral_block_t的大小就是1B,结束地址减去num正好是最小内存控制块数组的开始地址
acoral_mem_blocks = (acoral_blck_t *)end_adr - num;
save_adr = (unsigned int)acoral_mem_blocks;
//如果层数较小,则最大层用一块构成,如果层数较多,限制层数范围,最大层由多块构成
level = adjust_delevl;
if(adjust_level > LEVEL)
level = LEVEL;
//用刚刚计算的基本内存块num除以32,就变成了所需内存块位图数组bitmap的维数,即需要多个32位的内存位图块。
num = num/32;
//for循环是用来分配0~m-1层内存控制块的空间。由于内存块位图数组bitmap的某一位代表了两块内存块,因此,对于第0层需要num/2个32位的内存位图块,所以num/2,level变量用来记录以该基本内存块为起始地址的内存块分配时所在的层
for(i=0; i<level-1; i++)
{
num = num >> 1; //除去最大层,其它每层的32位图都是64个块构成,所以要除以2
if(num == 0)
{
num = 1; //不足一个位图的,用一个位图表示
}
save_adr -= num*4; //每一个32位位图4个字节
save_adr &= ~(4-1); //四字节对齐
acoral_mem_ctrl->bitmap[i] = (unsigned int *)save_adr;
acoral_mem_ctrl->num[i] = num;
save_adr -= num*4;
save_adr &= ~(4-1); //四字节对齐
acoral_mem_ctrl->free_list[i] = (int *)save_adr;
for(k=0; k<num; k++)
{
acoral_mem_ctrl->bitmap[i][k] = 0;
acoral_mem_ctrl->free_list[i][k] = -1;
}
acoral_mem_ctrl->free_cur[i] = -1;
}
//最大内存块层不足一个位图的,用一个位图表示,这里num没有除以二,因为最高内存块位图一位对应一个内存块,这一层的相邻内存块都空闲时无法向上回收,存在相邻两块空闲的情况,故需要1位对应一块
if(num == 0)
{
num = 1;
}
save_adr -= num*4; //每一个32位位图4个字节
save_adr &= ~(4-1); //四字节对齐
acoral_mem_ctrl->bitmap[i] = (unsigned int *)save_adr;
acoral_mem_ctrl->num[i] = num;
save_adr -= num*4;
save_adr &= ~(4-1); //四字节对齐
acoral_mem_ctrl->free_list[i] = (int *)save_adr;
for(k=0; k<num; k++)
{
acoral_mem_ctrl->bitmap[i][k] = 0;
acoral_mem_ctrl->free_list[i][k] = -1;
}
acoral_mem_ctrl->free_cur[i] = -1;
// 如果剩余内存大小不够形成现在的level,对伙伴系统层数m进行调整,如果将刚才描述的数据结构分配出去后,最初的层数比当前系统的层数少1,则减少层数。例如,基本内存块为1KB,而初始的堆内存大小为1.024MB,则可知最开始算的层数为11。但是将内存管理需要的控制块的内存分配后,可能只剩下999KB,只需要10层即可管理,所以对层数进行调整
if(save_adr <= (start_adr + (resize_size >> 1)))
adjust_level--;
if(adject_level > LEVEL)
level = LEVEL;
//初始化内存控制块
acoral_mem_ctrl->level = level;
acoral_mem_ctrl->start_adr = start_adr;
num = (save_adr - start_adr) >> BLOCK_SHIFT;
acoral_mem_ctrl->end_adr = start_adr + (num << BLOCK_SHIFT);
acoral_mem_ctrl->block_num = num;
acoral_mem_ctrl->free_num = num;
acoral_mem_ctrl->block_size = BASIC_BLOCK_SIZE;
i = 0;
max_num = (1 << level) - 1;
o_num = 0;
//设置第level-1层(即最高层)空闲位图块链表头的值,可见空闲位图块指向该层的0号空闲内存块free_list。
if (num > 0)
{ // 有内存块,则最大内存块层的free_cur为0
acoral_mem_ctrl->free_cur[level - 1] = 0;
}
else
{ // 无内存块,则最大内存块层的free_cur为-1
acoral_mem_ctrl->free_cur[level - 1] = -1;
}
//有了前面的准备工作,接下来开始把实际可用的内存分配到各个逻辑层,分配的原则是:首先将内存都尽量分配给高层,直到剩下的内存不够这一层的一个内存块大小,再依次分配给低层。
//首先考虑最高层:(level-1)层,如果该层的内存块数是32的倍数,整块内存优先分给最大内存块层,计算当前可分配内存容量能否直接形成一个最大内存块层的32位图
while (num >= max_num * 32)
{
acoral_mem_ctrl->bitmap[level - 1][i] = -1;
;
acoral_mem_ctrl->free_list[level - 1][i] = i + 1;
num -= max_num * 32;
o_num += max_num * 32;
i++;
}
if (num == 0)
{ // 所有块正好分配到最大内存块层的32位图
acoral_mem_ctrl->free_list[level - 1][i - 1] = -1;
}
// 计算当前可分配内存是否还能形成最大内存块层的一块
while (num >= max_num)
{
index = (o_num >> level) - 1;
acoral_set_bit(index, acoral_mem_ctrl->bitmap[level - 1]);
num -= max_num;
o_num += max_num;
}
acoral_mem_ctrl->free_list[level - 1][i] = -1;
// 接下来的每层初始化
while (--level > 0)
{
index = o_num >> level;
if (num == 0)
break;
cur = index / 32;
max_num = (1 << level) - 1; // 每层的内存块大小
if (num >= max_num)
{
acoral_mem_blocks[BLOCK_INDEX(o_num)].level = -1;
acoral_set_bit(index, acoral_mem_ctrl->bitmap[level - 1]);
acoral_mem_ctrl->free_list[level - 1][cur] = -1;
acoral_mem_ctrl->free_cur[level - 1] = cur;
o_num += max_num;
num -= max_num;
}
}
return 0;
}
起始地址要增加它的值然后对齐,结束地址要减少它的值然后对齐。
如起始地址:0x7 = 0111 加3 = 1010 & (1100) = 1000 = 0x8
结束地址:0x7 = 0111 & (1100) = 0100 = 0x4
递归把剩下的内存块分配给其它逻辑层。例如最高层(N)的一个内存块大小为16个基本内存块,而给顶层分配完了后,可能只剩下15个基本内存块(不足以形成一个最高层的内存块),则给N-1层分配8个基本内存块大小的内存,N-2层分配4个基本内存块大小,N-3层分配2个,0层分配1个基本内存块大小的内存。
经过buddy_init()对伙伴系统进行初始化后,内存都尽量分配给了高层 ,低层的free_list的值都被置为-1了,这意味着,底层的内存块不能之间被使用,直到剩下的内存不够某层的一个内存块大小,再依次分配给低层。