在多任务操作系统中的每一个进程都运行在一个属于它自己的内存沙盘中。这个沙盘就是虚拟地址空间(virtual address space)。

1 32位虚拟内存布局

在32位模式下虚拟地址空间总是一个4GB的内存地址块。这些虚拟地址通过页表(page table)映射到物理内存,页表由操作系统维护并被处理器引用。每一个进程拥有一套属于它自己的页表,但是还有一个隐情。只要虚拟地址被使用,那么它就会作用于这台机器上运行的所有软件,包括内核本身。因此一部分虚拟地址必须保留给内核使用:

图 1

这并不意味着内核使用了那么多的物理内存,仅表示它可支配这么大的地址空间,可根据内核需要,将其映射到物理内存。内核空间在页表中拥有较高的特权级(ring 2或以下),因此只要用户态的程序试图访问这些页,就会导致一个页错误(page fault),用户程序不可访问内核页。在Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址的,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化:

图 2

图2中,蓝色区域表示映射到物理内存的虚拟地址,而白色区域表示未映射的部分。在上面的例子中,Firefox使用了相当多的虚拟地址空间,因为它是传说中的吃内存大户。地址空间中的各个条带对应于不同的内存段(memory segment),如:堆、栈之类的。记住,这些段只是简单的内存地址范围,与Intel处理器的段没有关系。

1.1 32位经典内存布局

图 3

32位经典内存布局,程序起始1GB地址为内核空间,接下来是向下增长的栈空间和由0×40000000向上增长的mmap地址。而堆地址是从底部开始,去除ELF、代码段、数据段、常量段之后的地址并向上增长。但是这种布局有几个问题,首先是容易遭受溢出攻击;其次是,堆地址空间只有不到1G有木有?如果mmap内存比较少地址很浪费有木有?所以后来就有了另一种内存布局

1.2 32位默认内存布局

图 4

当计算机开心、安全、可爱、正常的运转时,几乎每一个进程的各个段的起始虚拟地址都与图4完全一致,这也给远程发掘程序安全漏洞打开了方便之门。一个发掘过程往往需要引用绝对内存地址:栈地址,库函数地址等。远程攻击者必须依赖地址空间布局的一致性,摸索着选择这些地址。如果让他们猜个正着,有人就会被整了。因此,地址空间的随机排布方式逐渐流行起来。Linux通过对栈、内存映射段、堆的起始地址加上随机的偏移量来打乱布局。不幸的是,32位地址空间相当紧凑,给随机化所留下的空当不大,削弱了这种技巧的效果。

进程地址空间中最顶部的段是栈,大多数编程语言将之用于存储局部变量和函数参数。调用一个方法或函数会将一个新的栈桢(stack frame)压入栈中。栈桢在函数返回时被清理。也许是因为数据严格的遵从LIFO的顺序,这个简单的设计意味着不必使用复杂的数据结构来追踪栈的内容,只需要一个简单的指针指向栈的顶端即可。因此压栈(pushing)和退栈(popping)过程非常迅速、准确。另外,持续的重用栈空间有助于使活跃的栈内存保持在CPU缓存中,从而加速访问。进程中的每一个线程都有属于自己的栈。

通过不断向栈中压入的数据,超出其容量就有会耗尽栈所对应的内存区域。这将触发一个页故障(page fault),并被Linux的expand_stack()处理,它会调用acct_stack_growth()来检查是否还有合适的地方用于栈的增长。如果栈的大小低于RLIMIT_STACK(通常是8MB),那么一般情况下栈会被加长,程序继续愉快的运行,感觉不到发生了什么事情。这是一种将栈扩展至所需大小的常规机制。然而,如果达到了最大的栈空间大小,就会栈溢出(stack overflow),程序收到一个段错误(Segmentation Fault)。当映射了的栈区域扩展到所需的大小后,它就不会再收缩回去,即使栈不那么满了。这就好比联邦预算,它总是在增长的。

动态栈增长是唯一一种访问未映射内存区域(图中白色区域)而被允许的情形。其它任何对未映射内存区域的访问都会触发页故障,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。

内存映射段

在栈的下方,是我们的内存映射段。此处,内核将文件的内容直接映射到内存。任何应用程序都可以通过Linux的mmap()系统调用(实现)或Windows的CreateFileMapping()/ MapViewOfFile()请求这种映射。内存映射是一种方便高效的文件I/O方式,所以它被用于加载动态库。创建一个不对应于任何文件的匿名内存映射也是可能的,此方法用于存放程序的数据。在Linux中,如果你通过malloc()请求一大块内存,C运行库将会创建这样一个匿名映射而不是使用堆内存。‘大块’意味着比MMAP_THRESHOLD还大,缺省是128KB,可以通过mallopt()调整。

说到堆,它是接下来的一块地址空间。与栈一样,堆用于运行时内存分配;但不同点是,堆用于存储那些生存期与函数调用无关的数据。大部分语言都提供了堆管理功能。因此,满足内存请求就成了语言运行时库及内核共同的任务。在C语言中,堆分配的接口是malloc()系列函数,而在具有垃圾收集功能的语言(如C#)中,此接口是new关键字。

如果堆中有足够的空间来满足内存请求,它就可以被语言运行时库处理而不需要内核参与。否则,堆会被扩大,通过brk()系统调用(实现)来分配请求所需的内存块。堆管理是很复杂的,需要精细的算法,应付我们程序中杂乱的分配模式,优化速度和内存使用效率。处理一个堆请求所需的时间会大幅度的变动。实时系统通过特殊目的分配器来解决这个问题。堆也可能会变得零零碎碎,如下图所示:

图 5

BSS 数据段 代码段

最后,我们来看看最底部的内存段:BSS,数据段,代码段。在C语言中,BSS和数据段保存的都是静态(全局)变量的内容。区别在于BSS保存的是未被初始化的静态变量内容,它们的值不是直接在程序的源代码中设定的。BSS内存区域是匿名的:它不映射到任何文件。如果你写static int cntActiveUsers,则cntActiveUsers的内容就会保存在BSS中。

另一方面,数据段保存在源代码中已经初始化了的静态变量内容。这个内存区域不是匿名的。它映射了一部分的程序二进制镜像,也就是源代码中指定了初始值的静态变量。所以,如果你写static int cntWorkerBees = 10,则cntWorkerBees的内容就保存在数据段中了,而且初始值为10。尽管数据段映射了一个文件,但它是一个私有内存映射,这意味着更改此处的内存不会影响到被映射的文件。也必须如此,否则给全局变量赋值将会改动你硬盘上的二进制镜像,这是不可想象的。

下图中数据段的例子更加复杂,因为它用了一个指针。在此情况下,指针gonzo(4字节内存地址)本身的值保存在数据段中。而它所指向的实际字符串则不在这里。这个字符串保存在代码段中,代码段是只读的,保存了你全部的代码外加零零碎碎的东西,比如字符串字面值。代码段将你的二进制文件也映射到了内存中,但对此区域的写操作都会使你的程序收到段错误。这有助于防范指针错误,虽然不像在C语言编程时就注意防范来得那么有效。下图展示了这些段以及我们例子中的变量:

图 6

你可以通过阅读文件/proc/pid_of_process/maps来检验一个Linux进程中的内存区域。记住一个段可能包含许多区域。比如,每个内存映射文件在mmap段中都有属于自己的区域,动态库拥有类似BSS和数据段的额外区域。下一篇文章讲说明这些“区域”(area)的真正含义。有时人们提到“数据段”,指的就是全部的数据段+ BSS + 堆。

2 64位虚拟内存布局

64位系统的寻址空间比较大,所以仍然沿用了32位的经典布局,但是加上了随机的mmap起始地址,以防止溢出攻击。反正一时半会是用不了这么大的内存地址了,所以至少N多年不会变了。

首先, 目前大部分的操作系统和应用程序并不需要16EB( 264 )如此巨大的地址空间, 实现64位长的地址只会增加系统的复杂度和地址转换的成本, 带不来任何好处. 所以目前的x86-64架构CPU都遵循AMD的Canonical form, 即只有虚拟地址的最低48位才会在地址转换时被使用, 且任何虚拟地址的48位至63位必须与47位一致(sign extension). 也就是说, 总的虚拟地址空间为256TB( 248 ).


图 7

然后, 在这256TB的虚拟内存空间中, 0000000000000000 - 00007fffffffffff(128TB)为用户空间, ffff800000000000 - ffffffffffffffff(128TB)为内核空间. 这里需要注意的是, 内核空间中有很多空洞, 越过第一个空洞后, ffff880000000000 - ffffc7ffffffffff(64TB)才是直接映射物理内存的区域, 也就是说默认的PAGE_OFFSET为ffff880000000000. 从这里我们也可以看出, 这么大的直接映射区域足够映射所有的物理内存, 所以目前x86-64架构下是不存在高端内存, 也就是ZONE_HIGHMEM这个区域的(参考这篇).

它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。其实这一章还是讲解虚拟内存的,也就是和内核中的内存管理相关。

  • 它为每个进程提供了一致的地址空间,从而简化了存储器管理

  • 它保护了每个进程的地址空间不被其他进程破坏。

  • 在硬件异常、汇编器、链接器、加载器、共享对象、文件和进程的设计中扮演着重要角色。

  • 存储器让应用程序有了强大的能力,可以创建和销毁存储器片、将存储器片映射到磁盘文件的某个部分,以及与其他进程共享存储器。

一、物理和虚拟寻址

    现在都开始虚拟地址,使用虚拟地址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫做地址翻译。就像异常处理一样,地址翻译需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做存储器管理单元的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容是由操作系统管理的。

二、地址空间

    地址空间时一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间。CPU从一个有N=2n个地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间。一个地址空间的大小事由表示最大地址所需要的位数来描述的。一个包含N=2n个地址的虚拟地址空间就叫做一个n位地址空间。一个系统还有一个物理地址空间,它与系统中物理存储器的字节数目相对应。主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地。

三、虚拟存储器作为缓存的工具

    虚拟存储器被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,这个唯一的虚拟地址是作为到数组的索引的。磁盘上数据的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传输单元。

四、虚拟存储器作为存储器管理的工具

    操作系统为每个进程提供了一个单独的页表,因而也就是一个独立的虚拟地址空间。在下图中,进程I的页表VP1映射到PP2,VP2映射到PP7。相似地,进程J的页表将VP1映射到PP7,VP2映射到PP0。多个虚拟页面可以映射到同一个共享物理页面上。

将一组连续的虚拟页映射到任意一个文件中的任意位置的表示法称作存储器映射。

五、虚拟存储器作为存储器保护的工具

六、地址翻译

    实现翻译的过程,如何利用页表来实现这种映射。CPU中的一个控制寄存器,页表基址寄存器(PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移和一个(n-p)位的虚拟页号。MMU利用VPN来选择适当的PTE(页表条目,一个页表其实就是多个页表条目的数组)。页面命中,CPU硬件执行的步骤:

  • 处理器生成一个虚拟地址,并把它传送给MMU
  • MMU生成PTE地址,并从高速缓存/主存请求得到它
  • 高速缓存/主存向MMU返回PTE
  • MMU构造物理地址,并把它传送给高速缓存/主存
  • 高速缓存/主存返回所请求的数据字给处理器

    页面命中完全是由硬件来处理的,与之不同的是,处理缺页要求硬件和操作系统内核协作完成:

  • 前三步都是一样的
  • PTE中的有效位为0,所以MMU触发了一次异常,传递CPU中的控制到操系统内核中的缺页异常处理程序。
  • 缺页处理程序确定出物理存储器中的牺牲页,如果这个页面被修改了,则把它换出到磁盘。
  • 缺页处理程序页面调入新的页面,并更新存储器中的PTE
  • 缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU

    上面是前三步的过程,在页面命中的时候进行的处理。如果页面没有命中。

整体过程将是上图所示。最后还是处理器再次发送虚拟地址给MMU,不过这次不会导致缺页异常。

ps:由上图可知,虚拟页偏移量(VPO)和物理页偏移量(PPO)大小是相等的,因为虚拟页偏移量就是虚拟页的大小,对于给定的虚拟地址位数和物理地址位数就可以构造各个量的大小。比如32位虚拟地址空间和24为物理地址,对于大小为1KB的页面。那么VPO和PPO的大小为10位,22位的虚拟页号,物理页号为12位。

    但是在现在的系统中,都是结合高速缓存和虚拟存储器一起使用的,大多数系统都是选择物理地址来访问高速缓存。

    每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这又会要求从存储器取一次数据,代价是几十到几百个周期。如果PTE碰巧在L1中,那么开销下降到1个或2个周期,试图先去这种开销,他们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB),它是一个小的。虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。

下面看看在TLB命中和不命中的情况下都是什么处理流程:

    CPU产生一个虚拟地址

    MMU从TLB中取出相应的PTE

    MMU将这个虚拟地址翻译成一个屋里地址,并且将它发送到高速缓冲/主存

    高速缓存/主存将所请求的数据字返回给CPU。

    如果TLB不命中时,MMU必须从L1缓存中取出相应的PTE,新取出的PTE存放在TLB中,可能覆盖一个已经存在的条目。

    多级页表

    如果我们有一个32位的地址空间,4KB的页面和一个4字节的PTE(就按照这样的说法,如果4KB的页面,那么虚拟页面的偏移位需要12位),那么即使应用所引用的只是虚拟地址空间中的很小的一部分,页总是需要一个4MB的页表驻留在存储器中(为啥需要4MB的空间呢,因为32位地址中间,4KB的页面,可能会用1MB的页面,PTE中存储的就是页面的地址,那么就是1MB个地址,每个地址需要4字节,那么需要4MB的空间存储一个地址空间)。

    常用的方法就是压缩页表,使用层次结构的页表,假设32位虚拟地址空间被分为4KB的页,而每个页表条目都是4字节。还假设在这一时刻,虚拟地址空间有如下形式:存储器的前2K个页面分配给了代码和数据,接下来的6K个页面还未分配,再接下来的1023个页面页未分配,接来下的1个页面分配给了用户栈。

    一级页表的每个PTE负责映射虚拟地址空间中一个4MB的片,这里每一片都是由1024个连续的页面组成的。假设地址空间为4GB,1024个PTE已经足够覆盖整个空间了。

    如果偏i中的每个页面都未被分配,那么一级PTEi就为空,上图中的片2~7是未被分配的。如果在片i中至少有一个页是分配了的,那么一级PTEi就指向一个二级页表的基址。上图中的片0、1、8的所有或则部分已被分配,所以他们的一级PTE就指向二级页表。

    二级页表中的每个PTE都负责映射一个4KB的虚拟存储器页面,就像我们查看一级页表一样。注意,使用4字节的PTE,每个一级和二级页表都是4KB字节,这刚好和一个页面的大小一样的(为啥呢?4字节的PTE,一个页表有1024个页表项,所以一个页表就是4KB)。

    如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在,这代表这一种巨大的潜在节约;只有一级页表才需要总是存在主存中;虚拟存储器系统可以在需要时创建、页面调入或调出二级页表,这就减少了主存的压力;只有最经常使用的二级页表才需要缓存在主存中。

    上图展示了使用k级页表层次结构的地址翻译。虚拟地址被划分为k个VPN和1个VPO,每个VPNi都是一个到第i级页表的索引,其中1<=i&lt;=k。第k级页表中的每个PTE包含某个物理页面的PPN或一个磁盘块的地址。在能够确定PPN之前,MMU必须访问K个PTE。对于只有一级的页表结构,PPO和VPO是相同的。

    访问k个PTE,第一眼看上去昂贵不切实际,这TLB能够起作用,是通过将不同层次上页表的PTE缓存起来。实际上,带多级页表的地址翻译并不比单级页表的慢很多。

七、存储器映射

    linux虚拟存储器系统的存储映射问题:

    linux为每个进程维护一个单独的虚拟地址空间。

    内核虚拟存储器包含内核中的代码和数据结构。内核虚拟存储器的某些区域被映射到所有进程共享的物理页面。例如,每个进程共享内核的代码和全局数据结构。linux页将一组连续的虚拟页面映射到相应的一组连续的物理页面。这就为内核提供了一种遍历的方法来访问物理存储器中任何特定的位置,例如,当它需要访问页表,或在一些设备上执行存储器映射的I/O操作,而这些设备被映射到特定的物理储存器位置时。

    内核虚拟存储器的其他区域包含每个进程都不相同的数据。例如,页表,内核在进程的上下文中执行代码时使用的栈,以及记录虚拟地址空间当前组织的各种数据结构。

    上图强调了记录一个进程中虚拟存储器区域的内核数据结构。内核为系统中的每个进程维护一个单独的任务结构(源代码中的task_struct)。任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(例如,PID,指向用户栈的指针、可执行目标文件的名字以及程序计数器)

    task_struct中的一个条码指向mm_struct,它描述了虚拟存储器的当前状态。其中pgd指向第一级页表(页全局目录)的基址。而mmap指向一个vm_area_structs的链表,其中每个vm_area_structs都描述了当前虚拟地址空间的一个区域。当内核运行这个进程时,它就将pgdf存放在CR3控制寄存器中。

  • vm_start:指向这个区域的起始处
  • vm_end:指向这个区域的结束处
  • vm_port:描述这个区域内包含的所有页的读写徐和权限
  • vm_flags:描述这个区域内的页面是与其他进程共享的,还是这个进程私有的
  • vm_next:指向链表中下一个区域结构。

 ps:虚拟存储器是对主存的一个抽象。支持虚拟存储器的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址翻译要求硬件和软件紧密合作。专门的硬件通过使用页表来翻译虚拟地址,页表的内容是由操作系统提供的。

内存管理之程序内存分布相关推荐

  1. linux 物理内存用完了_12张图解Linux内存管理,程序员内功修炼,看过都说懂了!...

    本文已收录Github:imcoderlemon/CodeClass从小白到架构师,关于编程所有你需要掌握的内容都在这里 今天来带大家研究一下Linux内存管理.对于精通 CURD 的业务同学,内存管 ...

  2. 操作系统4小时速成:内存管理,程序执行过程,扩充内存,连续分配,非连续分配,虚拟内存,页面替换算法

    操作系统4小时速成:内存管理,程序执行过程,扩充内存,连续分配,非连续分配,虚拟内存,页面替换算法 2022找工作是学历.能力和运气的超强结合体,遇到寒冬,大厂不招人,可能很多算法学生都得去找开发,测 ...

  3. JVM自动内存管理机制——Java内存区域(下)

    一.虚拟机参数配置 在上一篇<Java自动内存管理机制--Java内存区域(上)>中介绍了有关的基础知识,这一篇主要是通过一些示例来了解有关虚拟机参数的配置. 1.Java堆参数设置 a) ...

  4. 属性与内存管理(属性与内存管理都是相互关联的)

    <span style="font-size:18px;"> 属性与内存管理(属性与内存管理都是相互关联的)第一部分一,属性:属性是OC2.0之后出来的新语法,用来取代 ...

  5. Java内存管理:Java内存区域 JVM运行时数据区

    Java内存管理:Java内存区域 JVM运行时数据区 在前面的一些文章了解到javac编译的大体过程.Class文件结构.以及JVM字节码指令. 下面我们详细了解Java内存区域:先说明JVM规范定 ...

  6. C++:内存管理:C++内存管理详解

    C++语言内存管理是指:对系统的分配.创建.使用这一系列操作.在内存管理中,由于是操作系统内存,使用不当会造成很麻烦的后果.本文将从系统内存的分配.创建出发,并且结合例子来说明内存管理不当会造成的结果 ...

  7. 鸿蒙系统内存管理,嵌入式系统内存管理-鸿蒙HarmonyOS技术社区-鸿蒙官方战略合作伙伴-51CTO.COM...

    1.概述 操作系统的内存管理功能用于向操作系统提供一致的地址映射功能和内存页面的申请.释放操作.在嵌入式实时系统中,内存管理根据不同的系统,有不同的策略,对于有些系统支持的虚拟内存管理机制,对于另外一 ...

  8. 【C 语言必知必会】内存管理、动态分配内存、野指针

    C 语言内存管理.动态分配内存.野指针 文章目录 C 语言内存管理.动态分配内存.野指针 前言: 1.内存分区 1.1 代码区 1.2.1 全局初始化数据区(静态数据区data段) 1.2.2 未初始 ...

  9. java 内存管理 —— 《Hotspot内存管理白皮书》

    说明   要学习Java或者任意一门技术,我觉得最好的是从官网的资料开始学习.官网所给出的资料总是最权威最知道来龙去脉的.而Java中间,垃圾回收与内存管理是Java中非常重要的一部分.<Hot ...

最新文章

  1. YSlow简介与使用(转)
  2. SGM:Sequence Generation Model for Multi-Label Classification(SGM)
  3. 北大OJ百练——4073:最长公共字符串后缀(C语言)
  4. QML 界面切换的几种方法
  5. c# 操作oracle数据库,C#连接oracle数据库增删改查实例
  6. maven项目使用jacoco插件检测代码覆盖率详细配置
  7. js substring和substr的区别实例,一目了然
  8. 怎样做一个卓越的程序员
  9. 调查问卷java源码_2020年Java技术趋势
  10. oracle分析函数大全非常详细
  11. 软件开发模型2:增量模型/螺旋模型/敏捷模型
  12. 三极管何时工作在饱和状态
  13. 管仲(约前723年-前645年)
  14. 蒲丰投针实验原理_神奇的圆周率——布丰投针实验
  15. gentoo mysql_gentoo
  16. Kafka分区分配策略以及重平衡过程总结
  17. OSPF单区域配置实验
  18. C++中使用placement new
  19. python模拟投掷色子并做出数据可视化统计图
  20. 阿里云ECS服务器+WordPress快速搭建个人博客

热门文章

  1. jQuery中attr和prop方法的区别
  2. 编译预处理 -- 带参数的宏定义--【原创】
  3. 硬回车与软回车[转]
  4. 自定义Title(可以实现类似于携程网上价格的显示方式)
  5. iOS tabBarController 初始化就执行了 viewDidLoad
  6. 【转载】x86和x64
  7. 09、HTLM中直接写get请求和模板标签分片功能
  8. 联机共享 Windows Live Writer 草稿
  9. Hadoop日记Day7---HDFS的WED端口
  10. rsync备份之windows+linux