理解虚拟内存

简介

虚拟内存管理子系统是操作系统的核心之一. 有了虚拟内存(Virtual Machine-VM), 操作系统中诸如进程间隔离, 文件缓存, 存储交换(swapping)等一系列高级的功能才得以实现. 因此, 系统管理员只有在掌握操作系统中的虚拟内存管理的原理以及如何配置虚拟内存相关参数, 才能在一定的工作负载下配置出机器的最优的性能. 读完了本篇文章后, 你应该能基本掌握红帽公司企业级的Linux系统(RHEL3)的虚拟内存控制及其背后的实现算法. 不仅如此, 你更应该对通用的Linux虚拟内存管理的参数配置有一定的心得. 你也应该注意到, Linux作为操作系统其设计的革新是值得称道的. 内核中不适用的设计被摒弃了, 新的更好的设计也取代了旧的设计. 因而本文中所描述的配置参数对新版或者老版的Linux内核可能不再适用. 但是也不要气馁. 对虚拟内存管理有了深入的认识后, 配置其它的虚拟内存系统也就是小菜一碟了, 因为基本的原理是相通的. 对于特定的Linux内核版本, 可以在内核源码中Documentation/sysctl/vm.txt文件中找到虚拟内存相关的帮助, VM可供配置的参数也会描述于此文件中.

定义

为了正确的理解虚拟内存管理器的工作原理,磨刀不误砍柴工,我们先来了解一下虚拟内存的组成。虽然对于虚拟内存低层级组成概念很有益处,但是有必要更深入地了解虚拟内存如何工作以及怎样才能优化其性能。

虚拟内存是由什么构成?

图表1. 高级虚拟内存子系统组成图

Linux系统中的虚拟内存子系统复杂极其复杂,但是我们可以通过下面的组件更深入地了解虚拟内存:

MMU

内存管理单元(MMU, Memory Management Unit,下面简称MMU)是作为实现虚拟内存系统的物理硬件基础,MMU可以允许软件通过一个别名的地址跟物理地址建立映射,通常是多于一个。这是通过使用分页(pages)和分页表(分页表:分页表是一种数据结构,为使用电脑操作系统之虚拟内存技术,将内存空间切割成分页的形式,用于储存虚拟内存及实体内存间的对应). MMU再使用一部分内存,通过一系列的查找表(Table lookups)来翻译虚拟地址到物理地址的映射

Zoned Buddy Allocator (暂译为:区域内存分配器 没有找到中文标准的翻译, Buddy Allocator暂译为友内存分配器)

区域内存分配器负责整个虚拟内存系统分页存储管理。 这部分代码管理连续物理内存分页的链表并且让他们映射到MMU的分页表(page tables),当其他系统和核心子系统请求分配物理地址的时候,由其提供有效的物理地址(物理地址到虚拟内存地址的映射是被虚拟内存系统较高层处理的)。通过友内存分配器的名字我们就可以推断出子系统用来维护空闲列表的算法。所有在内存中的物理分页是被内存分配器分类和分组进入列表的。每一个列表代表了2n分页个的簇,这里的n会随着每个逐步自增。如果在请求列表中没有任何请求,下一个里诶包的请求将会被分在两个隔离的簇中并且在下一个请求到达的时候返回给请求者。当分配返回请求给到好友分配器内存分配器的时候,反转处理便开始了;注意到内存分配器也管理着定义不同用途的内存池的内存区域。目前内存分配器能够管理进入一下三种内存池:

Slab 分配器

Slab分配器提供了一种可用性更高的前端实现来配合Buddy(伙伴算法)分配器,它主要用来应对内核中某些部分需求大小更加灵活内存(并非常用的4KB)的请求。Slab分配器允许内核组件创建给定大小的内存对象缓存。Slab分配器负责将尽可能多的缓存对象放在一页并且监控哪些对象已经释放,哪些内存已经被分配。当有内存分配请求但是页面中没有内存可用时,Slab分配器会向Buddy分配器请求更多的页来满足分配请求。这就使得内核组件用一种更简单的方法来使用内存。使用这种方法,很多只利用一小部分内存的组件就不需要各自独立实现内存管理的代码,从而不需要浪费很多的页。Slab分配器只可能从DMA和NORMAL区域分配内存。

有关于slab分配器请参考:http://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/

内核线程

最后一个虚拟内存子系统的组件是内核线程,包括:kscand, kswapd, kupdated, 和bdflush。这些线程负责正在使用的内存的恢复和管理。虚拟内存中的所有页面都有一个关联的状态(更多关于内存状态机的信息请参考"页面的生命周期"章节)一般来说,内核中虚拟内存相关的活跃线程负责尝试将页面移出RAM的操作。它们定期的检查RAM,尝试识别和释放非活跃的内存,从而使得这一部分内存可以在系统中另作他用。

页面的生命周期

所有由虚拟内存管理的内存都会被一个状态标记。这些状态帮助虚拟内存知道在各种各样的情形下对给定的页面该做些什么。依赖于当前系统的需要,虚拟内存可能依据状态机(图示2. "虚拟内存页面状态机")将页面从一种状态转移到下一个状态。利用这些状态, 虚拟内存可以决定操作系统在某个时间对某个页面做了什么,并且它还可以决定对这个页面做什么操作。这些有特殊意义的状态如下所示

1.FREE —— 所有可被分配的页面从这个状态开始。这个状态告诉虚拟内存本页面没有被用于任何目的,并且可分配。

2.ACTIVE —— 页面已经被Buddy分配器分配了之后进入ACTIVE状态。这个状态告诉虚拟内存本页面已经被分配,并且它已经被内存进程或者用户进程所使用。

3. INACTIVE DIRTY —— 这个状态预示着本页面已经被要求分配它的进程所抛弃,并且它成为将要从主存中被剔除的候选者。kscand任务会定期扫描内存中的页面,并记下页面自从最后一次访问的到当前呆在内存的总时间。如果kscand任务发现自从上次它扫面这个页面以来,这个页面有被访问,它会增加这个页面的年龄计数器的值,否则,它会减少这个页面的年龄计数器的值。当kscand任务发现这个页面的年龄计数器的值为0,它会将这个页面的状态置成INACTIVE DIRTY状态。在INACTIVE DIRTY状态下的页面被保存在将要被清除的页面列表里面。

4. INACTIVE LAUNDERED —— 这是一个临时的状态,在这个状态下的页面已经被选择出要从主存中剔除,与此同时这个页面的内容将被保存在磁盘上。只有在INACTIVE DIRTY状态下的页面才能进入这个状态。一旦磁盘I/O操作(写磁盘操作)完成,这个页面的状态转移到INACTIVE CLEAN,在INACTIVE CLEAN 状态下,这个页面可能会被释放或者由于其他目的而被重写。如果在(写)磁盘操作期间,这个页面被访问了, 它的状态将变成ACTIVE。

5. INACTIVE CLEAN —— 这个状态下的页面已经被从内存中清除了。这意味着此页面的内容已经同步到磁盘上。从而,此页面可能会被虚拟内存释放或者由于其他目的而被重写。

VM Page State Machine

图示2. 虚拟内存页面状态机

VM调优

上图充分描述了VM的工作机制, 那么它是经过怎样的调整来适应特定的工作负载?在Linux VM中有两种方法可以修改一些可调参数。第一个是sysctl接口。 这个sysctl接口是一个面向对象的编程接口, 它可以让我们的应用程序直接修改各种系统的可调参数。 sysctl非常实用,它允许管理员通过命令行为任何一个可调VM参数指定一个值。举个例子:

sysctl -w vm.max map count=65535

sysctl工具同样支持配置文件(/etc/sysctl.conf), 写入此文件中的配置可以保存起来, 下次开机的时候会自动加载. 使用这种方式, 你可以保证你的配置设置一次后, 常久有用. 配置文件语法使用键-值对的格式, 辅以一定的注释(译者注: 注释以#号开头), 一目了然, 示例如下:

#Adjust the min and max read-ahead for files
vm.max-readahead=64
vm.min-readahead=32
#turn on memory over-commit 
vm.overcommit_memory=2
#bump up the percentage of memory in use to activate bdflush
vm.bdflush="40 500 0 0 500 3000 60 20 0"
还有第二种内存参数调优的方法是通过proc文件系统来实现。  这种方式里,每一个可以调整的内存参数被对应到不同的虚拟文件,然后通过系统中通用的文件读写命令来修改这些文件的内容以达到调整参数的目的。 内存相关的参数文件在/proc/sys/vm/目录下,一般都是用cat和echo命令来分别完成参数文件的读写操作。 举个例子, 执行命令 cat /proc/sys/vm/kswapd就可以查看kswapd参数的值,输出结果类似如下:
512 32 8

然后通过下面的命令可以修改这个参数:

echo 511 31 7 > /proc/sys/vm/kswapd

再使用cat /proc/sys/vm/kswapd 命令来核实一下参数是否被修改,这次的输出应该是下面这样:

511 31 7

proc文件系统接口十分便捷,有助于于我们快速调整内存参数以使系统性能达到最佳。为了方便起见,接下来的小节中将会列出/proc/sys/vm/目录里的文件所对应的各个参数及其含义。在无特殊说明的情况下这些参数适用于RHEL3 2.4.21-4版本的内核。

bdflush

bdflush文件包含9个参数,其中6个是可以调整的。这些参数影响交换到硬盘的页的比率(存贮文件内容的缓冲页)。通过调整文件里的这些值,可以使需要进行大量I/O操作的系统获得更好的性能。表1,“bdflush参数”以在文件出现的顺序描述了bdflush的参数。

参数 描述
nfract 缓冲中脏页所占半分比。用来激活bdflush任务
ndirty 每次执行bdflush时,缓存中将要写入硬盘的脏页的最大数量
reserved1 预留
reserved2 预留
interval 每次bdflush之间的延迟量jiffies(以10毫秒为单位)
age_buffer 缓冲区被认为可以写回到硬盘的时间
nfract_sync 内存中脏页占缓冲区的百分比,用来引发把脏页写入到磁盘
nfract_stop_bdflush 缓冲中脏页所占半分比,这会要求bdflush停止工作
reserved3 预留
表1,“bdflush参数”


总的来说,当系统需要更多的可用内存来给应用程序使用时,可以把bdflush值适当调高(除了age_buffer,应该把它调低),这样,内存中用来缓冲文件内容的页会更频繁地大量写入磁盘,就为应用程序留下了更多的可用内存页。当然,这样做会占用很多的CPU,因为系统要花费更多的时间移动数据,如此应用程序占用的CPU资源就少了。相反的,如果系统需要进行大量的I/O操作,那么就需要更多的内存来缓冲,因此调整bdflush的值是与上面相反的。

dcache_priority

这个文件负责控制文件夹缓冲区的优先级。当处于高负荷状态,系统会有选择地减少文件系统的各种缓冲区,以此来增加可用内存。增加这个值,拓展可用内存的时候就不会倾向于减少文件夹缓冲区;减少这个值,则会倾向于减少文件夹缓冲区。这也许不是很有用的调整参数,但是对于在其他方面负载很重的操作系统,修改这个值可能在交互反应上有所帮助。如果你无法忍受你操作的时候,系统在忙于别的事情而反应缓慢,那么增加这个值可能会有所帮助。

hugetlb_pool

hugetlb_pool负责记录大型页(原文为“huge pages”,暂译为大型页。因为一般页大小为4KB,但是大型页一般为2M,并可调。在Linux2.6以后引入)所用的兆字节数(MB)。大型页和其他一般的虚拟内存页一样,只是更大了些。注意,大型页是不能被交换到磁盘上的。大型页对系统来说是一把双刃剑。它使页表涵盖更大的内存。用这种方法,让大量的虚拟地址缓冲在TLB(TLB是Translation Look-aside Buffer的缩写:这个设备用来高速缓存虚拟地址翻译数据,以此加快地址翻译的速度)中,以此来提高TLB命中率。而TLB命中率对提升性能来说是必不可少的。但是另一方面,当应用程序不需要大量内存的时候,这会浪费内存资源。如果一些软件的设计者意识到了这一点,那么使用大量内存时就会充分利用大型页表的优势。如果系统运行这种软件,那么增加这个值会让此类软件性能优异。

inactive_clean_percent

这指定了一个内存区域(page zone)内,可用内存页(因为被缓存到磁盘里或将要被缓存磁盘里的那一部分)所占的最小百分比。如果任何内存区域内,可用内存页百分比低于这个值,并且系统急需更多的内存,那么此时,标记为可以缓存到磁盘的内存就会被写入磁盘。注意,这个控制参数仅仅在2.4.21-5EL和之后的内核版本里可用。增加这个值,可以让内存更快的缓存到磁盘,腾出更多的可用空间。虽然这耗费了额外的CPU资源,但能解决对内存的需求。降低这个值可以让数据呆在RAM里,虽然会增加系统性能,但也会让你冒着内存过度消耗的危险。

kwapd

这一组参数以前定义了操作系统将没有缓存的页面转移到硬盘的频率和数量,然而在RHEL3里面,这些控制参数都是无用的。

max_map_count

max_map_count文件包含限制一个进程可以拥有的VMA(虚拟内存区域)的数量。虚拟内存区域是一个连续的虚拟地址空间区域。在进程的生命周期中,每当程序尝试在内存中映射文件,链接到共享内存段,或者分配堆空间的时候,这些区域将被创建。调优这个值将限制进程可拥有VMA的数量。限制一个进程拥有VMA的总数可能导致应用程序出错,因为当进程达到了VMA上线但又只能释放少量的内存给其他的内核进程使用时,操作系统会抛出内存不足的错误。如果你的操作系统在NORMAL区域仅占用少量的内存,那么调低这个值可以帮助释放内存给内核用。

max-readahead

max-readahead可以调整Linux的虚拟文件系统(Virtual File System)如何预先读取文件的下一块到内存中。文件的readahead值是根据文件本身和程序对文件的操作调整的。任何时候,从磁盘读取文件内容时,都是读取当前位置加上预读值的一段内容到内存里,然后让文件指针指向这个内存块。对于预判为线性访的问文件,通过增加这个值,Linux内核会允许预读量变得更多。这会导致性能的提升,但也会过度使用内存(读取多余的内容)。降低这个值则会有相反的影响。如果这个值变得适中,那么系统会留出足够的内存,也会有很好性能。

min-readahead

与max-readahead的作用相似,min-readhead是预读值的下限。增加这个值会强制预读范围无条件地增加。如果把整个文件预读到内存里,性能当然会有所提升,但这也会导致缓冲占用更多的内存。相反地,降低这个值,能让内核减少缓冲页来降低潜在的性能开销。

overcommit_memory

overcommit_memory是设置内核如何对待内存分配的值。如果这个值是0,那么在应用程序通过malloc申请内存时,内核会检查是否有足够的内存。如果有足够的内存,那么授权请求,并为其分配内存。否则,拒绝请求,返回一个错误代码。如果把这个值设置为1,那么内核会允许分配出去的内存比物理内存多,这个过程是通过由overcommit_ratio值指定的交换方法实现的。对于分配大量的内存而不使用,以期待造成糟糕情况,启用这个特性能在某种程度上缓解这个问题。如果把这个值设定为2,那么内核将允许所有的内存分配行为而不顾及当前的内存分配状态。

overcommit_ratio

overcommit_ratio用来定义超出物理内存资源的可用内存数量,但是当overcommit_memory为2的时候是无效的。当考虑分配特别多的内存时,在这个文件里的值表示额外可用内存占物理内存的半分比。举个例子,如果这个值是50,那么内核会把有1GB内存和1GB交换区的系统看成有2.5GB可用内存的系统。调节公式如下:

allocatable_memory=(swap_size + (RAM_size * overcommit_ratio))
要谨慎使用前两个参数。只有在你的应用程序和参数相适应的时候,启用overcommit_memory才能以很小的代价换来显著的性能提升。如果你的应用程序充分使用它申请的内存,那么使用超额内存(overcommit)会导致性能下降。因为这种超额使用内存的方法是让内存交换到磁盘上,以此来增加可用内存,而充分使用内存的应用程序会让系统频繁地进行这种交换,因而产生更长的延时,影响性能。还有,你要确保交换区有足够的空间来容纳这些超额内存(这意味着,你的交换区应该足够大,至少要容纳超额内存,通常推荐超额内存为内存的百分之五十)。

pagecahe

pagecache文件用来调整页面缓存所用的内存数。页面缓存保存多种数据,例如,打开硬盘上的文件,内存映射文件和可执行文件的页。更改这个文件中的值决定了多少内存会被用于页面缓存。表2. 页面缓存参数 定义了按顺序出现在pagecache文件中用于调整页面缓存的参数。

参数 描述
min 预留给页面缓存用的最小内存数
borrow kswap线程用于回收内存和页面缓存所占有的页面缓存中页面的百分比
max 如果页面缓存占用内存的百分比超过这个数,kswap线程将会将某些页换出内存。一旦页面缓存所用的内存百分比低于这个数,kswap线程会将页换入内存。
Table 2.页面缓存参数

    提升这些值有助于更多的程序和缓存文件在内存中停留更久,从而使应用程序执行得更快。然而,在内存资源缺乏的系统中,这可能导致应用程序延迟,这是因为进程必须等待内存可用。降低这些值使换出进程和其他磁盘备份数据更快速,允许其他进程更容易获取内存而提高执行速度。对于大多数工作负荷自动调优就足够了。但是,如果有过度交换和巨大缓存的负载,你可能想要减小值,直到存储交换问题(swapping problem)消失。

page-cluster

当发生缺页错误(page fault)的时候,内核会尽量在硬盘中一次读取多个被缓冲的内存,以防过度使用硬盘驱动器。这个参数定义了在每次发生缺页错误的时候,内核读取内存页的数量。这个值被解读为2的指数个内存页(即:读取2page-cluster个内存页)。那么,什么是缺页错误呢?缺页错误会在以下两种情况下发生:与虚拟地址对应的物理地址尚未被分配。或者对应的物理内存已被交换到硬盘上。如果是合法申请的内存地址(例如,应用程序包含的虚拟内存映射区域内的地址),那么内核会为其分配一个物理内存页,并使之对应那个虚拟地址。或者从硬盘上取回被交换的内存页,并放到内存中。在这之后,内核会让应用程序从出现缺页错误的地方重新开始。通过增加pager-cluster,在上述过程中操作的一个内存页就会变成2page-cluster个内存页,原先申请的那个内存页之后的2page-cluster个页也会经过相同的操作。这意味着如果对于一个特定的系统,它以线性的方式访问内存中的数据,那么增加这个值能获得明显的性能提升(其原理非常像之前描述的file_readahead)。当然,如果你的程序随机访问分开的内存区域,那么增加这个值只能会让性能下降。

典型场景

现在我们已经解释了不少内核调整的细节,让我们看一些典型负载的例子,然后看看应该如何调节系统以提高性能。

File(IMAP,Web,etc.) Server

这种负载趋于要求在大量I/O操作和硬盘读取中能表现良好,因此,让更多文件在内存中驻留,会让性能有所提升。这种提速,是通过在内存中缓冲更多的文件,以此减少等待硬盘I/O操作来实现的。一个简单的sysctl.conf更改如下,这会使这种负载受益:


#increase the amount of RAM pagecache is allowed to use 
#before we start moving it back to disk 
vm.pagecache="10 40 100"


有较多活跃用户的一般电脑服务器
对这种负载的配置,是非常典型的。这种系统包含了很多活动用户,而这些活跃用户很可能各自运行很多进程,而所有这些进程即可能是CPU密集型(CPU instensive),也可能是I/O密集型(I/O instensive),又可能是混合型(混合了前两者)。因为默认的虚拟内存设置会尝试在I/O操作和内存使用中寻找平衡点,所以在这种情况下,保留大多数的默认设置是最好的选择。然而,这种环境也可能包含许多小的进程,虽然不会带来很大的负载,但会消耗内存资源,尤其是低端内存(lowmem。指的是系统所用内存空间,例如32位架构上指的是0xC0000000到0xFFFFFFFF)因此,保留低端内存也许会有所帮助:

#lower the pagecache max to keep from eating all memory up with cache
#降低pagecache上限来保证缓冲不会占用太多内存 
vm.pagecache=10 25 50 
#lower max-readahead to reduce the amount of unneeded IO 
#降低max-readahead来减少不需要的IO消耗
vm.max-readahead=16

非交互计算服务器(Batch)

非交互计算服务器常常和文件服务器相对。应用程序不和人交互,而且通常不会进行I/O操作。而运行的进程数也是被限制的。因而,系统应该允许最大的吞吐量

#Reduce the amount of pagecache normally allowed
#通常,减少pagecache是不错的
vm.pagecache="1 10 100"
#do not worry about conserving lowmem, not that many processes
#不要担心占用低端内存,因为没多少进程
vm.max_map_count=128000 14
#crank up overcommit, processes can sleep as they are not interactive
#开启overcommit。当不在交互,进程会进入睡眠状态。所以清空它们占用的内存来留作他用。
vm.overcommit=2 
vm.overcommit_ratio=75


进一步阅读


  1. Understanding the Linux Kernel(中文名:深入理解LINUX内核)作者:Daniel Bovet 和 Marco Cesati (O'Reilly & Associates)
  2. Virtual Memory Behavior in Red Hat Enterprise Linux AS 2.1 作者:Bob Matthews 和 Norm Murray
  3. Towards an O(1) VM 作者:Rik Van Riel
  4. The Linux Kernel Source Tree, versions 2.4.21-4EL & 2.4.21-5EL


关于作者
Neil Horman是在Red Hat工作的软件工程师,与妻子和一岁大的孩子住在Raleigh,NC。拥有北卡罗莱纳州立大学的理学学士和理学硕士学位。除了享受家庭生活,他也很享受开发,维护,编写软件的过程。

Norm Murray在Red Hat工作超过三年。由于对基因工程发展情况的不满,转业为程序员,对信息科学和相关的学习十分痴迷。