再读内核存储管理(3):bootmem分配策略

本文详细解析了 ADI BF561 DSP 中 bootmem 的内存分配策略,包括 bootmem 数据结构、初始化过程、内存分配与回收机制、保留页操作及 bootmem 的终结等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 
快乐虾
http://blog.csdn.net/lights_joy/
lights@hb165.com
  
 
本文适用于
ADI bf561 DSP
uclinux-2008r1-rc8 (移植到vdsp5)
Visual DSP++ 5.0
  
 
欢迎转载,但请保留作者信息
 
1.1    bootmem分配策略
bootmem是内核中使用的一种较简单的内存分配策略,它用于在系统启动时使用,在buddy等内存分配系统初始化完成后将不再使用。其基本思想是将SDRAM的可用存储空间分成许多页,每页的大小为4K,在分配时以页为单位分配,分配方法是从低往高找直到找到一块或连续多块满足大小要求的空闲页面为止。
/*
 * node_bootmem_map is a map pointer - the bits represent all physical
 * memory pages (including holes) on the node.
 */
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; /* Previous allocation point. To speed
                        * up searching */
     struct list_head list;
} bootmem_data_t;
l        node_bootmem_map
node_bootmem_map初始化为内核结束后第一个页的页面地址,而不是页号。
bootmem试图用一个二进制位表示一页是否占用,如果空闲则该位为0,如果已经使用则该位为1对于64M的SDRAM来讲,其页面大小为4K,因此其总共的页数为0x3fff(16K),所需要的字节数为0x800。
bootmem用内核代码结束后的第1页来保存所有的SDRAM页面使用情况,即node_bootmem_map指向的页面。
l        node_boot_start
取值为0。
l        node_low_pfn
指向SDRAM中最后一个页的页序号。
l         bdata_list
在内核中有一个bdata_list的链表中,它的定义在bootmem.c中:
static LIST_HEAD(bdata_list);
内核将所有的bootmem_data放在这个链表里,但是对于BF561来讲,bdata_list的链表将只有一个元素!其定义在mm/page_alloc.c中:
static bootmem_data_t contig_bootmem_data;
 
 
在setup_arch(arch/blackfin/kernel/setup.c)函数中,与bootmem相关的代码有:
 
     /*
      * give all the memory to the bootmap allocator, tell it to put the
      * boot mem_map at the start of memory
      */
     bootmap_size = init_bootmem_node(NODE_DATA(0), memory_start >> PAGE_SHIFT, /* map goes here */
                        PAGE_OFFSET >> PAGE_SHIFT,
                        memory_end >> PAGE_SHIFT);
     /*
      * free the usable memory, we have to make sure we do not free
      * the bootmem bitmap so we then reserve it after freeing it :-)
      */
     free_bootmem(memory_start, memory_end - memory_start);
 
     reserve_bootmem(memory_start, bootmap_size);
此时,内存管理的初始化函数paging_init尚未调用,因此可知bootmem的作用是在内核启动完成前提供一种简单的内存管理策略。
其中PAGE_OFFSET的定义位于include/asm/page.h:
#define PAGE_OFFSET         (PAGE_OFFSET_RAW)
PAGE_OFFSET的定义位于include/asm/page_offset.h:
#ifdef CONFIG_BLACKFIN
#define PAGE_OFFSET_RAW     0x00000000
#endif
在setup_arch函数中调用了init_bootmem_node,这个函数的实现在mm/bootmem.c中:
 
/*
 * Called once to set up the allocator itself.
 */
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;
 
     bdata->node_bootmem_map = phys_to_virt(PFN_PHYS(mapstart));
     bdata->node_boot_start = PFN_PHYS(start);
     bdata->node_low_pfn = end;
     link_bootmem(bdata);
 
     /*
      * Initially all pages are reserved - setup_arch() has to
      * register free RAM areas explicitly.
      */
     mapsize = get_mapsize(bdata);
     memset(bdata->node_bootmem_map, 0xff, mapsize);
 
     return mapsize;
}
 
unsigned long __init init_bootmem_node(pg_data_t *pgdat, unsigned long freepfn,
                   unsigned long startpfn, unsigned long endpfn)
{
     return init_bootmem_core(pgdat, freepfn, startpfn, endpfn);
}
当参数传递到init_bootmem_core时,各个参数的对应值为:
mapstart取的是memory_start >> PAGE_SHIFT,即内核代码结束后的第一个页序号
start取的是PAGE_OFFSET >> PAGE_SHIFT,即0。
end取的是memory_end >> PAGE_SHIFT,即SDRAM的最后一个页号。
从这个初始化函数可以大致看出几个成员的意义。
l        node_bootmem_map
     bdata->node_bootmem_map = phys_to_virt(PFN_PHYS(mapstart));
在这行语句中,mapstart是内核代码结束后的第一个页序号,PFN_PHYS定义为:
#define PFN_PHYS(x)    ((x) << PAGE_SHIFT)
phys_to_virt则定义为:
#define phys_to_virt(vaddr) ((void *) (vaddr))
因此node_bootmem_map保存了内核结束后第一个页的页面地址,而不是页号。
l        node_boot_start
     bdata->node_boot_start = PFN_PHYS(start);
取值为0。
l        node_low_pfn
     bdata->node_low_pfn = end;
指向SDRAM中最后一个页的页序号。
1.1.2.1.1       link_bootmem
init_bootmem_core函数中调用了link_bootmem,这个函数实现为:
 
/*
 * link bdata in order
 */
static void __init link_bootmem(bootmem_data_t *bdata)
{
     bootmem_data_t *ent;
 
     if (list_empty(&bdata_list)) {
         list_add(&bdata->list, &bdata_list);
         return;
     }
     /* insert in order */
     list_for_each_entry(ent, &bdata_list, list) {
         if (bdata->node_boot_start < ent->node_boot_start) {
              list_add_tail(&bdata->list, &ent->list);
              return;
         }
     }
     list_add_tail(&bdata->list, &bdata_list);
}
它的作用就是将bdata放到一个叫bdata_list的链表中,bdata_list的定义在bootmem.c中:
static LIST_HEAD(bdata_list);
对于BF561来讲,这个函数将只调用一次,用红色标出的代码将不会执行。因此bdata_list的链表也将只有一个元素!
1.1.2.1.2       get_mapsize
init_bootmem_core函数中调用了get_mapsize实现为:
 
/*
 * Given an initialised bdata, it returns the size of the boot bitmap
 */
static unsigned long __init get_mapsize(bootmem_data_t *bdata)
{
     unsigned long mapsize;
     unsigned long start = PFN_DOWN(bdata->node_boot_start);
     unsigned long end = bdata->node_low_pfn;
 
     mapsize = ((end - start) + 7) / 8;
     return ALIGN(mapsize, sizeof(long));
}
其中bdata->node_boot_start指向SDRAM的起始位置,其值为0。bdata->node_low_pfn则为SDRAM最后一个页的页号。至此我们可以看出,bootmem是试图用一个二进制位表示一页是否占用,如果空闲则该位为0,如果已经使用则该位为1。
对于64M的SDRAM来讲,其页面大小为4K,因此其总共的页数为0x3fff(16K),所需要的字节数为0x800。
从这里还可以知道,bootmem用内核代码结束后的第1页来保存所有的SDRAM页面使用情况。
内核定义了4个宏用于内存分配:
#define alloc_bootmem(x) /
     __alloc_bootmem(x, SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))
#define alloc_bootmem_low(x) /
     __alloc_bootmem_low(x, SMP_CACHE_BYTES, 0)
#define alloc_bootmem_pages(x) /
     __alloc_bootmem(x, PAGE_SIZE, __pa(MAX_DMA_ADDRESS))
#define alloc_bootmem_low_pages(x) /
     __alloc_bootmem_low(x, PAGE_SIZE, 0)
但是实际上只使用了alloc_bootmemalloc_bootmem_pages两个宏,它们都调用__alloc_bootmem进行实际的内存分配。在这两个宏中出现的常量定义为:
l         SMP_CACHE_BYTES
/*
 * Bytes per L1 cache line
 * Blackfin loads 32 bytes for cache
 */
#define L1_CACHE_SHIFT 5
#define L1_CACHE_BYTES (1 << L1_CACHE_SHIFT)
#define SMP_CACHE_BYTES L1_CACHE_BYTES
 即64。
l         MAX_DMA_ADDRESS
#define MAX_DMA_ADDRESS PAGE_OFFSET
而PAGE_OFFSET是定义成0的。
__pa的定义为:
#define __pa(vaddr)         virt_to_phys((void *)(vaddr))
virt_to_phys的定义为:
#define virt_to_phys(vaddr) ((unsigned long) (vaddr))
看看__alloc_bootmem的实现代码:
 
void * __init __alloc_bootmem(unsigned long size, unsigned long align,
                    unsigned long goal)
{
     void *mem = __alloc_bootmem_nopanic(size,align,goal);
 
     if (mem)
         return mem;
     /*
      * Whoops, we cannot satisfy the allocation request.
      */
     printk(KERN_ALERT "bootmem alloc of %lu bytes failed!/n", size);
     panic("Out of memory");
     return NULL;
}
再看看的__alloc_bootmem_nopanic实现:
 
void * __init __alloc_bootmem_nopanic(unsigned long size, unsigned long align,
                         unsigned long goal)
{
     bootmem_data_t *bdata;
     void *ptr;
 
     list_for_each_entry(bdata, &bdata_list, list) {
         ptr = __alloc_bootmem_core(bdata, size, align, goal, 0);
         if (ptr)
              return ptr;
     }
     return NULL;
}
前面说过bdata_list实际上只有一个元素,因此这一循环将只执行一次。
__alloc_bootmem_core的实现在bootmem.c中,代码较长。先看看注释和声明:
 
/*
 * We 'merge' subsequent allocations to save space. We might 'lose'
 * some fraction of a page if allocations cannot be satisfied due to
 * size constraints on boxes where there is physical RAM space
 * fragmentation - in these cases (mostly large memory boxes) this
 * is not a problem.
 *
 * On low memory boxes we get it right in 100% of the cases.
 *
 * alignment has to be a power of 2 value.
 *
 * NOTE: This function is _not_ reentrant.
 */
void * __init
__alloc_bootmem_core(struct bootmem_data *bdata, unsigned long size,
           unsigned long align, unsigned long goal, unsigned long limit)
{
当调用到这里的时候goal和limit的值都为0,align为页面大小4K或者64。
以下代码显示了__alloc_bootmem的页面分配策略:
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 (i >= eidx)
              break;
         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);
     }
在上面的代码中,prefeered为0,areasize表示总共要求分配的页数,incr取1。很简单,就是沿着页表从低往高找,找到第一个可用的页块。
当分配成功后,此函数将记录这次分配的信息:
found:
     bdata->last_success = PFN_PHYS(start);
     BUG_ON(start >= eidx);
 
     /*
      * Is the next page of the previous allocation-end the start
      * of this allocation's buffer? If yes then we can 'merge'
      * the previous partial page with this allocation.
      */
     if (align < PAGE_SIZE &&
         bdata->last_offset && bdata->last_pos+1 == start) {
        
     } else {
         bdata->last_pos = start + areasize - 1;
         bdata->last_offset = size & ~PAGE_MASK;
         ret = phys_to_virt(start * PAGE_SIZE + bdata->node_boot_start);
     }
 
     /*
      * Reserve the area now:
      */
     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;
还有一个值得注意的是在分配成功之后,此函数还负责将这段内存清0。
为了加快分配速度,内核意图使用bootmem_data中的三个变量last_pos,last_offset和last_success。
从上述分配的代码可以看出,只要搜索的起点preferred选择合适,它将快速跳过已经分配的页,从而加快分配的速度。那么preferred的值是如何来的呢?在这个函数的开头:
     /*
      * We try to allocate bootmem pages above 'goal'
      * first, then we try to allocate lower pages.
      */
     if (goal && goal >= bdata->node_boot_start && PFN_DOWN(goal) < end_pfn) {
         preferred = goal - bdata->node_boot_start;
 
         if (bdata->last_success >= preferred)
              if (!limit || (limit && limit > bdata->last_success))
                   preferred = bdata->last_success;
     } else
         preferred = 0;
 
     preferred = PFN_DOWN(ALIGN(preferred, align)) + offset;
     areasize = (size + PAGE_SIZE-1) / PAGE_SIZE;
     incr = align >> PAGE_SHIFT ? : 1;
由于两个宏调用中都将goal参数设置为0,因此preferred的值也将恒为0,也就是说bootmem没有使用任何的加速策略,老老实实地从0开始往后找!
 
 
void __init free_bootmem(unsigned long addr, unsigned long size)
{
     free_bootmem_core(NODE_DATA(0)->bdata, addr, size);
}
static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr,
                        unsigned long size)
{
     unsigned long sidx, eidx;
     unsigned long i;
 
     /*
      * round down end of usable mem, partially free pages are
      * considered reserved.
      */
     BUG_ON(!size);
     BUG_ON(PFN_DOWN(addr + size) > bdata->node_low_pfn);
 
     if (addr < bdata->last_success)
         bdata->last_success = addr;
 
     /*
      * Round up the beginning of the address.
      */
     sidx = PFN_UP(addr) - PFN_DOWN(bdata->node_boot_start);
     eidx = PFN_DOWN(addr + size - bdata->node_boot_start);
 
     for (i = sidx; i < eidx; i++) {
         if (unlikely(!test_and_clear_bit(i, bdata->node_bootmem_map)))
              BUG();
     }
}
调用这个函数的时候,bdata将指向全局变量contig_bootmem_data
在这里有:
#define PFN_UP(x) (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)
#define PFN_DOWN(x)    ((x) >> PAGE_SHIFT)
即把一个地址按页面大小(4K)对齐并转换为它的页号。
bdata->node_boot_start的值则为0。
因而这个函数的功能就是将表示该页的位设置为0,与此表示这个页是空闲的。
此函数用于将一个或者多个指定的页标记为已分配,其实现在mm/bootmem.c中:
void __init reserve_bootmem(unsigned long addr, unsigned long size)
{
     reserve_bootmem_core(NODE_DATA(0)->bdata, addr, size);
}
/*
 * Marks a particular physical memory range as unallocatable. Usable RAM
 * might be used for boot-time allocations - or it might get added
 * to the free page pool later on.
 */
static void __init reserve_bootmem_core(bootmem_data_t *bdata, unsigned long addr,
                       unsigned long size)
{
     unsigned long sidx, eidx;
     unsigned long i;
 
     /*
      * round up, partially reserved pages are considered
      * fully reserved.
      */
     BUG_ON(!size);
     BUG_ON(PFN_DOWN(addr) >= bdata->node_low_pfn);
     BUG_ON(PFN_UP(addr + size) > bdata->node_low_pfn);
 
     sidx = PFN_DOWN(addr - bdata->node_boot_start);
     eidx = PFN_UP(addr + size - bdata->node_boot_start);
 
     for (i = sidx; i < eidx; i++)
         if (test_and_set_bit(i, bdata->node_bootmem_map)) {
#ifdef CONFIG_DEBUG_BOOTMEM
              printk("hm, page %08lx reserved twice./n", i*PAGE_SIZE);
#endif
         }
}
调用这个函数的时候,bdata将指向全局变量contig_bootmem_data
在这里有:
#define PFN_UP(x) (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)
#define PFN_DOWN(x)    ((x) >> PAGE_SHIFT)
即把一个地址按页面大小(4K)对齐并转换为它的页号。
bdata->node_boot_start的值则为0。
因而这个函数的功能就是将表示指定页的位设置为1,与此表示这个页已经使用。
1.1.6   bootmem的终结:free_all_bootmem
在mem_init函数(arch/blackfin/mm/init.c)的开头,就有如下调用:
     /* This will put all memory onto the freelists. */
     totalram_pages = free_all_bootmem();
这将回收所有未被bootmem分配的存储空间。看看(mm/bootmem.c):
unsigned long __init free_all_bootmem(void)
{
     return free_all_bootmem_core(NODE_DATA(0));
}
跟踪free_all_bootmem_core:
static unsigned long __init free_all_bootmem_core(pg_data_t *pgdat)
{
     struct page *page;
     unsigned long pfn;
     bootmem_data_t *bdata = pgdat->bdata;
     unsigned long i, count, total = 0;
     unsigned long idx;
     unsigned long *map;
     int gofast = 0;
 
     BUG_ON(!bdata->node_bootmem_map);
 
     count = 0;
     /* first extant page of the node */
     pfn = PFN_DOWN(bdata->node_boot_start);
     idx = bdata->node_low_pfn - pfn;
     map = bdata->node_bootmem_map;
     /* Check physaddr is O(LOG2(BITS_PER_LONG)) page aligned */
     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) {
              int order;
 
              page = pfn_to_page(pfn);
              count += BITS_PER_LONG;
              order = ffs(BITS_PER_LONG) - 1;
              __free_pages_bootmem(page, order);
              i += BITS_PER_LONG;
              page += BITS_PER_LONG;
         } else if (v) {
              unsigned long m;
 
              page = pfn_to_page(pfn);
              for (m = 1; m && i < idx; m<<=1, page++, i++) {
                   if (v & m) {
                       count++;
                       __free_pages_bootmem(page, 0);
                   }
              }
         } else {
              i += BITS_PER_LONG;
         }
         pfn += BITS_PER_LONG;
     }
     total += count;
 
     /*
      * Now free the allocator bitmap itself, it's not
      * needed anymore:
      */
     page = virt_to_page(bdata->node_bootmem_map);
     count = 0;
     idx = (get_mapsize(bdata) + PAGE_SIZE-1) >> PAGE_SHIFT;
     for (i = 0; i < idx; i++, page++) {
         __free_pages_bootmem(page, 0);
         count++;
     }
     total += count;
     bdata->node_bootmem_map = NULL;
 
     return total;
}
在这个函数中做了几件事:首先收回了未被bootmem分配的页,并把它们链接到buddy的链表中。然后收回了用于保存bootmem内存分配信息的map页,最后将node_bootmem_map成员设置为NULL,以此表示bootmem将不再使用。此函数将返回回收的SDRAM页面的数量。
buddy算法在后文分析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

嵌云阁主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值