计算机原理(四)

来源:互联网 发布:mac office下载 编辑:程序博客网 时间:2024/05/02 04:41

2. 虚拟存储器

 

虚拟存储器我们一般也称为虚拟内存(和Windows中的虚拟内存不是一个概念,但是有关联),它的基本思想是:

  • 每个进程都有自己的地址空间;
  • 每个地址空间被分为多个块,每个块称为页,每个页有连续的地址空间;
  • 这些页被映射到物理内存,但不是所有也都在内存中程序才能运行;
  • 当使用的页不在物理内存中时,由操作系统负责载入相应的页;

 

在实模式下,CPU将偏移地址和段寄存器,基址寄存器等进行计算得到的实际的物理地址。 而在保护模式下,引入了虚拟内存的概念,在虚拟内存中使用的地址称为虚拟地址(线性地址),虚拟地址通过MMU将虚拟地址映射为物理地址,然后送到总线,进行内存访问。这里最关键的就是虚拟地址的映射。

 

 

2.1 分页

 

对于虚拟内存来说,是对物理内存的抽象,整个虚拟内存空间被划分成了多个大小固定的页(page),每个页连续的虚拟地址,组合成了一个完整的虚拟地址空间。同样,操作系统也会把物理内存划分为多个大小固定的块,我们称为页框(page frame),它只是操作系统为了方便管理在逻辑上的划分的一个数据结构,并不存放实际内存数据,但是我们可以简单的认为它就是内存。这样一个虚拟内存的page就可以和一个物理内存的page frame对应起来,产生映射关系。

 

关于一个虚拟页的大小,现在的操作系统中一般是512B-64K(准确的说是地址范围大小,而非容纳数据的大小)。但是内存页的大小会对系统性能产生影响,内存页设得太小,内存页会很多,管理内存页的数组会比较大,耗内存。内存页设大了,因为一个进程拥有的内存是内存页大小的整数倍,会导致碎片,即申请了很多内存,真正用到的只有一点。目前Windows和Linux上默认的内存页面大小都是4K。

 

从上图我们也可以看出,虚拟内存的页和物理内存的页框并不一定是一一对应的,虚拟内存的大小和系统的寻址能力相关,也就是地址线的位数,而物理内存的页框数取决于实际的内存大小。所以可能只有一部分页有对应的页框,而当访问的数据不在物理内存中时就会出现缺页,这个时候操作系统会负责调入新的页,也就是建立新的映射。这样就不需要一次把程序全部加载到内存。

 

 

 

2.1.1 虚拟页是什么?

 

很多人会有一个疑问,虚拟页到底是实际存在的还是虚拟的?我们知道内存中存放的是执行文件的代码和数据,而程序在运行前,它的数据和代码是存放在这个程序的可执行文件中的(比如.exe和.so),而在运行时需要把可执行文件加载到内存。所以我们把这个硬盘上的文件也划分为4K大小的页(逻辑上划分,实际是加载过程中加载器完成的),这就是虚拟页里面实际的东西。但是程序在运行是可能会申请内存,这个时候需要新的虚拟页来映射,所以我们可以得知虚拟页应该有3种状态:

  1. 已映射:虚拟页面被创建已经被加载到物理内存,和物理页之间存在映射关系。
  2. 未映射:虚拟页面被创建,但是没有被加载到内存或已经被调出内存,和物理页面之间没有映射关系,当需要使用时调入内存建立映射。
  3. 未创建:虚拟页面没有被创建,可能是因为还没有访问到此页面所以没有加载或者是调用macllo来分配内存,只有在运行是才会被创建。

 

 

2.1.2 存储器映射

 

加载应用程序到内存时,因为和虚拟地址有关,我们需要把应用程序文件和虚拟内存关联起来,在Linux中称为存储器映射,同时提供了一个mmap的系统调用来实现次功能。文件被分成页面大小的片,每一片包含一个虚拟页面的内容,因为页面调度程序是按需求调度,所以在这些虚拟页面并没有实际的进入内存,而是在CPU发出访问请求时,会创建一个虚拟页并加载到内存。我们在启动一个进程时,加载器为我们完成了文件映射的功能,所以此时我们的执行文件又可以称为映像文件。实际上加载器并不真正负责从磁盘加载数据到内存,因为和虚拟内存建立了映射关系,所以这个操作是虚拟内存自动进行的。 正是有了存储器映射的存在,使得我们可以很方便的将程序和数据加载到内存中。

 

 

2.1.3 交换分区

 

当CPU请求一个虚拟页是,虚拟页会被创建并加载到内存,而页面调度算法可能在页面休眠或在内存满的情况下更具调度算法将虚拟页交换出去,在适当的时候可能被交换回来。这个时候就需要一个区域来存放被交换出来的虚拟页,这个区域称为交换分区。 这个分区在Linux中称为swap分区,而在Windows中我们称为虚拟内存(注意这里和我们谈到的虚拟内存技术不是一回事)。

 

以前电脑内存很小,特别是玩一些游戏时经常会提示内存不足,网上一般会告诉你增大你的虚拟内存(交换分区),这样一来在内存不足的时候可以存放更多交换出来的虚拟页,看起来好像内存变大了一样。从这方面来说Windows把他叫虚拟内存(交换分区)也是很正确的。  交换分区虽然也是硬盘的一部分,但是交换分区没有普通的文件系统,这样消除了将文件偏移转换为页地址的开销。但是过于频繁的交换页面,IO操作会导致系统性能下降。但是在内存不足时可以保证系统 正常运行。当然这也和交换分区的大小有关。

 

而如今,一般使用的电脑都已经4G,8G内存了,对于普通需求来说足够大了。所以虚拟页会长时间存在与内存中而不被交换出去。所以我们可以禁用掉交换分区,以便提高性能。对于Windwos 从Vista开始有一个Superfetch的内存管理机制,而linux有Preload与之类似。这种内存机制会将用户经常用的应用的部分虚拟页提前加载到内存,当用户使用时就无需在从硬盘加载。而当应用休眠或关闭时,也不会将这些虚拟页交换出去。


如下图就是Windows 8上内存使用情况,其中最左灰色部分是给BIOS和硬件保留的内存映射区域;绿色为操作系统,驱动以及用户进程使用的内存;橙色表示已经修改的内存也,当交换出来时需要先写回到硬盘;而蓝色部分5G内存则是用来缓存了未激活进程的数据和代码页;最后剩余的3M才是空闲内存。 当活动进行需要更多内存时会优先使用可用部分,当可用部分没有内存可用时,会释放一部分备用区域的内存。


 


2.2 页表

 
 

上面我们看到当实际物理内存小于虚拟内存时,会存在缺页以及页面交换等问题。此时操作系统会处理这些事情,是的这一切对于程序来说是透明的,它们不知道发生了什么,只知道自己可以使用全部的虚拟内存空间。而对于操作系统来说,它们需要负责一切,需要知道程序的那些页在实际内存中,那些不在。于是出现了页表,就是用来记录虚拟内存的页和物理页框之间的映射关系。MMU也正是利用页表来进行虚拟地址和物理地址的转换。

上面这张图是一张虚拟内存页和物理内存页框之间通过页表的映射关系,其中虚拟页面从VP0-VP7,物理页为PP0-PP3,我们从图中可以得到几点信息:

  1. 不是所有的虚拟内存页都加载到了物理内存中(VP3,VP6未映射状态);
  2. 不是所有虚拟内存页都被创建(VP0,VP5未被创建)
  3. 所有的虚拟内存页在页表中都有一项纪录,我们称为PTE(Page Table Entry);
  4. 虚拟内存的页是存放在磁盘上的;
  5. 页表纪录需要占用内存空间;
  6. MMU通过页表,将虚拟地址转换为物理地址;

 

这里可能会有疑问,为什么VP5没有被创建?虚拟页不是应该连续的吗?这就涉及到内存分段,程序编译和加载一些列问题了,这个会在介绍程序加载时解释。

 

 

最后我们看下页表中PTE的结构,一个PTE大小是32位,系统在操作页表时则会根据这些属性进行相应操作。

 

  • P:存在标志(1表示当前页是加载到了物理内存中)
  • W:读写标志(0时表示只读)
  • U/S: 用户/超级用户(0时表示用超级用户权限)
  • PWT:连续写入
  • PCD:禁用缓存
  • A:访问过
  • D:脏位(1表示被写过)
  • PAT:页面属性索引表
  • G:全局标志(TLB中使用)
  • Avail:方便操作系统使用


 


2.3 虚拟地址转换



通过上图我们来分析一下虚拟地址转换的过程:

  1. CPU送出要访问的虚拟地址,地址的结构是【页号+页内地址】;
  2. 页表存放在内存中,页表的地址和长度信息则存放在一个页表专用的寄存器中;通过读取寄存器的信息获得页表的起始地址;
  3. 将虚拟地址的页号与页表其实地址相加可以得到页表的实际地址
  4. 通过页表的映射项目,可以得到对应的物理内存页的号码
  5. 通过物理页号和业内偏移地址就能得到实际要访问的物理地址

 

当然,如果访问过程中出现缺页,会产生一个中断,然后操作系统会载入需要的页面并进行映射(设置页表),最后返回物理页号得到物理内存。从上面的过程我们可以知道,每次进行地址变化,MMU都要访问内存。回忆8086地址变换时是不需要访问内存的,于是虚拟地址的转换会影响系统性能,但是相当于虚拟内存带来的好处,这点代价还是值得的。

 


  

2.4 页表分级



在IA-32平台上,地址线为32位,所以最大的寻址范围是4G,那么最多能够支持使用4GB的内存(内存按字节编码)。那么对于虚拟内存来说,它的地址范围为4G(0x00000000 ~ 0xFFFFFFFF),而一个内存页的大小是4K,那么一个程序虚拟内存空间中有1048576个页(实际上进程可访问的虚拟地址范围没有4G,Linux是3G,Windows是2G或3G)。

从上面我们知道每个虚拟页都会在页表中有一个PTE,每个PTE为32位,那么对于一个进程至少需要4MB的内存来维护自己的页表;而一个系统中可能存在多个进行,仅仅维护页表这一项就需要消耗比较多内存。但实际上很多PTE项并没有映射到对应的物理页,这就造成了浪费。

 

有人会说那我们就动态建立页表,在映射时才增加这一项。但是从虚拟地址转换我们可以看到,找到PTE是通过PT首地址+页号得到的,所以页表PTE必须是连续的,但我们又知道并不是所有的虚拟页都会马上被创建,在访问是就会出现问题,比如VP0-VP8中的VP5没有被创建,当访问页号是5时,就会错误的访问到VP6。所以为了解决页表占用内存过多的问题,引入了分级页表。注意分级页表也需要CPU硬件提供支持。

 


 

2.4.1 二级页表

 

 

上图是Linux系统上二级页表的示意图。与一级页表不同的是,多增加了一层目录,虚拟地址的组成变为了【目录地址+页表地址+页内偏移】。其中页内偏移地址为12位,页表(PGT)地址为10位,页表目录(PGD)地址为10位。因为总过是32位,他们表示的PTE的个数是不变的。同样,PEG的每一个项目也有自己的结构。

 

 

二级页表地址转换的过程也很简单:

  1. 首先从cr3寄存器中找到PGD的首地址;(cr3寄存器用来保存PGD的地址)
  2. PGD首地址和目录号进行计算得到页表的首地址;
  3. 页表的首地址和页号进行计算得到物理页的首地址;
  4. 页内偏移和物理页地址通过计算得到最终的物理地址;

 

现在谈一谈为什么页表分级可以解决内存问题。首先因为页表需要连续的大的内存空间,通过引入目录级,我们可以离散对连续大空间的需求,这样,只有在同一个目录下的页表才需要连续的空间。另一方面,如果某个目录下的页表中没有任何映射的记录,那么这一张页表就不需要加载。因为其他页表可以通过其他目录项来获得,而不会存在一级表中不加载页表项导致访问出错的问题。但是同一个页表中,如果只有一个PTE被使用,这张页表也是需要被加载的。分级的方法同样用到了程序的局部性原理。

 

 

2.4.2 多级页表


 

 

 

对于32位系统最多能使用4G内存,为了让系统可以使用更多内存,加入了 物理地址扩展(Physical Address Extension,缩写为PAE)功能,可以支持36位。在前面8086时候我们见过类似的技术来使用更多内存。这个时候2级页表就无法满足要求了,于是引入了三级页表。其中增加了PMD中间目录一级。

 

 


为了适应64位CPU,操作系统又引入了4级页表。但是总体上来说他们工作原理都是相同的,这里就不叙述工作工程了。但是要注意的是,分级页表需要处理器的支持,对于只支持二级或三级页表的CPU来说,内核中体系结构相关的代码必须通过空页表对缺少的页表进行仿真。因此,内存管理代码剩余部分的实现是与CPU无关的。目前Windows 2000/XP使用的是二级页表,而使用PAE时则使用的是三级页表,对于64位操作系统则采用了四级页表。Linux则使用了四级页表。

 

 

2.4.3 倒排页表

 

在64位操作系统中,因为有64位地址线,所以页表的大小可能非常非常大,虽然分级页表可以不必加载全部页表,IA-32,IA64系统一般使用四级页表来处理,而在PowerPC等体系中则使用倒排页表来解决这个问题。与传统页表的区别: 使用页框号而不是虚拟页号来索引页表项。因为不是X86体系中常用的方法,这里就不相信介绍了。具体可以查看《现代操作系统》P113


 


2.5 TLB缓存


前面介绍虚拟地址转换时说过,相对于8086上的地址转换而言,这里多了一次内存访问查找页表的过程。我们知道内存速度比CPU慢很多,每次从内存取数据都要访问2次内存,会使得系统性能下降。


为了解决这个问题,在MMU中包含了一个关于PTE的缓冲区TLB(Translation Lookaside Buffer ),TLB是一个寄存器,所以它运行的速度和CPU速度相同。TLB中每一行保存了一个PTE,如上图所示,每次在去页表中查找之前,可以先在TLB中进行查找,如果命中则直接拿到物理页地址,如果不命中则再次访问内存。我们多次提到程序的局部性,在这里下一个要访问的地址很可能和前一个是在同一个内存页中,于是我们可以直接从寄存器中拿到物理内存页号,而不需要在访问内存,这样大大提高的了系统的速度。

 

上图是TLB的一个基本结构,对于多级页表来说,TLB可以缓存每一级的地址,所以同样能起作用。因为局部性原理,多级页表的访问速度并不比一级页表差。关于TLB更详细的内容,可以查看《深入理解计算机系统》P607 - P619

 

 

 

2.6 进程调度和虚拟内存

 

我们知道在系统中,每个进程都有自己独立的虚拟空间,于是每个进程都有一张属于自己的内页表。 而我们翻译地址时,从cr3中取出页表目录的首地址。对于不同的进程,他们都使用同一个寄存器。于是在CPU调度进程的时候,虚拟的地址空间也需要切换。于是对于普通用户程序需要做下面几件事情:

  1. 保存当前进程的页表目录地址,也就是保存cr3中存放的地址到进程上下文中
  2. 清空TLB中缓存的数据
  3. 调度新的进程到CPU,并设置cr3寄存器的值为页表目录的首地址

 

但是内存中除了用户程序之外还存在操作系统自身占用的内存。我们可以简单的把操作系统看成一个超大的进程,他和其他普通进程一样需要使用虚拟内存,需要使用到页表。当然作为内核程序,它必须是有一些特权的,下一篇我们将会介绍虚拟内存的布局。而对于内核而言不是存在进程调度的。因为所有的内核进程都是共享内核的虚拟地址空间,而我们一般都称之为内核线程,而非进程。 当然对于Linux而言,没有线程的概念,线程和进程唯一不同就是是否共享虚拟地址空间。一般来说内核代码是常驻在内存的,那么内核会不会缺页呢?

 

内存管理中的分页试内存管理,分页的主要作用就是使得每个进程有一个独立的,完整的内存空间,通过虚拟内存技术,使得程序可以在较小的内存上运行,而进程之间内存空间相互独立,提高了安全性。这一篇将主要介绍内存管理中分段管理,以及两种的结合,也是目前计算机普遍采用的段页式内存管理。这也直接决定了的后面程序的编译,加载以及允许时的内存布局。

 

 

 

1. 内存分段

 

 

1.1 为什么分段?

 

 

在x86-16体系中,为了解决16位寄存器对20位地址线的寻址问题,引入了分段式内存管理。而CPU则使用CS,DS,ES,SS等寄存器来保存程序的段首地址。当CPU执行指令需要访问内存时,只会送出段内的偏移地址,而通过指令的类型类确定访问那一个段寄存器。具体可以参考:计算机原理学习(5)-- x86-16 CPU和内存管理

 

到了IA-32,Intel引入了保护模式,所以在IA-32中为了保持兼容性,所以同样支持内存分段管理。另外我们讨论过了内存分页,页面中包含了程序的代码,数据等信息,它们都有各自的地址。这些地址是在编译的时候就确定的,因为每个进程都有独立完整的内存空间,只需要把页和物理页映射就能运行,所以这个地址是可以在编译时就决定的。在编译时,编译器会等程序进行语法词法等分析,在编译过程中会建立许多的表,来确定代码和变量的虚拟地址:

  • 被保存起来供打印清单的源程序正文;
  • 符号表,包含变量的名字和属性;
  • 包含所有用到的整形和浮点型数据的表;
  • 语法分析树,包括程序语法分析的结果;
  • 编译器内部过程调用的堆栈。

前面4张表会随着编译的进行不断增大,而堆栈的数据也会变化,现在的问题就是,每一张表的大小都不确定,那么如何指定每一张表在虚拟内存空间的地址呢?

如上图,没一张表都有自己的起始地址,但是当变量很多的时候,符号表需要的空间可能会超过程序正文的起始地址,这个时候就会把源程序的表的地址覆盖掉。当然编译器没有这么傻,它可以提示无法继续编译,当然这样并不合适,另一个办法就是拿出一部分没有使用的空间给符号表。造成这个问题的原因就是分页系统中的虚拟地址是一维的,所以在编译过程中必须给变量,代码分配虚拟地址。这个有点类似没有采用分页之前,进程之间使用物理地址导致相互覆盖的问题。

所以我们可以为不同的表分配自己的空间地址,也就是分段,这样他们地址都是相对地址,全部编译完成后确定了每张表的大小,就可以计算出实际的虚拟地址了。

 

 

 

1.2 分段的作用

 

 

分页实际是一个纯粹逻辑上的概念,因为实际的程序和内存并没有被真正的分为了不同的页面。而分段则不同,他是一个逻辑实体。一个段中可以是变量,源代码或者堆栈。一般来说每个段中不会包含不同类型的内容。而分段主要有以下几个作用:

  1. 解决编译问题: 前面提到过在编译时地址覆盖的问题,可以通过分段来解决,从而简化编译程序。
  2. 重新编译: 因为不同类型的数据在不同的段中,但其中一个段进行修改后,就不需要所有的段都重新进行编译。
  3. 内存共享: 对内存分段,可以很容易把其中的代码段或数据段共享给其他程序,分页中因为数据代码混合在一个页面中,所以不便于共享。
  4. 安全性: 将内存分为不同的段之后,因为不同段的内容类型不同,所以他们能进行的操作也不同,比如代码段的内容被加载后就不应该允许写的操作,因为这样会改变程序的行为。而在分页系统中,因为一个页不是一个逻辑实体,代码和数据可能混合在一起,无法进行安全上的控制。
  5. 动态链接: 动态链接是指在作业运行之前,并不把几个目标程序段链接起来。要运行时,先将主程序所对应的目标程序装入内存并启动运行,当运行过程中又需要调用某段时,才将该段(目标程序)调入内存并进行链接。可见,动态链接也要求以段作为管理的单位。
  6. 保持兼容性

所以在现在的x86的体系结构中分段内存管理是必选的,而分页管理则是可选的。

 

 

 

1.3 与x86-16分段管理的区别

 

 

Intel对分段内存管理逻辑地址的定义是【段号+段内地址】。在x86-16体系的分段管理中,CPU给出的内存地址是16位的段内偏移地址,段的基址从段寄存器中获得,最后计算出24位的物理地址。 而在IA-32体系中引入了保护模式,每个进程有4G的独立地址空间,CPU直接给出的是32位的段内偏移地址,段的基址从内存中获得,最后计算出32为的物理地址。他们最大的不同就在于获取基址的方式以及计算方法。

 

 

IA-32为了保持向前兼容,保留了CS/DS/ES/SS这4个寄存器,但因为不在从段寄存器中获得段价值,这4个段寄存器实际上已经失去了原本的作用(但不代表没有使用)。IA-32在内存中使用一张段表来记录各个段映射的物理内存地址(如下图)。

在译地的过程中,x86-16是通过16位的段基址和16位的段内偏移不是简单的相加,而是通过 段值*0x10 + 偏移地址 对基址重定向的方式计算得到物理地址,而IA-32中则相对简单,不需要对基址重定向,这一点和前面分页内存管理是相似的。而CPU只需要为这个段表提供一个记录其首地址的寄存器就可以了。 同样也可以使用TLB来加速。

与x86-16中分段管理另一个不同是,在IA-32中,因为有了独立的地址空间,对多程序也支持的非常好。而分段可以很好的支持进程间数据的共享。

 

 

 

 

2. 分段内存管理

 

 

2.1 段选择器

 

在IA-32中保留的CS/DS/ES/SS这4个16位段寄存器不再被解释为段的基地址,Intel为了保持兼容性将这些寄存器的16个位分成3个用于不同功能的域,称为段选择器

其中3-15是选择子,存放的是段描述符的索引(可以理解为段号),该描述符为64bit用于描述存储器段的位置、长度和访问权限。而段描述符可以分为两种,全局描述符(GDT)局部描述符(LDT),对应着第2位,而0,1两位是表示CPU的权限级别(0-4级)。在IA-32中一共有6个段选择器

 

  • CS保存了代码段描述符的索引;
  • DS保存了数据段描述符的索引;
  • SS保存堆栈段描述符索引;
  • ES、FS、GS则作为一般用途,可以指向任意的数据段,实现自定义寻址。

 

 

2.2 段描述符

 

段描述符就是前面说到的段表中的每一个项目的,一个段描述符由8个字节组成。它描述了段的特征。前面提到段描述符可以分为GDT和LDT两类。通常来说系统只定义一个GDT,而每个进程如果需要放置一些自定义的段,就可以放在自己的LDT中。IA-32中引入了GDTR和LDTR两个寄存器,就是用来存放当前正在使用的GDT和LDT的首地址。

 

上面的图是Linux中不同段的描述符,结构基本是一致的,只有少数字段有差别。其中最重要的就是BASE字段,一共32位,保存的是当前段的首地址。 在Linux系统中,每个CPU对应一个GDT。一个GDT中有18个段描述符和14个未使用或保留项。其中用户和内核各有一个代码段和数据段,然后还包含一个TSS任务段来保存寄存器的状态。其他的段则包括局部线程存储,电源管理,即插即用等多个段。而Linux系统中,大多数用户态的程序都不使用LDT。

 

 

 

2.3 段地址转换

 

 

在IA-32中,逻辑地址是16位的段选择符+32位偏移地址,段寄存器不在保存段基址,而是保存段描述符的索引。

 

 

  1. IA-32首选确定要访问的段(方式x86-16相同),然后决定使用的段寄存器。
  2. 根据段选择符号的TI字段决定是访问GDT还是LDT,他们的首地址则通过GTDR和LDTR来获得。
  3. 将段选择符的Index字段的值*8,然后加上GDT或LDT的首地址,就能得到当前段描述符的地址。(乘以8是因为段描述符为8字节)
  4. 得到段描述符的地址后,可以通过段描述符中BASE获得段的首地址。
  5. 将逻辑地址中32位的偏移地址和段首地址相加就可以得到实际要访问的物理地址。

 

 

2.4 缓存段描述符

 

为了加速地址转换的过程,根据程序的局部性原理,我们可以讲当前段寄存器指向的段描述符缓存在特定的寄存器中。这里为6个段寄存器准备了6个用来缓存段描述符的非编程寄存器。这样就能加快地址转换的过程。而仅当段寄存器内容变化时,才有必要去访问内存中的GDT或LDT。

 

 

 

3. 段页式内存管理

 

 

分段内存管理的优势在于内存共享和安全控制,而分页内存管理的优势在于提高内利用率。他们之间并不是相互对立的竞争关系,而是可以相互补充的。也就是可以把2种方式结合起来,也就是目前计算机中最普遍采用的段页式内存管理。段页式管理的核心就是对内存进行分段,对每个段进行分页。这样在拥有了分段的优势的同时,可以更加合理的使用内存的物理页。

 

 

2.1  段页式内存管理结构

 

对于段页式管理来说,我们需要通过段表来保存每一个段的信息,通过页表保存每个段中虚拟页的信息。在段页管理的系统中,CPU给出的不再是分页系统中的虚拟地址,而是给出的逻辑地址。(前面2篇有介绍逻辑地址和虚拟地址,简单的说逻辑地址是二维的,而虚拟地址是一维的,平坦的)。

 

 

 

2.2 段页式地址转换

 

 

 

上面的图简单的描述了在段页式内存管理的系统中,地址转换的过程。实际上就是我们前面介绍的分段和分页地址转换的结合。

  1. CPU给出要访问的逻辑地址;
  2. 通过分段内存管理的地址转换机制,将逻辑地址转换为线性地址,也就是分页系统中的虚拟地址;
  3. 通过分页内存管理的地址转换机制,将虚拟地址转换为物理地址;

2. 虚拟存储器

 

虚拟存储器我们一般也称为虚拟内存(和Windows中的虚拟内存不是一个概念,但是有关联),它的基本思想是:

  • 每个进程都有自己的地址空间;
  • 每个地址空间被分为多个块,每个块称为页,每个页有连续的地址空间;
  • 这些页被映射到物理内存,但不是所有也都在内存中程序才能运行;
  • 当使用的页不在物理内存中时,由操作系统负责载入相应的页;

 

在实模式下,CPU将偏移地址和段寄存器,基址寄存器等进行计算得到的实际的物理地址。 而在保护模式下,引入了虚拟内存的概念,在虚拟内存中使用的地址称为虚拟地址(线性地址),虚拟地址通过MMU将虚拟地址映射为物理地址,然后送到总线,进行内存访问。这里最关键的就是虚拟地址的映射。

 

 

2.1 分页

 

对于虚拟内存来说,是对物理内存的抽象,整个虚拟内存空间被划分成了多个大小固定的页(page),每个页连续的虚拟地址,组合成了一个完整的虚拟地址空间。同样,操作系统也会把物理内存划分为多个大小固定的块,我们称为页框(page frame),它只是操作系统为了方便管理在逻辑上的划分的一个数据结构,并不存放实际内存数据,但是我们可以简单的认为它就是内存。这样一个虚拟内存的page就可以和一个物理内存的page frame对应起来,产生映射关系。

 

关于一个虚拟页的大小,现在的操作系统中一般是512B-64K(准确的说是地址范围大小,而非容纳数据的大小)。但是内存页的大小会对系统性能产生影响,内存页设得太小,内存页会很多,管理内存页的数组会比较大,耗内存。内存页设大了,因为一个进程拥有的内存是内存页大小的整数倍,会导致碎片,即申请了很多内存,真正用到的只有一点。目前Windows和Linux上默认的内存页面大小都是4K。

 

从上图我们也可以看出,虚拟内存的页和物理内存的页框并不一定是一一对应的,虚拟内存的大小和系统的寻址能力相关,也就是地址线的位数,而物理内存的页框数取决于实际的内存大小。所以可能只有一部分页有对应的页框,而当访问的数据不在物理内存中时就会出现缺页,这个时候操作系统会负责调入新的页,也就是建立新的映射。这样就不需要一次把程序全部加载到内存。

 

 

 

2.1.1 虚拟页是什么?

 

很多人会有一个疑问,虚拟页到底是实际存在的还是虚拟的?我们知道内存中存放的是执行文件的代码和数据,而程序在运行前,它的数据和代码是存放在这个程序的可执行文件中的(比如.exe和.so),而在运行时需要把可执行文件加载到内存。所以我们把这个硬盘上的文件也划分为4K大小的页(逻辑上划分,实际是加载过程中加载器完成的),这就是虚拟页里面实际的东西。但是程序在运行是可能会申请内存,这个时候需要新的虚拟页来映射,所以我们可以得知虚拟页应该有3种状态:

  1. 已映射:虚拟页面被创建已经被加载到物理内存,和物理页之间存在映射关系。
  2. 未映射:虚拟页面被创建,但是没有被加载到内存或已经被调出内存,和物理页面之间没有映射关系,当需要使用时调入内存建立映射。
  3. 未创建:虚拟页面没有被创建,可能是因为还没有访问到此页面所以没有加载或者是调用macllo来分配内存,只有在运行是才会被创建。

 

 

2.1.2 存储器映射

 

加载应用程序到内存时,因为和虚拟地址有关,我们需要把应用程序文件和虚拟内存关联起来,在Linux中称为存储器映射,同时提供了一个mmap的系统调用来实现次功能。文件被分成页面大小的片,每一片包含一个虚拟页面的内容,因为页面调度程序是按需求调度,所以在这些虚拟页面并没有实际的进入内存,而是在CPU发出访问请求时,会创建一个虚拟页并加载到内存。我们在启动一个进程时,加载器为我们完成了文件映射的功能,所以此时我们的执行文件又可以称为映像文件。实际上加载器并不真正负责从磁盘加载数据到内存,因为和虚拟内存建立了映射关系,所以这个操作是虚拟内存自动进行的。 正是有了存储器映射的存在,使得我们可以很方便的将程序和数据加载到内存中。

 

 

2.1.3 交换分区

 

当CPU请求一个虚拟页是,虚拟页会被创建并加载到内存,而页面调度算法可能在页面休眠或在内存满的情况下更具调度算法将虚拟页交换出去,在适当的时候可能被交换回来。这个时候就需要一个区域来存放被交换出来的虚拟页,这个区域称为交换分区。 这个分区在Linux中称为swap分区,而在Windows中我们称为虚拟内存(注意这里和我们谈到的虚拟内存技术不是一回事)。

 

以前电脑内存很小,特别是玩一些游戏时经常会提示内存不足,网上一般会告诉你增大你的虚拟内存(交换分区),这样一来在内存不足的时候可以存放更多交换出来的虚拟页,看起来好像内存变大了一样。从这方面来说Windows把他叫虚拟内存(交换分区)也是很正确的。  交换分区虽然也是硬盘的一部分,但是交换分区没有普通的文件系统,这样消除了将文件偏移转换为页地址的开销。但是过于频繁的交换页面,IO操作会导致系统性能下降。但是在内存不足时可以保证系统 正常运行。当然这也和交换分区的大小有关。

 

而如今,一般使用的电脑都已经4G,8G内存了,对于普通需求来说足够大了。所以虚拟页会长时间存在与内存中而不被交换出去。所以我们可以禁用掉交换分区,以便提高性能。对于Windwos 从Vista开始有一个Superfetch的内存管理机制,而linux有Preload与之类似。这种内存机制会将用户经常用的应用的部分虚拟页提前加载到内存,当用户使用时就无需在从硬盘加载。而当应用休眠或关闭时,也不会将这些虚拟页交换出去。


如下图就是Windows 8上内存使用情况,其中最左灰色部分是给BIOS和硬件保留的内存映射区域;绿色为操作系统,驱动以及用户进程使用的内存;橙色表示已经修改的内存也,当交换出来时需要先写回到硬盘;而蓝色部分5G内存则是用来缓存了未激活进程的数据和代码页;最后剩余的3M才是空闲内存。 当活动进行需要更多内存时会优先使用可用部分,当可用部分没有内存可用时,会释放一部分备用区域的内存。


 


2.2 页表

 
 

上面我们看到当实际物理内存小于虚拟内存时,会存在缺页以及页面交换等问题。此时操作系统会处理这些事情,是的这一切对于程序来说是透明的,它们不知道发生了什么,只知道自己可以使用全部的虚拟内存空间。而对于操作系统来说,它们需要负责一切,需要知道程序的那些页在实际内存中,那些不在。于是出现了页表,就是用来记录虚拟内存的页和物理页框之间的映射关系。MMU也正是利用页表来进行虚拟地址和物理地址的转换。

上面这张图是一张虚拟内存页和物理内存页框之间通过页表的映射关系,其中虚拟页面从VP0-VP7,物理页为PP0-PP3,我们从图中可以得到几点信息:

  1. 不是所有的虚拟内存页都加载到了物理内存中(VP3,VP6未映射状态);
  2. 不是所有虚拟内存页都被创建(VP0,VP5未被创建)
  3. 所有的虚拟内存页在页表中都有一项纪录,我们称为PTE(Page Table Entry);
  4. 虚拟内存的页是存放在磁盘上的;
  5. 页表纪录需要占用内存空间;
  6. MMU通过页表,将虚拟地址转换为物理地址;

 

这里可能会有疑问,为什么VP5没有被创建?虚拟页不是应该连续的吗?这就涉及到内存分段,程序编译和加载一些列问题了,这个会在介绍程序加载时解释。

 

 

最后我们看下页表中PTE的结构,一个PTE大小是32位,系统在操作页表时则会根据这些属性进行相应操作。

 

  • P:存在标志(1表示当前页是加载到了物理内存中)
  • W:读写标志(0时表示只读)
  • U/S: 用户/超级用户(0时表示用超级用户权限)
  • PWT:连续写入
  • PCD:禁用缓存
  • A:访问过
  • D:脏位(1表示被写过)
  • PAT:页面属性索引表
  • G:全局标志(TLB中使用)
  • Avail:方便操作系统使用


 


2.3 虚拟地址转换



通过上图我们来分析一下虚拟地址转换的过程:

  1. CPU送出要访问的虚拟地址,地址的结构是【页号+页内地址】;
  2. 页表存放在内存中,页表的地址和长度信息则存放在一个页表专用的寄存器中;通过读取寄存器的信息获得页表的起始地址;
  3. 将虚拟地址的页号与页表其实地址相加可以得到页表的实际地址
  4. 通过页表的映射项目,可以得到对应的物理内存页的号码
  5. 通过物理页号和业内偏移地址就能得到实际要访问的物理地址

 

当然,如果访问过程中出现缺页,会产生一个中断,然后操作系统会载入需要的页面并进行映射(设置页表),最后返回物理页号得到物理内存。从上面的过程我们可以知道,每次进行地址变化,MMU都要访问内存。回忆8086地址变换时是不需要访问内存的,于是虚拟地址的转换会影响系统性能,但是相当于虚拟内存带来的好处,这点代价还是值得的。

 


  

2.4 页表分级



在IA-32平台上,地址线为32位,所以最大的寻址范围是4G,那么最多能够支持使用4GB的内存(内存按字节编码)。那么对于虚拟内存来说,它的地址范围为4G(0x00000000 ~ 0xFFFFFFFF),而一个内存页的大小是4K,那么一个程序虚拟内存空间中有1048576个页(实际上进程可访问的虚拟地址范围没有4G,Linux是3G,Windows是2G或3G)。

从上面我们知道每个虚拟页都会在页表中有一个PTE,每个PTE为32位,那么对于一个进程至少需要4MB的内存来维护自己的页表;而一个系统中可能存在多个进行,仅仅维护页表这一项就需要消耗比较多内存。但实际上很多PTE项并没有映射到对应的物理页,这就造成了浪费。

 

有人会说那我们就动态建立页表,在映射时才增加这一项。但是从虚拟地址转换我们可以看到,找到PTE是通过PT首地址+页号得到的,所以页表PTE必须是连续的,但我们又知道并不是所有的虚拟页都会马上被创建,在访问是就会出现问题,比如VP0-VP8中的VP5没有被创建,当访问页号是5时,就会错误的访问到VP6。所以为了解决页表占用内存过多的问题,引入了分级页表。注意分级页表也需要CPU硬件提供支持。

 


 

2.4.1 二级页表

 

 

上图是Linux系统上二级页表的示意图。与一级页表不同的是,多增加了一层目录,虚拟地址的组成变为了【目录地址+页表地址+页内偏移】。其中页内偏移地址为12位,页表(PGT)地址为10位,页表目录(PGD)地址为10位。因为总过是32位,他们表示的PTE的个数是不变的。同样,PEG的每一个项目也有自己的结构。

 

 

二级页表地址转换的过程也很简单:

  1. 首先从cr3寄存器中找到PGD的首地址;(cr3寄存器用来保存PGD的地址)
  2. PGD首地址和目录号进行计算得到页表的首地址;
  3. 页表的首地址和页号进行计算得到物理页的首地址;
  4. 页内偏移和物理页地址通过计算得到最终的物理地址;

 

现在谈一谈为什么页表分级可以解决内存问题。首先因为页表需要连续的大的内存空间,通过引入目录级,我们可以离散对连续大空间的需求,这样,只有在同一个目录下的页表才需要连续的空间。另一方面,如果某个目录下的页表中没有任何映射的记录,那么这一张页表就不需要加载。因为其他页表可以通过其他目录项来获得,而不会存在一级表中不加载页表项导致访问出错的问题。但是同一个页表中,如果只有一个PTE被使用,这张页表也是需要被加载的。分级的方法同样用到了程序的局部性原理。

 

 

2.4.2 多级页表


 

 

 

对于32位系统最多能使用4G内存,为了让系统可以使用更多内存,加入了 物理地址扩展(Physical Address Extension,缩写为PAE)功能,可以支持36位。在前面8086时候我们见过类似的技术来使用更多内存。这个时候2级页表就无法满足要求了,于是引入了三级页表。其中增加了PMD中间目录一级。

 

 


为了适应64位CPU,操作系统又引入了4级页表。但是总体上来说他们工作原理都是相同的,这里就不叙述工作工程了。但是要注意的是,分级页表需要处理器的支持,对于只支持二级或三级页表的CPU来说,内核中体系结构相关的代码必须通过空页表对缺少的页表进行仿真。因此,内存管理代码剩余部分的实现是与CPU无关的。目前Windows 2000/XP使用的是二级页表,而使用PAE时则使用的是三级页表,对于64位操作系统则采用了四级页表。Linux则使用了四级页表。

 

 

2.4.3 倒排页表

 

在64位操作系统中,因为有64位地址线,所以页表的大小可能非常非常大,虽然分级页表可以不必加载全部页表,IA-32,IA64系统一般使用四级页表来处理,而在PowerPC等体系中则使用倒排页表来解决这个问题。与传统页表的区别: 使用页框号而不是虚拟页号来索引页表项。因为不是X86体系中常用的方法,这里就不相信介绍了。具体可以查看《现代操作系统》P113


 


2.5 TLB缓存


前面介绍虚拟地址转换时说过,相对于8086上的地址转换而言,这里多了一次内存访问查找页表的过程。我们知道内存速度比CPU慢很多,每次从内存取数据都要访问2次内存,会使得系统性能下降。


为了解决这个问题,在MMU中包含了一个关于PTE的缓冲区TLB(Translation Lookaside Buffer ),TLB是一个寄存器,所以它运行的速度和CPU速度相同。TLB中每一行保存了一个PTE,如上图所示,每次在去页表中查找之前,可以先在TLB中进行查找,如果命中则直接拿到物理页地址,如果不命中则再次访问内存。我们多次提到程序的局部性,在这里下一个要访问的地址很可能和前一个是在同一个内存页中,于是我们可以直接从寄存器中拿到物理内存页号,而不需要在访问内存,这样大大提高的了系统的速度。

 

上图是TLB的一个基本结构,对于多级页表来说,TLB可以缓存每一级的地址,所以同样能起作用。因为局部性原理,多级页表的访问速度并不比一级页表差。关于TLB更详细的内容,可以查看《深入理解计算机系统》P607 - P619

 

 

 

2.6 进程调度和虚拟内存

 

我们知道在系统中,每个进程都有自己独立的虚拟空间,于是每个进程都有一张属于自己的内页表。 而我们翻译地址时,从cr3中取出页表目录的首地址。对于不同的进程,他们都使用同一个寄存器。于是在CPU调度进程的时候,虚拟的地址空间也需要切换。于是对于普通用户程序需要做下面几件事情:

  1. 保存当前进程的页表目录地址,也就是保存cr3中存放的地址到进程上下文中
  2. 清空TLB中缓存的数据
  3. 调度新的进程到CPU,并设置cr3寄存器的值为页表目录的首地址

 

但是内存中除了用户程序之外还存在操作系统自身占用的内存。我们可以简单的把操作系统看成一个超大的进程,他和其他普通进程一样需要使用虚拟内存,需要使用到页表。当然作为内核程序,它必须是有一些特权的,下一篇我们将会介绍虚拟内存的布局。而对于内核而言不是存在进程调度的。因为所有的内核进程都是共享内核的虚拟地址空间,而我们一般都称之为内核线程,而非进程。 当然对于Linux而言,没有线程的概念,线程和进程唯一不同就是是否共享虚拟地址空间。一般来说内核代码是常驻在内存的,那么内核会不会缺页呢?

 

内存管理中的分页试内存管理,分页的主要作用就是使得每个进程有一个独立的,完整的内存空间,通过虚拟内存技术,使得程序可以在较小的内存上运行,而进程之间内存空间相互独立,提高了安全性。这一篇将主要介绍内存管理中分段管理,以及两种的结合,也是目前计算机普遍采用的段页式内存管理。这也直接决定了的后面程序的编译,加载以及允许时的内存布局。

 

 

 

1. 内存分段

 

 

1.1 为什么分段?

 

 

在x86-16体系中,为了解决16位寄存器对20位地址线的寻址问题,引入了分段式内存管理。而CPU则使用CS,DS,ES,SS等寄存器来保存程序的段首地址。当CPU执行指令需要访问内存时,只会送出段内的偏移地址,而通过指令的类型类确定访问那一个段寄存器。具体可以参考:计算机原理学习(5)-- x86-16 CPU和内存管理

 

到了IA-32,Intel引入了保护模式,所以在IA-32中为了保持兼容性,所以同样支持内存分段管理。另外我们讨论过了内存分页,页面中包含了程序的代码,数据等信息,它们都有各自的地址。这些地址是在编译的时候就确定的,因为每个进程都有独立完整的内存空间,只需要把页和物理页映射就能运行,所以这个地址是可以在编译时就决定的。在编译时,编译器会等程序进行语法词法等分析,在编译过程中会建立许多的表,来确定代码和变量的虚拟地址:

  • 被保存起来供打印清单的源程序正文;
  • 符号表,包含变量的名字和属性;
  • 包含所有用到的整形和浮点型数据的表;
  • 语法分析树,包括程序语法分析的结果;
  • 编译器内部过程调用的堆栈。

前面4张表会随着编译的进行不断增大,而堆栈的数据也会变化,现在的问题就是,每一张表的大小都不确定,那么如何指定每一张表在虚拟内存空间的地址呢?

如上图,没一张表都有自己的起始地址,但是当变量很多的时候,符号表需要的空间可能会超过程序正文的起始地址,这个时候就会把源程序的表的地址覆盖掉。当然编译器没有这么傻,它可以提示无法继续编译,当然这样并不合适,另一个办法就是拿出一部分没有使用的空间给符号表。造成这个问题的原因就是分页系统中的虚拟地址是一维的,所以在编译过程中必须给变量,代码分配虚拟地址。这个有点类似没有采用分页之前,进程之间使用物理地址导致相互覆盖的问题。

所以我们可以为不同的表分配自己的空间地址,也就是分段,这样他们地址都是相对地址,全部编译完成后确定了每张表的大小,就可以计算出实际的虚拟地址了。

 

 

 

1.2 分段的作用

 

 

分页实际是一个纯粹逻辑上的概念,因为实际的程序和内存并没有被真正的分为了不同的页面。而分段则不同,他是一个逻辑实体。一个段中可以是变量,源代码或者堆栈。一般来说每个段中不会包含不同类型的内容。而分段主要有以下几个作用:

  1. 解决编译问题: 前面提到过在编译时地址覆盖的问题,可以通过分段来解决,从而简化编译程序。
  2. 重新编译: 因为不同类型的数据在不同的段中,但其中一个段进行修改后,就不需要所有的段都重新进行编译。
  3. 内存共享: 对内存分段,可以很容易把其中的代码段或数据段共享给其他程序,分页中因为数据代码混合在一个页面中,所以不便于共享。
  4. 安全性: 将内存分为不同的段之后,因为不同段的内容类型不同,所以他们能进行的操作也不同,比如代码段的内容被加载后就不应该允许写的操作,因为这样会改变程序的行为。而在分页系统中,因为一个页不是一个逻辑实体,代码和数据可能混合在一起,无法进行安全上的控制。
  5. 动态链接: 动态链接是指在作业运行之前,并不把几个目标程序段链接起来。要运行时,先将主程序所对应的目标程序装入内存并启动运行,当运行过程中又需要调用某段时,才将该段(目标程序)调入内存并进行链接。可见,动态链接也要求以段作为管理的单位。
  6. 保持兼容性

所以在现在的x86的体系结构中分段内存管理是必选的,而分页管理则是可选的。

 

 

 

1.3 与x86-16分段管理的区别

 

 

Intel对分段内存管理逻辑地址的定义是【段号+段内地址】。在x86-16体系的分段管理中,CPU给出的内存地址是16位的段内偏移地址,段的基址从段寄存器中获得,最后计算出24位的物理地址。 而在IA-32体系中引入了保护模式,每个进程有4G的独立地址空间,CPU直接给出的是32位的段内偏移地址,段的基址从内存中获得,最后计算出32为的物理地址。他们最大的不同就在于获取基址的方式以及计算方法。

 

 

IA-32为了保持向前兼容,保留了CS/DS/ES/SS这4个寄存器,但因为不在从段寄存器中获得段价值,这4个段寄存器实际上已经失去了原本的作用(但不代表没有使用)。IA-32在内存中使用一张段表来记录各个段映射的物理内存地址(如下图)。

在译地的过程中,x86-16是通过16位的段基址和16位的段内偏移不是简单的相加,而是通过 段值*0x10 + 偏移地址 对基址重定向的方式计算得到物理地址,而IA-32中则相对简单,不需要对基址重定向,这一点和前面分页内存管理是相似的。而CPU只需要为这个段表提供一个记录其首地址的寄存器就可以了。 同样也可以使用TLB来加速。

与x86-16中分段管理另一个不同是,在IA-32中,因为有了独立的地址空间,对多程序也支持的非常好。而分段可以很好的支持进程间数据的共享。

 

 

 

 

2. 分段内存管理

 

 

2.1 段选择器

 

在IA-32中保留的CS/DS/ES/SS这4个16位段寄存器不再被解释为段的基地址,Intel为了保持兼容性将这些寄存器的16个位分成3个用于不同功能的域,称为段选择器

其中3-15是选择子,存放的是段描述符的索引(可以理解为段号),该描述符为64bit用于描述存储器段的位置、长度和访问权限。而段描述符可以分为两种,全局描述符(GDT)局部描述符(LDT),对应着第2位,而0,1两位是表示CPU的权限级别(0-4级)。在IA-32中一共有6个段选择器

 

  • CS保存了代码段描述符的索引;
  • DS保存了数据段描述符的索引;
  • SS保存堆栈段描述符索引;
  • ES、FS、GS则作为一般用途,可以指向任意的数据段,实现自定义寻址。

 

 

2.2 段描述符

 

段描述符就是前面说到的段表中的每一个项目的,一个段描述符由8个字节组成。它描述了段的特征。前面提到段描述符可以分为GDT和LDT两类。通常来说系统只定义一个GDT,而每个进程如果需要放置一些自定义的段,就可以放在自己的LDT中。IA-32中引入了GDTR和LDTR两个寄存器,就是用来存放当前正在使用的GDT和LDT的首地址。

 

上面的图是Linux中不同段的描述符,结构基本是一致的,只有少数字段有差别。其中最重要的就是BASE字段,一共32位,保存的是当前段的首地址。 在Linux系统中,每个CPU对应一个GDT。一个GDT中有18个段描述符和14个未使用或保留项。其中用户和内核各有一个代码段和数据段,然后还包含一个TSS任务段来保存寄存器的状态。其他的段则包括局部线程存储,电源管理,即插即用等多个段。而Linux系统中,大多数用户态的程序都不使用LDT。

 

 

 

2.3 段地址转换

 

 

在IA-32中,逻辑地址是16位的段选择符+32位偏移地址,段寄存器不在保存段基址,而是保存段描述符的索引。

 

 

  1. IA-32首选确定要访问的段(方式x86-16相同),然后决定使用的段寄存器。
  2. 根据段选择符号的TI字段决定是访问GDT还是LDT,他们的首地址则通过GTDR和LDTR来获得。
  3. 将段选择符的Index字段的值*8,然后加上GDT或LDT的首地址,就能得到当前段描述符的地址。(乘以8是因为段描述符为8字节)
  4. 得到段描述符的地址后,可以通过段描述符中BASE获得段的首地址。
  5. 将逻辑地址中32位的偏移地址和段首地址相加就可以得到实际要访问的物理地址。

 

 

2.4 缓存段描述符

 

为了加速地址转换的过程,根据程序的局部性原理,我们可以讲当前段寄存器指向的段描述符缓存在特定的寄存器中。这里为6个段寄存器准备了6个用来缓存段描述符的非编程寄存器。这样就能加快地址转换的过程。而仅当段寄存器内容变化时,才有必要去访问内存中的GDT或LDT。

 

 

 

3. 段页式内存管理

 

 

分段内存管理的优势在于内存共享和安全控制,而分页内存管理的优势在于提高内利用率。他们之间并不是相互对立的竞争关系,而是可以相互补充的。也就是可以把2种方式结合起来,也就是目前计算机中最普遍采用的段页式内存管理。段页式管理的核心就是对内存进行分段,对每个段进行分页。这样在拥有了分段的优势的同时,可以更加合理的使用内存的物理页。

 

 

2.1  段页式内存管理结构

 

对于段页式管理来说,我们需要通过段表来保存每一个段的信息,通过页表保存每个段中虚拟页的信息。在段页管理的系统中,CPU给出的不再是分页系统中的虚拟地址,而是给出的逻辑地址。(前面2篇有介绍逻辑地址和虚拟地址,简单的说逻辑地址是二维的,而虚拟地址是一维的,平坦的)。

 

 

 

2.2 段页式地址转换

 

 

 

上面的图简单的描述了在段页式内存管理的系统中,地址转换的过程。实际上就是我们前面介绍的分段和分页地址转换的结合。

  1. CPU给出要访问的逻辑地址;
  2. 通过分段内存管理的地址转换机制,将逻辑地址转换为线性地址,也就是分页系统中的虚拟地址;
  3. 通过分页内存管理的地址转换机制,将虚拟地址转换为物理地址;
0 0