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;
} bootmem_data_t;
· node_boot_start表示存放bootmem位图的第一个页面(即内核映象结束处的第一个页面)。
· node_low_pfn表示物理内存的顶点,最高不超过896MB。
· node_bootmem_map指向bootmem位图
· last_offset 用来存放在前一次分配中所分配的最后一个字节相对于last_pos的位移量。
· last_pos 用来存放前一次分配的最后一个页面的页面号。这个域用在__alloc_bootmem_core()函数中,通过合并相邻的内存来减少内部碎片。
下面介绍与bootmem相关的几个函数,这些函数位于mm/bootmeme.c中。
1. init_bootmem()函数
unsigned long __init init_bootmem (unsigned long start, unsigned long pages)
{
max_low_pfn = pages;
min_low_pfn = start;
return(init_bootmem_core(&contig_page_data, start, 0, pages));
}
这个函数仅在初始化时用来建立bootmem分配器。这个函数实际上是init_bootmem_core()函数的封装函数。init_bootmem()函数的参数start表示内核映象结束处的页面号,而pages表示物理内存顶点所在的页面号。而函数init_bootmem_core()就是对contig_page_data变量进行初始化。下面我们来看一下对该变量的定义:
int numnodes = 1; /* Initialized for UMA platforms */
static bootmem_data_t contig_bootmem_data;
pg_data_t contig_page_data = { bdata: &contig_bootmem_data };
变量contig_page_data的类型就是前面介绍过的pg_data_t数据结构。每个pg_data_t数据结构代表着一片均匀的、连续的内存空间。在连续空间UMA结构中,只有一个节点contig_page_data,而在NUMA结构或不连续空间UMA结构中,有多个这样的数据结构。系统中各个节点的pg_data_t数据结构通过node_next连接在一起成为一个链。有一个全局量pgdat_list则指向这个链。从上面的定义可以看出,contig_page_data是链中的第一个节点。这里假定整个物理空间为均匀的、连续的,以后若发现这个假定不能成立,则将新的pg_data_t结构加入到链中。
pg_data_t结构中有个指针bdata,contig_page_data被初始化为指向bootmem_data_t数据结构。下面我们来看init_bootmem_core()函数的具体代码:
/*
* 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 = ((end - start)+7)/8;
pgdat->node_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;
/*
* Initially all pages are reserved - setup_arch() has to
* register free RAM areas explicitly.
*/
memset(bdata->node_bootmem_map, 0xff, mapsize);
return mapsize;
}
下面对这一函数给予说明:
· 变量mapsize存放位图的大小。(end - start)给出现有的页面数,再加个7是为了向上取整,除以8就获得了所需的字节数(因为每个字节映射8个页面)。
· 变量pgdat_list用来指向节点所形成的循环链表首部,因为只有一个节点,因此使pgdat_list指向自己。
· 接下来的一句使memsize成为下一个4的倍数(4为CPU的字长)。例如,假设有40个物理页面,因此,我们可以得出memsize为5个字节。所以,上面的操作就变为(5+(4-1))&~(4-1)即(00001000&11111100),最低的两位变为0,其结果为8。这就有效地使memsize变为4的倍数。
· phys_to_virt(mapstart << PAGE_SHIFT)把给定的物理地址转换为虚地址。
· 用节点的起始物理地址初始化node_boot_start(这里为0x00000000)
· 用物理内存节点的页面号初始化node_low_pfn。
· 初始化所有被保留的页面,即通过把页面中的所有位都置为1来标记保留的页面
· 返回位图的大小。
2. free_bootmem()函数
这个函数把给定范围的页面标记为空闲(即可用),也就是,把位图中某些位清0,表示相应的物理内存可以投入分配。
原函数为:
void __init free_bootmem (unsigned long addr, unsigned long size)
{
return(free_bootmem_core(contig_page_data.bdata, addr, size));
}
从上面可以看出,free_bootmem()是个封装函数,实际的工作是由free_bootmem_core()函数完成的:
static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)
{
unsigned long i;
unsigned long start;
/*
* round down end of usable mem, partially free pages are
* considered reserved.
*/
unsigned long sidx;
unsigned long eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE;
unsigned long end = (addr + size)/PAGE_SIZE;
if (!size) BUG();
if (end > bdata->node_low_pfn)
BUG();
/*
* Round up the beginning of the address.
*/
start = (addr + PAGE_SIZE-1) / PAGE_SIZE;
sidx = start - (bdata->node_boot_start/PAGE_SIZE);
for (i = sidx; i < eidx; i++) {
if (!test_and_clear_bit(i, bdata->node_bootmem_map))
BUG();
}
}
对此函数的解释如下:
· 变量edix被初始化为页面总数。
· 变量end被初始化为最后一个页面的页面号。
· 进行两个可能的条件检查.
· start初始化为第一个页面的页面号(向上取整),而sidx(start index)初始化为相对于node_boot_start.的页面号。
· 清位图中从sidx到eidx的所有位,即把这些页面标记为可用。
3. reserve_bootmem()函数
这个函数用来保留页面。为了保留一个页面,只需要在bootmem位图中把相应的位置为1即可。
原函数为:
void __init reserve_bootmem (unsigned long addr, unsigned long size)
{
reserve_bootmem_core(contig_page_data.bdata, addr, size);
}
reserve_bootmem()为封装函数,实际调用的是reserve_bootmem_core()函数:
static void __init reserve_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)
{
unsigned long i;
/*
* round up, partially reserved pages are considered
* fully reserved.
*/
unsigned long sidx = (addr - bdata->node_boot_start)/PAGE_SIZE;
unsigned long eidx = (addr + size - bdata->node_boot_start +
PAGE_SIZE-1)/PAGE_SIZE;
unsigned long end = (addr + size + PAGE_SIZE-1)/PAGE_SIZE;
if (!size) BUG();
if (sidx < 0)
BUG();
if (eidx < 0)
BUG();
if (sidx >= eidx)
BUG();
if ((addr >> PAGE_SHIFT) >= bdata->node_low_pfn)
BUG();
if (end > bdata->node_low_pfn)
BUG();
for (i = sidx; i < eidx; i++)
if (test_and_set_bit(i, bdata->node_bootmem_map))
printk("hm, page %08lx reserved twice.\n", i*PAGE_SIZE);
}
对此函数的解释如下:
· sidx (start index)初始化为相对于node_boot_start的页面号
· 变量eidx初始化为页面总数(向上取整)。
· 变量end初始化为最后一个页面的页面号(向上取整)。
· 进行各种可能的条件检查.
· 把位图中从sidx到eidx的所有位置1
4.__alloc_bootmem()函数
这个函数以循环轮转的方式从不同节点分配页面。因为在i386上只有一个节点,因此只循环一次。
函数原型为:
void * __alloc_bootmem (unsigned long size,
unsigned long align,
unsigned long goal);
void * __alloc_bootmem_core (bootmem_data_t *bdata,
unsigned long size,
unsigned long align,
unsigned long goal);
其中__alloc_bootmem()为封装函数,实际调用的函数为__alloc_bootmem_core (),因为__alloc_bootmem_core ()函数比较长,下面分片断来进行仔细分析:
unsigned long i, start = 0;
void *ret;
unsigned long offset, remaining_size;
unsigned long areasize, preferred, incr;
unsigned long eidx = bdata->node_low_pfn -
(bdata->node_boot_start >> PAGE_SHIFT);
把eidx初始化为本节点中现有页面的总数。
if (!size) BUG();
if (align & (align-1))
BUG();
进行条件检查
/*
* We try to allocate bootmem pages above 'goal'
* first, then we try to allocate lower pages.
*/
if (goal && (goal >= bdata->node_boot_start) &&
((goal >> PAGE_SHIFT) < bdata->node_low_pfn)) {
preferred = goal - bdata->node_boot_start;
} else
preferred = 0;
preferred = ((preferred + align - 1) & ~(align - 1)) >> PAGE_SHIFT;
开始分配后首选页的计算分为两步:
(1)如果goal为非0且有效,则给preferred赋初值,否则,其初值为0。
(2)根据参数align 来对齐preferred的物理地址。
areasize = (size+PAGE_SIZE-1)/PAGE_SIZE;
获得所需页面的总数(向上取整)
incr = align >> PAGE_SHIFT ? : 1;
根据对齐的大小来选择增加值。除非大于4K(很少见),否则增加值为1。
restart_scan:
for (i = preferred; i < eidx; i += incr) {
unsigned long j;
if (test_bit(i, bdata->node_bootmem_map))
continue;
这个循环用来从首选页面号开始,找到空闲的页面号。test_bit()宏用来测试给定的位,如果给定位为1,则返回1
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;
}
这个循环用来查看在首次满足内存需求以后,是否还有足够的空闲页面。如果没有空闲页,就跳到fail_block。
start = i;
goto found;
如果一直到了这里,则说明从i开始找到了足够的页面,跳过fail_block并继续。
fail_block:;
}
if (preferred) {
preferred = 0;
goto restart_scan;
}
return NULL;
如果到了这里,从首选页面中没有找到满足需要的连续页面,就忽略preferred的值,并从0开始扫描。如果preferred为1,但没有找到满足需要的足够页面,则返回NULL。
found:
已经找到足够的内存,继续处理请求。
if (start >= eidx)
BUG();
进行条件检查。
/*
* 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) {
offset = (bdata->last_offset+align-1) & ~(align-1);
if (offset > PAGE_SIZE)
BUG();
remaining_size = PAGE_SIZE-offset;
if语句检查下列条件:
(1)所请求对齐的值小于页的大小(4k)。
(2)变量last_offset为非0。如果为0,则说明前一次分配达到了一个非常好的页面边界,没有内部碎片。
(3)检查这次请求的内存是否与前一次请求的内存是相临的,如果是,则把两次分配合在一起进行。
如果以上三个条件都满足,则用前一次分配中最后一页剩余的空间初始化remaining_size。
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);
如果请求内存的大小小于前一次分配中最后一页中的可用空间,则没必要分配任何新的页。变量last_offset增加到新的偏移量,而last_pos保持不变,因为没有增加新的页。把这次新分配的起始地址存放在变量ret中。宏phys_to_virt()返回给定物理地址的虚地址。
} 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;
所请求的大小大于剩余的大小。首先求出所需的页面数,然后更新变量last_pos 和 last_offset。
例如,在前一次分配中,如果分配了9k,则占用3个页面,内部碎片为12k-9k=3k。因此,page_offset为1k,且剩余大小为3k。如果新的请求为1k,则第3个页面本身就能满足要求,但是,如果请求的大小为10k,则需要新分配((10 k- 3k) + PAGE_SIZE-1)/PAGE_SIZE,即2个页面,因此,page_offset为3k。
}
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);
}
如果因为某些条件未满足而导致不能进行合并,则执行这段代码,我们刚刚把last_pos 和last_offset直接设置为新的值,而未考虑它们原先的值。last_pos的值还要加上所请求的页面数,而新page_offset值的计算就是屏蔽掉除了获得页偏移量位的所有位,即“size & PAGE_MASK”, PAGE_MASK 为 0x00000FFF,用PAGE_MASK的求反正好得到页的偏移量。
/*
* Reserve the area now:
*/
for (i = start; i < start+areasize; i++)
if (test_and_set_bit(i, bdata->node_bootmem_map))
BUG();
memset(ret, 0, size);
return ret;
现在,我们有了内存,就需要保留它。宏test_and_set_bit()用来测试并置位,如果某位原先的值为0,则它返回0,如果为1,则返回1。还有一个条件判断语句,进行条件判断(这种条件出现的可能性非常小,除非RAM坏)。然后,把这块内存初始化为0,并返回给调用它的函数。
5. free_all_bootmem()函数
这个函数用来在引导时释放页面,并清除bootmem分配器。
函数原型为:
void free_all_bootmem (void);
void free_all_bootmem_core(pg_data_t *pgdat);
同前面的函数调用形式类似,free_all_bootmem()为封装函数,实际调用free_all_bootmem_core()函数。下面,我们对free_all_bootmem_core()函数分片断来介绍:
struct page *page = pgdat->node_mem_map;
bootmem_data_t *bdata = pgdat->bdata;
unsigned long i, count, total = 0;
unsigned long idx;
if (!bdata->node_bootmem_map) BUG();
count = 0;
idx = bdata->node_low_pfn - (bdata->node_boot_start
>> PAGE_SHIFT);
把idx初始化为从内核映象结束处到内存顶点处的页面数。
for (i = 0; i < idx; i++, page++) {
if (!test_bit(i, bdata->node_bootmem_map)) {
count++;
ClearPageReserved(page);
set_page_count(page, 1);
__free_page(page);
}
}
搜索bootmem位图,找到空闲页,并把mem_map中对应的项标记为空闲。set_page_count()函数把page结构的count域置1,而__free_page()真正的释放页面,并修改伙伴(buddy)系统的位图。
total += count;
/*
* Now free the allocator bitmap itself, it's not
* needed anymore:
*/
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);
}
获得bootmem位图的地址, 并释放它所在的页面。
total += count;
bdata->node_bootmem_map = NULL;
return total;
把该存储节点的bootmem_map域置为NULL,并返回空闲页面的总数。