Linux内核如何管理内存

翻译一篇老文章,基础原理十年未变 https://manybutfinite.com/post/how-the-kernel-manages-your-memory/

进程虚拟地址空间布局出来后,我们回到内核探究其管理用户态内存的机制,这里仍旧是gonzo进程:

Linux进程在内核空间是以一个task_struct实现的,这个实例就是进程描述符。

task_struct 里的mm指针指向内存描述符mm_struct,这是程序运行的内存概要。

    如上图所示,内存描述符里保存着memory segment的起始地址,进程使用物理内存页的大小(rss代表实际使用的物理内存), 已经使用的虚拟地址空间大小,以及其他数据位。在内存描述符里,我们找到了程序内存管理的两匹马,virtual memory areas 和 page tables的集合。Gonzo进程的memory areas 如下所示:

    每一个virtual memory area (VMA) 描述了一段连续的虚拟地址空间,不同地址空间之间不会重叠。一个VMA的实例完全描述了一段内存区域,包括起始地址,决定访问权限和行为的flags,以及vm_file指针(貌似了被映射到该地址区域的文件,如果有的话)。一个没有map文件的VMA称为anonymous

    一个程序的VMAs都存在内存描述符中,用链表的方式存在mmap域,同时也用红黑树的存储于mm_rb域,红黑树方便内核快速查找一个给定虚拟地址所在的VMA。当读取文件/proc/pid_of_process/maps的时候,内核遍历该进程的VMA链表并逐一打印。

    在windows系统中,EPROCESS块是一个task_struct和mm_struct的混合体,windows中与VMA类似的是虚拟地址描述符VAD,VAD存储于AVL树中。

    4GB的虚拟地址空间按页进行划分,32位的X86处理器支持4KB, 2MB 和 4MB 的页大小。Linux和windows 都用4KB的页映射用户态虚拟地址空间。字节0到4095落在第0页,字节4096到8191落在第1页,以此类推。VMA的大小必须是页大小的整数倍,如下是3GB用户态地址空间按4KB页大小的划分。

    处理器查询page tables,将虚拟地址转换为物理内存地址。每个进程都有自己的页表组;每当发生进程切换时,用户空间的page tables 也会切换。Linux在内存描述符的pgd字段中存储着指向进程页表的指针。对于每个虚拟页,在页表中对应一个页表条目(pte),在常规x86分页中,用4字节记录PTE,如下所示:

 

    Linux有函数可以读取和设置PTE中的每个flag,标志位P告诉处理器虚拟页是否存在于物理内存中。如果清除(等于0),则访问页面会触发一个page fault。请记住,当这个位为零时,内核可以对剩下的字段做任何它想做的事情。R/W表示读和写,如果清除,则页面为只读。U/S代表普通用户和特权用户,如果清除,则页面只能由内核访问。这些标志用于实现我们以前看到的只读内存和受保护的内核空间。

    D和A位用于脏页和访问属性, D等于0表示该页未写过,1表示该页被写过,A等于0 表示该页未被访问,1表示已被访问。 这两个标志都是粘性的:处理器只设置它们,它们必须由内核清除。最后,PTE存储与此页对应的起始物理地址(以4KB对齐)。这个看似天真的字段是一些痛苦的根源,因为它将可寻址物理内存限制为4GB。当然Physical Address Extension可以将32位的系统扩展出大于4GB的物理寻址空间,S公司的XX95芯片也早就可以将32位虚拟地址翻译为36位物理地址。

    虚拟页是内存保护的单位,因为它的所有字节共享U/S和R/W标志。但是相同的物理内存可以由不同的页面映射,可能具有不同的保护标志。请注意,执行权限是无法在pte中看到的。缺少pte no execute标志说明了一个更广泛的事实:VMA中的权限标志可能不一定会完全转换为硬件保护。内核尽其所能,但架构限制了可能。

    虚拟内存不存储任何东西,只是将程序的地址空间映射到底层物理内存上,处理器将其作为一个被称为物理地址空间的大块进行访问。这个物理地址空间被内核分解成页帧,虽然处理器并不知道也不关心页帧,但它们对内核至关重要,因为页帧是物理内存管理的基本单元。在32位模式下,Linux和Windows都使用4KB页帧,下面是一个具有2GB RAM的计算机示例:

 

    在Linux中,每个页帧都由一个描述符和一些flags跟踪。计算机中的所有物理内存都被这些描述符一起跟踪,可以知道每个页帧的准确状态。物理内存使用伙伴算法进行管理,如果页帧可以通过伙伴系统进行分配,则页帧的状态为free。分配的页帧可能是匿名的,持有程序数据,或者它可能在页面缓存中,拥有存储在文件或块设备中的数据。页帧还有其他的使用方式,现在暂且不去管。让我们把虚拟内存区、页表条目(PTE)和页帧放在一起去理解这一切是如何工作的,下面是一个用户态堆的示例:

 

    蓝色矩形框代表VMA范围内的页,箭头表示PTE将页映射到页帧上。有些虚拟空间的页缺少箭头,这意味着相应的pte清除了P标志位。可能是因为页面从未被分配过,也可能是它们的内容已被交换出去。不管哪种情况,访问这些页都会产生page fault,即使它们在VMA中。对于VMA和Page table这种不一致的情况,虽然看起来很奇怪,但是这种情况经常发生。

    一个VMA就像是程序和内核之间的一个契约,用户态要求做一些事情(内存分配、文件映射等),内核说“当然可以”,然后它创建或者更新合适的VMA。但实际上它并没有立刻满足请求,而是等到page fault发生的时候才去做实际的工作。内核就是一个既懒惰又狡猾的渣。这是虚拟内存的基本原理,它应用于大多数情况,规则是VMA将契约达成一致的内容记录下来,而PTE反映了懒惰的内核实际上已经做了什么。这两种数据结构一起管理一个程序的内存;它们都在解决page fault、释放内存、交换内存等方面发挥作用。让我们来看一个简单的内存分配例子:

 

 

    当程序通过系统调用brk()申请更多内存时,内核只是简单地更新堆VMA并告诉它已经完成了。这个时候,实际上并没有分配页帧,新的页也没有出现在物理内存中。一旦程序尝试访问页,处理器产生page fault并调用do_page_fault()函数。它使用find_vma()函数寻找覆盖fault虚拟地址的VMA, 如果找到,还将检查VMA上试图访问(读或写)的权限。如果没有合适的VMA,就没有包含相关内存访问的契约,该进程会受到Segmentation Fault的惩罚。

    当找到了VMA时,内核必须通过查看PTE目录和VMA类型来处理fault。在我们的例子中,PTE显示了页不存在。事实上,我们的PTE是完全空白的(全部为零),这在Linux中意味着该页从未被映射过。这是一个匿名VMA,所以必须由do_anonymous_page()来处理纯粹的RAM事务,它分配一个页帧并准备一个PTE将fault的虚拟页映射到新分配的页帧上。

    对于换出页面的PTE,事情可能有所不同。例如,PTE的P标志位为0但是不为空。相反,它存储着保存页面内容的交换位置,这些内容必须从磁盘读取,并由do_swap_page()函数加载到页帧中,这称为一个major fault.

    内核的用户态内存管理的前半部分就此结束,在下一篇章中,我们将构建一个完整的内存基本原理图,包括对性能的影响。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值