Linux内存管理之bootmem分配器

      为什么要使用bootmem分配器,内存管理不是有buddy系统和slab分配器吗?由于在系统初始化的时候需要执行一些内存管理,内存分配的任务,这个时候buddy系统,slab分配器等并没有被初始化好,此时就引入了一种内存管理器bootmem分配器在系统初始化的时候进行内存管理与分配,当buddy系统和slab分配器初始化好后,在mem_init()中对bootmem分配器进行释放,内存管理与分配由buddy系统,slab分配器等进行接管。

     bootmem分配器使用一个bitmap来标记物理页是否被占用,分配的时候按照第一适应的原则,从bitmap中进行查找,如果这位为1,表示已经被占用,否则表示未被占用。为什么系统运行的时候不使用bootmem分配器呢?bootmem分配器每次在bitmap中进行线性搜索,效率非常低,而且在内存的起始端留下许多小的空闲碎片,在需要非常大的内存块的时候,检查位图这一过程就显得代价很高。bootmem分配器是用于在启动阶段分配内存的,对该分配器的需求集中于简单性方面,而不是性能和通用性。

 

一、bootmem分配器核心数据结构

bootmem核心数据结构bootmem_data:

typedef struct bootmem_data {
    unsigned long node_boot_start;
    unsigned long node_low_pfn;
    void *node_bootmem_map;
    unsigned long last_offset;
    unsigned long last_pos;
    unsigned long last_success;
} bootmem_data_t;

     系统内存的中每一个结点(如果是内存是UMA,就只有一个这样的结构)都有一个bootmem_data_t结构,它含有bootmem分配器给结点分配内存时所需的信息。

     1、node_boot_start是这个结点内存的起始地址(如果内存是UMA的话,为0);

     2、node_low_pfn是低端内存最后一个page的页帧号;

     3、node_bootmem_map指向内存中bitmap所在的位置;

     4、last_offset是分配的最后一个页内的偏移,如果该页完全使用,则offset为0;

     5、last_pos是分配最后一个页帧号;

     6、last_success是最后一次成功分配的位置。

 

二、bootmem分配器初始化

     是在init_bootmem_core函数对bootmem分配器进行初始化的。

static unsigned long __init init_bootmem_core (pg_data_t *pgdat,
    unsigned long mapstart, unsigned long start, unsigned long end)
{
    bootmem_data_t *bdata = pgdat->bdata;
    unsigned long mapsize = ((end - start)+7)/8;  /*映射的字节数*/

    pgdat->pgdat_next = pgdat_list;
    pgdat_list = pgdat;

    mapsize = (mapsize + (sizeof(long) - 1UL)) & ~(sizeof(long) - 1UL);
    bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT);
    bdata->node_boot_start = (start << PAGE_SHIFT);
    bdata->node_low_pfn = end;

    memset(bdata->node_bootmem_map, 0xff, mapsize);  //初始化所有内存都保留

    return mapsize;
}

     linux系统初始化过程中通过init_bootmem_core(NODE_DATA(0), min_low_pfn, 0, max_low_pfn)来调用这个函数初始化bootmem分配器。其中min_low_pfn是临时页表后的第一个可用页框,max_low_pfn是低端内存结束的页框。

     前面说的bootmem分配器核心数据结构bootmem_data_t是内嵌在pg_data_t数据结构中(也就是节点的描述符中)。内存中的节点描述符是通过链表pgdat_list链起来的,如果内存为UMA的话,这个链表就只有一个节点。

     mapsize按字节来计算系统低端内存对应的bitmap的大小,并且保证它是4的倍数。bitmap存放在min_low_pfn开始的页框中,并将bitmap的每一位置1,表示所有的内存页保留(为1是表示bootmem分配器是不能分配该页框的,为0表示可以分配)。最后函数返回bitmap的大小。

 

三、bootmem分配器可使用内存注册

     在bootmem分配器初始化中把所有的页都标记为保留,即不可分配的,所以必须要给bootmem分配器注册可使用的内存。Linux在初始化中通过调用函数register_bootmem_low_pages(max_low_pfn)来向bootmem分配器注册低端内存中可使用的内存,该函数根据e820检测到的内存信息(e820.map[]),将类型为E820_RAM的page页框对应的位图中的位置为0,表示对应的page页框为空闲状态。然后调用reserve_bootmem(1024*1024, (PFN_PHYS(start_pfn) + bootmap_size + PAGE_SIZE-1) – 1024*1024)来保留bootmem分配器中内存从1MB到bitmap所用的内存页框,这些保留就包括了kernel内核,临时页表,bitmap。

 

四、bootmem分配器分配内存

     内核在初始化阶段使用的分配内存函数alloc_bootmem,alloc_bootmem_low,alloc_bootmem_pages,alloc_bootmem_low_pages都会调用__alloc_bootmem,只是一层封装,实际上是传递不同的参数调用__alloc_bootmem。

static void * __init
__alloc_bootmem_core(struct bootmem_data *bdata, unsigned long size,
        unsigned long align, unsigned long goal)
{
    unsigned long offset, remaining_size, areasize, preferred;
    unsigned long i, start = 0, incr, eidx;
    void *ret;

    eidx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT);
    offset = 0;
    if (align &&
        (bdata->node_boot_start & (align - 1UL)) != 0)
        offset = (align - (bdata->node_boot_start & (align - 1UL)));
    offset >>= PAGE_SHIFT;

    if (goal && (goal >= bdata->node_boot_start) && 
        ((goal >> PAGE_SHIFT) < bdata->node_low_pfn)) {
        preferred = goal - bdata->node_boot_start;

        if (bdata->last_success >= preferred)
            preferred = bdata->last_success;
    } else
        preferred = 0;

    preferred = ((preferred + align - 1) & ~(align - 1)) >> PAGE_SHIFT;
    preferred += offset;
    areasize = (size+PAGE_SIZE-1)/PAGE_SIZE;
    incr = align >> PAGE_SHIFT ? : 1;

     参数size表示需要分配的内存大小,align表示对齐的大小,goal指定了希望分配内存的起始地址,会从这个位置开始查找。eidx表示在bootmem中页框的最大索引号。如果align不为0,则计算出还需要偏移的页框数offset来满足align的要求。如果goal不为0,且goal在bootmem分配的内存范围内,则将preferred设置为相对于内存起始地址的偏移量,如果最近一次分配成功的地址偏移量大于preferred,则修改preferred为这个地址偏移量,其它情况都将preferred设置为0。然后preferred转化为相对于起始地址的页索引号,并加上offset来满足align的对齐要求。

restart_scan:
    for (i = preferred; i < eidx; i += incr) {
        unsigned long j;
        i = find_next_zero_bit(bdata->node_bootmem_map, eidx, i);
        i = ALIGN(i, incr);
        if (test_bit(i, bdata->node_bootmem_map))
            continue;
        for (j = i + 1; j < i + areasize; ++j) {
            if (j >= eidx)
                goto fail_block;
            if (test_bit (j, bdata->node_bootmem_map))
                goto fail_block;
        }
        start = i;
        goto found;
    fail_block:
        i = ALIGN(j, incr);
    }

    if (preferred > offset) {
        preferred = offset;
        goto restart_scan;
    }
    return NULL;
}

     find_next_zero_bit函数从第i个页框开始搜索空的页框,并返回空的页框i,如果后面有连续areasize个空的页,则找到了并记下起始位置start,如果没有连续的areasize个页框,就从新查找连续areasize个空的页。

found:
    bdata->last_success = start << PAGE_SHIFT;
    if (align < PAGE_SIZE &&
        bdata->last_offset && bdata->last_pos+1 == start) {
        offset = (bdata->last_offset+align-1) & ~(align-1);
        BUG_ON(offset > PAGE_SIZE);
        remaining_size = PAGE_SIZE-offset;
        if (size < remaining_size) {
            areasize = 0;
            /* last_pos unchanged */
            bdata->last_offset = offset+size;
            ret = phys_to_virt(bdata->last_pos*PAGE_SIZE + offset +
                        bdata->node_boot_start);
        } else {
            remaining_size = size - remaining_size;
            areasize = (remaining_size+PAGE_SIZE-1)/PAGE_SIZE;
            ret = phys_to_virt(bdata->last_pos*PAGE_SIZE + offset +
                        bdata->node_boot_start);
            bdata->last_pos = start+areasize-1;
            bdata->last_offset = remaining_size;
        }
        bdata->last_offset &= ~PAGE_MASK;
    } else {
        bdata->last_pos = start + areasize - 1;
        bdata->last_offset = size & ~PAGE_MASK;
        ret = phys_to_virt(start * PAGE_SIZE + bdata->node_boot_start);
    }

    for (i = start; i < start+areasize; i++)
        if (unlikely(test_and_set_bit(i, bdata->node_bootmem_map)))
            BUG();
    memset(ret, 0, size);
    return ret;

     找到连续areasize个空的页后,重新设置last_success。如果align小于PAGE_SIZE并且上一次分配的页框就是这次分配页框的上一个页框并且上一次分配的页框还没用完,则合并前面分配的未用的页框。合并有两种情况,如果前一个页框剩下未用的大小remaining_size大于这次需要请求分配的空间大小size,则直接从上一个页框未使用的空间分配给这次需要的size空间;如果前一个页框剩下未用的大小remaining_size不大于这次需要请求分配的空间大小size,则将请求的一部分分在前一个页框中,另一部分从新计算需要分配的页框数。然后更新相应的last_pos,last_offset字段。如果不能合并,就从start开始分配,更新last_pos,last_offset字段。

 

五、bootmem分配器释放内存

     释放bootmem分配器分配的内存,很简单,直接把对应的bitmap位清0,并更新last_success,代码如下:

static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)
{
    unsigned long i;
    unsigned long start;
    unsigned long sidx;
    unsigned long eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE;
    unsigned long end = (addr + size)/PAGE_SIZE;

    if (addr < bdata->last_success)
        bdata->last_success = addr;
    start = (addr + PAGE_SIZE-1) / PAGE_SIZE;
    sidx = start - (bdata->node_boot_start/PAGE_SIZE);

    for (i = sidx; i < eidx; i++) {
        if (unlikely(!test_and_clear_bit(i, bdata->node_bootmem_map)))
            BUG();
    }
}

 

六、bootmem分配器的销毁

      在buddy系统和slab分配器初始化之后,就可以把bootmem分配器销毁,把内存的管理交给buddy系统和slab分配器。bootmem分配器销毁是在mem_init函数中调用free_all_bootmem_core函数实现。

static unsigned long __init free_all_bootmem_core(pg_data_t *pgdat)
{
    struct page *page;
    bootmem_data_t *bdata = pgdat->bdata;
    unsigned long i, count, total = 0;
    unsigned long idx;
    unsigned long *map; 
    int gofast = 0;
    count = 0;

    page = virt_to_page(phys_to_virt(bdata->node_boot_start));  //bootmem管理的第一个页框
    idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT);  //最大bootmem索引号
    map = bdata->node_bootmem_map;
    /*如果起始物理地址页框号是32对齐的*/
    if (bdata->node_boot_start == 0 ||
        ffs(bdata->node_boot_start) - PAGE_SHIFT > ffs(BITS_PER_LONG))
        gofast = 1;
    for (i = 0; i < idx; ) {
        unsigned long v = ~map[i / BITS_PER_LONG];
        if (gofast && v == ~0UL) {  //从i开始的32个页都未使用
            int j, order;
            count += BITS_PER_LONG;
            __ClearPageReserved(page);  //清除页框描述符的保留位
            order = ffs(BITS_PER_LONG) - 1;
            set_page_refs(page, order);  //设置从page开始的32个页的引用为1
            for (j = 1; j < BITS_PER_LONG; j++) {
                if (j + 16 < BITS_PER_LONG)
                    prefetchw(page + j + 16);
                __ClearPageReserved(page + j);
            }
            __free_pages(page, order);  //将这些页回收到buddy系统中
            i += BITS_PER_LONG;
            page += BITS_PER_LONG;
        } else if (v) {  //从i开始的32页存在未使用的
            unsigned long m;
            for (m = 1; m && i < idx; m<<=1, page++, i++) {  //每次移动一位
                if (v & m) {
                    count++;
                    __ClearPageReserved(page);
                    set_page_refs(page, 0);
                    __free_page(page);
                }
            }
        } else {  //从i开始的32页都使用
            i+=BITS_PER_LONG;
            page += BITS_PER_LONG;
        }
    }
    total += count;

     如果起始物理地址页框号是32对齐的,设置gofast为1。如果gofast等于1,并且从page开始的32个页都未使用,则将page开始的32个页框回收到buddy系统中,这个通过调用函数__free_pages(page, order),一次回收32个页框。如果从page开始的32个页框中存在未使用的,查找这32个页框中空的页框,并回收到buddy系统中,一次回收一个页框。如果32个页框都使用了,则什么都不做。然后查找下一组32个页框回收,直到查找完bootmem分配器管理的所有页框。

total += count;
    /*
    释放bootmem的bitmap所在的页
     */
    page = virt_to_page(bdata->node_bootmem_map);
    count = 0;
    for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++) {
        count++;
        __ClearPageReserved(page);
        set_page_count(page, 1);
        __free_page(page);
    }
    total += count;
    bdata->node_bootmem_map = NULL;

    return total;
}

      既然bootmem分配器要释放了,那么bootmem分配器所使用的bitmap所在的页框也应该回收到buddy系统中去。((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE计算bitmap所占的页框数,然后从bitmap所在的起始页框开始回收bitmap占用的所有页框到buddy系统中,一次回收一个页框。

posted @ 2013-11-16 20:35  在于思考  阅读(5195)  评论(3编辑  收藏  举报