本文基于linux-3.11.1代码, arm cpu, flat memory model(平坦连续的内存,用于嵌入式)为背景来说明。
start_kernel() -> setup_arch()
arch/arm/kernel/setup.c
883行,调用setup_machine_fdt() -> early_init_dt_scan() -> early_init_dt_scan_memory() -> early_init_dt_add_memory_arch() -> arm_add_memory()去解析device tree文件中描述的内存信息,并保存到meminfo中。
904行,将解析出来的系统中拥有的内存bank[]按物理地址从低到高进行堆排序。这样就保存了bank[n]的起始地址+size < bank[n+1]的起始地址。
909行,调用arm_memblock_init()将系统中的所有内存都记录到memblock.memory中,同时记录系统当前被使用掉的内存信息到memblock.reserved中,比如内核镜像占据的内存范围、Initrd占据的内存范围,device tree占据的内存范围、mmu内存页表占据的内存范围、以及DMA占用的内存范围等。
911行调用paging_init(), paging_init()主要是配置一些虚地址映射,比如低端内存地址映射,dma内存地址映射等。最后,调用bootmem_init()。
arch/arm/mm/init.c
我们接着来看bootmem_init()。
start_kernel() -> setup_arch() -> paging_init() -> bootmem_init()
arch/arm/mm/init.c
find_limits()从meminfo.bank[]数组中低端内存里找出最小内存的物理帧号(bank[0]),低端内存的最大内存的物理帧号,以及高端内存的最大内存的物理帧号,并分别记录到min_low_pfn, max_low_pfn, max_pfn全局变量中。meminfo.bank[]各bank记录的内存有可能不连续,但我们讲述的是flat memory model,所以假定bank[]中的内存是前后衔接,且连续的。即bank[i].end == bank[i+1].start.
我们继续看bootmem_init(), 来看一下349行zone_sizes_init()做了什么。
arch/arm/mm/init.c
200-202行,计算flat mem model下的低端内存帧数和高端内存帧数,保存在zone_size[0]和zone_size[1]中。
209-224行,对于非flat mem model下,计算不连续的memory的帧数(hole pfn number),并保存于zhole_size[]中。对于flat mem model,zhole_size[]都为0.
236行,调用free_area_init_node().
mm/page_alloc.c
4935行,pgdat->node_start_pfn 表示物理内存的起始帧号(物理内存并非一定是从0地址开始的)。
4940行,调用calculate_node_totalpages(),用于计算flat mem model下的物理内存帧数,保存于pgdat-> node_spanned_pages, 同时计算非flat mem model下的实际的物理内存帧数,并保存于pgdat-> node_present_pages.在flat memory模型下,node_spanned_pages和node_present_pages相等。
4943行,alloc_node_mem_map(), 调用memblock_virt_alloc_node_nopanic(),为flat mem model下的所有物理内存从低端内存的高地址处申请管理结构体数组struct page[], 数组地址保存于pgdat->node_mem_map和mem_map中。memblock_virt_alloc_node_nopanic()分配时,从memblock.memory中寻找非memblock.reserved的且满足size的低端物理内存(0~768M),然后返回其虚拟地址。
内存视图如下:
我们回到函数free_area_init_node().
4950行,调用free_area_init_core()
mm/memblock.c
free_area_init_core()主要是初始化pgdat的list, lock等。
4802-4845, 循环,计算normal_zone和highmem_zone两个zone所用所有的内存页数,并记录在zone->spanned_pages和zone->present_pages.(flat memory模型中,这两个相等)。
4873行,调用setup_usemap()主要是构建该zone管理的内存页的使用情况的位图(bit map)。
mm/page_alloc.c
usemap_size()计算zonesize对应位图所需要的内存字节数,1bit对应1Kbytes(pageblock_order)。zone管理它的内存,分为了多个area,如下:
计算处mapsize后,调用memblock_virt_alloc_node_nopanic()从低端内存高地址处申请bitmap内存。
free_area_init_core()最后调用memmap_init().
mm/page_alloc.c
4060-4079行,定位zone中的每个内存页对应的struct page结构,将其设置为reserved状态。
4097行,调用set_pageblock_migratetype()。
mm/page_alloc.c
set_pageblock_migratetype()计算struct page所代表的物理内存帧pfn(6079行), 然后计算该pfn对应的位图段位置,将其对应的位图段清零。6088-6089行怎么理解?
假设,bitidx为40,即bitidx & (BITS_PER_LONG-1) = 8(取模运算),即该pfn对应位图字节的字节内的偏移为8,然后加上end_bitidx,即字节内偏移为8+4=12, 但是这个偏移是从高bit开始偏移的,即pfn对应的位图字节的字节内的bit位置为bit22 ~ bit19, 这样mask << (BITS_PER_LONG – bitidx - 1)就是该pfn对应的位图字节内的占据的bits。
最后,6093行,设置pfn占据的4bits(mask = 0b 111)中的PB_migrate - PB_migrate_end,PB_migrate_skip为0(word & ~mask),然后将这4bits设置为MIGRATE_MOVABLE(2).(|flags)
即表示这个pfn是movable的。
pfn bitmap所在位置如下:
我们接着看相关代码:
start_kernel() -> mm_init() -> mem_init()
arch/arm/mm/init.c
532行,pfn_to_page(max_pfn)指向最大的物理内存帧对应的struct page[i],mem_map指向struct page[0].
535行,对于flat mem model相当于什么都不用做。对于非连续内存,它是用于释放meminfo.bank[]之间的hole的内存,将这些hole内存移到memblock.reserved中。
536行,调用free_all_bootmem().
mm/nobootmem.c
(linux早期版本时候用的bootmem,后面都用memblock了,我们基于memblock来讲解)
这个函数相等于buddy算法的初始化了,我们重点来看一下。
假设,我们的内存是flatten的,即平坦连续的,不存在hole。如下图所示:
free_all_bootmem()在174行,设置所有zone的managed_pages为0,即z->managed_pages = 0.().
之后调用free_low_memory_core_early()去释放所有的低端内存页。free_low_memory后,在182行,totalram_pages(系统总共free的pages数,包括低端内存页和高端内存页)记录了低端内存的free的pages数。
mm/nobootmem.c
122行,从memblock.memory中找出没有被reserved(memblock.reserved)的内存段的起始地址(start)和结束地址(end).
123行,调用__free_memory_core(start, end)去释放这些未被reserved的内存。
mm/nobootmem.c
__free_memory_core()将start, end地址转成pfn(物理内存帧号),且将结束帧限定在最大的地段内存帧内。
最后,调用__free_pages_memory(),去释放这段连续的内存页。
mm/nobootmem.c
__free_pages_memory()计算从start_pfn到end_pfn这段连续的内存的order的最大值(不超过10),然后调用__free_pages_bootmem()。注意,这里第一个参数为连续(1<<order)个内存页(start_pfn, start_pfn + (1<<order)-1)的首页,第二参数为order。__free_pages_bootmem()会将这一片的内存页的struct page结构中的reserved标记清除。
start += (1 << order)为下一个连续内存页的首页。
mm/page_alloc.c
__free_pages_bootmem() -> __free_pages() -> __free_pages_ok() -> free_one_page() -> __free_one_page().
最终,函数调用来到了__free_one_page(),第一个参数为当前连续的内存页的首页的帧号,第二个参数为order。我们来分析这个函数(略去不关注或不重点的代码)
mm/page_alloc.c
560行,将起始内存页的帧号取bit0~bit10。什么意思呢?就是将内存按2K划分成块,即2K个内存页为一块。这一行取起始内存页的块内的index.
562行,order为0-9的进入while()循环处理,order=10的不进while()。
563行,buddy_idx = page_idx ^ (1<<order),相当于buddy_idx = page_idx + 2order, page_idx为该片连续内存页的第一页的索引(2K块内的索引,即相对索引),故buddy_idx为该片(page_idx)连续内存页的紧接着的后面的连续内存页的首页,即这两个连续内存页为buddy关系(邻居?).
564行,buddy指向后面邻居连续内存页的首页的struct page结构。
566行,对于初始化过程,从这一行跳出。
586行,该片连续内存页的首页struct page设置order值,即表明从该页内存开始,以直到+ 2order - 1内存页,都是在同一order下管理的。
609行,将该片连续内存页的首页,放到zone->free_area[order].free_list[MOVEABLE]链表中。
之后程序回到__free_pages_memory(),将start到end之间剩余的内存页加到对应的order链表中。
最后,回到free_low_memory_core_early(),将其他的未reserved的内存段加到对应的order链表中。
处理完后的效果如下:
我们回到mem_init(),继续后面的代码。
538行,调用free_highpages() -> free_area_high()去释放高端内存中没有reserved的page,即将这些page中reserved标记去掉。
arch/arm/mm/init.c
mm/page_alloc.c
include/linux/mm/mm.h
include/linux/gpf.h
mm/page_alloc.c
由于order为0,故这里进入free_hot_cold_page()。
mm/page_alloc.c
所以,对于高端内存,都是放入了pcp链表list里面了。zone pcp初始化如下:
buddy内存相关的初始化就到这基本结束了,下面我们来看看buddy内存使用的内核API吧。
linux/gpf.h
__get_free_page(gfp_mask)
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
__get_free_pages()调用alloc_pages()来分配一个struct page,并返回这个page代表的内存页的虚地址。
mm/mm.h
alloc_page(gfp_mask)
alloc_pages(gfp_mask, order)
include/linux/gpf.h
mm/page_alloc.c
alloc_pages() -> alloc_pages_node() -> __alloc_pages() -> __alloc_pages_nodemask() ->get_page_from_freelist(), 这个get_page_from_freelist()就是从前面介绍的freelist(order > 0)或pcp(order ==0)中取空闲的page。代码如下:
mm/page_alloc.c
1545-1566, order == 0时从pcp中取page.
1567-1582, order > 0时,调用__rmqueue() -> __rmqueue_smallest()从freelist中取page.
mm/page_alloc.c
free_page(addr)
void free_pages(unsigned long addr, unsigned int order)
mm/page_alloc.c
__free_page(page)
__free_pages(struct page *page, unsigned int order)
__free_pages()就是将page对应的内存页释放到对应的freelist或pcp上。