内存管理算法--伙伴算法

本来想年前就把内存管理这部分结束,可是计划赶不上变化啊,过年事情太多,只能一拖再拖了,今天终于把伙伴算法机制写完了。这一节不再像前面似的只给出理论框架,毕竟linux内存管理的两个算法--伙伴算法和slab网上资料太多了,在家时间比起上班毕竟是多一些,所以想直接用代码实现算了,这样不仅可以理解更深一些,或许哪天还能直接用的上。

在贴代码之前还是先得说一下算法思想,伙伴算法是linux内核用来分配页级别内存的管理算法,我们平时在申请内存时候,通常是以字节为单位的,但是有些时候需要大段的内存,这时候内核就以(页)4KB为单位来分配,这样每次分配的结果就是4KB的倍数。分配出来的线性地址一般都是连续的,但是不能保证物理地址连续,而有时候我们需要分配一些大段的物理地址也连续的内存,比如DMA操作。但是从前面两节分页管理机制可以看出,线性地址在进行映射时候是以4KB为单位随机的,所以必然会导致内存碎片,什么是内存碎片呢,以下图为例:

内存管理算法--伙伴算法_第1张图片

这是一个真实的物理内存,前面1-6表示6个页面,如果分配了3个4KB,占用了1、3、5,这时候我们想分配一个连续的8KB物理内存,2和4是8KB,但是不连续,不能分配使用。这时候相对于8KB连续物理内存来说,2和4就成了内存碎片了,这很容易理解。所以linux使用了伙伴管理算法来改善这个问题,对于算法基本理论,引用自谋篇博客:

http://www.cnblogs.com/leaven/archive/2010/12/03/1895134.html,内容如下:

Buddy System是一种经典的内存管理算法。在Unix和Linux操作系统中都有用到。其作用是减少存储空间中的空洞、减少碎片、增加利用率。避免外碎片的方法有两种:

a.利用分页单元把一组非连续的空闲页框映射到非连续的线性地址区间。

b.开发适当的技术来记录现存的空闲连续页框块的情况,以尽量避免为满足对小块的请求而把大块的空闲块进行分割。

基于下面三种原因,内核选择第二种避免方法:

a.在某些情况下,连续的页框确实必要。

b.即使连续页框的分配不是很必要,它在保持内核页表不变方面所起的作用也是不容忽视的。假如修改页表,则导致平均访存次数增加,从而频繁刷新TLB。

c.通过4M的页可以访问大块连续的物理内存,相对于4K页的使用,TLB未命中率降低,加快平均访存速度。

buddy算法将所有空闲页框分组为10个块链表,每个块链表分别包含1,2,4,8,16,32,64,128,256,512个连续的页框,每个块的第一个页框的物理地址是该块大小的整数倍。如,大小为16个页框的块,其起始地址是16*2^12的倍数。

例,假设要请求一个128个页框的块,算法先检查128个页框的链表是否有空闲块,如果没有则查256个页框的链 表,有则将256个页框的块分裂两份,一份使用,一份插入128个页框的链表。如果还没有,就查512个页框的链表,有的话就分裂为 128,128,256,一个128使用,剩余两个插入对应链表。如果在512还没查到,则返回出错信号。

回收过程相反,内核试图把大小为b的空闲伙伴合并为一个大小为2b的单独块,满足以下条件的两个块称为伙伴:

a.两个块具有相同的大小,记做b。

b.它们的物理地址是连续的。

c.第一个块的第一个页框的物理地址是2*b*2^12的倍数。

该算法迭代,如果成功合并所释放的块,会试图合并2b的块来形成更大的块。

算法思想上面说的很清楚了,概况如下:

1.把不同大小的内存块形成链表,在分配内存时候直接到链表查找有没有空闲块。

2.如果链表中没有空闲内存块,那么到更大的内存块链表中查找,查找到以后对其进行拆分

3.释放某个内存块时候,看其有无相邻的空闲内存块,如果有,合并为更大内存块链接入新的链表


下面就是源码实现了:

1.因为算法实现大量依赖与链表操作,所以我直接把linux比较经典的链表结构移植过来使用。里边有个宏定义 list_entry 有点特殊,里边某些关键字可能只有GNU支持,或者是C99的一些扩展,具体没有去查证。记得VC6.0是不支持的,我用的开发环境是windows环境的qt5.2,它也是支持的。

2.这并不是linux真实源码,而是在用户空间模拟内核伙伴算法

3.为了不对释放过程造成干扰,人为对内存块做了特殊处理:初始化时候让相同大小的内存块做到不相邻


首先是链表操作:
#ifndef _LIST_H
#define _LIST_H

//定义核心链表结构
struct list_head
{
    struct list_head *next, *prev;
};

//链表初始化
static inline void INIT_LIST_HEAD(struct list_head *list)
{
    list->next = list;
    list->prev = list;
}

//插入结点
static inline void __list_add(struct list_head *new_list,
                            struct list_head *prev, struct list_head *next)
{
    next->prev = new_list;
    new_list->next = next;
    new_list->prev = prev;
    prev->next = new_list;
}

//在链表头部插入
static inline void list_add(struct list_head *new_list, struct list_head *head)
{
    __list_add(new_list, head, head->next);
}

//删除任意结点
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
    next->prev = prev;
    prev->next = next;
}

static inline void list_del(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
}

//后序(指针向后走)遍历链表
#define list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)

//前序(指针向前走)遍历链表
#define list_for_each_prev(pos, head) \
    for (pos = (head)->prev; pos != (head); pos = pos->prev)

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

//这个宏中某些可能只有GNU支持,我实验的环境是windows下qt5.2,很幸运,也支持
#define list_entry(ptr, type, member) ({			\
    const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
    (type *)( (char *)__mptr - offsetof(type,member) );})

#endif


头文件
#ifndef MEM_MANAGE_H
#define MEM_MANAGE_H

#include "list.h"
#include 
#include 
#include 

#define MEM_NUM 11  //不同种类内存块总数目
#define BLOCK_BASE_SIZE 4096  //以4KB为单位分配

#define TRUE  1
#define FALSE 0

#define random(x) (rand()%x + 1)  //随机数范围(1到x),用于属性随机数产生
#define cloth2cover(num) ((num & num-1) == 0)? TRUE:FALSE  //判断是否是2^n

typedef unsigned int u32;

//内存链表数据结构
struct free_area_head
{
    u32 size;  //链表内存大小
    u32 num;  //相应内存块数目
    struct list_head list;
};

//内存块数据结构
struct free_chunk
{
    u32 size;   //单位是KB
    u32 addr;  //内存地址
    int dir;    //标识这块内存块是否被占用,1被占用,0空闲
    struct list_head list;
};

//函数声明
int mem_init();
int mem_alloc(int size);
inline int mem_avail(int size);
int mem_free(int size);

#endif // MEM_MANAGE_H

伙伴算法实现文件:
#include "mem_manage.h"

struct free_area_head mem_arr[MEM_NUM];  //指向不同种类内存块的指针数组
char *pmem = NULL;

/**
 * @brief 内存初始化函数,包括申请内存,分割内存,挂入管理链表等操作
 * @return 成功返回0,失败返回-1
 */
int mem_init(void)
{
    int i, j, total_size = 0;
    u32 trunk_size;
    struct free_chunk *tmp_chunk = NULL;

    for(i=0; i0; i--)
    {
        //第一个连续内存  4KB/8KB/16KB...2048KB/4096KB
        for(j=0; jsize = trunk_size / 1024;  //以KB为单位
            tmp_chunk->addr = pmem;  //记录下这个地址
            tmp_chunk->dir = 0;  //初始化内存卡未被占用
            list_add(&tmp_chunk->list, &mem_arr[j].list); //插入链表
            mem_arr[j].num++;  //相应链表内存块数目加1
            pmem += trunk_size; //指针相应往后移动4KB/8KB/16KB...2048KB/4096KB
        }
    }

    //我们把各个链表相关内容打印出来看一下
    struct list_head *pos;
    struct free_chunk *tmp;

    //首先是每个链表的内存块大小和内存块数目
    for(i=0; isize);
        }
        printf("\n");
    }

    //怎么验证内存地址的正确性呢?
    printf("\nthe 4KB list chunk addr is:\n");
    //由于list_add是头部插入,所以这里按照从尾部到头部的顺序打印
    list_for_each_prev(pos, &mem_arr[0].list)
    {
        tmp = list_entry(pos, struct free_chunk, list);
        printf("%u ", tmp->addr);
    }
    printf("\n\n");

    /*
        malloc mem success
        alloc mem init addr is 19136544

        the 0 list mem num is 11:4 4 4 4 4 4 4 4 4 4 4
        the 1 list mem num is 10:8 8 8 8 8 8 8 8 8 8
        the 2 list mem num is 9:16 16 16 16 16 16 16 16 16
        the 3 list mem num is 8:32 32 32 32 32 32 32 32
        the 4 list mem num is 7:64 64 64 64 64 64 64
        the 5 list mem num is 6:128 128 128 128 128 128
        the 6 list mem num is 5:256 256 256 256 256
        the 7 list mem num is 4:512 512 512 512
        the 8 list mem num is 3:1024 1024 1024
        the 9 list mem num is 2:2048 2048
        the 10 list mem num is 1:4096

        the 4KB list chunk addr is:
        19136544 27521056 31711264 33804320 34848800 35368992
        35627040 35754016 35815456 35844128 35856416

        这些地址每次运行程序都不一样,根据实际情况来看
        得到上面打印结果,首先我们看到11个链表的数目和每个内存卡的大小是正确的
        然后如何验证地址呢?理解这个需要自己画下图,以第一个链表的前两个4KB块的
        首地址为例,这两个首地址应该隔着这么大一块地址空间:
        4+8+16+32+64+128+256+512+1024+2048+4096 = ?KB
        那么 27521056 - 19136544 = 8384512(byte) = 8188KB
        你可以验算一下,这两个值是相等的,同理,你可以验证第2个和第3个的差值
        看是否和理论上的值一样
    */
    return 0;
}


/**
 * @brief 得到内存数组索引,实际上就是求size是2的几次幂
 */
static int get_index(int size)
{
    int i, tmp = 0;
    size /= 4;

    for(i=0; tmp < size; i++)
    {
        tmp = 1<list);  //把被拆分的大内存块从相应链表执行上断链
    mem_arr[src_index].num--;  //内存块数目-1
    printf("%d list separate 1 block\n", src_index);

    //拆分为block_num块
    pmem = block->addr;  //记录首地址
    dst_size = mem_arr[dst_index].size;  //目的内存块大小

    //拆分并入链
    struct free_chunk *tmp_chunk = NULL;

    printf("%d list increase %d block\n", dst_index, block_num);
    for(i=0; isize = dst_size / 1024;  //以KB为单位
        tmp_chunk->addr = pmem;  //记录下这个地址
        tmp_chunk->dir = 0;  //初始化内存卡未被占用
        list_add(&tmp_chunk->list, &mem_arr[dst_index].list); //插入链表
        mem_arr[dst_index].num++;  //相应链表内存块数目加1
        pmem += dst_size; //指针相应往后移动dst_size字节
    }

    //经过上面的循环,拆分入链操作就完成了,下面只需要把拆分的内存块选一块返回即可
    struct list_head *pos;
    struct free_chunk *tmp;

    //肯定能找到的
    list_for_each(pos, &mem_arr[dst_index].list)
    {
        tmp = list_entry(pos, struct free_chunk, list);
        if(tmp->dir == 0)
        {
            printf("malloc success,addr = %u\n", tmp->addr);
            tmp->dir = 1; //标记内存块为占用
            return;
        }
    }
}

/**
 * @brief 内联函数,判断输入的size是否合法
 */
inline int mem_avail(int size)
{
    if(size<4 || size>(4<<10))  //最小4KB,最大4096KB
    {
        printf("size must > 4 and <= 4096\n");
        return FALSE;
    }
    else if(!cloth2cover(size))  //必须是2的幂
    {
        printf("size must be 2^n\n");
        return FALSE;
    }
    else
    {
        return TRUE;
    }
}

/**
* @brief 模拟内核申请内存过程
* @param size 申请内存大小,单位为KB
* @return 成功返回0,失败返回-1
*/
int mem_alloc(int size)
{
    int index, i;

    if(!mem_avail(size))
    {
        return -1;
    }

    /*
      下面是伙伴算法内存申请的过程,思想如下:
      1.首先根据size大小去相应的连表上去查找有没有空闲的内存块
        如果有,那么直接返回地址,并把相应内存块的的dir标志置位
      2.如果没有,那么去它上一级链表中找空闲块,比如4KB没有,那就去8KB找
        如果8KB也没有,就去16KB找...
      3.如果上一级链表中找到了空闲块,那么把这个空闲块从上一级链表中分类
        拆分为相应大小的内存卡后链入查找的链表中
      4.如果直到4MB的内存卡都没有空闲块,那么返回错误,提示内存不足

      这段代码的难点在穷尽的查找比比size大的链表操作
    */
    index = get_index(size);

    printf("first find %d list\n", index);

    struct list_head *pos;
    struct free_chunk *tmp;

    list_for_each(pos, &mem_arr[index].list)
    {
        tmp = list_entry(pos, struct free_chunk, list);
        if(tmp->dir == 0)  //找到了一块
        {
            printf("malloc success,addr = %u\n", tmp->addr);
            tmp->dir = 1; //标记内存块为占用
            return 0;
        }
    }

    //如果执行到这里,那么说明对应的内存块链表没有找到空闲内存块
    printf("the %d list has no suitable mem block\n", index);

    //我们从比它大的内存块中再去查找,最大就是4MB的链表了
    for(i=index+1; idir == 0)  //找到了一块
            {
                printf("find a free block from %d list,addr = %u\n", i, tmp->addr);
                //把这块大的内存块拆分为小的,并进行分配处理
                separate_block(index, i, tmp);
                return 0;
            }
        }
    }

    //如果执行到这里,那么说明相应大小的内存块无法分配成功
    printf("can't malloc mem\n");
    return -1;
}

/**
 * @brief 判断两个地址是否相邻
 * @param compare_addr 比较的地址
 * @param target_addr  目标地址
 * @param size  链表上内存块的大小
 * @return 相邻:TRUE 不相邻:FALSE
 */
static int inline is_neighbor(u32 compare_addr, u32 target_addr, u32 size)
{
    //这里是无符号数,不能用绝对值
    if(compare_addr > target_addr)
    {
        if(compare_addr - target_addr == size)
            return TRUE;
        else
            return FALSE;
    }
    else
    {
        if(target_addr - compare_addr == size)
            return TRUE;
        else
            return FALSE;
    }
}

/**
 * @brief 从索引值为index的链表上查找block的伙伴内存块并返回
 * @param block
 * @param index
 * @return
 */
struct free_chunk *find_buddy(struct free_chunk *block, int index)
{
    //伙伴内存块:大小相同,地址相邻,并且也没有被占用
    struct list_head *pos;
    struct free_chunk *tmp;

    list_for_each(pos, &mem_arr[index].list)
    {
        tmp = list_entry(pos, struct free_chunk, list);
        if(tmp->dir == 0)  //没有被占用才有比较的资格
        {
            if(is_neighbor(tmp->addr, block->addr, block->size*1024))
            {
                return tmp;
            }
        }
    }

    //到这里就是没找到
    return NULL;
}

enum BUDDY_TYPE
{
    NO_BUDDY = 0,  //没有伙伴
    LAST_LIST,      //到了最后的链表了
};

/**
 * @brief 递归的查找伙伴并释放内存
 * @param block 需要释放的内存块
 * @param index 内存块所在的链表索引
 */
int recursive_free(struct free_chunk *block, int index)
{
    struct free_chunk *buddy;

    if(index > MEM_NUM)  //递归到了4MB的链表上
    {
        printf("max index list\n");
        return LAST_LIST;
    }

    buddy = find_buddy(block, index);  //在本链表上为它找一个“伙伴内存块”
    if(buddy == NULL)  //这个内存块没有”伙伴“
    {
        printf("this block has no buddy\n");
        block->dir = 0;  //释放它既可
        return NO_BUDDY;
    }
    else
    {
        printf("this block find a buddy\n");
        //两个内存块从原来链表断链
        list_del(&block->list);
        list_del(&buddy->list);
        mem_arr[index].num -= 2;  //少了两块
        printf("%d list decrease 2 block\n", index);

        //合并为新的内存块并入链下一级链表
        int new_addr = (block->addr < buddy->addr) ? block->addr:buddy->addr;

        struct free_chunk *tmp_chunk = NULL;
        tmp_chunk = (struct free_chunk *)malloc(sizeof(struct free_chunk));
        tmp_chunk->size = block->size * 2;  //是原来内存块的两倍大
        tmp_chunk->addr = new_addr;  //记录下这个地址
        tmp_chunk->dir = 0;  //初始化内存块未被占用
        index++;
        list_add(&tmp_chunk->list, &mem_arr[index].list); //插入链表
        mem_arr[index].num++;  //相应链表内存块数目加1
        printf("%d list increase 1 block\n", index);

        //循环这个过程,在上一级链表中查找伙伴,直到找不到伙伴或者到了4MB链表
        recursive_free(tmp_chunk, index);
    }
}

/**
* @brief 模拟内核释放内存过程
* @param size 释放内存大小,单位为KB
* @return 成功返回0,失败返回-1
*/
int mem_free(int size)
{
    int index, i;

    if(!mem_avail(size))
    {
        return -1;
    }

    /*
      下面是伙伴算法释放内存的过程,思想如下:
      1.首先根据size大小去相应的连表上去查找第一个被占用的内存块
      2.判断这个内存块是否有伙伴(大小相同,地址相邻)
      3.如果没有,直接把dir位清0即可
      4.如果有,那么把这两个内存块分别从所在的链表上断链,然后入链到上一级链表中
      5.到上一级链表中继续 2 3 4 操作,直到某一级链表没有伙伴为止
    */
    index = get_index(size);

    printf("first find %d list\n", index);

    struct list_head *pos;
    struct free_chunk *tmp;

    list_for_each(pos, &mem_arr[index].list)
    {
        tmp = list_entry(pos, struct free_chunk, list);
        if(tmp->dir == 1)  //找到了第一块被占用的内存
        {
            printf("find an occupy block,addr = %u\n", tmp->addr);
            recursive_free(tmp, index);
            return 0;
        }
    }

    //如果执行到这里,那么说明对应的内存块链表没有占用内存块
    printf("the %d list has no occupy mem block\n", index);
    return -1;
}
主函数:
#include "mem_manage.h"

int main()
{
    mem_init();  //初始化内存

    /*
        到这了为止我们的内存分配及链表的链接工作就已经做完了,下面就是在应用层模拟内核的伙伴算法
        我们怎么模拟呢,我们以4MB和2MB为实验材料,因为这两个消耗的快
        可以想象这么一种情况:
        1.第一次分配一个2MB,ok,没有问题,分配给你,然后又分配一个2MB,还是没有问题
        2.第三次分配2MB就出问题了,2MB的链表上已经没有2MB了,怎么办
        3.拆4MB的,把4MB的内存卡拆为两个2MB的,这两个地址是连续的,并把这个内存块从
          4MB的链表删除,链入2MB的链表中,我们记这两个内存块为a和b
        4.这时候如果再分配4MB的,就会提示没有可分配的内存,而2MB的又有了
        5.当我们释放a时候,没有什么发生,但是a释放后如果再释放了b,就会把a和b组成新的4MB
          内存块重新链入4MB的内存链表
    */
    char c;
    int size;
    while(1)
    {
        printf("\nmalloc type m, free type f:");
        scanf("%c",&c);

        if(c == 'm')
        {
            printf("\ninput alloc mem size,unit is KB :");
            scanf("%d",&size);
            if(!mem_avail(size))
            {
                continue;
            }

            mem_alloc(size);
        }
        else if(c == 'f')
        {
            printf("\nfree mem size,unit is KB :");
            scanf("%d",&size);
            if(!mem_avail(size))
            {
                continue;
            }

            mem_free(size);
        }
        else
        {
//            printf("\nerr input, again\n");
            continue;
        }
    }
}

看下运行截图,首先是分配过程,为了快速演示,使用2MB的内存块进行分配。因为它只有两个块,分配到第三次时候,就需要拆分4MB的内存块了。

图中m是输入malloc的意思


内存管理算法--伙伴算法_第2张图片

从图上可以看出,第一次分配2048KB(2MB)时候,直接从链表9上分配成功,第二次分配,还是从链表9分配成功,但是到第三次,由于链表9上只有两个2MB的内存块,故在第一次查找链表9的时候,已经没有空闲的内存块了,然后就去4MB的链表上查找,发现有空闲的4MB内存块,所以就把这个内存块拆分为两个2MB的内存块,然后加到链表9上,模拟了伙伴算法的分配过程。


再来看释放过程。在释放之前,又进行了一次分配,把上面链表9刚得到的2MB内存块都占用,然后开始释放2MB的内存块。如下图


内存管理算法--伙伴算法_第3张图片

首先第一次释放2MB内存块时候,因为已经没有空闲的内存块了,所以肯定不会找到“伙伴”,第二次释放时候,就和第一次释放的空闲内存块组成“伙伴”,所以会打印找到一个伙伴,然后合成4MB的内存块,所以链表9会失去两个2MB的内存块,而链表10会增加一个4MB的内存块。然后再从链表10上查找有没有刚刚合成的4MB内存块的“伙伴”,没有找到,所以打印最后一句没有找到“伙伴”。模拟了伙伴算法的释放过程。




你可能感兴趣的:(linux驱动内核框架)