Linux | 虚拟地址空间

文章详细阐述了程序执行过程中,从直接访问到虚拟地址空间的变化,重点讲解了段基址+段偏移地址、段选择子+段偏移地址以及现代的虚拟地址机制。还涉及了页表、物理地址、分页和分段的概念,展示了虚拟内存如何提供更大的地址空间和内存保护。
摘要由CSDN通过智能技术生成

目录

直接访问

段基址+段偏移地址

段选择子+段偏移地址

虚拟地址

虚拟地址空间

进程虚拟地址空间

页表

Page

总结


承接上文,程序经过编译后,变成了可执行的文件,可执行文件主要包括代码和数据两部分,代码是只读的,数据则是可读可写的。可执行文件由操作系统加载到内存中,交由CPU去执行,现在问题来了,CPU怎么去访问代码和数据?,访问的方式经历过四个阶段:

1.直接访问

2.段基址+段偏移地址

3.段选择子+段偏移地址

4.虚拟地址

现代操作系统采用的是虚拟地址,这也是本篇文章阐述的重点,但虚拟地址是由1~3阶段发展而来的,所以也有必要阐述1~3三种访问方式。

直接访问

直接访问很好理解,程序经过编译后,生成了可执行文件,编译器给每行数据和代码分配了一个唯一的地址,如下图

如上图可执行文件中1000~1024之间的地址,加载到内存后,内存的地址也是1000~1024,在可执行文件中分配的唯一地址就是内存中的物理地址,这就叫直接访问,直接定访问干脆直接,没有那些弯弯绕绕。

当时问题也不少,例如同一个可执行文件不能同时执行,它们的物理地址一样,冲突了,必须一个接一个,还有就是可执行文件的物理地址已经固定了,如果想在其它物理地址运行,必须地重新编译,生成新的物理地址。

可见直接定位是计算机发展早期的产物,早期没有那么多的程序要运行,程序都是一个接一个地去执行的,因此早期这种定位比较简单,直接高效。

段基址+段偏移地址

随着多任务需求的来临,现在内存中要并发运行多个程序,虽然采用直接定位把每个不同的程序放在不同的内存段中,勉强可以满足,但是一个可执行文件不能同时运行多个,另外程序必须在固定的物理地址运行,灵活性大大减弱,调度起来也是非常麻烦,因此CPU设计师和操作系统开发人员发明了段基址+段偏移地址

Inter 8086处理器就是采用这种定位方式,我们知道可执行文件主要分为数据段内存段,如下图

由上图红色部分可知,0,4,8就是相对于数据段的偏移地址,0,4,8,12是相对于代码段的偏移地址。

在可执行文件中,一个段的偏移地址是固定的,无论可执行文件加载到内存的什么位置,这个偏移地址是固定的。

当可执行文件加载到内存时,先在内存中分配一个数据段和代码段,这两个段理论上可以不挨着,一般情况下,代码段和数据段是挨着的,代码段和数据段在内存中都有一个起始地址,这个起始地址就叫做段基址,这个段基址是放在段寄存器里,例如代码段基址放在CS寄存器,数据段基址放在DS寄存器,当然还有其他的段例如栈段,栈段刚开始大小为0,随着程序的运行入栈,出栈,这个栈段在不断扩展,当然,咋们主要说的是数据段和代码段,栈段只是简单带过。

假设可执行文件被加载到了内存中,如下图

如上图所示,代码段被布局到以0x00600000为起始地址的内存中,数据段被布局到以0x00601000为起始地址的内存中。

当CPU开始执行代码段的第一条指令时,会将代码段的起始地址放入到段寄存器中,此时CS代码段寄存器中存储的就是0x00600000,然后开始从起始地址处开始执行第一条代码指令,此时把代码指令的偏移地址放入到IP寄存器中,IP寄存器存储的就是0,所以CPU要定位一条代码指令时通过CS:IP的方式定位的,如下图所示

当CPU执行到0x00600000处的代码指令时,该指令为MOV AX,[0],该指令的意思是把地址0处的数据存储到AX寄存器,这个0就是数据段的偏移地址,此时CPU会将数据段的起始地址加入到DS段寄存器中,然后将数据段寄存器的值+偏移地址即0x00601000+0=0x00601000定位到了数据123,然后将123存储到AX寄存器中。

上述过程就是【段基址+段偏移地址】的定位方式,之所以把起始地址加入到寄存器中,也是为了后续再执行指令或者获取数据时,可以直接从寄存器获取,加快CPU执行的速度。

段选择子+段偏移地址

【段选择子+段偏移地址】与【段基址+段偏移地址】有些相似之处,之所以采用【段选择子+段偏移地址】主要是为了安全,原来的【段基址+段偏移地址】方式,程序员可以直接跳转到其他代码段和数据段,没有任何限制,安全性全依赖于程序员的职业操守和水平,因此CPU设计者就发明了【段选择子+段偏移地址】。

段选择子+段偏移地址】中的段选择子可以认为是一个索引,这个索引指向了全局段描述符表中的一项,全局段描述表存储在内存中,它的起始地址存储在全局段描述符寄存器中。

全局段描述符表有很多个段描述符,每个段描述占用8个字节,这个段描述符里面就包括了段基址,另外还有一些安全性相关的描述信息例如段的可读,可写,可执行,段的大小等。

段选择子存储在了段寄存器中,总共16位,其中高13位就是全局段描述表的索引。

当CPU开始执行代码段的第一条指令时,会将代码段的选择子放入到CS段寄存器中,然后CPU从段寄存器中的获取段选择子,然后截取选择子的高13位获取索引,然后根据全局描述符表寄存器的地址找到全局描述符表的起始地址,根据起始地址+索引*8找到段描述符,然后根据段描述符获取段的基址,段的基址加上ip寄存器中的偏移地址就是指令的物理地址,如下图所示1~6步骤所示

当CPU执行到0x00600000处的代码指令时,该指令为MOV AX,[0],该指令的意思是把地址0处的数据存储到AX寄存器,这个0就是数据段的偏移地址,此时CPU会将数据段的选择子加入到DS段寄存器中,然后CPU获取段选择的高13位获取索引,然后根据全局描述符表寄存器的地址找到全局描述符表的起始地址,根据起始地址+索引*8找到段描述符,然后根据段描述符获取段的基址,段的基址加上数据段的偏移地址就是数据的物理地址,如下图1~6步骤所示

上述过程就是段选择子+段偏移地址】的定位方式。

虚拟地址

现代的操作系统和CPU未打开分页时采用的是【段选择子+段偏移地址】访问代码和数据,而一旦打开分页时,经过【段选择子+段偏移地址】得到的地址不再是物理地址了,而是叫做虚拟地址,默认则是打开分页的。

现代的操作系统和CPU采用的平坦模型,平坦模型就是整个内存就一个段,因此段基址就是0,段偏移地址就等于虚拟地址了。

虚拟地址空间

虚拟地址空间是虚拟地址的集合,假设虚拟地址空间是N位的,它的地址范围为{0~2的N次方-1}即它有2的N次方个虚拟地址,例如16位的虚拟地址空间,它的地址范围为{0~65535},这意味着16位的虚拟地址空间有65536个虚拟地址。

物理地址空间是物理地址的集合,假设物理地址空间有M个字节,它的地址范围为{0~M-1},M不一定是2的多少次幂,例如M=100,表示物理地址空间大小为100个字节,它的地址范围为{0~99},通常情况下物理地址空间是2的幂次方,例如65536,这也是为了计算机方便处理而已,并不是强制要求的。

物理内存可以认为是一个的物理字节数组,每个物理地址指向这个物理字节数组中的一项。

虚拟内存也一样,它也可以认为是一个物理字节数组,不过这个字节数组是存储在磁盘上。

物理地址空间是物理内存的范围,虚拟地址空间是虚拟内存的范围,物理地址空间中的每个物理地址都是实打实地指向了具体的存储单元,虚拟地址空间中每个虚拟地址指向哪里有3种情况:

a.未分配:这个虚拟地址仅仅是个数字而已,没有任何指向。

b.未缓冲:这个虚拟地址指向了磁盘的某个字节存储单元,里面存储了指令或者数据。

c.已缓冲:这个虚拟地址指向了物理内存的某个字节存储单元,里面存储了指令或者数据。

进程虚拟地址空间

操作系统加载可执行文件后,创建了一个进程,这个进程就有了自己的虚拟地址空间,每个进程的虚拟地址空间都一样,如下图所示

  1. 代码段(Text Segment):存放程序的指令,通常是只读的。

  2. 数据段(Data Segment):存放程序的全局变量和静态变量,通常是可读写的。

  3. (Heap):用于动态分配内存,例如通过malloc或new等函数申请的内存。堆是可变大小的,可以动态扩展和收缩。

  4. (Stack):用于存放函数调用的局部变量、函数参数和返回地址等。栈是自动管理的,每个函数调用都会在栈上分配一块内存,函数返回后会自动释放。

  5. 共享库区域(Shared Libraries):存放共享库的代码和数据,多个进程可以共享同一个共享库的实例,以节省内存空间。

  6. 内核区域(Kernel):这个区域是操作系统自己代码,数据,栈空间,内核在物理内存中只存储一份,每个进程将这个区域的虚拟地址映射到同一份内核物理内存上。

页表

进程的虚拟地址空间在内存中是通过页表来保存的。每个进程都有自己的页表,用于将虚拟地址映射到物理地址。如下图

当我们双击打开一个进程时,进程的pcb从磁盘中加载到内存的内核空间中,此时无论进程是否运行或者阻塞,进程的pcb都存在内核空间中,当运行时,cpu通过pcb找到mm_struct 再通过页表映射找到物理内存地址空间

页表是一种数据结构,用于记录虚拟地址与物理地址之间的映射关系。它通常由多级页表组成,每一级页表都有自己的页表项。页表项记录了虚拟页号与物理页号之间的对应关系。

Page

现代操作操作和CPU将物理内存按照固定的页大小分成很多份,每一份叫做物理页(PP),每一份有一个编号叫做物理页号(PPN),这个物理页大小通常是4KB,例如一个物理内存大小为20KB,这个物理内存可以分成5个物理页,那么物理页号(PPN)就是0,1,2,3,4。

虚拟内存也一样,它的页大小与物理内存的页大小相同,虚拟内存也被分成了很多份,每一份叫做虚拟页(VP),每一份的编号叫做虚拟页号(VPN),例如假设虚拟页大小为4KB,一个虚拟内存大小为10KB,这个虚拟内存可以分成2个虚拟页(VP),虚拟页号(VPN)就是0,1

每个物理页存储在物理内存上,每个虚拟页存储在磁盘上,如下图所示

上图的虚拟内存有8个虚拟页,物理内存有6个物理内存页,虚拟页存储在磁盘上,物理页则存储在DRARM上。

每个虚拟页可以有三种状态,未分配,已缓冲,未缓冲

未分配:虚拟页还没有分配磁盘空间

已缓冲:虚拟页缓冲或者映射在了物理页上。

未缓冲:虚拟页分配了磁盘空间,但没有在物理页上缓冲。

通常操作系统加载可执行文件后,创建了一个进程,这个进程就有了虚拟地址空间,这并不意味着可执行文件已经从磁盘加载到内存中了,操作系统只是为了进程虚拟地址空间的每个区域分配了虚拟页。

代码和数据区域的虚拟页被分配到了可执行文件的适当位置,此时虚拟页状态为未缓冲,虚拟页指向了磁盘地址。

操作系统和共享库的虚拟页被映射到了物理内存,因为操作系统和共享库已经在物理内存了,这些虚拟页的状态为已缓冲。

用户栈,运行时堆的虚拟页没有任何分配,不占用任何空间,这些虚拟页的状态为未分配。

在操作系统中,我们把物理内存划分的若干个4KB的单位称为页框.除了物理内存之外, 磁盘中的程序在编译的时候, 也是按照4KB为单位划分好的. 程序中的4KB单位被称为页帧

虽然页表是进程地址空间和物理内存之间的相互映射,但是实际上, 页表并不是只有简单的两栏,我们把整个页表结构抽象出来用下图表示

页表可以看作, 除了有虚拟地址或物理地址两栏之外, 其实还有其他栏:名中、RWX权限、U/K权限

这三个栏中, 最简单理解的是两个权限:

RWX权限:即为读、写、执行三权限,表示进程对物理内存的访问权限。

我们都知道在代码中,常量数据是不能进行修改的,其原因就是页表对进程做出了限制,实际上物理内存(硬件)是不具备访问控制的,谁都可以读写,而实际情况是我们并不能随意读写,这是页表的功劳

U/K权限:其实是User/Kernel权限,即表示用户和内核,这里就可以区分访问内存的用户权限和内核权限

除了这两个权限之外,还有一栏是 是否命中

是否命中:当CPU需要访问指定内存的数据的时候,会用虚拟地址通过页表向物理内存中查询数据,但是,程序的数据并不是一下子全部加载到物理内存的,即页表中可能不存在指定的物理内存,所以CPU需要访问数据的时候,可能存在第一次在页表中找不到的情况,这种情况我们称为没有命中,反正如果找到了我们称为命中

当CPU访问数据没有命中时,整个进程会从CPU上面拉下来暂时不运行,此时操作系统会将未命中的数据从磁盘中加载到指定的物理内存中(页面置换算法),然后CPU再恢复该进程继续运行。

所以,是否名字这一栏其实是表示此次CPU访问数据是否在物理内存中找到了

实际上我们的页表并非一张,我们以32位环境(进程地址空间和物理内存最大都为4GB)为例:

如果我们使用一级页表(即使用一张页表),页表的一行并不只存储一个地址(32位环境,一个地址的大小为4字节),至少要存储两个地址,那么一行条目大小就是8字节。那么要存储着2^32个地址,我们的页表需要2^21 * 8 = 34,359,738,368 字节 ,一共是32GB,而我们32位环境下的物理内存最大才4GB.很明显,以一级页表来将虚拟内存对应的物理内存全部映射到,是不切合实际的。

所以,页表实际在操作系统中是以多级页表的形式存在的,在32位系统中,采用两级页表的形式。

我们将32位二进制地址分成了 10 + 10 + 12 的形式。

CPU会分别用这三部分查找到物理内存。

主要是因为页表的设计是这样的,在32位环境下,页表映射的实现使用的是二级页表,情况如下:

首先使用虚拟地址的最高10位,在页目录中找到一个相应的页表,然后再在找到的页表中,找到page的起始地址。虚拟地址的最低12位起到了一个偏移量的作用, 我们称虚拟地址的最后12位为 页内偏移量 我们可以找到 page的起始地址, 在将虚拟地址的最低12位作为偏移量, 就能够找到一个准确的物理地址.


用一张图表示整个流程就是:

那么介绍到这里, 其实针对页表中是否名中这一栏目, 就可以有一个更加具体的理解了.

即, 二级页表中 应该记录指定page的位置, 存储的是null. 出现此情况时, 也就说明了程序的数据并没有加载到某page中, CPU此次查找也就无法找到指定的物理地址, 此时就是未命中. 也就是说, CPU查找物理内存, 其实只关心page是否存在, 不关心内容. 并且, 资源也是以page为单位加载到内存中的.

补充知识:实际上我们在cpu内部还有MMU内存管理单元(Memory Management Unit)以及TLB(Translation Lookaside Buffer)转译后备缓冲区俗称快表技术帮助我们进行虚拟地址到物理地址的转换,有兴趣的读者可以自行了解。

另外在计算机中,内存管理可以通过分页分段的方式

分段是将程序的地址空间划分为若干个逻辑段,每个段可以表示程序的一部分,如代码段、数据段、堆栈段等。每个段都有自己的起始地址和长度,可以独立地进行管理和保护。分段的好处是可以提高内存的利用率,同时也可以提高程序的可重定位性和保护性。

分页是将程序的地址空间划分为固定大小的页,通常为4KB或者8KB。内存也被划分为相同大小的物理页框,每个页框可以存放一页的数据。分页的主要作用是实现虚拟内存,将程序的逻辑地址空间映射到物理内存中。通过分页,程序可以访问比实际物理内存更大的地址空间,同时也实现了内存的保护和共享。

分段和分页可以结合使用,形成分段分页的内存管理模式。在这种模式下,程序的地址空间首先被划分为若干个段,每个段再被划分为若干个页。这样可以同时享受分段和分页的优势,提高内存管理的效率和灵活性。

总结

虚拟内存是计算机操作系统中的一种技术,它允许程序使用比实际物理内存更大的内存空间。虚拟内存通过将部分数据从内存转移到硬盘上的交换文件中,来释放物理内存空间,并提供了更大的可用内存空间。

虚拟内存的工作原理是将内存划分为固定大小的页面(通常为4KB),并将页面映射到物理内存或交换文件中。当程序需要访问一个页面时,操作系统会将该页面加载到物理内存中,如果物理内存已满,则会根据一定的页面置换算法将不常用的页面转移到交换文件中,以腾出空间给新的页面。

虚拟内存的优点包括:

  1. 提供了比物理内存更大的地址空间,允许程序使用更多的内存。
  2. 允许多个程序同时运行,每个程序都可以使用其自己的虚拟内存空间。
  3. 提供了内存保护机制,防止程序越界访问或修改其他程序的内存。

虚拟内存的缺点包括:

  1. 由于需要将数据从硬盘加载到内存中,访问速度较慢。
  2. 需要额外的硬盘空间来存储交换文件。
  3. 页面置换算法的选择会影响系统性能。

本文中部分内容及图片转载于:彻底搞懂虚拟内存,虚拟地址,虚拟地址空间-今日头条 (toutiao.com)

【Linux】页表的深入分析_页表未命中_七月.cc的博客-CSDN博客

以上就是今天要讲的内容,本文介绍了进程虚拟地址空间的相关概念。本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值