王道操作系统—内存篇

逻辑地址和物理地址

程序的执行过程:

  1. 源代码编译目标模块,高级语言翻译成机器语言
  2. 链接程序将目标模块和它的库函数进行链接,形成完整的装入模块,装入模块从0开始编址,这称为逻辑地址
  3. 装入程序将装入模块装入内存,装入后形成物理地址
逻辑地址:编程使用的地址,编译的时候只关心程序的相对地址
物理地址:只有在放入内存的时候,才需要使用物理地址,根据地址转换方式可以从逻辑地址转变为物理地址

用户在编程的时候只需要关系数据的逻辑地址,而这些逻辑上连续的数据在计算机内存中的存放可能不是连续的,根据内存管理方式的不同,数据的实际存放也会不同,地址转换作为操作系统的职责之一,就是进行逻辑地址和物理地址的转换,保证了程序员在写代码的时候只需要考虑数据的逻辑关系,而在操作系统底层这些数据是如何存放就不需要关系了,操作系统帮我们完成了;

目标模块不同的装入方式地址转换的时机也会不同

程序的连接和装入

一. 程序连接:

程序的连接是将编译后的目标模块和它的函数库连接在一起;

1. 静态连接:

在程序运行之前, 先将各目标模块及它们所需 的库函数连接成一个完整的 可执行文件(装入模块), 之后不再拆开。

静态连接的内容:
  • 空间与地址的分配。扫描所有的目标文件,合并相似段,收集当中所有的符号信息。
  • 符号解析与重定位。调整代码位置。
静态连接的特点:
  • 简单,浪费内存,内存中可能存在多个重复的函数库,一个程序可能有多个模块,每一个模块的函数库可能重复
  • 不容易扩展,如果有新的目标模块必须要重新编译,链接
2. 动态连接:

在程序执 行中需要该目标模块时,才 对它进行链接。其优点是便 于修改和更新,便于实现对 目标模块的共享。

二. 程序的装入

1. 绝对装入:编译时就得到了绝对地址,按照编译时得到的绝对地址装入
  • 只适用于单道程序环境
2. 静态重定位装入:根据内存的当前情况,将装入模块装入内存的合适位置,地址转换通常在装入时一次完成;
  • 装入时,必须为程序分配全部空间,分配后,进程不能在内存中移动,不能再申请额外内存;
3. 动态重定位装入:程序可以在内存中移动,装入程序将目标模块装入内存后,不会直接将其逻辑地址转换为物理地址,而是在程序真正运行时去进行地址转换,地址转换的过程通过一个重定位寄存器来完成

后续的内存管理方式的基础,虚拟内存的基础

  • 可以使用不连续的内存
  • 只需要将程序的部分代码放入内存就可以启动程序
  • 可以动态的调整程序的内存大小

内存保护

我们知道进程是不能直接访问别的进程的资源的,进程也不能直接访问内存的资源,这是因为操作系统提供了一个内存保护机制,一般内存保护的实现方式有两种:

1. 上下界寄存器:

CPU在执行进程的指令去访问一个地址时,会通过上下限寄存器判断当前访问的这个地址是否越界



2. 界地址寄存器+重定位寄存器:

界地址寄存器存放最大的逻辑地址,重定位寄存器存放最小的物理地址,CPU在访问内存地址时,通过重定位寄存器判断地址是否越界,如果没有越界则结合重定位寄存器将逻辑地址转换为物理地址访问;

逻辑地址0-179的进程(物理地址是100-279)

覆盖与交换

内存的扩充:

  1. 覆盖
  2. 交换
  3. 虚拟内存(后面细说)

覆盖:

将内存分成一个固定区和若干个覆盖区,将进程分成多个段,需要常驻在内存的进程段放入固定区,不常用的段放在覆盖区,需要时调入,不需要时调出;不能同时执行的程序段共享一块内存区域

  • 覆盖区需要程序员指定,增加了编程负担,只用于早期的计算机系统,现在已经成为历史

交换:

交换技术的思想:内存中被阻塞展示得不到CPU运行的进程调出内存至外存中,再将外存可以运行的进程调入内存(中级调度)

  • 将磁盘分成文件区对换区,对换区存放被换出的进程数据,对换区只占磁盘的很小部分,追求IO速度,一般采用的是连续分配方式;而文件区追求的是磁盘利用率,采用离散分配方式;

内存分配方式:

连续内存分配:

1. 单一连续:

将内存分成系统区和用户区,用户区只能同时运行一个用户进程;

  • 无外部碎片,有内部碎片,内存利用率低
  • 单用户,单任务的操作系统

2. 固定连续:

将用用户空间的内存分为多个固定大小的内存,每一个可以小的内存分区可以运行一个进程,分区可以是内存相等也可以是内存不相等的;
操作系统建立一张分区说明表用来对分区内存进行管理

  • 会产生内部碎片
  • 当进程太大或者太小时就合适了,灵活性低

3. 动态连续:

不会事先将用户内存划分好,根据进程的大小,动态的划分内存;

  • 会产生外部碎片,原本的大进程撤销了再放入一个小进程,多出来的内存就是外部碎片

非连续内存分配:

1. 页式内存管理:

将用户进程分成多个页,将内存分成多个块,页和块大小相等,通过页表一一对应;

地址转换:

在分页存储管理中,逻辑地址由页号页内偏移量构成;

  • 页表:操作系统为每一个进程都创建了一个页表,页表用来建立页号与块号的一一对应关系;
  • 页表寄存器:操作系统会将进程的页表信息(页表地址,页表长度)存放到PCB中,在切换到该进程时,会将PCB的页表信息交给页表寄存器,用于运行时的地址转换;
  1. 页表越界中断判断;
  2. 页表寄存器结合页号计算出当前页的页表项,找出对应的块号页表项地址 = 页表始址+页表长度*页号
  3. 找到对应的块始址,块始址+偏移量就是物理地址
快表TLB:Translation Lookaside Buffer

一个比内存访问速度快很多的高速缓冲存储器,用来缓存已经进行过地址转换的页表项,用于加速地址转换的过程,内存中的页表称为慢表,具有快表的地址转换过程如下

如果没有快表,每一次地址转换都需要访问两次内存,一次查页表项,一次查物理地址

二级页表:

上面的页表是一级页表,一个进程的页表必须连续的存放在内存中,如果页表过长,那么就会占用很大一段连续的内存,这就违背了非连续存储管理的思想,可以为一级页表再创建一个页表非连续的存放一级页表的内容;这个页表称为页目录表

二级页表的逻辑地址结构:
二级页表的地址转换

首先通过一级页号在页目录表中查找对应的二级页表的内存,在通过二级页号和二级页表查找到对应的实际内存块;

  • 二级页表解决了一级页表过长占用大量的连续内存的问题;
  • 但是二级页表需要三次访问操作,一级页表只需要两次访存,时间换空间;

2. 段式内存管理:

将程序按照逻辑分成多个段,比如:主程序段,子程序段,数据段;将内存也分成多个段,要求段内的内存连续,段和段之间的内存不连续;操作系统系统通过段表建立进程段和内存段之间的关联;

分段存储的逻辑地址:
段表:
  • 因为每一个段的长度是不一样的,所以需要记录每一个段的长度
地址转换过程:
  • 进程开始运行:从PCB将当前进程的段表始址段表长度恢复给段表寄存器
  • 界地址寄存器对段号进行越界中断判断
  • 查找段表项,段表始址+段表长*段号,得到段始址
  • 段始址+段内偏移量 = 物理地址
分页和分段的对比:
  • 分页对用户不可见,分段对用户可见,用户需要指定各个段
  • 分段存储有利于数据的共享,分页内存利用率高
  • 分页无外部碎片,分段有外部碎片
  • 在不使用快表的情况下,分页和分段都需要两次访存

3. 段页式内存管理:

结合分段和分页方式的优点,先将进程按照逻辑分成段,在将每一个段分成多个页,将内存分成多个块,建立一张段表和多张页表,保证页和块的一一对应;

段页式管理的逻辑地址:
地址转换过程:
  1. 段号越界中断判断
  2. 段表寄存器+段号找到段表项,即可找到对应的页表地址,页表长度
  3. 页号*页表长度+页表始址得到块号
  4. 块号+页内偏移量 = 物理地址

虚拟内存:

传统内存分配方式的缺点:
  • 需要一次性将进程全部装入内存,这样会导致两个问题:
  1. 当进程过大无法装入内存运行
  2. 内存的利用率非常的低

局部性原理:

  1. 时间局部性:当一条指令被执行后,在一段时间内,这条指令很可能再次被执行,当访问了一个数据后,一段时间内这个数据很可能再次被访问;
  2. 空间局部性:一旦访问了某一块内存,这个块内存附近的内存也很大可能被访问;

CPU的高速缓存就是基于局部性原理思想实现的

虚拟内存:

基于局部性原理,我们在装入进程到内存的时候可以只将进程的启动那部分代码装入内存,就可以启动进程,操作系统提供请求调页页面置换功能;

  • 请求调页:当进程在运行过程中需要使用外存的数据时,系统会将外存的页调入到内存中
  • 页面置换:当内存不足时,需要通过页面置换算法选择暂时不用的页放到外存;

请求分页存储管理方式(分页+虚拟内存):

  • 页表:
缺页中断机制:

当访问一个页时,会通过页表的状态位判断当前这个页是否在内存中,如果不在,会触发一个缺页中断,内核会阻塞当前进程,执行IO操作,将外存的页调入到内存,如果内存不够,会通过页面置换算法置换出一个页;

地址转换过程:

整体的地址转换和分页存储差不多,说一下不同的地方:

  • 当找到对应的页表项时会判断当前页是否在内存中,如果不在,触发缺页中断,内核会从外存进行IO操作,将页从外存调入内存,如果内存不够,还会进行页面置换

页面置换算法:

另一篇文章有详述
  1. 最佳置换 :
  2. 先进先出
  3. LRU
  4. 时钟算法
  5. 改进的时钟算法

你可能感兴趣的:(王道操作系统—内存篇)