FreeBSDVM内核内存管理
本文涉及到的源码是FreeBSD5.0Release,参考4.4BSD设计与实现相关章节,Matt Dillon的文章。VM系统涉及的主要数据结构描述
1. vmspace
该结构用于描述一个进程的虚拟地址空间,其包含了平台无关性的vm_map结构和平台相关性的pmap结构,以及该进程内存使用的一些统计计量。
2. vm_map
该结构是描述与平台无关性的虚拟地址空间的最高层数据结构,其包含了一系列虚拟地址有效地址映射实体和这些映射的属性。
3. vm_map_entry
该结构描述了一段虚拟地址空间(start – end),以及该段地址空间代表的是一种VM对象、另一个地址映射还是一个地址子映射,及其相应的共享保护和继承等属性。
4. vm_object
该结构描述了一段虚拟地址空间的数据来源,它可以描述一个文件、一段为零的内存和一个设备等等。
5. vm_page
该结构描述了一页物理内存,是VM用于表述物理内存的低层数据结构。页尺寸是在系统启动时,由平台决定的。
6. pagerops (vm_pager)
该结构描述了VM对象的后台存储如何访问,在FreeBSD中,是通过pagerops结构描述函数指针,实现不同类型的对象的具体操作,在vm_object结构中,通过handle成员指定具体类型对象对应的数据结构,比如设备类型对应dev_t (cdev结构指针)。在一般OS描述中,采用vm_pager描述该目的的数据结构。
本文集中讨论FreeBSD内核虚拟地址空间的管理,涉及到内核地址空间分配和内核地址空间动态分配。FreeBSD的内核空间总是被映射到每一个进程的地址空间的最高部分。和任何其它进程一样,内核也是通过包含一系列的vm_map_entry结构实体的vm_map结构来管理一段地址空间的使用。子映射是内核映射特有的,用于隔离、限制一段地址空间以提供给内核子系统使用,比如mbuf操作。本文主要讨论与平台无关性的内容,涉及到平台相关性时,以i386为例简要说明。
1. SI_SUB_VM初始化
在系统启动时,mi_startup()函数会调用SI_SUB_VM初始化与平台无关的VM系统,其定义是:“SYSINIT(vm_mem, SI_SUB_VM, SI_ORDER_FIRST, vm_mem_init, NULL)”。在vm_mem_init函数初始化之后,我们就只使用虚拟内存了,现在分析该函数的实现:
vm_set_page_size();
该函数设置页面尺寸,i386是PAGE_SIZE(4K),记录在系统统计vmmeter结构类型的全局变量cnt的v_page_size成员中。
virtual_avail = vm_page_startup(avail_start, avail_end, virtual_avail);
该语句初始化常驻内存模块。分析函数vm_page_startup的参数和返回值:avail_start的值是从系统启动时,汇编语言调用init386的入参first,指明有效内存的起始地址;avail_end是在getmemsize()函数结束时,通过avail_end = phys_avail[pa_indx];语句获得,该函数是一个与平台相关的函数,这里不作详细讨论,只须明白getmemsize()函数是获得具体物理内存的尺寸;virtual_avail是指向第一个可用页面的虚拟地址,在调用vm_page_startup函数前,是在pmap_bootstrap函数中获得初始值,并在vm_page_startup函数中调整获得真实的值。函数vm_page_startup将物理内存整理、分配为页面单元,并初始化页面管理模块所需信息,每一个页面单元被放置在自由链表中,该函数实现的详细讨论在页调度中讨论,作为内核管理涉及到的区域分配器初始化的一部分是在该函数中通过调用uma_startup函数实现的,该函数的实现在随后讨论。
vm_object_init();
初始化VM的对象模块,FreeBSD是通过统一的vm_object结构使用虚拟内存,该函数完成虚拟内存对象模块所需信息的初始化。
vm_map_startup();
初始化VM地址映射模块。
kmem_init(virtual_avail, virtual_end);
该函数创建内核虚拟地址映射关系,将内核文本、数据、BSS和所有系统启动时已经分配了的空间做一个映射,插入VM_MIN_KERNEL_ADDRESS和virtual_avail之间,余下的virtual_avail和virtual_end之间的地址空间是可用的自由空间。
pmap_init(avail_start, avail_end);
该函数初始化物理内存地址空间的映射关系。
vm_pager_init();
该函数实现系统所支持的所有页面接口类型的初始化,页面接口为数据在其支持的存储空间和物理内存之间的移动提供了一种机制,比如磁盘设备与内存之间,文件系统与内存之间。
至此,vm_mem_init函数执行完成,VM系统初始化完成。
2. 内核地址空间分配
VM系统内核使用的虚拟地址空间段提供了一套用于分配和释放的函数,这些空间段可以从内核地址映射和子映射中分配获得。
根据申请的页是否可以被pageout守护进程调度,内核内存分配有两种路径。在VM子系统初始化时,调用了kmem_init函数创建了内核映射。我们分析该函数的具体实现:
函数void kmem_init(vm_offset_t start, vm_offset_t end)
m = vm_map_create(kernel_pmap, VM_MIN_KERNEL_ADDRESS, end);
函数vm_map_create根据给定了kernel_map物理地址,创建一个新的地址映射m,而VM_MIN_KERNEL_ADDRESS和end给出了该映射范围的下水位(lower address bound)和上水位(upper address bound)。
kernel_map = m;
kernel_map->system_map = 1;
(void) vm_map_insert(m, NULL, (vm_offset_t) 0,
VM_MIN_KERNEL_ADDRESS, start, VM_PROT_ALL, VM_PROT_ALL, 0);
vm_map_unlock(m);
由于函数kmem_init仅用于系统初始化,创建内核地址映射,因此,将获得的地址映射赋给全局变量kernel_map保存,通过vm_map_insert函数创建一个vm_map_entry实体记录相关值,VM_PROT_ALL和VM_PROT_ALL标识这段虚拟地址的访问权限,参见/sys/vm/vm.h定义。
2.1 Wired (nonpageable,不可被pageout调度的页)分配函数
固定页(wired page)是从来不会产生页错误(page fault)。其分配是由kmem_alloc函数和kmem_malloc函数实现的。
函数vm_offset_t kmem_alloc(vm_map_t map, vm_size_t size)
该函数用于在内核地址映射或子映射中,分配内存。
size = round_page(size);
调整申请内存的尺寸,使之为PAGE_SIZE的整数倍。
vm_map_lock(map);
if (vm_map_findspace(map, vm_map_min(map), size, &addr)) {
vm_map_unlock(map);
return (0);
}
offset = addr - VM_MIN_KERNEL_ADDRESS;
vm_object_reference(kernel_object);
vm_map_insert(map, kernel_object, offset, addr, addr + size,
VM_PROT_ALL, VM_PROT_ALL, 0);
vm_map_unlock(map);
在vm_map结构的lock锁机制保护下。通过调用vm_map_findspace函数查询map地址映射是否有足够的空间满足申请的内存尺寸,如果成功,则可用空间的起始地址存于addr中;如果失败则返回1,则kmem_alloc函数调用失败。获得该内存分配空间起始地址与VM_MIN_KERNEL_ADDRESS的偏移。通过vm_object_reference函数对内核对象kernel_obj计数器ref_count加1,kernel_obj的初始化是在VM初始化时,调用vm_object_init实现的,其类型是OBJT_DEFAULT。调用vm_map_insert函数将刚找到的虚拟地址空间插入地址映射map的vm_map_entry链表中。
for (i = 0; i < size; i += PAGE_SIZE) {
vm_page_t mem;
mem = vm_page_grab(kernel_object, OFF_TO_IDX(offset + i),
VM_ALLOC_ZERO | VM_ALLOC_RETRY);
if ((mem->flags & PG_ZERO) == 0)
pmap_zero_page(mem);
mem->valid = VM_PAGE_BITS_ALL;
vm_page_flag_clear(mem, PG_ZERO);
vm_page_wakeup(mem);
}
接下来的这段代码非常有意思,对于申请的内存空间每一页,通过调用vm_page_grab函数,查看该页是否已经被kernel_object持有,如果是,则根据vm_page成员flags标识,如果是PG_BUSY,则等待该标识清PG_BUSY,将该页重新设置为PG_BUSY,返回该地址映射(mem),如果该页没有被kernel_object持有,则分配一个新页(mem)。通过判断PG_ZERO标识,保证该页已经清零。最后通过vm_page_wakeup函数,给正在等待该页分配的线程一个唤醒的机会。这段代码在查找kernel_object持有页时,采用了自顶向下的展开算法(Sleator and Tarjan's top-down splay algorithm)。
(void) vm_map_wire(map, addr, addr + size, FALSE);
设置该段内存是wired。
函数kmem_alloc是非常低层的,一般和平台相关性的函数在申请内存会调用该函数,比如sysarch()。通常kmem_alloc是使用kernel_map地址映射和kernel_object VM对象。
函数vm_offset_t kmem_malloc(vm_map_t map, vm_size_t size, int flags)
该函数同样是用于在内核地址映射或子映射中,分配内存。区别是:
a) kmem_alloc函数在不能获得内存时,可以阻塞等待,而在中断层,分配内存是不能阻塞的,因此需要kmem_malloc以M_NOWAIT标识调用。