您的位置:首页 > 运维架构 > Linux

【Linux系统】内存管理(二)

2015-08-26 16:37 477 查看
内核如何管理内存?

Linux的进程在内核中是由task_struct来实现的。该结构中有一个mm域指向这个进程所使用的进程描述符——mm_struct。进程描述符描述了虚拟内存的当前状态,它包含了我们感兴趣的两个字段:pgd和mmap,其中pgd指向第一级页表的基址,而mmap指向一个vm_area_struct(区域结构)的链表,其中每个vm_area_struct都描述了当前地址空间的一个区域。

如下图所示:



每个虚拟内存区域都是一个连续的虚拟地址空间;这些区域是不会重叠的。一个vm_area_struct表示一个这样的内存区域,包括该区域的起始和结束地址,flags描述了它的访问权限和行为,vm_file表示哪一个文件被映射到这块区域。一个VMA没有映射一个文件被称为是匿名的。上面的每个内存段(堆,栈…)都对应着一个VMA,唯一的例外是memory mapping segment,它会有几个VMA来描述。这虽然不是必须的,但是在x86下一般都是这样。任何一个VMA是不关心它是属于哪个段的。

一个程序的所有VMA都会保存在它自己的内存描述符中(mm_struct),会同时被一个mmap指向的链表所表示和用每一个vma的其实虚拟地址作为key的一个红黑树中。红黑树提供了在给定一个虚拟地址而能快速的找到它所在的VMA的效率。而链表的表示则被用来依次列出多有该进程所使用VMA的空间时被使用,比如当你从/proc/pid_of_process/maps读取数据的时候,内核就使用链表的结构来得到所有的VMA信息。

所有4GB的虚拟地址空间被划分为许多页。x86处理器在32位模式下支持4kb,2MB和4MB的页大小。linux和windows都默认采用4kb的页来映射用户的虚拟地址空间。0-4095属于0页,4096-8191属于1页,等等。所有的VMA的大小都必须是页大小的整数倍。下面是3GB的用户空间在页大小为4kb时的描述:



处理器通过页表来把一个虚拟地址转化为实际的物理内存地址。每个进程有属于它自己的一组页表;无论何时发生了进程切换,相应的会发生用户空间的页表切换。mm_struct中有一个pdg域,就指向该进程所使用的页表集。对每一个虚拟页在页表中都有对应的一个页表项(PTE),通常x86下的一页十一个4字节的如下记录:



Linux提供一些列用来读写PTE的函数。P位告诉处理器该虚拟页目前是否在物理内存中。0表示不在,这是访问该页的动作会触发一个缺页异常。请记住,当P位为0时,内核是不检测其它所有的位的,内核可以用这些位另做它用。R/W位表示读/写;0表示只读。U/S位代表用户和系统;0表示该页只能被kernel访问。

D和A位分别对应表示脏数据和已被访问过。一个脏页表示被写过,而被访问包括被读和被写。这两个标志都有些特别:都只有处理器来设置它们,但是由内核来复位它们。最后,PTE中储存的是对应的page的开始物理地址,并且会与4KB对其。这些控制标志常会带来一些不便,它们限制了物理内存只能达到4GB.其它的一些符号位被用来支持物理地址扩展(PAE)。

虚拟内存不储存任何东西,它只是简单的映射一个程序的地址到底层的物理内存,该物理内存会被处理器访问,并被叫做物理内存地址空间。如果内存操作发生在总线上,我们可以忽略虚拟内存,而可以假设物理地址是从0到最大可用的内存并且是1个字节1个字节递增的。这物理地址空间现在却会被内核分为一系列的页帧。处理器不关心这些帧,它们对于内核很重要,因为内核利用页帧来作为管理物理内存的最小单位。Linux和windows在32位模式下都使用4kb的页帧;下面是一个有2G物理内存的机器的例子:



linux中每个页帧是通过一个描述符(即page struct)和一些标志来追踪。而通过把这些描述符结合起来来追踪计算机内的所有物理内存。每个页帧的具体状态是完全可知的。物理内存是通过伙伴内存分配系统来管理的,因此如果一个页帧对于伙伴系统来说是可用的那么这个页帧就是空闲的。一个被分配出来的页帧可能是匿名的,可以用来保存程序数据或者可能是一个页cache,或者保存一个文件中的数据或是一个块设备。还有一些其它的特别的页帧的使用,但是我们先不考虑它们。Windows有一个类似的Page Frame Number(PFN)的数据库来帮助跟踪物理内存。

一个VMA就像是你的程序和内核之间的一份契约。你请求一些事情要被完成(内存分配,文件映射等等),kernel会告诉你OK,并且帮你建立或者更新适当的VMA。但是它不会真正的马上响应你的要求,它会等到一个页错误发生才会去做相应的事情。kernel是懒惰的,有点虚伪的:)这确实虚拟内存之所以存在的意义。它应用在一些常见的场景,一些熟悉,一些可能看起来奇怪的场景中,但是事实是VMA记录了什么是被kernel所认知同意的,而PTEs则反映了什么是被懒惰的内核真正做的。这两个关键数据结构合起来管理了一个程序的内存;同时来配合实现了一个决定是否发生页错误/释放内存/交换内存等等的逻辑。

当一个程序通过brk系统调用要求更多的内存时,内核只是简单的更新了它的堆所在的VMA。没有页帧被真正的分配出来,并且新的页是没有在物理内存上建立出来。一旦程序尝试访问这些页,则处理器出发一个页错误,即执行do_page_fault()函数。它通过find_vma()来找到导致页错误的VMA中的虚拟地址。如果找到了,且各种VMA中的权限检查没有通过,则表示没有合适的VMA,即没有建立好适合的契约,那么这次的内存访问就是非法的,会导致内核发出Segmentation Fault。

当VMA被找到,并且各种权限检查通过,则PTE会表示该页不在现有页帧当中。如果PTE是空的,这意味着linux kernel认为虚拟页还没有被映射。因为这是一个匿名的VMA,我们就必须使用do_anonymous_page()处理这种情况,它会把之前PTE的错误的虚拟页映射到新分配出来的页帧上。当然,事情也有可能有所区别。比如PTE中是一个换出的页,此时Present标示是0但是该PTE却不是空的。相反,它存储的交换区的地址里面保存的是页内容,它必须由do_swap_page()从硬盘里读取到页帧中。这叫做major fault。

最后贴上一张完整的描述内存管理的图:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: