C++应用程序性能优化学习笔记:操作系统的内存管理

以前应用程序必须在载入内存后才能执行,现代操作系统的内存管理为解决这个问题而引入了虚拟内存。

本质上虚拟内存就是要让一个程序的代码和数据在没有全部载入内存时即可运行。运行过程中,当执行到尚未载入内存的代码,或者要访问还没有载入到内存的数据时,虚拟内存管理器动态地将这部分代码或数据从硬盘载入到内存中。而且在通常情况下,虚拟内存管理器会相应地先将内存中某些代码或者数据置换到硬盘中,为即将载入的代码或数据腾出空间。

因为内存和硬盘之间的数据传输相对代码执行来说,是非常慢的操作 ,因此虚拟内存管理器在保证工作正确的前提下,还必须考虑效率因素。比如,它需要优化置换算法,尽量避免就要执行的代码或访问的数据刚被置换出内存,而很久没有访问的代码或数据却一直驻留在内存中。另外它还需要将驻留在内存的各个进程的代码或数据维持在一个合理的数量上,并且根据该进程的性能表现动态调整此数量,等等,使得程序运行时将其涉及的磁盘I/O次数降到尽可能低,以提高程序的运行性能。

一、windows内存管理

win32虚拟内存管理器为每一个win32进程提供了进程私有且基于页的4GB(32位)大小的线性虚拟地址空间。其理解如下:

“进程私有”意味着每个进程都只能访问属于自己的地址空间,而无法访问其他进程的地址空间,也不用担心自己的地址空间会被其他进程看到(父子进程例外,比如调试器利用父子进程关系来访问被调试进程的地址空间)。需要注意的是,进程运行时用到的dll并没有属于自己的虚拟地址空间,而用的是其所进程的虚拟地址空间。dll的全局数据,以及通过dll函数申请的内存都是从调用其进程的虚拟地址空间中开辟。

“基于页”是指虚拟地址空间被划分为多个称为“页”的单元,页的大小由底层物理器决定,x86中页的大小为4KB。页是win32虚拟内存管理器处理的最小单元,相应的物理内存也被划分为多个页。虚拟内存地址空间的申请和释放,以及内存和磁盘的数据传输或置换都以页为最小单位进行。

“4GB大小”意味着进程中的地址取值范围可以从0x00000000到0xffffffff。win32将低区的2GB留给进程使用,高区的2GB则留给系统使用。

win32中用来辅助实现虚拟内存的硬盘文件称为“调页文件”,可以有16个,调页文件用来存放被虚拟内存管理器置换出内存的数据。当这些数据再次被进程访问时,虚拟内存管理器会先将它们从调页文件中置换进内存,这样进程可以正确访问这些数据。用户可以自己配置调页文件。出于空间利用效率和性能的考虑,程序代码(包括exe和dll文件)不会被修改,所以当它们所在的页被置换出内存时,并不会写入调页文件中,而是直接抛弃。当再次被需要时,虚拟内存管理器直接从存放它们的exe或dll文件中找到它们并调入内存。另外对exe和dll文件中包含的只读数据的处理与此类似,也不会为它们在调页文件中开辟空间。

当进程执行某段代码或访问某些数据,而这些代码或者数据还没有在内存时,这种情形称为“缺页错误”。缺页错误的原因有很多种,最常见的一种就是已经提到的,即这些代码和数据被虚拟内存管理器置换出了内存。调页错误设计磁盘I/O,大量的调页错误会大大降低程序的总体性能。

使用虚拟内存

win32中分配内存分为两个步骤:“预留”和“提交”。因此在进程虚拟地址空间中的页有3种状态:自由(free)、预留(reserved)、提交(committed)

自由:表示此页尚未被分配,可以用来满足新的内存分配请求。

预留:从虚拟地址空间中划出一块区域(region,页的整数倍数大小),划出之后这个区域中的页不能用来满足新的内存分配请求,而是用来供要求“预留”此段区域的代码以后使用。预留时并没有分配物理存储,只是增加了一个描述进程虚拟地址空间使用状态的数据结构(VAD,虚拟地址描述符),用来记录这段区域已被预留。“预留”操作相对较快,因为没有真正分配物理存储。也正因为没有分配真正的物理存储,所以预留的空间并不能直接访问,对预留页的访问会引起“内存访问违例”(内存访问违例会导致整个进程立刻退出,而不仅仅是中止该线程)

提交:若想得到真正的物理存储,必须对预留的内存进行提交。提交会从调页文件中开辟空间,并修改VAD中的相应项。提交时也并没有立刻从物理内存中分配空间,而只是从磁盘的调页文件中开辟空间。这个空间用作以后置换的备份空间,知道有代码第一次访问这段提交内存中的某些数据时,系统发现并没有真正的物理内存,抛出缺页错误。虚拟内存管理器处理此缺页错误,直到这时才会真正分配物理内存,提交也可以在预留的同时一起进行。提交操作会从调页文件中开辟磁盘空间,所以比预留操作的时间长。

这也是win32虚拟内存管理中的demand-paging策略的一个体现,即不到真正访问时,不会为某虚拟地址分配真正的物理内存。这种策略一是出于性能考虑,将工作分段完成,提供总体性能;二是出于空间效率考虑,不到真正访问时,win32总是假定进程不会访问大多数的数据,因而也不必为它们开辟存储空间或将其置换进物理内存,这样可以提高存储空间(磁盘和物理内存)的使用效率。

设想某些程序对内存有很大的需求,但又不是立即需要所有这些内存,那么一次就从物理存储中开辟空间满足这些还只是“潜在”的需求,从执行性能和存储空间效率来说,都是一种浪费。因为只是“潜在”需求,极有可能这些分配的内存中很大一部分最后都没有真正被用到。如果在申请的时候就一次性为它们分配全部物理存储,无疑会极大降低空间的利用效率。

另一方面,如果完全不用预留和提交机制,只是随需分配内存来满足每次的请求,那么对一个会在不同时间点频繁请求内存的代码来说,因为它请求内存的不同时间点的间隙极有可能会有其他代码请求内存。这样这段在不同时间点频繁请求内存的代码请求得到的内存因为虚拟地址不连续,无法很好利用空间locality特性,对其整体进行访问(比如遍历)时就会增加缺页错误的数量,从而降低程序的性能。

预留和提交在win32中都使用VirtualAlloc函数完成,预留传入MEM_RESERVE参数,提交传入MEM_COMMIT参数。释放虚拟内存使用VirtualFree函数,此函数根据不同的传入参数,与VirtualAlloc相对应,可以释放与虚拟地址区域相对应的物理存储,但该虚拟地址区域还可出于预留状态,也可以连同虚拟地址区域一起释放,该段区域恢复为自由状态。

下面以线程栈为例来说明win32系统如何使用这种预留和提交两步机制的。

创建线程栈时,只是一个预留的虚拟地址区域,默认是1MB(此大小可在CreateThread或在链接时通过链接选项修改),初始时只有前两页是提交的。当线程因为函数的嵌套调用需要更多的提交页时,虚拟内存管理器会动态地提交该虚拟地址区域中的后续页以满足其需求,直到到达1MB的上限。当到达此预留区域大小的上限(默认1MB)时,虚拟内存管理器不会增加预留区域大小,而是在提交最后一页时抛出一个栈溢出异常,抛出栈溢出异常时该栈还有一页空间可用,程序仍可正常运行。而当程序继续使用栈空间,用完最后一页后,还继续需要存储空间,这是就超过了上限,直接导致进程退出。

所以为防止线程栈溢出导致整个程序退出,应该注意尽量控制栈的使用大小。比如减少函数的嵌套层数,减少递归函数的使用,尽量不要在函数中使用太大的局部变量(大的对象可以从堆中开辟空间存放,因为对会动态扩大,而线程栈的可用内存区域在线程创建时就已固定,之后整个线程生命期间无法扩展)

另外为了防止因为一个线程栈的溢出导致整个进程退出,可以对可能会产生线程栈溢出的线程体函数加异常处理,捕获在提交最后一页时抛出的溢出异常,并作出相应处理。

访问虚拟内存时的处理流程

对某虚拟内存区域进行了预留并提交之后,就可以对该区域中的数据进行访问了,流程如图


当该数据已在物理内存中时,虚拟内存管理器只需将指向该数据的虚拟地址映射为物理指针,即可访问到物理内存中的真正数据。这一步不涉及到磁盘I/O,速度相对较快。

当第一次访问一段刚刚提交的数据时,没有真正的物理内存分配给它。或者该数据以前已被访问过,但是被虚拟内存管理器置换出了内存。这两种情况都会引发缺页错误,虚拟内存管理器此时会处理这一缺页错误,它先检测此数据是否在调页文件中已有备份空间(exe和dll文件的代码页和只读数据页情形与此类似,但是其备份空间不在调页文件,而是在存放它们的文件中)。如果是这两种情况,表明访问的数据在磁盘中有备份,接下来虚拟内存管理器就需要在物理内存中找到合适的页,并将存放在磁盘的备份数据置换进物理内存。

虚拟内存管理器首先查询当前物理内存中是否有空闲页,虚拟内存管理器维护一个称为“页帧数据库”(page-frame database)的数据结构,此数据结构是操作系统全局的,当windows启动时被初始化,用来跟踪和记录物理内存中每一页的状态,它会用一个链表将所有空闲页连接起来,当需要空闲页时,直接查找此空闲页链表,如果有,直接使用某个空闲页;否则根据调页算法首先选出某个页。需要指出的是,虚拟内存管理器调页时不是只调入一个页,为了利用局部的特性,它在调入包含所需数据的页的同时,会将其附近的几个页一起调入内存。这里为了简单和清楚起见,假定只调入目标页。但应该意识到win32调页时的这个特性,因为可以利用它来提高程序效率。这个页将会用来存放即将从磁盘置换进来的页的内容。选出某个内存页后,接着检查此页状态,如果此页自上次调进内存后尚未被修改过,则直接使用此页(代码页和只读页也可以直接使用);反之,如果此页已被修改过(“脏”),则需要先将此页的内容“写”到调页文件中与此页相对应的备份页中,并随即将此页标为空闲页。

现在,有了一个空闲页用来存放即将要访问的数据。此时,虚拟内存管理器会再次检测,此数据是否是刚被申请的内存且是第一次访问。如果是,则直接将此空闲页清0使用即可(不必从磁盘中将其备份页的内容读进,因为该备份中的内容无意义);如果不是,则需要将调页文件中该页的备份页读到此空闲页中,并随即将此页的状态从空闲页改为活动页。

此时,此数据已在物理内存页中,通过虚拟地址映射到物理地址,即就可访问此数据了。


以上是访问成功时的情形,但情形并非总是如此。比如当用户定义了一个数组,而此数组刚好在其所在页的下边界,且此页的下一页刚好是自由或预留的(不是提交的,即没有真正的物理存储)。当程序不小心向下越界访问此数组,则首先引发缺页错误。随即虚拟内存管理器在处理缺页错误时检测到它也不在调页文件中,这就是所谓的“访问违例”(access violation)。访问违例意味着要访问的地址所在的虚拟内存页还没有被提交,即没有实际的物理存储与之对应,访问违例会直接导致整个进程退出(即crash)。

可知,指针越界访问的后果根据运行时实际情况有所不同。如上所述,当数组并非出于其所在页的边界,越界后还在同一页中,这时只会“误访问”(误读或误写,其中误读只会影响正在执行的代码;误写则会影响到其他处代码的执行)该页中其他数据,而不会导致整个进程的crash。即使该数组真的出于其所在页的边界,且越界后指针落到了其相邻页。但如果此相邻页碰巧也为一个提交页,此时仍然只是“误访问”,也不会导致进程的crash。这也意味着,同一个应用程序的代码中存在着指针越界访问错误,运行时有时crash,有时不会。

Microsoft提供了一个监测指针越界访问的工具pageheap,它的原理是强制使每次分配的内存都位于页的边界,同时强制该页的相邻页为自由页(即不分配相邻页给程序使用)。这样每次越界访问都会立即引起access violation,导致程序crash。从而使得指针越界访问错误在开发期间一定会暴露出来。

虚拟地址到物理地址的映射

在确保访问的数据已在物理内存中后,还需要先将虚拟地址转换为物理地址,即“地址映射”,才能真正访问此数据。

win32通过一个两层表结构来实现地址映射,因为4GB虚拟地址空间为每个进程私有,相应地,每个进程都维护一套自己的层次表结构用来实现其地址映射。第一层表称为“页目录”(page directory),实际上就是一个内存页(4KB=4096byte)。这一页以四个字节为单元分为1024项,每一项称为一个“页目录项”(Page Directory Entry,PDE);第二层表称为“页表”(page table),共有1024个页表。页目录中每一个页目录项PDE对应这一层中的某一个页表,每一个页表也占了一个内存页。这一页中的4KB,即4096个字节也像页目录那样被分成1024项,每项4个字节,页表的每一项则称为“页表项”(Page Table Entry,PTE)。每一个页表项PTE都指向物理内存中的某个页帧。如图


win32提供了4GB(32位)大小的虚拟地址空间。因此每个虚拟地址都是一个32位的整数值,这32位由3个部分组成如图


这三个部分中的第一部分,即前10位为页目录下标,用其可以定位在页目录的1024项中的某一项。根据定位到的那一项的项值,可以找到第2层页表中的某一个页表。虚拟地址的第二部分,即中间的10位为页表下标,可用来定位刚刚找到的页表的1024项中的某一项。此项值可以找到物理内存中的某一页,此页包含此虚拟地址所代表的数据。最后用虚拟地址的第三部分,即最后12位可用来定位此物理页中的特定的字节位置,12位刚好可以定位一个页中的任意位置的字节。具体例子如下:

假设在程序中访问一个指针(win32中的“指针”意味着虚拟地址),此指针值为0x2A8E317F,图为虚拟地址到物理地址的映射过程

0x2A8E317F的二进制写法为0010  1010  1000  1110  0011  0001  0111  1111,为了方便起见,将这32位分成10位、10位和12位。第一个10位可以定位1024项页目录项,可是因为页目录项总共4096字节,每个页目录项占用4个字节,所以需要在定位前将此10位左移两位(相当于乘4),即10  1010  1000(0x2A8)。再用此值作为下标找到对应的页目录项,此页目录项指向一个页表。同样方法再找到页表中的页表项。此页表项指向真正的物理内存,然后用最后12位定位页内的数据(此时这12位不用再左移,因为物理页内定位时,需要能定位到每个字节。而不像页目录和页表中,只需要定位每4个字节的第一个字节),即为此指针指向的数据。

上面假设的是此数据已在物理内存中,其实,“判断访问的数据是否在内存中”这一步骤,也是在这个地址映射过程中完成的,win32总是假使数据已在物理内存中,并进行地址映射。页表项中有一位用来标识包含此数据的页是否在物理内存中,当取得页表项时,检测此位,如果在,就是本节描述的过程,如果不在,则抛出缺页错误,此时此页表项中包含了此数据是否在调页文件中,如果不在,则为访问违例,如果在,此页表项可查出此数据页在哪个调页文件中,以及此数据页在该调页文件中的起始位置,然后根据这些信息将此数据页从磁盘中调入物理内存中,再继续进行地址映射过程。

已经说过,为了实现虚拟地址空间各进程私有,每个进程都拥有自己的页目录和页表结构,对不同进程而言,页目录中的页目录项值(PDE),以及页表中的页表项值(PTE)都是不同的,因此相同的指针(虚拟地址)被不同的进程映射到的物理地址也是不同的,这也意味着,在不同进程间传递指针是没有意义的。

虚拟内存空间使用状态记录

win32使用虚拟地址描述符树(Virtual Address Descriptor)来记录和维护每个进程的4GB虚拟地址空间的使用及状态信息。每一个进程都有一个自己的VAD集合,这个集合中的VAD被组织成一个自平衡二叉树,以提高查找效率。另外只有预留或提交的内存块才会有VAD,自由的内存块没有VAD(因此不在VAD树结构中的虚拟地址块就是自由的)。VAD组织如图:


当程序申请一块新内存时:虚拟内存管理器只需要访问VAD树。找到两个相邻VAD,只要小的VAD的上限与大的VAD的下限之间的差值满足所申请的内存块的大小需求,即可使用二者之间的虚拟内存。

当程序第一次访问提交的内存时:虚拟内存管理器总是假定该数据页已在物理内存中,并进行虚拟地址到物理地址的转换。当找到相应的页目录项后发现该目录项并没有指向一个合法的页表,它就会查找该进程的VAD树。找到包含该地址的VAD,并根据VAD中的信息,比如该内存块的大小、范围,以及在调页文件中的起始位置等,随需生成相应的页表项,然后从刚才发生缺页错误的地方继续进行地址映射。由此可知,一个虚拟内存页被提交时,除了在调页文件中开辟一个备份页之外,不会生成包含指向它的页表项的页表,也不会填充指向它的页表项,更不会为之开辟真正的物理内存页,而是直到第一次访问这个提交页时,才会“随需地”从VAD中取得包含该页的整个区域的信息,生成相应页表,并填充相应页的表项。

当访问预留的内存时:虚拟内存管理器也是先假定该数据页已在物理内存中,并进行虚拟地址到物理地址的转换。找到相应的页目录项后发现该页目录项并没有指向一个合法的页表,它就会查找该进程的VAD树,找到包含该地址的VAD。这时它会发现此段内存块只是预留的,而没有提交,即并没有对应的真正的物理存储,这时直接抛出访问违例,进程退出。

当访问自由内存时:根据上述流程,当发现并没有VAD包含此虚拟地址时,可知该地址所在的虚拟地址页是自由状态,直接抛出访问违例,进程退出。

进程工作集

频繁的调页操作引起的磁盘I/O会大大降低程序的运行效率,因此对每一个进程,虚拟内存管理器都会将其一定量的内存页驻留在物理内存中,并跟踪其执行的性能指标,动态调整这个数量。win32中驻留在物理内存中的内存页称为进程的“工作集”(working set),进程的工作集可以通过“任务管理器”查看,其中“内存使用”列即为工作集大小。

工作集是会动态变化的,进程初始化时只有很少的代码页和数据页被调入内存。当执行到未被调入内存的代码或者访问到尚未调入内存的数据时,这些代码页或者数据页会被调入物理内存,工作集也随之增长。但工作集不能无限增长,系统为每个进程都定义了一个默认的最小工作集(根据系统物理内存大小,此值可能为20~50MB)和最大工作集(根据系统物理内存大小,此值可能为45~345MB)。当工作集到达最大工作集,即进程需要再次调入新页到物理内存中时,虚拟内存管理器会将原来的工作集中的某些页先置换出内存,然后将需要调入的新页调入内存。

因为工作集的页驻留在物理内存中,因此对这些页的访问不涉及磁盘I/O,相对而言非常快;反之,如果执行的代码或者访问的数据不在工作集中,则会引发额外的磁盘I/O,从而降低程序的运行效率。一个极端的情况就是所谓的颠簸或抖动(thrashing),即程序的大部分执行时间都花在了调页操作上,而不是代码执行上

如前所述,虚拟内存管理器在调页时,不仅仅只是调入需要的页,同时还将其附近的页也一起调入内存中。综合这些知识,如果想提高程序运行效率,应该考虑:

尽量编写紧凑代码:最理想的情形就是工作集从不会到达最大阀值。在每次调入新页时,也就不需要置换已经载入内存的页。因为根据locality特性,以前执行的代码和访问的数据很可能在后面再次执行或访问。这样程序执行时,发声的缺页错误数会大大降低,即减少了磁盘I/O。即使不能达到理想情形,紧凑的代码也往往意味着接下来执行的代码更大可能在相同或相邻的页。根据时间locality特性,程序80%的时间花在了20%的代码上。如果能将这20%的代码尽量紧凑排在一起,无疑会大大提高程序的整体运行性能。

尽量将会一起访问的数据(比如链表)放在一起。

win32内存相关API

win32平台下,开发人员可以通过5组函数使用内存(完成申请和释放等操作):

传统的CRT函数(malloc/free系列):与平台无关,若程序被移植到非windows平台,则这组函数是首选。

global heap/local heap函数(GlobalAlloc/LocalAlloc系列):这组函数是为了向后兼容而保留的。在windows 3.1平台下,global heap为系统中所有进程共有的堆,这些进程包括系统进程和用户进程。它们对此global heap内存的申请会交错在一起,从而使得一个用户进程的不小心的内存使用错误会导致整个操作系统的崩溃。local heap又被称为“private heap”,与global heap相对应,local heap为每个进程私有。进程通过LocalAlloc从自己的local heap里申请内存,而不会相互干扰。除此之外,进程不能通过另外的用户自定义堆或者其他方式动态申请内存。到了win32平台,由于考虑到安全因素,global heap已经废弃,local heap也改名为“process heap”。为了使得以前针对windows3.1平台写的应用程序能够运行在新的win32平台上,GlobalAlloc/LocalAlloc系列函数仍然得到沿用,但是这一系列函数最后都是从process heap中分配内存。不仅如此,win32平台还允许进程除process heap之外生成和使用新的用户自定义堆,因此在win32平台下建议不使用GlobalAlloc/LocalAlloc系列函数进行内存操作。

虚拟内存函数(VirtualAlloc/VirtualFree系列):这组函数直接通过保留(reserve)和提交(commit)虚拟内存地址空间来操作内存,因此它们为开发人员提供最大的自由度,但相应地也为开发人员内存管理工作增加了更多的负担。此组函数适合为大型连续的数据结构数组开辟空间。

内存映射文件函数(CreateFileMapping/MapViewOfFile系列):系统使用内存映射文件函数系列来加载.exe或者.dll文件。对于开发人员,一方面通过这组函数可以方便地操作硬盘文件,而不用考虑那些繁琐的文件I/O操作;另一方面,运行在同一台机器上的多个进程可以通过内存映射文件函数来共享数据(这也是同一台机器上进程间进行数据共享和通信的最有效率和最方便的方法)

堆内存函数(HeapCreate/HeapAlloc系列):win32平台中的每个堆都是个进程私有的,每个进程除了默认的进程堆,还可以另外创建用户自定义堆。当程序需要动态创建多个数据结构时,堆函数系列最为合适。一般来说CRT函数(malloc/free)就是基于堆内存函数实现的。

二、linux内存管理机制

进程的内存布局

一个32位linux进程的地址空间为4GB,高位1GB空间留给内核,低位3GB由进程用户代码使用,如图


0x00000000~0xBFFFFFFF区域为用户地址空间,这一段被分成程序代码区、数据区(包括初始化数据区DATA和未初始化数据区BSS)、堆和栈(此区域动态增减)

堆从未初始化数据区开始,向上端动态增长,增长过程中虚拟地址值变大;而栈反之。

初始化数据区里存放的编译期就知道初始值的全局变量及静态变量等必须保存在最终生成的二进制文件中,并且在程序运行时会原封不动地将这个区域映射到进程的初始化数据区域。当程序声明N个这样的初始化数据,且其空间占据M大小,那么在二进制文件中,就会开辟M大小的区域,即依次存放N个数据,且每个都设置了相应的初始值。当程序运行时,这块区域就会原封不动地映射到该对应进程的“初始化数据区”。“原封不动”意味着在二进制文件中这块区域的大小与进程虚拟地址空间中一样大,而且排列和对应地址的值也一样。

未初始化数据区则不如此。当最终生成二进制文件时,这些未初始化数据区并不会像初始化数据区那样占据它们对应变量的总大小的区域,而只是用一个值来记录其总大小。但是在进程虚拟地址空间中,对应于这一区域的未初始化数据区的大小必须是数据大小的总和。

物理内存管理

windows使用页帧数据库来管理物理内存相对应,linux使用页分配器(page allocator)来管理物理内存,页分配器负责分配和回收所有的物理内存页(物理内存的分配与回收的最小单元为4KB大小的页)。页分配器核心算法称为“兄弟堆算法”(buddy-heap algorithm),思想是每个物理内存区域都会有一个与之相邻的所谓”兄弟“区域。当这两个区域被回收后,会被合并成一个区域。当有物理内存请求到来时,页分配器会首先检测是否有大小与之一致的区域,有则满足请求,没有则找一个更大的区域进行划分直到大小合适为止。配合这一算法的是必须有链表用来记录自由的物理内存区域,对于每个相同大小的自由区域,会有一个链表用来将它们串联起来。这样每种大小的自由区域,都会有一个链表来串联它们。这些自由区域的大小都是2的某次幂。

当有一个8KB(2页)的请求到来,当前最小可供分配的区域为64KB大小。这时会先分成两个32KB大小的区域,继而将低位的32KB大小区域分成两个16KB大小的区域,再讲最低位16KB大小的区域分成两个8KB大小的区域,然后分配高位的8KB以满足申请要求,如图


虚拟内存管理

主要任务是维护应用程序的虚拟地址空间使用信息,如哪些区域已被使用——映射,这些区域是否有硬盘文件作为备份存储。如果有,该区域对应在磁盘的哪个区域等;另外一个重要的功能就是调页,如在程序访问某些尚未调至物理内存的数据时,虚拟内存管理器负责定位它们,并将其置换进物理内存。如果物理内存这时没有自由页,还需要将物理内存中的某些页先置换出去。

用来维护应用程序的虚拟地址空间使用信息的数据结构是vm_area_struct,类似于windows的VAD。当vm_area_struct的个数不超过32时,这些vm_area_struct被链成一个链表结构;当超过32时,这些vm_area_struct则被组织成一个二叉平衡树。

虚拟地址映射为物理地址

linux采用3层映射策略。相比于windows,多了Middle这层。但是对于IA32体系(英特尔32位元架构)来说,Middle这层实际上是无用的。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值