内核是如何管理内存的?
2010-04-22 10:46
295 查看
文章来源:http://blog.csdn.net/drshenlei/archive/2009/07/15/4350928.aspx
内核是如何管理内存的?
原文标题:HowTheKernelManagesYourMemory
原文地址:http://duartes.org/gustavo/blog/
[注:本人水平有限,只好挑一些国外高手的精彩文章翻译一下。一来自己复习,二来与大家分享。]
在仔细审视了进程的虚拟地址布局之后,让我们把目光转向内核以及其管理用户内存的机制。再次从gonzo图示开始:
Linux进程在内核中是由task_struct的实例来表示的,即进程描述符。task_struct的mm字段指向内存描述符(memorydescriptor),即mm_struct,一个程序的内存的执行期摘要。它存储了上图所示的内存段的起止位置,进程所使用的物理内存页的数量(rss表示ResidentSetSize),虚拟内存空间的使用量,以及其他信息。我们还可以在内存描述符中找到用于管理程序内存的两个重要结构:虚拟内存区域集合(thesetofvirtualmemoryareas)及页表(pagetable)。Gonzo的内存区域如下图所示:
每一个虚拟内存区域(简称VMA)是一个连续的虚拟地址范围;这些区域不会交叠。一个vm_area_struct的实例完备的描述了一个内存区域,包括它的起止地址,决定访问权限和行为的标志位,还有vm_file字段,用于指出被映射的文件(如果有的话)。一个VMA如果没有映射到文件,则是匿名的(anonymous)。除memorymapping段以外,上图中的每一个内存段(如:堆,栈)都对应于一个单独的VMA。这并不是强制要求,但在x86机器上经常如此。VMA并不关心它在哪一个段。
一个程序的VMA同时以两种形式存储在它的内存描述符中:一个是按起始虚拟地址排列的链表,保存在mmap字段;另一个是红黑树,根节点保存在mm_rb字段。红黑树使得内核可以快速的查找出给定虚拟地址所属的内存区域。当你读取文件/proc/pid_of_process/maps时,内核只须简单的遍历指定进程的VMA链表,并打印出每一项来即可。
在Windows中,EPROCESS块可以粗略的看成是task_struct和mm_struct的组合。VMA在Windows中的对应物时虚拟地址描述符(VirtualAddressDescriptor),或简称VAD;它们保存在平衡树中(***Ltree)。你知道Windows和Linux最有趣的地方是什么吗?就是这些细小的不同点。
4GB虚拟地址空间被分割为许多页(page)。x86处理器在32位模式下所支持的页面大小为4KB,2MB和4MB。Linux和Windows都使用4KB大小的页面来映射用户部分的虚拟地址空间。第0-4095字节在第0页,第4096-8191字节在第1页,以此类推。VMA的大小必须是页面大小的整数倍。下图是以4KB分页的3GB用户空间:
处理器会依照页表(pagetable)来将虚拟地址转换到物理内存地址。每个进程都有属于自己的一套页表;一旦进程发生了切换,用户空间的页表也会随之切换。Linux在内存描述符的pgd字段保存了一个指向进程页表的指针。每一个虚拟内存页在页表中都有一个与之对应的页表项(pagetableentry),简称PTE。它在普通的x86分页机制下,是一个简单的4字节记录,如下图所示:
Linux有一些函数可以用于读取或设置PTE中的每一个标志。P位告诉处理器虚拟页面是否存在于(present)物理内存中。如果是0,访问这个页将触发页故障(pagefault)。记住,当这个位是0时,内核可以根据喜好,随意的使用其余的字段。R/W标志表示读/写;如果是0,页面就是只读的。U/S标志表示用户/管理员;如果是0,则这个页面只能被内核访问。这些标志用于实现只读内存和保护内核空间。
D位和A位表示数据脏(dirty)和访问过(accessed)。脏表示页面被执行过写操作,访问过表示页面被读或被写过。这两个标志都是粘滞的:处理器只会将它们置位,之后必须由内核来清除。最后,PTE还保存了对应该页的起始物理内存地址,对齐于4KB边界。PTE中的其他字段我们改日再谈,比如物理地址扩展(PhysicalAddressExtension)。
虚拟页面是内存保护的最小单元,因为页内的所有字节都共享U/S和R/W标志。然而,同样的物理内存可以被映射到不同的页面,甚至可以拥有不同的保护标志。值得注意的是,在PTE中没有对执行许可(executepermission)的设定。这就是为什么经典的x86分页可以执行位于stack上的代码,从而为黑客利用堆栈溢出提供了便利(使用return-to-libc和其他技术,甚至可以利用不可执行的堆栈)。PTE缺少不可执行(no-execute)标志引出了一个影响更广泛的事实:VMA中的各种许可标志可能会也可能不会被明确的转换为硬件保护。对此,内核可以尽力而为,但始终受到架构的限制。
虚拟内存并不存储任何东西,它只是将程序地址空间映射到底层的物理内存上,后者被处理器视为一整块来访问,称作物理地址空间(physicaladdressspace)。对物理内存的操作还与总线有点联系,好在我们可以暂且忽略这些并假设物理地址范围以字节为单位递增,从0到最大可用内存数。这个物理地址空间被内核分割为一个个页帧(pageframe)。处理器并不知道也不关心这些帧,然而它们对内核至关重要,因为页帧是物理内存管理的最小单元。Linux和Windows在32位模式下,都使用4KB大小的页帧;以一个拥有2GBRAM的机器为例:
在Linux中,每一个页帧都由一个描述符和一些标志所跟踪。这些描述符合在一起,记录了计算机内的全部物理内存;可以随时知道每一个页帧的准确状态。物理内存是用buddymemoryallocation技术来管理的,因此如果一个页帧可被buddy系统分配,则它就是可用的(free)。一个被分配了的页帧可能是匿名的(anonymous),保存着程序数据;也可能是页缓冲的(pagecache),保存着一个文件或块设备的数据。还有其他一些古怪的页帧使用形式,但现在先不必考虑它们。Windows使用一个类似的页帧编号(PageFrameNumber简称PFN)数据库来跟踪物理内存。
让我们把虚拟地址区域,页表项,页帧放到一起,看看它们到底是怎么工作的。下图是一个用户堆的例子:
蓝色矩形表示VMA范围内的页,箭头表示页表项将页映射到页帧上。一些虚拟页并没有箭头;这意味着它们对应的PTE的存在位(Presentflag)为0。形成这种情况的原因可能是这些页还没有被访问过,或者它们的内容被系统换出了(swapout)。无论那种情况,对这些页的访问都会导致页故障(pagefault),即使它们处在VMA之内。VMA和页表的不一致看起来令人奇怪,但实际经常如此。
一个VMA就像是你的程序和内核之间的契约。你请求去做一些事情(如:内存分配,文件映射等),内核说“行”,并创建或更新适当的VMA。但它并非立刻就去完成请求,而是一直等到出现了页故障才会真正去做。内核就是一个懒惰,骗人的败类;这是虚拟内存管理的基本原则。它对大多数情况都适用,有些比较熟悉,有些令人惊讶,但这个规则就是这样:VMA记录了双方商定做什么,而PTE反映出懒惰的内核实际做了什么。这两个数据结构共同管理程序的内存;都扮演着解决页故障,释放内存,换出内存(swappingmemoryout)等等角色。让我们看一个简单的内存分配的例子:
当程序通过brk()系统调用请求更多的内存时,内核只是简单的更新堆的VMA,然后说搞好啦。其实此时并没有页帧被分配,新的页也并没有出现于物理内存中。一旦程序试图访问这些页,处理器就会报告页故障,并调用do_page_fault()。它会通过调用find_vma()去搜索哪一个VMA含盖了产生故障的虚拟地址。如果找到了,还会根据VMA上的访问许可来比对检查访问请求(读或写)。如果没有合适的VMA,也就是说内存访问请求没有与之对应的合同,进程就会被处以段错误(SegmentationFault)的罚单。
当一个VMA被找到后,内核必须处理这个故障,方式是察看PTE的内容以及VMA的类型。在我们的例子中,PTE显示了该页并不存在。事实上,我们的PTE是完全空白的(全为0),在Linux中意味着虚拟页还没有被映射。既然这是一个匿名的VMA,我们面对的就是一个纯粹的RAM事务,必须由do_anonymous_page()处理,它会分配一个页帧并生成一个PTE,将出故障的虚拟页映射到那个刚刚分配的页帧上。
事情还可能有些不同。被换出的页所对应的PTE,例如,它的Present标志是0但并不是空白的。相反,它记录了页面内容在交换系统中的位置,这些内容必须从磁盘读取出来并通过do_swap_page()加载到一个页帧当中,这就是所谓的majorfault。
至此我们走完了“内核的用户内存管理”之旅的前半程。在下一篇文章中,我们将把文件的概念也混进来,从而建立一个内存基础知识的完成画面,并了解其对系统性能的影响。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/drshenlei/archive/2009/07/15/4350928.aspx
物理内存页框的管理
文章来源:http://www.tek-life.org/2010/03/30/%E7%89%A9%E7%90%86%E5%86%85%E5%AD%98%E9%A1%B5%E6%A1%86%E7%9A%84%E7%AE%A1%E7%90%86/
March30,2010bytek-life·LeaveaComment
Filedunder:Linux2.6内核分析
对于内存管理来讲,包括两个方面,一个是物理内存的管理,另一个是线性地址空间的管理。物理内存的管理是从“供应”的角度来看,我现在仓库里面有多少资源可以供分配,而线性地址空间的管理,则是从“需求”的角度去考虑,我需要多少的线性地址空间来满足进程的运行。把供应和需求连接起来的纽带就是缺页调用,以及页面的分配了。
下面从“供应”的角度谈谈,Linux系统如何来管理有限的物理内存资源的。
以I386为例,我们都知道,系统启动的时候,Bios检索内存,检查一下本机的物理内存有多大MB,对于CPU里面的MMU来讲,在寻址的时候,以4KB的页面来进行页面寻址的,既是每一个页面的大小为4KB,当然,如果是2MB的话,每一个页面大小就是2MB了,同样道理,某些I386CPU也支持4MB和1GB大小的页面,我们现在假定以4KB为例。以4KB为例,操作系统把全部的物理内存以4KB来分割,进行管理,在给进程分配的时候,最小的单位是1页,而进行SWAP和回收的时候,也是以最小的页为单位来进行处理的。因此,就需要有一个数据结构来描述一个物理页面的状态了:
1、这个页面分配了么?也就是这个页面在使用中么?如果在使用,那么被几个进程使用了。
2、这个页面被几个页表项映射了?
这个也挺重要,因为,如果,这个页要是换出的话,得告诉自己的“顶头上司”映射自己的页表项。让他们有所改变才行。
3、另外,如果页面被使用了,那么是存放的某些文件的内容,还是用于进程自己使用的?
如果是某些文件的内容,那么标识一下,以后的其他进程使用的话,就直接在内存中读,而不需要舍近求远了。—-这就是传说中的高速缓冲区
4、这个页面内容如果存放的是某个文件的内容,那么页面存放的数据相对于文件头来讲,具体是那一部分的数据?
这就是说,如果我要写回,我写到文件的什么位置捏?!
5、这个页面现在是被使用的么,还是暂时没有被使用?
如果没有被使用,就可以分配给其他进程了,如果被使用了,那么就不能给别的进程映射了。
由了这里的分析,那么我们就看一下Linux系统是怎样来描述一个物理页的状态的吧:
viewsource
print?
先看第一个字段:flags
这个字段就是说明了page对应页的属性:
内容是内核代码段么?
还是被保留的不能换出的,还是被锁定,不能被换出;还是刚刚被访问过的,如果刚刚被访问过那就是比较年轻,距离换出还有相当长的时间;还是将要被回收的;该页的内容是不是”脏”的。那么这些选项都对应一个标志位,一共有20个标志位。在include/linux/page-flags.h里面有定义。
第二个字段:_count.
这个字段,说明对应页被用到的次数。
第三个字段:map_count
这个字段是,该页在几个页表项中存在,这个字段和第二个字段_count很相似,但一般情况下,对页进行操作的时候,先把该页描述符的_count+1,然后操作完后,再进行-1。而map_count是说明这个页在页表项中映射的次数。有可能的情况是映射了到了好几个页表项,但_count的值为1。这个map_count字段最大的作用就是在页面被换出的时候,我要查找一下,有几个页表项映射的是这个页,然后通过反向查找,把所有对应的页表项的值该为swap里面的索引。
第四个字段是一个union结构体。mapping指向的是映射文件的inode的address_spaces字段。可以根据它和page里面的index字段,去查找是否在页高速缓存中,如果在也页高速缓存中没有找到,那么,就看一下是否被交换到了swap去里面,利用private里面的值去找到swap里面的数据内容,然后交换到页高速缓冲区中。
第五个字段,就是index。它的值代表了,页框的内容如果是映射文件的话,相对于文件开始的地方偏移量。
第六个字段,是lru。这是链接的活动、非活动链表。根据第一个字段flag属性,确定链接的是活动链表,还是非活动链表。
第七个字段,还不清楚,但根据注释,指向的是该页描述符对应页框的线性地址。我想,这个线性地址在对应内核线性地址空间的永久影射区的空间地址吧。
好了,以上就是对于页框的管理。似乎到这里就可以管理内存的每一个页框了。
我们现在假设一种情况,内核在给进程分配页框的时候,随机分配,假如把低16MB的地址,除去BIOS等占用的保留页框之外,都已经分配结束了。而在高于16MB的地方大部分还没有分配。这个时候,有一个用户进程请求了一个DMA传输,而这个DMA控制器是基于ISA总线的,我们清楚,对于ISA总线上的DMA控制器,是不经过MMU的,因此,它只认识总线地址而且是低于16M(0x10,00000)的线性地址才认识。但这个时候,0~16MB的物理地址区间内的页框都已经被用完了,而高于16MB的地方,还有很多内存可用,但是这个时候,是用不了的。所以,我们在进行页框管理的时候,还要在页框之上再加一个管理层,就是“区”的概念了。
一般情况下,内存分为3个区:
ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM。
除了ZONE_DMA必须之外,为什么还要再分一个ZONE_HIGHMEM区呢?因为,对于正常的区,内核区间的线性地址进行映射的时候是物理地址加上一个3G偏移量,就得到线性地址了,我们知道,内核直接映射的区域是0~896MB,对于大于1G的内存,内核是不能通过物理地址加上3G的偏移量,直接映射的,因此在高于896MB的物理内存在被映射的时候,是通过其他方法来映射的,因此要单独拿出来作为一个分区。
好了,现在两个特殊的分区,都划出来了,中间剩下的就作为一个区了—Normal区。
我们来看一下ZONE区的结构:
viewsource
print?
区作为物理页框的上级管理者,它首先需要知道,我所管辖的区里面所管辖的页框在物理内存的那个范围之内,总共有多少个页框,有多少空闲的内存可以分配,有哪些页框是永远也不能分配的。
另外,作为管理者,我有必要在空闲内存少的时候,启动回收方案,将暂时不用的页进行回收,或者交换,以腾出页面供将要进行的进程进行使用。那么为了进行有效的回收,我就必须知道什么时候来进行回收,哪些页暂时没有用,哪些页正在使用,因此就需要用两个对应的链表,把对应的页给穿起来(穿起来要依靠page结构里面的lru—有一个前指针,有一个后指针)。另外,为了管理的方便,需要知道有多少非活动页,有多少活动页。另外为了提高性能,Linux系统为管理区中单独分配一个页面的情况作了特殊处理。如果分配一个页面的时候,将用到的数据与前面的数据是相关的,可能已经在高速缓存中存在了,那么,就给它分配一个特殊的页面,也就是“热”页,操作系统为每一个CPU都维护了一个热页面的集合,通过链表,将热页面给管理起来,相对,如果我用到的数据,和之前的数据是无关的,那么我就分配一个一般的一面,相对与“热”页面,就是冷页面了。
根据上面的分析,基本上Linux都安排了相应的字段。
随着体系结构的发展,出现了NUMA的体系结构,在NUMA的体系结构中,存储器是分布式的。每一个节点都有一个本地的存储器,既可以访问本地的存储器,又可以访问系统其他节点的存储器,但访问其他节点和本地节点的时间是不一样的。在NUMA体系结构中,我们再假象一个场景,在一个节点中,我的物理内存用的差不多了,但其他节点上的存储器还有很多闲置的内存存在,因此,我就想从其他节点上分一些页框归我所用。那么我怎么定位其他节点上的页框地址呢?当然具体一点是先定位到节点,然后再定义到节点下面的存储区,然后再定义到区里面的页框。
很显然,在NUMA的体系结构中,在管理物理内存的时候,除了页描述符和管理区之外,还需要有一个新的管理机构就是节点。
在Linux中,节点的数据结构是pglist_data。
viewsource
print?
作为物理内存最高的管理机构,Pglist_data所做的事情主要是大局的。比如,我下面有几个管理区,如果给进程分配内存的时候,我没有那么多的内存,我首先该向哪一个管理区去借。另外,对于我本身的存储器而言,页描述符在物理内存的什么位置放着,第一个页框的序号是什么(一般都是0),总共有多少个页面,除去不能分配的,还有多少的页面。在进行页框分配的时候,我允许一次最多能分配多少的连续页框。另外,如果剩余的内存比较少,我应该调用哪一个进程去回收页框,回收页框完毕后,需要唤醒当初被阻塞的进程。
上面的分析,基本上也就是pglist_data结构下面的字段了。
内核是如何管理内存的?
原文标题:HowTheKernelManagesYourMemory
原文地址:
[注:本人水平有限,只好挑一些国外高手的精彩文章翻译一下。一来自己复习,二来与大家分享。]
在仔细审视了进程的虚拟地址布局之后,让我们把目光转向内核以及其管理用户内存的机制。再次从gonzo图示开始:
Linux进程在内核中是由task_struct的实例来表示的,即进程描述符。task_struct的mm字段指向内存描述符(memorydescriptor),即mm_struct,一个程序的内存的执行期摘要。它存储了上图所示的内存段的起止位置,进程所使用的物理内存页的数量(rss表示ResidentSetSize),虚拟内存空间的使用量,以及其他信息。我们还可以在内存描述符中找到用于管理程序内存的两个重要结构:虚拟内存区域集合(thesetofvirtualmemoryareas)及页表(pagetable)。Gonzo的内存区域如下图所示:
每一个虚拟内存区域(简称VMA)是一个连续的虚拟地址范围;这些区域不会交叠。一个vm_area_struct的实例完备的描述了一个内存区域,包括它的起止地址,决定访问权限和行为的标志位,还有vm_file字段,用于指出被映射的文件(如果有的话)。一个VMA如果没有映射到文件,则是匿名的(anonymous)。除memorymapping段以外,上图中的每一个内存段(如:堆,栈)都对应于一个单独的VMA。这并不是强制要求,但在x86机器上经常如此。VMA并不关心它在哪一个段。
一个程序的VMA同时以两种形式存储在它的内存描述符中:一个是按起始虚拟地址排列的链表,保存在mmap字段;另一个是红黑树,根节点保存在mm_rb字段。红黑树使得内核可以快速的查找出给定虚拟地址所属的内存区域。当你读取文件/proc/pid_of_process/maps时,内核只须简单的遍历指定进程的VMA链表,并打印出每一项来即可。
在Windows中,EPROCESS块可以粗略的看成是task_struct和mm_struct的组合。VMA在Windows中的对应物时虚拟地址描述符(VirtualAddressDescriptor),或简称VAD;它们保存在平衡树中(***Ltree)。你知道Windows和Linux最有趣的地方是什么吗?就是这些细小的不同点。
4GB虚拟地址空间被分割为许多页(page)。x86处理器在32位模式下所支持的页面大小为4KB,2MB和4MB。Linux和Windows都使用4KB大小的页面来映射用户部分的虚拟地址空间。第0-4095字节在第0页,第4096-8191字节在第1页,以此类推。VMA的大小必须是页面大小的整数倍。下图是以4KB分页的3GB用户空间:
处理器会依照页表(pagetable)来将虚拟地址转换到物理内存地址。每个进程都有属于自己的一套页表;一旦进程发生了切换,用户空间的页表也会随之切换。Linux在内存描述符的pgd字段保存了一个指向进程页表的指针。每一个虚拟内存页在页表中都有一个与之对应的页表项(pagetableentry),简称PTE。它在普通的x86分页机制下,是一个简单的4字节记录,如下图所示:
Linux有一些函数可以用于读取或设置PTE中的每一个标志。P位告诉处理器虚拟页面是否存在于(present)物理内存中。如果是0,访问这个页将触发页故障(pagefault)。记住,当这个位是0时,内核可以根据喜好,随意的使用其余的字段。R/W标志表示读/写;如果是0,页面就是只读的。U/S标志表示用户/管理员;如果是0,则这个页面只能被内核访问。这些标志用于实现只读内存和保护内核空间。
D位和A位表示数据脏(dirty)和访问过(accessed)。脏表示页面被执行过写操作,访问过表示页面被读或被写过。这两个标志都是粘滞的:处理器只会将它们置位,之后必须由内核来清除。最后,PTE还保存了对应该页的起始物理内存地址,对齐于4KB边界。PTE中的其他字段我们改日再谈,比如物理地址扩展(PhysicalAddressExtension)。
虚拟页面是内存保护的最小单元,因为页内的所有字节都共享U/S和R/W标志。然而,同样的物理内存可以被映射到不同的页面,甚至可以拥有不同的保护标志。值得注意的是,在PTE中没有对执行许可(executepermission)的设定。这就是为什么经典的x86分页可以执行位于stack上的代码,从而为黑客利用堆栈溢出提供了便利(使用return-to-libc和其他技术,甚至可以利用不可执行的堆栈)。PTE缺少不可执行(no-execute)标志引出了一个影响更广泛的事实:VMA中的各种许可标志可能会也可能不会被明确的转换为硬件保护。对此,内核可以尽力而为,但始终受到架构的限制。
虚拟内存并不存储任何东西,它只是将程序地址空间映射到底层的物理内存上,后者被处理器视为一整块来访问,称作物理地址空间(physicaladdressspace)。对物理内存的操作还与总线有点联系,好在我们可以暂且忽略这些并假设物理地址范围以字节为单位递增,从0到最大可用内存数。这个物理地址空间被内核分割为一个个页帧(pageframe)。处理器并不知道也不关心这些帧,然而它们对内核至关重要,因为页帧是物理内存管理的最小单元。Linux和Windows在32位模式下,都使用4KB大小的页帧;以一个拥有2GBRAM的机器为例:
在Linux中,每一个页帧都由一个描述符和一些标志所跟踪。这些描述符合在一起,记录了计算机内的全部物理内存;可以随时知道每一个页帧的准确状态。物理内存是用buddymemoryallocation技术来管理的,因此如果一个页帧可被buddy系统分配,则它就是可用的(free)。一个被分配了的页帧可能是匿名的(anonymous),保存着程序数据;也可能是页缓冲的(pagecache),保存着一个文件或块设备的数据。还有其他一些古怪的页帧使用形式,但现在先不必考虑它们。Windows使用一个类似的页帧编号(PageFrameNumber简称PFN)数据库来跟踪物理内存。
让我们把虚拟地址区域,页表项,页帧放到一起,看看它们到底是怎么工作的。下图是一个用户堆的例子:
蓝色矩形表示VMA范围内的页,箭头表示页表项将页映射到页帧上。一些虚拟页并没有箭头;这意味着它们对应的PTE的存在位(Presentflag)为0。形成这种情况的原因可能是这些页还没有被访问过,或者它们的内容被系统换出了(swapout)。无论那种情况,对这些页的访问都会导致页故障(pagefault),即使它们处在VMA之内。VMA和页表的不一致看起来令人奇怪,但实际经常如此。
一个VMA就像是你的程序和内核之间的契约。你请求去做一些事情(如:内存分配,文件映射等),内核说“行”,并创建或更新适当的VMA。但它并非立刻就去完成请求,而是一直等到出现了页故障才会真正去做。内核就是一个懒惰,骗人的败类;这是虚拟内存管理的基本原则。它对大多数情况都适用,有些比较熟悉,有些令人惊讶,但这个规则就是这样:VMA记录了双方商定做什么,而PTE反映出懒惰的内核实际做了什么。这两个数据结构共同管理程序的内存;都扮演着解决页故障,释放内存,换出内存(swappingmemoryout)等等角色。让我们看一个简单的内存分配的例子:
当程序通过brk()系统调用请求更多的内存时,内核只是简单的更新堆的VMA,然后说搞好啦。其实此时并没有页帧被分配,新的页也并没有出现于物理内存中。一旦程序试图访问这些页,处理器就会报告页故障,并调用do_page_fault()。它会通过调用find_vma()去搜索哪一个VMA含盖了产生故障的虚拟地址。如果找到了,还会根据VMA上的访问许可来比对检查访问请求(读或写)。如果没有合适的VMA,也就是说内存访问请求没有与之对应的合同,进程就会被处以段错误(SegmentationFault)的罚单。
当一个VMA被找到后,内核必须处理这个故障,方式是察看PTE的内容以及VMA的类型。在我们的例子中,PTE显示了该页并不存在。事实上,我们的PTE是完全空白的(全为0),在Linux中意味着虚拟页还没有被映射。既然这是一个匿名的VMA,我们面对的就是一个纯粹的RAM事务,必须由do_anonymous_page()处理,它会分配一个页帧并生成一个PTE,将出故障的虚拟页映射到那个刚刚分配的页帧上。
事情还可能有些不同。被换出的页所对应的PTE,例如,它的Present标志是0但并不是空白的。相反,它记录了页面内容在交换系统中的位置,这些内容必须从磁盘读取出来并通过do_swap_page()加载到一个页帧当中,这就是所谓的majorfault。
至此我们走完了“内核的用户内存管理”之旅的前半程。在下一篇文章中,我们将把文件的概念也混进来,从而建立一个内存基础知识的完成画面,并了解其对系统性能的影响。
本文来自CSDN博客,转载请标明出处:
物理内存页框的管理
文章来源:
March30,2010by
Filedunder:
对于内存管理来讲,包括两个方面,一个是物理内存的管理,另一个是线性地址空间的管理。物理内存的管理是从“供应”的角度来看,我现在仓库里面有多少资源可以供分配,而线性地址空间的管理,则是从“需求”的角度去考虑,我需要多少的线性地址空间来满足进程的运行。把供应和需求连接起来的纽带就是缺页调用,以及页面的分配了。
下面从“供应”的角度谈谈,Linux系统如何来管理有限的物理内存资源的。
以I386为例,我们都知道,系统启动的时候,Bios检索内存,检查一下本机的物理内存有多大MB,对于CPU里面的MMU来讲,在寻址的时候,以4KB的页面来进行页面寻址的,既是每一个页面的大小为4KB,当然,如果是2MB的话,每一个页面大小就是2MB了,同样道理,某些I386CPU也支持4MB和1GB大小的页面,我们现在假定以4KB为例。以4KB为例,操作系统把全部的物理内存以4KB来分割,进行管理,在给进程分配的时候,最小的单位是1页,而进行SWAP和回收的时候,也是以最小的页为单位来进行处理的。因此,就需要有一个数据结构来描述一个物理页面的状态了:
1、这个页面分配了么?也就是这个页面在使用中么?如果在使用,那么被几个进程使用了。
2、这个页面被几个页表项映射了?
这个也挺重要,因为,如果,这个页要是换出的话,得告诉自己的“顶头上司”映射自己的页表项。让他们有所改变才行。
3、另外,如果页面被使用了,那么是存放的某些文件的内容,还是用于进程自己使用的?
如果是某些文件的内容,那么标识一下,以后的其他进程使用的话,就直接在内存中读,而不需要舍近求远了。—-这就是传说中的高速缓冲区
4、这个页面内容如果存放的是某个文件的内容,那么页面存放的数据相对于文件头来讲,具体是那一部分的数据?
这就是说,如果我要写回,我写到文件的什么位置捏?!
5、这个页面现在是被使用的么,还是暂时没有被使用?
如果没有被使用,就可以分配给其他进程了,如果被使用了,那么就不能给别的进程映射了。
由了这里的分析,那么我们就看一下Linux系统是怎样来描述一个物理页的状态的吧:
01 | 223 struct page{ |
02 | 224unsigned long flags; /*Atomicflags,somepossibly |
03 | 225*updatedasynchronously*/ |
04 |
05 | 226atomic_t_count; /*Usagecount,seebelow.*/ |
06 | 227atomic_t_mapcount; /*Countofptesmappedinmms,有几个pet映射此页面 |
07 | 228*toshowwhenpageismapped |
08 | 229*&limitreversemapsearches. |
09 | 230*/ |
10 | 231 union { |
11 | 232 struct { |
12 |
13 | 233unsigned long private ; /*Mapping-privateopaquedata: |
14 |
15 | 234*usuallyusedforbuffer_heads |
16 |
17 | 235*ifPagePrivateset;usedfor |
18 |
19 | 236*swp_entry_tifPageSwapCache; |
20 |
21 | 237*indicatesorderinthebuddy |
22 |
23 | 238*systemifPG_buddyisset. |
24 |
25 | 239*/ |
26 |
27 | 240 struct address_space*mapping; /*Iflowbitclear,pointsto |
28 |
29 | 241*inodeaddress_space,orNULL. |
30 |
31 | 242*Ifpagemappedasanonymous |
32 |
33 | 243*memory,lowbitisset,and |
34 |
35 | 244*itpointstoanon_vmaobject: |
36 |
37 | 245*seePAGE_MAPPING_ANONbelow. |
38 |
39 | 246*/ |
40 |
41 | 247}; |
42 |
43 | 248# if NR_CPUS>=CONFIG_SPLIT_PTLOCK_CPUS |
44 |
45 | 249spinlock_tptl; |
46 |
47 | 250#endif |
48 |
49 | 251}; |
50 |
51 | 252pgoff_tindex; /*Ouroffsetwithinmapping.*/ |
52 |
53 | 253 struct list_headlru; /*Pageoutlist,eg.active_list |
54 |
55 | 254*protectedbyzone->lru_lock! |
56 |
57 | 255*/ |
58 |
59 | 256 /* |
60 |
61 | 257*OnmachineswhereallRAMismappedintokerneladdressspace, |
62 |
63 | 258*wecansimplycalculatethevirtualaddress.Onmachineswith |
64 |
65 | 259*highmemsomememoryismappedintokernelvirtualmemory |
66 |
67 | 260*dynamically,soweneedaplacetostorethataddress. |
68 |
69 | 261*Notethatthisfieldcouldbe16bitsonx86...<IMGclass=wp-smileyalt=;)src=" |
70 |
71 | 262* |
72 |
73 | 263*Architectureswithslowmultiplicationcandefine |
74 |
75 | 264*WANT_PAGE_VIRTUALinasm/page.h |
76 |
77 | 265*/ |
78 |
79 | 266# if defined(WANT_PAGE_VIRTUAL) |
80 |
81 | 267 void * virtual ; /*Kernelvirtualaddress(NULLif |
82 |
83 | 268notkmapped,ie.highmem)*/ |
84 |
85 | 269#endif /*WANT_PAGE_VIRTUAL*/ |
86 |
87 | 270}; |
这个字段就是说明了page对应页的属性:
内容是内核代码段么?
还是被保留的不能换出的,还是被锁定,不能被换出;还是刚刚被访问过的,如果刚刚被访问过那就是比较年轻,距离换出还有相当长的时间;还是将要被回收的;该页的内容是不是”脏”的。那么这些选项都对应一个标志位,一共有20个标志位。在include/linux/page-flags.h里面有定义。
第二个字段:_count.
这个字段,说明对应页被用到的次数。
第三个字段:map_count
这个字段是,该页在几个页表项中存在,这个字段和第二个字段_count很相似,但一般情况下,对页进行操作的时候,先把该页描述符的_count+1,然后操作完后,再进行-1。而map_count是说明这个页在页表项中映射的次数。有可能的情况是映射了到了好几个页表项,但_count的值为1。这个map_count字段最大的作用就是在页面被换出的时候,我要查找一下,有几个页表项映射的是这个页,然后通过反向查找,把所有对应的页表项的值该为swap里面的索引。
第四个字段是一个union结构体。mapping指向的是映射文件的inode的address_spaces字段。可以根据它和page里面的index字段,去查找是否在页高速缓存中,如果在也页高速缓存中没有找到,那么,就看一下是否被交换到了swap去里面,利用private里面的值去找到swap里面的数据内容,然后交换到页高速缓冲区中。
第五个字段,就是index。它的值代表了,页框的内容如果是映射文件的话,相对于文件开始的地方偏移量。
第六个字段,是lru。这是链接的活动、非活动链表。根据第一个字段flag属性,确定链接的是活动链表,还是非活动链表。
第七个字段,还不清楚,但根据注释,指向的是该页描述符对应页框的线性地址。我想,这个线性地址在对应内核线性地址空间的永久影射区的空间地址吧。
好了,以上就是对于页框的管理。似乎到这里就可以管理内存的每一个页框了。
我们现在假设一种情况,内核在给进程分配页框的时候,随机分配,假如把低16MB的地址,除去BIOS等占用的保留页框之外,都已经分配结束了。而在高于16MB的地方大部分还没有分配。这个时候,有一个用户进程请求了一个DMA传输,而这个DMA控制器是基于ISA总线的,我们清楚,对于ISA总线上的DMA控制器,是不经过MMU的,因此,它只认识总线地址而且是低于16M(0x10,00000)的线性地址才认识。但这个时候,0~16MB的物理地址区间内的页框都已经被用完了,而高于16MB的地方,还有很多内存可用,但是这个时候,是用不了的。所以,我们在进行页框管理的时候,还要在页框之上再加一个管理层,就是“区”的概念了。
一般情况下,内存分为3个区:
ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM。
除了ZONE_DMA必须之外,为什么还要再分一个ZONE_HIGHMEM区呢?因为,对于正常的区,内核区间的线性地址进行映射的时候是物理地址加上一个3G偏移量,就得到线性地址了,我们知道,内核直接映射的区域是0~896MB,对于大于1G的内存,内核是不能通过物理地址加上3G的偏移量,直接映射的,因此在高于896MB的物理内存在被映射的时候,是通过其他方法来映射的,因此要单独拿出来作为一个分区。
好了,现在两个特殊的分区,都划出来了,中间剩下的就作为一个区了—Normal区。
我们来看一下ZONE区的结构:
01 | 139 struct zone{ |
02 |
03 | 140 /*Fieldscommonlyaccessedbythepageallocator*/ |
04 |
05 | 141unsigned long free_pages; //有多少空闲页 |
06 |
07 | 142unsigned long pages_min,pages_low,pages_high; //阈值,在分配物理页的时候对不同的阈值,正常,或者减慢页的分配速度 |
08 |
09 | 152unsigned long lowmem_reserve[MAX_NR_ZONES]; //每一个区需要保留的内存,应急用 |
10 |
11 | 154#ifdefCONFIG_NUMA |
12 |
13 | 155 /* |
14 |
15 | 156*zonereclaimbecomesactiveifmoreunmappedpagesexist. |
16 |
17 | 157*/ |
18 |
19 | 158unsigned long min_unmapped_ratio; //换出的页太多了,就要启动reclaim |
20 |
21 | 159unsigned long min_slab_pages; //用于slab页太少了,因此也要reclaim |
22 |
23 | 160 struct per_cpu_pageset*pageset[NR_CPUS]; //每个CPU需要维护的冷热页表,在高速缓存中的页为热页 |
24 |
25 | 161# else |
26 |
27 | 162 struct per_cpu_pagesetpageset[NR_CPUS]; |
28 |
29 | 163#endif |
30 |
31 | 164 /* |
32 |
33 | 165*freeareasofdifferentsizes |
34 |
35 | 166*/ |
36 |
37 | 167spinlock_tlock; |
38 |
39 | 168#ifdefCONFIG_MEMORY_HOTPLUG |
40 |
41 | 169 /*seespanned/present_pagesformoredescription*/ |
42 |
43 | 170seqlock_tspan_seqlock; |
44 |
45 | 171#endif |
46 |
47 | 172 struct free_areafree_area[MAX_ORDER]; |
48 |
49 | 177 /*Fieldscommonlyaccessedbythepagereclaimscanner*/ |
50 |
51 | 178spinlock_tlru_lock; |
52 |
53 | 179 struct list_headactive_list; |
54 |
55 | 180 struct list_headinactive_list; |
56 |
57 | 181unsigned long nr_scan_active; //回收内存时需要扫描的活动页数 |
58 |
59 | 182unsigned long nr_scan_inactive; //回收内存时需要扫描的非活动页数 |
60 |
61 | 183unsigned long nr_active; //活动链表上的页数 |
62 |
63 | 184unsigned long nr_inactive; //非活动链表上的页数 |
64 |
65 | 185unsigned long pages_scanned; /*sincelastreclaim*/ |
66 |
67 | 186 int all_unreclaimable; /*Allpagespinned*/ //当填满不可回收页时置位 |
68 |
69 | 187 |
70 |
71 | 188 /*Acountofhowmanyreclaimersarescanningthiszone*/ |
72 |
73 | 189atomic_treclaim_in_progress; //有几个回收进程在扫描该区 |
74 |
75 | 190 |
76 |
77 | 191 /*Zonestatistics*/ |
78 |
79 | 192atomic_long_tvm_stat[NR_VM_ZONE_STAT_ITEMS]; //存放Zone属性的统计信息 |
80 |
81 | 207 int prev_priority; //回收内存时,先处理的优先级范围0~12 |
82 |
83 | 244 struct pglist_data*zone_pgdat; //隶属于哪个节点 |
84 |
85 | 245 /*zone_start_pfn==zone_start_paddr>>PAGE_SHIFT*/ |
86 |
87 | 246unsigned long zone_start_pfn; //zone区从哪一个页框号开始 |
88 |
89 | 258unsigned long spanned_pages; /*totalsize,includingholes*/ |
90 |
91 | 259unsigned long present_pages; /*amountofmemory(excludingholes)*/ |
92 |
93 | 264 char *name; |
94 |
95 | 265}____cacheline_internodealigned_in_smp; |
另外,作为管理者,我有必要在空闲内存少的时候,启动回收方案,将暂时不用的页进行回收,或者交换,以腾出页面供将要进行的进程进行使用。那么为了进行有效的回收,我就必须知道什么时候来进行回收,哪些页暂时没有用,哪些页正在使用,因此就需要用两个对应的链表,把对应的页给穿起来(穿起来要依靠page结构里面的lru—有一个前指针,有一个后指针)。另外,为了管理的方便,需要知道有多少非活动页,有多少活动页。另外为了提高性能,Linux系统为管理区中单独分配一个页面的情况作了特殊处理。如果分配一个页面的时候,将用到的数据与前面的数据是相关的,可能已经在高速缓存中存在了,那么,就给它分配一个特殊的页面,也就是“热”页,操作系统为每一个CPU都维护了一个热页面的集合,通过链表,将热页面给管理起来,相对,如果我用到的数据,和之前的数据是无关的,那么我就分配一个一般的一面,相对与“热”页面,就是冷页面了。
根据上面的分析,基本上Linux都安排了相应的字段。
随着体系结构的发展,出现了NUMA的体系结构,在NUMA的体系结构中,存储器是分布式的。每一个节点都有一个本地的存储器,既可以访问本地的存储器,又可以访问系统其他节点的存储器,但访问其他节点和本地节点的时间是不一样的。在NUMA体系结构中,我们再假象一个场景,在一个节点中,我的物理内存用的差不多了,但其他节点上的存储器还有很多闲置的内存存在,因此,我就想从其他节点上分一些页框归我所用。那么我怎么定位其他节点上的页框地址呢?当然具体一点是先定位到节点,然后再定义到节点下面的存储区,然后再定义到区里面的页框。
很显然,在NUMA的体系结构中,在管理物理内存的时候,除了页描述符和管理区之外,还需要有一个新的管理机构就是节点。
在Linux中,节点的数据结构是pglist_data。
01 | 303 typedef struct pglist_data{ |
02 |
03 | 304 struct zonenode_zones[MAX_NR_ZONES]; //节点中管理区描述符的数组 |
04 |
05 | 305 struct zonelistnode_zonelists[GFP_ZONETYPES]; //页分配器使用的管理区数组,代表不同的分配策略 |
06 |
07 | 306 int nr_zones; // |
08 |
09 | 307#ifdefCONFIG_FLAT_NODE_MEM_MAP |
10 |
11 | 308 struct page*node_mem_map; //page链表 |
12 |
13 | 309#endif |
14 |
15 | 310 struct bootmem_data*bdata; |
16 |
17 | 311#ifdefCONFIG_MEMORY_HOTPLUG |
18 |
19 | 319spinlock_tnode_size_lock; |
20 |
21 | 320#endif |
22 |
23 | 321unsigned long node_start_pfn; //节点中第一个页框的下标 |
24 |
25 | 322unsigned long node_present_pages; /*totalnumberofphysicalpages*/ |
26 |
27 | 323unsigned long node_spanned_pages; /*totalsizeofphysicalpage |
28 |
29 | 324range,includingholes*/ |
30 |
31 | 325 int node_id; //node编号 |
32 |
33 | 326wait_queue_head_tkswapd_wait; //在运行进程时,kswapd在运行,那么将进程阻塞,放入等待队列中 |
34 |
35 | 327 struct task_struct*kswapd; //指向kswapd内核线程 |
36 |
37 | 328 int kswapd_max_order; //允许创建空闲块的最大值,取对数 |
38 |
39 | 329}pg_data_t; |
上面的分析,基本上也就是pglist_data结构下面的字段了。
相关文章推荐
- 内核如何管理内存【转】
- 内核是如何管理内存的?
- 【译】内核是如何管理内存的
- 内核是如何管理内存的?
- 内核如何管理你的内存(转自melody_lu123)
- linux 内核如何管理内存
- 内核是如何管理内存的
- 内核是如何管理内存的?
- 内核如何管理内存 | Linux 中国
- 内核是如何管理内存的?
- 内核是如何管理内存的(翻译)
- 内核是如何管理内存的
- 内核如何管理内存
- 内核如何管理内存
- How The Kernel Manages Your Memory(内核如何管理程序的内存)
- 内核是如何管理内存的?
- 内核如何管理内存
- 内核是如何管理内存的
- JVM如何管理内存
- [Android 性能优化系列]内存之基础篇--Android如何管理内存