linux进程地址空间 进程内存布局,程序的内存布局

虚拟地址空间

内存管理是操作系统的核心;它对于编程和系统管理都是至关重要的。

多任务操作系统中的每个进程都在自己的内存沙箱中运行。这个沙箱是虚拟地址空间,在32位模式下,它总是一个4GB内存地址块。这些虚拟地址由页表映射到物理内存,页表由操作系统内核维护并由处理器查询。每个进程都有自己的一组页表。一旦虚拟地址被启用,它们将应用于机器中运行的所有软件,包括内核本身。因此,虚拟地址空间的一部分必须保留给内核:

479cb9e91335

image.png

这并不意味着内核使用了那么多物理内存,只是它有一部分地址空间可用来映射它希望映射的任何物理内存。内核空间在页表中被标记为特权代码独享,因此,如果用户模式程序试图接触它,就会触发页面错误。在Linux中,内核空间一直存在,如下图,(因为内核是一直运行的),并在所有进程中映射相同的物理内存。内核代码和数据总是可寻址的,随时可以处理中断或系统调用。相比之下,每当发生进程切换时,地址空间的用户模式部分的映射就会发生变化:

479cb9e91335

image.png

蓝色区域表示映射到物理内存的虚拟地址,而白色区域表示未映射。在上面的例子中,由于传说中的内存饥渴(应该是吃内存的意思),Firefox使用了更多的虚拟地址空间。地址空间中的不同段对应于堆、堆栈等内存段。请记住,这些段只是内存地址的一个范围,与intel风格的段没有任何关系。不管怎样,这是Linux进程中的标准内存布局:

479cb9e91335

image.png

进程地址空间中最顶层的段是栈,大多数编程语言存储局部变量和函数参数的地方。调用方法或函数会压入新的栈帧。当函数返回时,栈帧被销毁。这种简单的设计很容易实现,因为数据遵循严格的LIFO顺序,这意味着不需要复杂的数据结构来跟踪堆栈内容——一个指向栈顶部的简单指针就可以了。因此,压入和弹出是非常快而且确定。另外,堆栈区域的不断重用往往会使cpu缓存中的堆栈内存保持活动状态,从而加快访问速度。进程中的每个线程都有自己的堆栈。

不断压入超出堆栈所能容纳的数据会耗尽映射堆栈的区域。这将触发一个由expand_stack()在Linux中处理的页面错误,该错误反过来调用acct_stack_growth()来检查是否适合扩展堆栈。如果堆栈大小低于RLIMIT_STACK(通常是8MB),那么通常堆栈会增长,程序会愉快地继续运行,而不知道刚刚发生了什么。这是堆栈大小根据需要进行调整的正常机制。但是,如果已经达到最大堆栈大小,就会出现堆栈溢出,程序会接收到段错误(Segmentation Fault)。当映射的堆栈区域扩展以满足需求时,它不会在堆栈变小时收缩回来。就像联邦预算一样,它只会扩张。

在堆栈下面,我们有内存映射段。在这里,内核直接将文件的内容映射到内存。任何应用程序都可以通过Linux mmap()系统调用请求这样的映射。内存映射是执行文件I/O的一种方便高效的方法,因此常用于加载动态库。还可以创建不对应于任何文件的匿名内存映射,用于程序数据。

堆提供运行时内存分配,就像栈一样,这意味着数据必须比执行分配的函数活得更久,这与栈不同。大多数语言都提供了堆管理。在C语言中,堆分配的接口是malloc()。如果堆中有足够的空间来满足内存请求,那么程序运行时可以在不涉及内核的情况下处理它。否则,通过brk()系统调用(实现)来扩大堆,为请求的块腾出空间。

堆管理是复杂的,需要复杂的算法来实现高效的内存使用。堆请求所需的时间可以有很大的不同。实时系统有专门的分配器来处理这个问题。堆也会变得支离破碎,如下所示:

479cb9e91335

image.png

最后,我们讨论内存的最低段:BSS、数据段和程序段(程序段有时也翻译成文本段)。BSS和数据段都为c中的静态(全局)变量存储内容。区别在于BSS存储未初始化的静态变量的内容,这些静态变量的值在源代码中没有被初始化设置。BSS内存区域是匿名的:它不映射任何文件。

另一方面,数据段保存源代码中初始化的静态变量的内容。这个内存区域不是匿名的。它映射程序二进制映像中包含源代码中给定的初始静态值的部分。因此,如果定义了 static int cntWorkerBees = 10,则cntWorkerBees的内容位于数据段中,内容为10。即使数据段映射一个文件,它也是一个私有内存映射,这意味着对内存的更新不会反映在底层文件中。必须是这样,否则分配给全局变量将改变磁盘上的二进制映像。不可思议!

479cb9e91335

image.png

内核如何管理内存

479cb9e91335

image.png

在内核中,进程是以进程描述符task_struct来表示的,task_struct中的mm字段指向内存描述符mm_struct,它是程序内存的执行摘要。它存储如上所示的内存段的开始和结束、进程所使用的物理内存页的数量(rss表示驻留内存大小)、所使用的虚拟地址空间的大小等。在内存描述符中,我们还可以找到管理程序内存的两个设计:虚拟内存区域集和页表。Gonzo的内存分配如下图所示:

479cb9e91335

image.png

每个虚拟内存区域(VMA)是一个连续的虚拟地址范围;这些区域从不重叠。vm_area_struct的一个实例完整地描述了一个内存区域,如上图所示,包括它的起始地址和结束地址、用于确定访问权限和行为的标志,以及vm_file字段,用于指定该区域映射的文件(如果有的话),不映射文件的VMA是匿名的。

程序的VMAs存储在它的内存描述符中,既作为链表中的mmap字段,也作为红黑树的根,位于mm_rb字段。红黑树允许内核快速搜索覆盖给定虚拟地址的内存区域。当您读取文件/proc/pid_of_process/maps时,内核只是遍历进程的VMAs链表并打印每个VMAs。

4GB虚拟地址空间被划分为多个页面。32位模式下的x86处理器支持4KB、2MB和4MB的页面大小。Linux和Windows都使用4KB页面映射虚拟地址空间的用户部分。0-4095字节位于第0页,4096-8191字节位于第1页,以此类推。VMA的大小必须是页面大小的倍数。下图是页面大小为4KB的3GB用户空间。

479cb9e91335

image.png

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

479cb9e91335

image.png

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

位D和位A表示脏的和可访问的。脏页面表示这个页面已经发生了写操作,而可访问表示这个页面具有写操作或读操作权限。这两个标志都是粘滞的:处理器只设置它们,它们必须由内核清除。最后,PTE存储与此页面相对应的起始物理地址,对齐到4KB。这个看起来很天真的字段是一些痛苦的根源,因为它将可寻址物理内存限制为4GB。其他PTE字段用于物理地址的扩展。

虚拟内存不存储任何东西,它只是将程序的地址空间映射到底层物理内存上,处理器将这些物理内存作为一个称为物理地址空间的大块访问。虽然总线上的内存操作有点复杂,但是我们可以在这里忽略它,并假设物理地址以1字节增量的形式从0到可用内存的顶部。这个物理地址空间被内核分解成页面帧(Frame)。处理器不知道或不关心帧,但是它们对内核非常重要,因为页帧是物理内存管理的单元。Linux和Windows在32位模式下都使用4KB页帧;下面是一个2GB内存的机器示例:

479cb9e91335

image.png

让我们将虚拟内存区域、页表和页帧放在一起,以理解这一切是如何工作的。下面是一个用户堆的例子:

479cb9e91335

image.png

蓝色矩形表示VMA范围内的页面,箭头表示将页面映射到页面帧的页面表项。一些虚拟页面缺少箭头;这意味着它们对应的PTE将P位标志清除。这可能是因为这些页面从未被使用过,或者是因为它们的内容被交换了出来。无论在哪种情况下,对这些页面的访问都将导致页面错误,即使它们位于VMA中。VMA和页表不一致似乎很奇怪,但这种情况经常发生

VMA就像程序和内核之间的契约。当你请求做某事(分配内存、映射文件等)时,内核会说“当然”,然后它会创建或更新适当的VMA。但是它实际上不会立即执行请求,而是等到页面错误发生时才执行真正的工作,这是虚拟内存的基本原理,VMAs记录了已经分配的虚拟内存,而PTE反映的是内核对虚拟内存实际做了什么。这两种数据结构一起管理程序的内存;它们都在解决页面错误、释放内存、交换内存等等方面发挥作用。让我们以内存分配的简单情况为例:

479cb9e91335

image.png

当程序通过brk()系统调用请求更多内存时,内核只需更新堆的VMA。如上图所示,此时没有实际分配页帧,并且新的页不在物理内存中。一旦程序尝试访问页面,处理器就会发生页面错误并且调用do_page_fault()。它将使用find_vma()搜索报错的虚拟地址的VMA。如果找到,检查VMA上的权限。如果没有合适的VMA,进程将发生段错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值