计算机原理(七)-进程内存管理分段

我们知道,进程都有独立的虚拟内存空间,跟物理内存一样,因为物理内存大小有限定,存在被覆盖的风险,所以现代CPU一般通过虚拟内存通过MMU进行页表的映射管理。而进程内的空间也一样,因为编译程序的时候,进程内也需要生成各个虚拟地址表,而每个表的大小不固定,可能就会覆盖其他表的地址,而这时,采用的解决方案是早期A-16位内存采用的分段方式,防止被覆盖。

分段原因

在编译过程中会建立许多的表,来确定代码和变量的虚拟地址:

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

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

 

分段作用

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

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

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

计算机原理-X86 CPU和内存管理(六)

这里主要简单介绍下32位处理器的内存访问,内存管理以及应用程序的内存布局。而32位CPU主要就是从intel 的80386架构CUP开始的,这也是我们最开始熟悉的CPU。而CPU最直接的作用就是 取指令、指令译码、执行指令、访存取数和结果写回。说白了就是对指令进行操作。而前面的可执行程序在CPU看来就是编译好的指令集合及数据集合。

其总图可以抽象为如下:

image

我们分别看看CPU及内存的一些操作细节:

一.32CPU
CPU架构

从80386开始,地址线变为了32位,和CPU寄存器以及运算单元位数一样,最大寻址范围增加到4G。另外80386处理器都可以支持虚拟存储器,支持多任务。 而之后的CPU,主要的改进就在于:

  1. CPU内部集成DMA,计数器,定时器等;
  2. 制造工艺的提示,更多的晶体管,更快的速度
  3. 加入更多的指令集,如MMX,SSE,SSE2等
  4. 集成L1,L2,L3高速缓存,减少外部总线的访问
  5. 超线程,多核心提高CPU效率
CPU内存访问方式

80386引入多任务后,内存管理方式有如下方式:

  1. 地址空间:这个是对物理内存的一个抽象,就好像进程是对CPU的一个抽象。一个进程可用于寻址的一套地址的集合,每个进程都有自己的地址空间,相互独立,这就解决了安全问题。
  2. 交换:把程序全部加载到内存,当进程处于空闲时,把他移除内存,存放到硬盘上,然后载入其他程序。这样使得每个进程可以使用更多的内存。
  3. 虚拟内存:利用到计算机系统的局部性和存储分层,我们可以只加载一部分需要使用的代码和数据到内存,当访问的内容不在内存时,和已经使用完的部分进行交换,这样就能在小内存的机器上运行大的程序了。对于程序来说这是透明的,看起来自己好像使用了全部内存。而多个应用完全可以使用相同的虚拟地址。
  4.  

    由于上面内存管理方式,产生了如下的内存访问方式:

虚拟寻址

CPU在内部增加了一个MMU(Memory Management Unit)单元来管理内存地址的转换。除了可以转换地址,还能提供内存访问控制。如操作系统固定使用一段内存地址,当进程请求访问这一段地址时,MMU可以拒绝访问以保证系统的稳定性。而MMU的翻译过程则需要操作系统的支持。所以可见硬件和软件在计算机发展过程中是密不可分的。

CPU寄存器

通用寄存器 ,指令指针寄存器。。。

二.虚拟内存

他就是现在保护模式下的内存获得方式,它的基本思想是:

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

虚拟地址(线性地址),虚拟地址就是同上CPU的MMU将虚拟地址映射为物理地址,然后送到总线,进行内存访问。这里最关键的就是虚拟地址的映射。

内存分页

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

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

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

存储分页

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

MMU映射方式

如何将虚拟页映射到物理页中,MMU通过页表映射,页表是实时在内存中的,而虚拟页是运行时通过页表映射到实际的物理内存页上的。

页表分级

为了解决页表占用内存过多的问题,引入了分级页表

页表缓冲区

虚拟地址转换时,一次内存访问查找页表的过程。我们知道内存速度比CPU慢很多,每次从内存取数据都要访问2次内存,会使得系统性能下降。为了解决这个问题,在MMU中包含了一个关于PTE的缓冲区TLB(Translation Lookaside Buffer ),TLB是一个寄存器,所以它运行的速度和CPU速度相同。每次在去页表中查找之前,可以先在TLB中进行查找,如果命中则直接拿到物理页地址,如果不命中则再次访问内存。

进程调度和虚拟内存

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

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

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