bootmem allocator介绍

在系统初始化的时候需要执行一些内存管理,内存分配的任务,这个时候buddy system,slab等并没有被初始化好,此时就引入了一种内存管理器bootmem allocator在系统初始化的时候进行内存管理与分配,当buddy system等初始化好后,在mem_init()中对bootmem allocator进行释放,内存管理与分配由buddy system,slab等进行接管。bootmem allocator使用一个bitmap来标记页是否被占用,分配的时候按照first fit,从bitmap中进行查找,如果这位为1,表示已经被占用,否则表示未被占用。为什么系统运行的时候不使用bootmem allocator了呢?bootmem allocator每次在bitmap中进行线性搜索,效率非常低,而且在内存的起始端留下许多小的空闲碎片,在需要非配大的内存块的时候,检查位图这一过程就显得代价很高。

本文档从6个方面来讨论bootmem allocator:

  1. bootmem allocator 核心数据结构
  2. bootmem allocator 的初始化
  3. bootmem allocator 分配内存
  4. bootmem allocator 保留内存
  5. bootmem allocator 释放内存
  6. bootmem allocator 的销毁

bootmem allocator 核心数据结构

view plain
  1. typedef struct bootmem_data {  
  2.     unsigned long node_boot_start;  
  3.     unsigned long node_low_pfn;  
  4.     void *node_bootmem_map;  
  5.     unsigned long last_offset;  
  6.     unsigned long last_pos;  
  7.     unsigned long last_success; /* Previous allocation point.  To speed 
  8.                      * up searching */  
  9. } bootmem_data_t;  
系统内存的中每一个结点都有一个bootmem_data_t结构,它含有bootmem allocator给结点分配内存时所需的信息。
  • node_boot_start是这个结点内存的起始地址
  • node_low_pfn是低端内存最后一个page的页帧号
  • node_bootmem_map指向内存中bitmap所在的位置
  • last_offset是分配的最后一个页内的偏移,如果该页完全使用,则offset为0
  • last_pos是分配最后一个页帧号
  • last_success是最后一次成功分配的位置

bootmem allocator 的初始化

在setup_memory()函数中调用init_bootmem对bootmem allocator进行初始化:
view plain
  1. unsigned long __init init_bootmem (unsigned long start, unsigned long pages)  
  2. {  
  3.     max_low_pfn = pages;  
  4.     min_low_pfn = start;  
  5.     return(init_bootmem_core(NODE_DATA(0), start, 0, pages));  
  6. }  
  • max_low_pfn是低端内存结束page的帧号,在物理内存探测的文章中会介绍
  • min_low_pfn是内核镜像后的第一个page的帧号,在setup_memory()中有这么一句:
    view plain
    1. start_pfn = PFN_UP(init_pg_tables_end);  
    也就是获得符号_end的下一个page的页帧号,传过来就是这里的start了
可以看出这里调用了核心函数init_bootmem_core():
view plain
  1. static unsigned long __init init_bootmem_core (pg_data_t *pgdat,  
  2.     unsigned long mapstart, unsigned long start, unsigned long end)  
  3. {  
  4.     bootmem_data_t *bdata = pgdat->bdata;  
  5.     unsigned long mapsize = ((end - start)+7)/8;  
  6.   
  7.     pgdat->pgdat_next = pgdat_list;  
  8.     pgdat_list = pgdat;  
  9.   
  10.     mapsize = (mapsize + (sizeof(long) - 1UL)) & ~(sizeof(long) - 1UL);  
  11.     bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT);  
  12.     bdata->node_boot_start = (start << PAGE_SHIFT);  
  13.     bdata->node_low_pfn = end;  
  14.   
  15.     /* 
  16.      * Initially all pages are reserved - setup_arch() has to 
  17.      * register free RAM areas explicitly. 
  18.      */  
  19.     memset(bdata->node_bootmem_map, 0xff, mapsize);  
  20.   
  21.     return mapsize;  
  22. }  
  • 将结点添加到pgdat_list链表
  • 计算位图大小,使用公式:,加7是为了使相除后向上取整,除以8获得所需的字节数
  • 使mapsize对齐到sizeof(long)的倍数即4个字节的倍数,比如我们有80个页,mapsize为10,(10 + (4 -1 )) & ~(4 - 1)==》0000 1101 & 1111 1100,0000 1100,去掉低2位,为12,即4的倍数
  • 这里设置bitmap的位置为内核镜像后的第一个page
  • 设置内存块的起始物理地址
  • 初始化所有的区域被占用
  • 返回bitmap的大小

bootmem allocator 分配内存

bootmem allocator介绍_第1张图片

alloc_bootmem,alloc_bootmem_low,alloc_bootmem_pages,alloc_bootmem_low_pages都会调用__alloc_bootmem,只是一层封装,实际上是传递不同的参数调用__alloc_bootmem。下面来分析一下__alloc_bootmem的实现:
view plain
  1. void * __init __alloc_bootmem (unsigned long size, unsigned long align, unsigned long goal)  
  2. {  
  3.     pg_data_t *pgdat = pgdat_list;  
  4.     void *ptr;  
  5.   
  6.     for_each_pgdat(pgdat)  
  7.         if ((ptr = __alloc_bootmem_core(pgdat->bdata, size,  
  8.                         align, goal)))  
  9.             return(ptr);  
  10.   
  11.     /* 
  12.      * Whoops, we cannot satisfy the allocation request. 
  13.      */  
  14.     printk(KERN_ALERT "bootmem alloc of %lu bytes failed!\n", size);  
  15.     panic("Out of memory");  
  16.     return NULL;  
  17. }  
  • 每个结点是链在一个链表头为pgdat_list的链表上的,对于UMA的系统,只有一个结点,结点的描述符存放在contig_page_data变量中,因此这个pgdat_list指向一个只有一个元素的链表
  • align的参数是指定对齐
  • goal指定了希望分配内存的起始地址,会从这个位置开始查找
这里调用了核心函数__alloc_bootmem_core,下面看其实现:
view plain
  1. static void * __init  
  2. __alloc_bootmem_core(struct bootmem_data *bdata, unsigned long size,  
  3.         unsigned long align, unsigned long goal)  
  4. {  
  5.     unsigned long offset, remaining_size, areasize, preferred;  
  6.     unsigned long i, start = 0, incr, eidx;  
  7.     void *ret;  
  8.   
  9.     if(!size) {  
  10.         printk("__alloc_bootmem_core(): zero-sized request\n");  
  11.         BUG();  
  12.     }  
  13.     BUG_ON(align & (align-1));  
  14.   
  15.     eidx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT);  
  16.     offset = 0;  
  17.     if (align &&  
  18.         (bdata->node_boot_start & (align - 1UL)) != 0)  
  19.         offset = (align - (bdata->node_boot_start & (align - 1UL)));  
  20.     offset >>= PAGE_SHIFT;  
  21.   
  22.     /* 
  23.      * We try to allocate bootmem pages above 'goal' 
  24.      * first, then we try to allocate lower pages. 
  25.      */  
  26.     if (goal && (goal >= bdata->node_boot_start) &&   
  27.         ((goal >> PAGE_SHIFT) < bdata->node_low_pfn)) {  
  28.         preferred = goal - bdata->node_boot_start;  
  29.   
  30.         if (bdata->last_success >= preferred)  
  31.             preferred = bdata->last_success;  
  32.     } else  
  33.         preferred = 0;  
  34.   
  35.     preferred = ((preferred + align - 1) & ~(align - 1)) >> PAGE_SHIFT;  
  36.     preferred += offset;  
  37.     areasize = (size+PAGE_SIZE-1)/PAGE_SIZE;  
  38.     incr = align >> PAGE_SHIFT ? : 1;  
  • 首先检查分配的大小不能为0
  • 检查对齐方式,这里应该是4字节的倍数对齐,由于align是unsigned long型的,并且align & (align-1)代表最高bit位为1,其他bit位为0。
  • edix获得总共的页帧数
  • 如果align不为0,并且bootmem的内存起始地址是4字节倍数对齐,减去已经对齐的部分,剩下的部分通过offset来完成对齐
  • 如果goal为真(也就是进行查找的起始地址被指定),并且goal在node_boot_start和node_low_pfn所指向的物理地址之间,则将preffered设置称相对于起始地址的偏移,其实这个起始物理地址为0,所以preffered就是希望开始进行查找的物理地址
  • 如果上一次成功分配的地方大于preffered, 就可以从那个地方开始找,提高了效率
  • preffered对齐到align
  • preffered加上偏移
  • 请求大小页对齐
  • 根据对齐大小设置步进长度,小于一页为1

view plain
  1. restart_scan:  
  2.     for (i = preferred; i < eidx; i += incr) {  
  3.         unsigned long j;  
  4.         i = find_next_zero_bit(bdata->node_bootmem_map, eidx, i);  
  5.         i = ALIGN(i, incr);  
  6.         if (test_bit(i, bdata->node_bootmem_map))  
  7.             continue;  
  8.         for (j = i + 1; j < i + areasize; ++j) {  
  9.             if (j >= eidx)  
  10.                 goto fail_block;  
  11.             if (test_bit (j, bdata->node_bootmem_map))  
  12.                 goto fail_block;  
  13.         }  
  14.         start = i;  
  15.         goto found;  
  16.     fail_block:  
  17.         i = ALIGN(j, incr);  
  18.     }  
  19.   
  20.     if (preferred > offset) {  
  21.         preferred = offset;  
  22.         goto restart_scan;  
  23.     }  
  24.     return NULL;  
  • 在preffered~edix之间进行查找,使用 first fit。它会查找后面第一个为0的位
  • 然后以这个为0位开始查找areasize大小的返回个page
  • 如果失败重新找bitmap为0的bit
  • 如果找到记下起始位置start

view plain
  1. found:  
  2.     bdata->last_success = start << PAGE_SHIFT;  
  3.     BUG_ON(start >= eidx);  
  4.   
  5.     /* 
  6.      * Is the next page of the previous allocation-end the start 
  7.      * of this allocation's buffer? If yes then we can 'merge' 
  8.      * the previous partial page with this allocation. 
  9.      */  
  10.     if (align < PAGE_SIZE &&  
  11.         bdata->last_offset && bdata->last_pos+1 == start) {  
  12.         offset = (bdata->last_offset+align-1) & ~(align-1);  
  13.         BUG_ON(offset > PAGE_SIZE);  
  14.         remaining_size = PAGE_SIZE-offset;  
  15.         if (size < remaining_size) {  
  16.             areasize = 0;  
  17.             /* last_pos unchanged */  
  18.             bdata->last_offset = offset+size;  
  19.             ret = phys_to_virt(bdata->last_pos*PAGE_SIZE + offset +  
  20.                         bdata->node_boot_start);  
  21.         } else {  
  22.             remaining_size = size - remaining_size;  
  23.             areasize = (remaining_size+PAGE_SIZE-1)/PAGE_SIZE;  
  24.             ret = phys_to_virt(bdata->last_pos*PAGE_SIZE + offset +  
  25.                         bdata->node_boot_start);  
  26.             bdata->last_pos = start+areasize-1;  
  27.             bdata->last_offset = remaining_size;  
  28.         }  
  29.         bdata->last_offset &= ~PAGE_MASK;  
  30.     } else {  
  31.         bdata->last_pos = start + areasize - 1;  
  32.         bdata->last_offset = size & ~PAGE_MASK;  
  33.         ret = phys_to_virt(start * PAGE_SIZE + bdata->node_boot_start);  
  34.     }  
  35.   
  36.     /* 
  37.      * Reserve the area now: 
  38.      */  
  39.     for (i = start; i < start+areasize; i++)  
  40.         if (unlikely(test_and_set_bit(i, bdata->node_bootmem_map)))  
  41.             BUG();  
  42.     memset(ret, 0, size);  
  43.     return ret;  
  44. }  
  • 找到后设置一下last_success
  • 如果找到我们开始常识时候可以merge
  • 能构merge需要几个条件:1)align < PAGE_SIZE  2)上一次分配最后一个page没有完全使用 3)找到的页正好是上次分配最后一个page的下一个page
  • 如果可以merge,将offset按照align对齐
  • 计算这个要被merge的page还剩下多少空间,即remaining_size
  • 这时又分两种情况:1)请求的大小小于一个page且比前一个page剩下的大小小 2)反之
  • 如果恰好请求的大小小于一个page且比前一个page剩下的大小小,则分配这个page的剩下部分给请求
  • 如果请求大于等于剩下的大小则减一下,请求的一部分分在前一个page中,另一部分计算还需要多少个page
  • 然后更新相应的last_pos,last_offset字段
  • 如果不满足merge的条件,就从start开始分配,更新last_pos,last_offset字段 
  • 调用test_and_set_bit,对于没有设置bitmap的设置相应的位,像那种分配小块内存不足一个page的并且与前一个page merge的,当然test后就不用再设置了

我觉得读bootmem allocator我们应该思考几个问题:
  1. 怎样实现first fit
  2. 怎样分配小于一个页的内存的
  3. 找到内存后怎样merge的

bootmem allocator 保留内存

有的时候需要对部分内存进行保留,这些保留的内存在bootmem allocator存在的时候不会被释放,而且buddy system也并没有接管到这些page。
view plain
  1. void __init reserve_bootmem (unsigned long addr, unsigned long size)  
  2. {  
  3.     reserve_bootmem_core(NODE_DATA(0)->bdata, addr, size);  
  4. }  
reserve_bootmem只是进行了一次封装,看reserve_bootmem_core的实现过程:
view plain
  1. static void __init reserve_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)  
  2. {  
  3.     unsigned long i;  
  4.     /* 
  5.      * round up, partially reserved pages are considered 
  6.      * fully reserved. 
  7.      */  
  8.     unsigned long sidx = (addr - bdata->node_boot_start)/PAGE_SIZE;  
  9.     unsigned long eidx = (addr + size - bdata->node_boot_start +   
  10.                             PAGE_SIZE-1)/PAGE_SIZE;  
  11.     unsigned long end = (addr + size + PAGE_SIZE-1)/PAGE_SIZE;  
  12.   
  13.     BUG_ON(!size);  
  14.     BUG_ON(sidx >= eidx);  
  15.     BUG_ON((addr >> PAGE_SHIFT) >= bdata->node_low_pfn);  
  16.     BUG_ON(end > bdata->node_low_pfn);  
  17.   
  18.     for (i = sidx; i < eidx; i++)  
  19.         if (test_and_set_bit(i, bdata->node_bootmem_map)) {  
  20. #ifdef CONFIG_DEBUG_BOOTMEM  
  21.             printk("hm, page %08lx reserved twice.\n", i*PAGE_SIZE);  
  22. #endif  
  23.         }  
  24. }  
  • sidx 是起始页的索引
  • edix是终止页的索引
  • 调用test_and_set_bit函数将bitmap中相应位置位

bootmem allocator 释放内存

这里的释放是用bootmem allocator释放内存,而不是bootmem allocator本身,其调用了 free_bootmem_core函
view plain
  1. void __init free_bootmem (unsigned long addr, unsigned long size)  
  2. {  
  3.     free_bootmem_core(NODE_DATA(0)->bdata, addr, size);  
  4. }  
分析free_bootmem_core函 数实现:
view plain
  1. static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)  
  2. {  
  3.     unsigned long i;  
  4.     unsigned long start;  
  5.     /* 
  6.      * round down end of usable mem, partially free pages are 
  7.      * considered reserved. 
  8.      */  
  9.     unsigned long sidx;  
  10.     unsigned long eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE;  
  11.     unsigned long end = (addr + size)/PAGE_SIZE;  
  12.   
  13.     BUG_ON(!size);  
  14.     BUG_ON(end > bdata->node_low_pfn);  
  15.   
  16.     if (addr < bdata->last_success)  
  17.         bdata->last_success = addr;  
  18.   
  19.     /* 
  20.      * Round up the beginning of the address. 
  21.      */  
  22.     start = (addr + PAGE_SIZE-1) / PAGE_SIZE;  
  23.     sidx = start - (bdata->node_boot_start/PAGE_SIZE);  
  24.   
  25.     for (i = sidx; i < eidx; i++) {  
  26.         if (unlikely(!test_and_clear_bit(i, bdata->node_bootmem_map)))  
  27.             BUG();  
  28.     }  
  29. }  
  • sidx 是起始页的索引
  • edix是终止页的索引
  • 调用test_and_clear_bit函数将bitmap中相应位清除,可以看出在bootmem allocator时代,内存的释放还是很容易的,清除相应bitmap就行。这时你发现相应page并没有清零,但是在__alloc_bootmem_core函数中,每次分配页后都调用memset进行清零操作

bootmem allocator 的销毁

bootmem allocator介绍_第2张图片

        在mem_init函数中会调用bootmem allocator的释放函数free_all_bootmem,将bitmap中为0的page释放到buddy system,由buddy system接管这些页。在setup_memory函数中调用reserve_bootmem保存了kernel镜像,bitmap,page 0所占的页,在free_all_bootmem_core结尾处只对bitmap占用的页进行释放。可见,kernel镜像与page 0占用的页被保留下来,并没有释放给buddy system。
view plain
  1. unsigned long __init free_all_bootmem (void)  
  2. {  
  3.     return(free_all_bootmem_core(NODE_DATA(0)));  
  4. }  
释放的时候调用free_all_bootmem,它只是一个前段,里边封装了核心函数free_all_bootmem_core。

view plain
  1. static unsigned long __init free_all_bootmem_core(pg_data_t *pgdat)  
  2. {  
  3.     struct page *page;  
  4.     bootmem_data_t *bdata = pgdat->bdata;  
  5.     unsigned long i, count, total = 0;  
  6.     unsigned long idx;  
  7.     unsigned long *map;   
  8.     int gofast = 0;  
  9.   
  10.     BUG_ON(!bdata->node_bootmem_map);  
  11.   
  12.     count = 0;  
  13.     /* first extant page of the node */  
  14.     page = virt_to_page(phys_to_virt(bdata->node_boot_start));  
  15.     idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT);  
  16.     map = bdata->node_bootmem_map;  
  17.     /* Check physaddr is O(LOG2(BITS_PER_LONG)) page aligned */  
  18.     if (bdata->node_boot_start == 0 ||  
  19.         ffs(bdata->node_boot_start) - PAGE_SHIFT > ffs(BITS_PER_LONG))  
  20.         gofast = 1;  
  21.     for (i = 0; i < idx; ) {  
  22.         unsigned long v = ~map[i / BITS_PER_LONG];  
  23.         if (gofast && v == ~0UL) {  
  24.             int j, order;  
  25.   
  26.             count += BITS_PER_LONG;  
  27.             __ClearPageReserved(page);  
  28.             order = ffs(BITS_PER_LONG) - 1;  
  29.             set_page_refs(page, order);  
  30.             for (j = 1; j < BITS_PER_LONG; j++) {  
  31.                 if (j + 16 < BITS_PER_LONG)  
  32.                     prefetchw(page + j + 16);  
  33.                 __ClearPageReserved(page + j);  
  34.             }  
  35.             __free_pages(page, order);  
  36.             i += BITS_PER_LONG;  
  37.             page += BITS_PER_LONG;  
  38.         } else if (v) {  
  39.             unsigned long m;  
  40.             for (m = 1; m && i < idx; m<<=1, page++, i++) {  
  41.                 if (v & m) {  
  42.                     count++;  
  43.                     __ClearPageReserved(page);  
  44.                     set_page_refs(page, 0);  
  45.                     __free_page(page);  
  46.                 }  
  47.             }  
  48.         } else {  
  49.             i+=BITS_PER_LONG;  
  50.             page += BITS_PER_LONG;  
  51.         }  
  52.     }  
  • 首先获得第一个页的描述符
  • idx为页帧的数量
  • 如果结点内存的起始地址是32位对齐,则设置gofast为1
  • 由于v是unsigned long型的,所以是得到32位的bitmap取反,如果32位中没有被占用的,则v为0xffffffff
下面来看这个核心的for循环:
gofast为1且v为0xffffffff                ==》起始地址32位对齐且没有被占用的
gofast为0且v!=0 && v!=0xffffffff   ==》起始地址不是32位对齐但是没有被占用,此时32个page,一个一个检查是否被占用,然后释放
gofast为0且v为0                          ==》起始地址不是32位对齐,v为0表示32个page全部被占用,跳过

view plain
  1. total += count;  
  2.   
  3.     /* 
  4.      * Now free the allocator bitmap itself, it's not 
  5.      * needed anymore: 
  6.      */  
  7.     page = virt_to_page(bdata->node_bootmem_map);  
  8.     count = 0;  
  9.     for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++) {  
  10.         count++;  
  11.         __ClearPageReserved(page);  
  12.         set_page_count(page, 1);  
  13.         __free_page(page);  
  14.     }  
  15.     total += count;  
  16.     bdata->node_bootmem_map = NULL;  
  17.   
  18.     return total;  
  19. }  
将bitmap占用的空间释放给buddy system,此时bootmem allocator生命终结。



一个有趣的实验

修改start_kernel部分的代码使系统启动后检查不到我们隐藏的内存。我的系统修改前:

在init/main.c中添加:
全局的:
view plain
  1. void *bootmem_addr = NULL;  
  2. EXPORT_SYMBOL(bootmem_addr);  
  3. #define BOOTMEM_SIZE 524288000  
在start_kernel函数中,记得要在mem_init函数之前添加:
view plain
  1. bootmem_addr = alloc_bootmem(BOOTMEM_SIZE);  
  2.     if(!bootmem_addr)  
  3.         printk("can not alloc BOOTMEM\n");  
  4.     else  
  5.         printk("alloc BOOTMEM success!\n");  
重新编译内核,重启,修改后:

怎么样,是不是少了500MB内存阿,系统都检测不到了。这块内存的起始地址EXPORT出来后,可以在驱动等地方使用。

你可能感兴趣的:(数据结构,list,System,文档,each,merge)