三,储存管理
1. 虚拟地址空间布局
在一个 32 位机器上,可以分配 1-2GB 的虚拟内存空间供内核使用。
2. 虚拟内存的数据结构
vmspace |
Structure that encompasses both the machine-dependent and machine-independent structures describing a process's address space |
vm_map |
Highest-level data structure that describes the machine-independent virtual address space |
vm_map_entry |
Structure that describes a virtually contiguous range of addresses that share protection and inheritance attributes and that use the same backing-store object |
Structure that describes a source of data for a range of addresses |
|
shadow object |
Special object that represents modified copy of original data |
vm_page |
The lowest-level data structure that represents the physical memory being used by the virtual-memory system |
Data structures that describe a process address space.
3. 内核映射与子映射
与任何进程一样,内核有一个 vm_map 结构以及相应的一组 vm_map_entry 结构来描述一段地址范围的使用情况。
子映射是内核独有的结构,它用来隔离和限制给内核子系统分配的地址空间。在需要连续的若干内核地址空间的子系统中会用到它。
Kernel address-space maps.
4. 内核地址空间的分配
1> 不可调页的地址范围
也叫做固定的地址范围,在调用时就给它分配物理内存, pageout 守护进程不会替换这段内存。固定的页面从来不产生可能引发一次阻塞操作的缺页,固定的内存有 kmem_alloc() 和 kmem_malloc() 分配。
2> 可调页的地址范围
可调页的内核虚拟内存可以用 kmem_alloc_pageable() 和 kmem_alloc_wait() 来分配。可调页的地址范围是在需要时才分配物理内存的,而且作为正常替换策略的一部分,该内存可以由 pageout 守护进程回写到后备存储器上。
5. 内核的存储分配
1> 分配策略
利用率 = 需要量 / 请求量
需要量是已经获得但尚未释放的内存总量;请求量是已经为内存池分配的内存量,用内存池中的内存来满足需要量。由于碎片的存在以及要保留一些空闲内存为以后的需求做好准备,所以存储分配程序请求的内存量要比需要量更多。
内存对分配少量内存和分配大量内存采用了混合策略。分配少量内存使用 2- 幂表策略 (power-of-2 list strategy) 。如区域存储分配程序中,这些区域的大小按 2 的幂依次从 16 个字节增加到一个页面的大小。也即 2- 幂算法分配大小为 1,2,4,8,…,n 。分配大块内存时采用按页面的倍数分配内存,也即 1,2,3,4,….,n 。首先把需要分配的大内存向上取整为页面大小的倍数。接下来,分配程序使用“首次匹配”算法在为动态分配保留的内核地址空间中找到可以分配的空间。
2> 区域存储分配程序
比如进程,线程, vnode 和控制块结构,这些结构有几个共同的特征:
它们倾向于变大,因而容易浪费空间。
它们往往很常用。
它们经常会链接到一起构成长链表。
这些结构常常会包含许多必须在使用之前初始化过的链表和锁。
出于上述原因, FreeBSD 提供了一个区域分配程序 (zone allocator) ,有时候也称为分片分配程序 (slab allocator) 。区域分配程序提供了一个高效接口,用于管理一群大小和类型相同的结构。
区域是有大小相等的结构所构成的一个可以扩展的集合。新的区域用函数 uma_zcreate() 来创建。它必须指定要分配的结构的大小,并且要登记两组函数。第一组函数在从区域内分配或者释放一个结构时调用。第二组函数在给区域分配或者释放内存时调用。新的结构用 uma_zalloc() 函数来分配,它接受一个由 uma_zcreate() 返回的区域标识符作为参数, uma_zfree() 函数用来释放结构,它接受一个区域标识符和一个指向要被释放的结构的指针作为参数。区域存储分配程序不在分配的每块内存中保存它自己的大小信息,而是把大小信息和内存页关联起来。在分配的内存块之外保存所分配内存的大小。
6. 进程的虚拟地址空间的构成
这里的 object 通常用来保存有关一个文件或者一块匿名内存区域的信息。不管其是被系统中的单个进程还是多个进程所映射,总是用同一个 object 来表示它。
Object 保存如下信息:
该对象当前驻留在主存中的页面链表;其中每一个页面可能被同时映射到多个地址空间中,但是它始终只为一个 object 所有;
该对象被 vm_map_entry 结构或其他对象引用的次数;
该对象所描述的文件和匿名区域的大小;
该对象驻留内存的页面数;
指向影子对象的指针;
该对象所用调页器的类型;
系统中总共有 3 种类型的对象:
命名 (named) 对象表示文件;它们也可以用来表示那些能够提供可映射内存的硬件设备;
匿名 (anonymous) 对象用来表示那些在初次使用时被清零的内存区域;在不再需要的时候,他们会被直接丢弃;
影子 (shadow) 对象用来存放在页面被修改后的私有副本;当它们不再被引用时,就被自动丢弃。
7. 创建进程
FreeBSD 采用写时复制技术来创建进程, fork 操作所涉及到的父进程和子进程都引用相同的物理页面,而不是复制父进程的每一个内存页面,任何修改操作都产生保护错误,从而为其创建私有快照。
vfork 与普通的 fork 不同,其实现总是比“写时复制”方式的实现效率高,因为内核不用为子系统复制地址空间,而是简单地把父进程的地址空间传递给子进程,并挂起父进程。当然, vfork 这种调用在结构上是有缺陷地—子进程得到控制权后可以修改父进程的内容和地址空间的大小。 vfork 应该是在写时复制之前出现的。
8. 文件映射
系统调用 mmap 可以把一个文件映射到一块地址空间。
Five types of overlap that the kernel must consider when adding a new address mapping.
9. 调页器 (paper)
调页器接口提供了在后备存储 (backing store) 和物理内存 (physical memory) 之间转移数据的机制。其负责提供数据来填充页面,以及在页面被修改后提供一个位置保存该页。
调页器定义的操作如下:
Operation |
Description |
Pgo_init() |
initialize pager |
pgo_alloc() |
allocate pager |
pgo_dealloc() |
deallocate pager |
pgo_getpages() |
read page(s) from backing store |
pgo_putpages() |
write page(s) to backing store |
pgo_haspage() |
check whether backing store has a page |
pgo_pageunswapped() |
remove a page from backing store (swap pager only) |
目前系统支持 4 种类型的调页器:
1> vnode pager
vnode 调页器处理的对象映射了以讹文件系统中的文件。不管文件是通过 open 显式地打开,还是通过 exec 隐式地打开,系统都要给它分配一个 vnode 。在分配一个新的 vnode 的过程中,一是要分配一个对象来保存该文件的页面,二是要将该对象关联到 vnode 调页器。然后设置调页器句柄指向 vnode ,同时私有数据保存了文件的大小。如果对一个文件做了私有映射,那么修改过的页面就不能被写回文件系统。这种映射必须使用一个影子对象和交换调页器来处理所有修改过的页面。所以说,不会要求一个私有映射对象将脏数据写回它所属的文件。
2> device pager
设备调页器处理的对象代表映射到内存的硬件设备。映射到内存的设备提供的接口看起来就像是一段内存空间。设备调页器与其他三种调页器有根本区别,因为它并不在给定的物理内存页面中填入数据。相反,它创建并管理一个自己的 vm_page 结构,每一个结构表示设备空间的一个页面。这些页面的链表头保存在对象的调页器私有数据区内。这种方法使设备的存储器看上去就像系统直接安装的物理存储器。因此在虚拟内存中不需要特别的代码来处理设备的存储器。
3> physical-memory pager
物理内存调页器处理的对象包含有非嗲也内存。它们只用于 System V 的共享接口,该接口可以通过配置来使用非调页内存,代替默认使用的可交换内存。
4> swap pager
交换调页器一词是指两种功能不同的调页器。最常用的交换调页器是指由映射匿名内存的对象所使用的调页器。这种调页器有时候也称为 default pager ,它提供了通常所说的交换空间:在第一次访问时清零的临时性后备存储空间。当第一次创建一个匿名对象的时候,给它指派的就是默认调页器。默认调页器既不分配资源也不提供后备存储。但是,当 pageout 守 护进程第一次要求将一个处于活动状态的页面从某个匿名对象里移除的时候,默认调页器就会用交换调页器代替它自己。交换调页器负责管理交换空间:它计算出把 脏页保存在哪里,当再次需要它们的时候如何找到它们。影子对象要求这些操作必须高效执行,一个典型的影子对象分布会很分散。其次,对交换调页器要求能够以 异步方式写回脏页面,这对于单线程的 pageout 守护进程很有必要。
10. 调页机制
三个重要策略来定义调页系统:
何时系统把页面载入内存—取页策略 (fetch policy)
系统把页面放在何处—放置策略 (placement policy)
当放置操作向主存申请不到放置页面的空间时,如何选择要从主存里删除的其他页面—替换策略 (replacement policy)
缺页处理:
/*
* Handle a page fault occurring at the given address,
* requiring the given permissions, in the map specified.
* If successful, insert the page into the associated
* physical map.
*/
int vm_fault(
vm_map_t map,
vm_offset_t addr,
vm_prot_t type)
{
RetryFault:
lookup address in map returning object/offset/prot;
first_object = object;
[A] for (;;) {
page = lookup page at object/offset;
[B] if (page found) {
if (page busy)
block and goto RetryFault;
remove from paging queues;
mark page as busy;
break;
}
[C] if (object has nondefault pager or
object == first_object) {
page = allocate a page for object/offset;
if (no pages available)
block and goto RetryFault;
}
[D] if (object has nondefault pager) {
scan for pages to cluster;
call pager to fill page(s);
if (IO error)
return an error;
if (pager has page)
break;
if (object != first_object)
free page;
}
/* no pager, or pager does not have page */
[E] if (object == first_object)
first_page = page;
next_object = next object;
[F] if (no next object) {
if (object != first_object) {
object = first_object;
page = first_page;
}
first_page = NULL;
zero fill page;
break;
}
object = next_object;
}
[G] /* appropriate page has been found or allocated */
orig_page = page;
[H] if (object != first object) {
if (fault type ==WRITE) {
copy page to first_page;
deactivate page;
page =first_page;
object =first_object;
} else {
prot &=~ WRITE;
mark page copy-on-write;
}
}
[I] if (prot &WRITE)
mark page not copy-on-write;
enter mapping for page;
enter read-only mapping for clustered pages;
[J] activate and unbusy page;
if (first_page != NULL)
unbusy and free first_page;
}
硬件高速缓存的算法,实际的高速缓存全部由硬件实现:
struct cache {
vm_offset_t key; /* address of data */
char data[LINESIZE]; /* cache data */
} cache[CACHELINES][SETSIZE];
/*
* If present, get data for addr from cache. Otherwise fetch
* data from main memory, place in cache, and return it.
*/
hardware_cache_fetch(vm_offset_t addr)
{
vm_offset_t key, line;
line = (addr / LINESIZE) % CACHELINES;
for (key = 0; key < SETSIZE; key++)
if (cache[line][key] .key == addr)
break;
if (key < SETSIZE)
return (cache[line][key].data);
key = select_replacement_key(line);
cache[line][key].key = addr;
return (cache[line][key].data = fetch_from_RAM(addr));
}
12.page coloring
页面填色是一种性能优化措施,设计它是为了确保对虚拟内存中的连续地址的访问可以充分地利用物理地址的高速缓存。页面填色算法的作用是确保虚拟内存中连 续的页面在高速缓存看起来也是连续的。页面填色算法不是给虚拟地址随机地分配物理页面,而是把在高速缓存看起来是连续的物理页面分配给虚拟地址。两个物理 页面如何它们命中了高速缓存内的连续页面,则称它们是高速缓存连续的 (cache-contiguous) 。
为了确保均匀地使用颜色,下一个对象使用的起始颜色值要更换,变化幅度这样选择:即这个对象的大小和 L1 高 速缓存大小的三分之一相比,两者中较小的那一个。在一个对象上发生缺页时,新页面的首选颜色这样计算:对象所缺页的逻辑页号加上该对象的起始颜色,再对颜 色数取模。如果所得颜色的空闲链表里有页面可用,就从那个颜色的空闲链表里取得这一新页。否则,检查和所要求的颜色相距最远的颜色,直至找到一个可用的页 面。
从高速缓存性能的角度来看,页面填色机制让虚拟内存有了和物理内存一样的确定性。这样一来,在编写程序的时候就会有这样假设,下层硬件高速缓存的特性对于它们的虚拟地址空间来说,表现出了和程序直接运行在物理地址空间内时一样的特性。
简单说,页面填色减少了物理页冲突,也即多个虚拟地址通知映射到相同物理页的几率。当上述情况发生时,会频繁地发生缓存不命中从而产生缺页。
13. 主存链表
内核将主存分成了 5 个链表:
固定 (wired) 页面链表:固定页面被锁定在内存中,不能被调出。
活动 (active) 页面链表:活动页面是正在被一个或者多个虚拟内存区域使用的页面。
非活动 (inactive) 页面链表:非活动页面里的内容仍然是已知的内容,但是它们通常不是任何活动区域的一部分。如果页面的内容变“脏”,那么在重新使用该页面之前,必须先将变“脏”的内容写入后备存储中去。一旦页面已经被清理过了,就会把它转移到缓存链表去。
缓存 (cache) 页面链表:缓存页面里的内容依然是已知的内容,但是它们通常不是任何活动区域的一部分。如果它们被映射到了一个活动的区域,那么必须将它们标为只读,于是对它们执行任何写操作都会使得它们被移除缓存链表。
空闲 (free) 页面链表:空闲页面里面不含有用的内容,它们将用来满足新发生的缺页请求的需要。 Idle 进程保持空闲链表中 75% 的页面都清零,这样在用它们来解决匿名区域缺页问题时,就不必再清零了。
主存中可以由用户进程使用的页面是活动,非活动,缓存以及空闲链表中的页面。如果空闲链表中有页面的话,新申请的页面首先从空闲链表中取得,否则就从缓存链表中取。缓存和空闲链表又按颜色细分成了几个独立的链表。
14.pageout 进程
页面的替换操作由 pageout 守护进程 (pid 为 2) 来完成。其目标就是保持非活动,缓存和空闲链表中的页面数量处于希望的范围内。
/*
* Vm_pageout_scan does the dirty work for the pageout daemon.
*/
void vm_pageout_scan(void)
{
[A] page_shortage = free_target + cache_minimum -
(free_count + cache_count);
max_writes_in_progress = 32;
[B] for (page = FIRST(inactive list); page; page = next) {
next = NEXT(page);
if (page_shortage < 0)
break;
if (page busy)
continue;
[C] if (page's object has ref_count > 0 &&
page is referenced)
update page active count;
move page to end of active list;
continue;
}
if (page is invalid)
move page to front of free list;
page shortage--;
continue;
}
[D] if (first time page seen dirty) {
mark page as seen dirty;
move to end of inactive list;
continue;
}
check for cluster of dirty pages around page;
start asynchronous write of page cluster;
move to end of inactive list;
page_shortage--;
max_writes_in_progress--;
if (max_writes_in_progress == 0)
break;
}
[E] page_shortage = free_target + cache_minimum + inactive_target
- (free_count + cache_count + inactive_count);
[F] for (page = FIRST(active list); page; page = next) {
next = NEXT(page);
if (page_shortage < 0)
break;
if (page busy)
continue;
if (page's object has ref_count > 0) {
update page active count;
move page to end of active list;
continue;
}
[G] decrement page active count;
if (page active count > 0) {
move page to end of active list;
continue;
[G] decrement page active count;
if (page active count > 0) {
move page to end of active list;
continue;
}
page_shortage--;
if (page is clean)
move page to end of cache list;
else
move page to end of inactive list;
}
[H] page_shortage = free_minimum - free_count;
for (page = FIRST(cache list); page; page = next) {
next = NEXT(page);
if (page_shortage < 0)
break;
if (page busy)
continue;
move page to front of free list;
}
[I] if (targets not met)
request swap-out daemon to run;
[J] if (nearly all memory and swap in use)
kill biggest process;
}
15. 采用交换机制的原因
1> 系统内存太少以至于对进程采用调页机制不能够快的释放内存来满足需求。
2> 进程处于完全非活动状态操作 10 秒钟。另外,这样的进程会保留一些同用户结构和线程栈相关的内存页面。