Linux Kernel启动过程中的内存管理
好的操作系统必然要有好的内存管理系统来支持。好的内存管理系统就像一个艺术品,因为在其中我们可以看到空间优化和时间优化的完美平衡(既要省内存又要分配和释放足够快)。Linux为我们提供了这样一个范例,关于它的内存管理在很多讲kernel的书都可以找到。但在这一切还没有建立起来时,系统又是怎么工作的呢?
在系统启动时内存分配大致经历了这样几个阶段(基于kernel 2.6.29):
1. 静态分配(如果这也算一种的话。。。)
2. e820表
3. bootmem allocator
4. zone allocator(buddy system)
5. slab allocator
6. 虚拟空间分配,如用vmalloc, mmap这些函数分配
当然以上的几个阶段的时间界限并不总是很明显,有些时候是并存的。以下是初始化代码中几个关键点:
start_kernel()
setup_arch()
setup_memory_map() //从boot_params.e820_map读入e820表信息,以后就可以用find_e820_area()分配了。
init_memory_mapping()
kernel_physical_mapping_init() //在虚拟空间映射kernel页表的low mem部分,创建页表过程中需要分配内存就是通过find_e820_area()。
initmem_init()
setup_bootmem_allocator() //初始化bootmem allocator,bootmem allocator可用。
early_res_to_bootmem() //把之前静态分配或者从通过find_e820_area()分配的区域置成保留。
paging_init() //完成kernel页表high mem中persistent kernel mapping和temporary kernel mapping部分的初始化,之后可以通过kmap()或kmap_atomic()把物理页映射到high mem区域。
zone_sizes_init() //初始化zone allocator,但只是初始化,还没法用它分配,因为所有的freelist还是被置成空的。
vmalloc_init() //初始化分配noncontiguous memory area所需要的结构。vmalloc能分配high mem中从VMALLOC_START到VMALLOC_END的虚拟空间。加上前面paging_init()中提到的两种,针对kernel的high mem的三种映射方式就全了。该函数中通过bootmem allocator分配自身需要的内存。
mem_init() //完成zone allocator,也就是buddy system的初始化,之后alloc_page()就可以用了。这里将bootmem allocator中的未分配空间转到zone allocator中,然后禁用了bootmem allocator。
kmem_cache_init() //初始化slab allocator。它是zone allocator上的一层加强,弥补了zone allocator的一些固有不足,如只能以2的n次幂分配物理页。kmalloc()会从slab allocator上分配,而slab allocator中cache不够又会从zone allocator分配。
系统启动刚开始的一些数据是静态分配的,如kernel本身的代码段和数据段,因为这时还没有任何分配器存在。这些都被loader存放在固定的物理地址,并被临时页表映射到固定的虚拟地址。
e280表和find_e820_area()可以称得上最早的allocator,尽管它很简单。系统启动早期,detect_memory()函数中,系统通过15h中断从BIOS中读取物理内存信息,将之放到boot_params(也就是zeropage中)。之后set_memory_map()函数将这些信息再读入e820结构体里,之后find_e820_area()就可以从里面分配内存了。分配方式采用简单的线性查找,并把分配出去的空间通过reserve_early()记录到early_res这个结构中,这些信息将会在bootmem allocator的初始化时用来置位那些已分配的物理内存区域。举例来说,当系统要建立kernel页表时,需要申请页表本身所占的内存,于是调用one_page_table_init(),它发现bootmem allocator尚不可用,于是调用alloc_low_page(),这个函数就会到[table_start, start_end]这个区域里去拿内存,而这块内存是在之前find_early_table_space()中通过find_e820_area()申请出来的。当kernel页表建立完后,reserve_early()被调用,它将[table_start, table_end]这块区域以"PGTABLE"为label记录下来。
然后是bootmem allocator,它和e820直接分配一样,也是一个中间过渡产物。bootmem allocator,顾名思义就是在系统启动时候用的临时内存分配器。它在zone allocator建立好之后就被禁用,而在其中仍然空闲的区域会被回收到zone allocator中。bootmem allocator是一种基于bitmap的分配器,因此速度也很快。从bootmem allocator分配使用函数alloc_bootmem()。
再就是zone allocator。我们知道典型的系统上有三个zone:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。系统为每个zone都建立了我们熟悉的buddy system。简单地说,buddy system把物理内存区域按2的n次方(n称为order)挂在zone->free_area上。分配的时候拆分它们直到满足分配申请要求,释放的时候再进行合并。从zone allocator分配和释放分别用函数alloc_page()和free_page()。
Buddy system非常高效,但带来了内部碎片问题,因为它只能分配出2的0次方到2的MAX_ORDER次方的页,而且它也不利于硬件cache的利用。而内核中经常会频繁申请固定大小的内存如process descriptor, open file object等。如果每次都从buddy system中申请,既费时间又费空间。出于空间和时间的效率考虑,于是有了slab allocator。slab allocator相当于将内存资源按每种固定大小进行缓存,放在cache中。系统要的时候直接从这个cache里拿,而释放时则不是真的释放,而是放回到cache中。slab allocator维护很多cache,每个cache中包含同一类型的boject。cache又划分为slab,slab通常包含几个连接的物理页,其中存放在被分配的或者尚空闲的object。对于slab allocator中的内存资源,调用kmalloc()或者直接调用kmem_cache_alloc()进行分配,调用kfree()或者直接调用kmem_cache_free()进行释放。当kmem_cache_alloc()被调用且cache中没有object时,会调用cache_grow()来增加cache中的slab,这也是slab的创建过程。cache_grow()继而调用kmem_getpages()为object分配物理内存。而kmem_getpages最终会到buddy system中去分配(kmem_getpages() => alloc_pages_node() => __alloc_pages())。因此我们说slab allocator不是buddy system的替代,还是加强。
之后,系统的内存管理系统就初步建立好了。系统中的内存资源有两种-虚拟空间和物理空间。在kernel态,用vmalloc()申请虚拟空间,同时它也调用了alloc_page()申请物理空间,再映射到虚拟空间中。而kmalloc可以直接从slab allocator中申请物理空间,而slab allocator中如果内存不够了再到buddy system中去分配。这样申请来的物理空间在虚拟地址空间中还没有显式映射,当然了,如果是low mem部分则已经在系统初始化时被映射到PAGE_OFFSET处了。而user态中如果app调用malloc这样的函数,malloc会调用mmap,而mmap会申请调用进程的虚拟空间,这里虚拟空间还没有对应的物理页。只有真地访问时发生page fault了,在pagefault handler里才会从buddy system中去分配物理页。