【操作系统】内存管理

在这里插入图片描述

一、虚拟内存

单⽚机是没有操作系统的,所以每次写完代码,都需要借助⼯具把程序烧录进去,这样程序才能跑起来。

另外,单片机的 CPU 是直接操作内存的「物理地址」

在这里插入图片描述

在这种情况下,要想在内存中同时运⾏两个程序是不可能的。如果第⼀个程序在 2000 的位置写⼊⼀个新的值,将会擦掉第⼆个程序存放在相同位置上的所有内容,所以同时运⾏两个程序是根本行不通的,这两个程序会⽴刻崩溃。

1、操作系统是如何解决这个问题呢?

关键的问题是这两个程序都引⽤了绝对物理地址,⽽这正是我们最需要避免的。

我们可以把进程所使⽤的地址「隔离」开来,即让操作系统为每个进程分配独⽴的⼀套「虚拟地址」,⼈⼈都有,⼤家⾃⼰玩⾃⼰的地址就⾏,互不⼲涉。但是有个前提每个进程都不能访问物理地址,⾄于虚拟地址最终怎么落到物理内存⾥,对进程来说是透明的,操作系统已经把这些都安排的明明⽩⽩了。

在这里插入图片描述

操作系统会提供⼀种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。

如果程序要访问虚拟地址的时候,由操作系统转换成不同的物理地址,这样不同的进程运⾏的时候,写⼊的是不同的物理地址,这样就不会冲突了。

于是,这⾥就引出了两种地址的概念:

  • 程序所使⽤的内存地址叫做虚拟内存地址(Virtual Memory Address)
  • 实际存在硬件⾥⾯的空间地址叫物理内存地址(Physical Memory Address)

操作系统引⼊了虚拟内存,进程持有的虚拟地址会通过 CPU 芯⽚中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存,如下图所示:

在这里插入图片描述

2、操作系统是如何管理虚拟地址与物理地址之间的关系?

主要有两种⽅式,分别是内存分段和内存分⻚。

二、内存分段

程序是由若⼲个逻辑分段组成的,可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就⽤分段(Segmentation)的形式把这些段分离出来。

1、分段机制下,虚拟地址和物理地址是如何映射的?

分段机制下的虚拟地址由两部分组成,段选择⼦和段内偏移量。

在这里插入图片描述

  • 段选择⼦就保存在段寄存器⾥⾯。段选择⼦⾥⾯最重要的是段号,⽤作段表的索引。段表⾥⾯保存的是这个段的基地址、段的界限和特权等级等。
  • 虚拟地址中的段内偏移量应该位于 0 和段界限之间, 如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。

虚拟地址是通过段表与物理地址进⾏映射的,分段机制会把程序的虚拟地址分成 4 个段,
每个段在段表中有⼀个项,在这⼀项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址,如下图:

在这里插入图片描述

如果要访问段 3 中偏移量 500 的虚拟地址,我们可以计算出物理地址为,段 3 基地址 7000 + 偏移量 500= 7500。

分段的办法很好,解决了程序本身不需要关⼼具体的物理内存地址的问题,但它也有⼀些不⾜之处:

  • 内存碎⽚
  • 内存交换的效率低

2、分段为什么会产生内存碎片的问题?

假设有 1G 的物理内存,⽤户执⾏了多个程序,其中:

  • 游戏占⽤了 512MB 内存
  • 浏览器占⽤了 128MB 内存
  • ⾳乐占⽤了 256 MB 内存

这个时候,如果我们关闭了浏览器,则空闲内存还有 1024 - 512 - 256 = 256MB。

如果这个 256MB 不是连续的,被分成了两段 128 MB 内存,这就会导致没有空间再打开⼀个 200MB 的程序。

在这里插入图片描述

这⾥的内存碎⽚的问题共有两处地⽅:

  • 外部内存碎⽚,也就是产⽣了多个不连续的小物理内存,导致新的程序⽆法被装载;
  • 内部内存碎⽚,程序所有的内存都被装载到了物理内存,但是这个程序有部分的内存可能并不是很常使⽤,这也会导致内存的浪费;

针对上⾯两种内存碎⽚的问题,解决的⽅式会有所不同:

解决外部内存碎⽚的问题就是内存交换。

**可以把⾳乐程序占⽤的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存⾥。**不过再读回的时候,我们不能装载回原来的位置,⽽是紧紧跟着那已经被占⽤了的 512MB 内存后⾯。这样就能空缺出连续的 256MB 空间,于是新的 200MB 程序就可以装载进来。
这个内存交换空间,在 Linux 系统⾥,也就是我们常看到的 Swap 空间,这块空间是从硬盘划分出来的,⽤于内存与硬盘的空间交换。

3、分段为什么会导致内存交换效率低的问题?

对于多进程的系统来说,⽤分段的⽅式,内存碎⽚是很容易产⽣的,产⽣了内存碎⽚,那不得不重新Swap 内存区域,这个过程会产⽣性能瓶颈。

**因为硬盘的访问速度要⽐内存慢太多了,**每⼀次内存交换,我们都需要把⼀⼤段连续的内存数据写到硬盘上。

所以,如果内存交换的时候,交换的是⼀个占内存空间很⼤的程序,这样整个机器都会显得卡顿。

为了解决内存分段的内存碎⽚和内存交换效率低的问题,就出现了内存分⻚。

三、内存分页

分段的好处就是能产⽣连续的内存空间,但是会出现内存碎⽚和内存交换的效率太低的问题。

要解决这些问题,那么就要想出能少出现⼀些内存碎⽚的办法。另外,当需要进⾏内存交换的时候,让需要交换写⼊或者从磁盘装载的数据更少⼀点,这样就可以解决问题了。这个办法,也就是内存分⻚(Paging)。

分⻚是把整个虚拟和物理内存空间切成⼀段段固定尺⼨的⼤⼩。 这样⼀个连续并且尺⼨固定的内存空间,我们叫⻚(Page)。在 Linux 下,每⼀⻚的⼤⼩为 4KB 。虚拟地址与物理地址之间通过⻚表来映射,如下图:

在这里插入图片描述

⻚表是存储在内存⾥的,内存管理单元 (MMU)就做将虚拟内存地址转换成物理地址的⼯作。

⽽当进程访问的虚拟地址在⻚表中查不到时,系统会产⽣⼀个缺⻚异常,进⼊系统内核空间分配物理内存、更新进程⻚表,最后再返回⽤户空间,恢复进程的运⾏。

1、分页是怎么解决分段的内存碎片、内存交换效率低的问题?

由于内存空间都是预先划分好的,也就不会像分段会产⽣间隙⾮常⼩的内存,这正是分段会产⽣内存碎⽚的原因。⽽采⽤了分⻚,那么释放的内存都是以⻚为单位释放的,也就不会产⽣⽆法给进程使⽤的⼩内存。

如果内存空间不够,操作系统会把其他正在运⾏的进程中的「最近没被使⽤」的内存⻚⾯给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。⼀旦需要的时候,再加载进来,称为换⼊(Swap In)。所以,⼀次性写⼊磁盘的也只有少数的⼀个⻚或者⼏个⻚,不会花太多时间,内存交换的效率就相对⽐较⾼。
在这里插入图片描述

分⻚的⽅式使得我们在加载程序的时候,不再需要⼀次性都把程序加载到物理内存中。 我们完全可以在进⾏虚拟内存和物理内存的⻚之间的映射之后,并不真的把⻚加载到物理内存⾥,⽽是只有在程序运⾏中,需要⽤到对应虚拟内存⻚⾥⾯的指令和数据时,再加载到物理内存⾥⾯去。

2、分⻚机制下,虚拟地址和物理地址是如何映射的?

在分⻚机制下,虚拟地址分为两部分,⻚号和⻚内偏移。**⻚号作为⻚表的索引,⻚表包含物理⻚每⻚所在物理内存的基地址,**这个基地址与⻚内偏移的组合就形成了物理内存地址,⻅下图。

在这里插入图片描述

总结⼀下,对于⼀个内存地址转换,其实就是这样三个步骤:

  • 把虚拟内存地址,切分成⻚号和偏移量;
  • 根据⻚号,从⻚表⾥⾯,查询对应的物理⻚号;
  • 接拿物理⻚号,加上前⾯的偏移量,就得到了物理内存地址。

举例说明,虚拟内存中的页通过⻚表映射为了物理内存中的页,如下图所示:

在这里插入图片描述

3、简单的分页有什么缺陷吗?

有空间上的缺陷。

因为操作系统是可以同时运⾏⾮常多的进程的,那这不就意味着⻚表会⾮常的庞⼤。
在 32 位的环境下,虚拟地址空间共有 4GB,假设⼀个⻚的⼤⼩是 4KB(2^12),那么就需要⼤约 100 万(2^20) 个⻚,每个「⻚表项」需要 4 个字节⼤⼩来存储,那么整个 4GB 空间的映射就需要有 4MB的内存来存储⻚表。

这 4MB ⼤⼩的⻚表,看起来也不是很⼤。但是要知道每个进程都是有⾃⼰的虚拟地址空间的,也就说都有⾃⼰的⻚表。那么,100 个进程的话,就需要 400MB 的内存来存储⻚表,这是⾮常⼤的内存了。

4、多级页表

要解决上⾯的问题,就需要采⽤⼀种叫作多级⻚表(Multi-Level Page Table)的解决⽅案。

对于单⻚表的实现⽅式,在 32 位和⻚⼤⼩ 4KB 的环境下,⼀个进程的⻚表需要装
下 100 多万个「⻚表项」,并且每个⻚表项是占⽤ 4 字节⼤⼩的,于是相当于每个⻚表需占⽤ 4MB ⼤⼩的空间。

我们把这个 100 多万个「⻚表项」的单级⻚表再分⻚,将⻚表(⼀级⻚表)分为 1024 个⻚表(⼆级⻚表),每个表(⼆级⻚表)中包含 1024 个「⻚表项」,形成⼆级分⻚。如下图所示:

在这里插入图片描述

分了⼆级表,映射 4GB 地址空间就需要 4KB(⼀级⻚表)+ 4MB(⼆级⻚表)的内存,这样占⽤空间不是更⼤了吗?

当然如果 4GB 的虚拟地址全部都映射到了物理内存上的话,⼆级分⻚占⽤空间确实是更⼤了,但是,我们往往不会为⼀个进程分配那么多内存。

每个进程都有 4GB 的虚拟地址空间,⽽显然对于⼤多数程序来说,其使⽤到的空间远未达到 4GB,因为会存在部分对应的⻚表项都是空的,根本没有分配,对于已分配的⻚表项,如果存在最近⼀定时间未访问的⻚表,在物理内存紧张的情况下,操作系统会将⻚⾯换出到硬盘,也就是说不会占⽤物理内存。

如果使⽤了⼆级分⻚,⼀级⻚表就可以覆盖整个 4GB 虚拟地址空间,但如果某个⼀级⻚表的⻚表项没有被⽤到,也就不需要创建这个⻚表项对应的⼆级⻚表了,即可以在需要时才创建⼆级⻚表。做个简单的计算,假设只有 20% 的⼀级⻚表项被⽤到了,那么⻚表占⽤的内存空间就只有 4KB(⼀级⻚表) + 20% *

4MB(⼆级⻚表)= 0.804MB ,这对⽐单级⻚表的 4MB 是不是⼀个巨⼤的节约?
那么为什么不分级的⻚表就做不到这样节约内存呢?我们从⻚表的性质来看,保存在内存中的⻚表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在⻚表中找不到对应的⻚表项,计算机系统就不能⼯作了。所以⻚表⼀定要覆盖全部虚拟地址空间,不分级的⻚表就需要有 100 多万个⻚表项来映射,⽽⼆级分⻚则只需要 1024 个⻚表项(此时⼀级⻚表覆盖到了全部虚拟地址空间,⼆级⻚表在需要时创建)。

我们把⼆级分⻚再推⼴到多级⻚表,就会发现⻚表占⽤的内存空间更少了,这⼀切都要归功于对局部性原理的充分应⽤。

对于 64 位的系统,两级分⻚肯定不够了,就变成了四级⽬录,分别是:

  • 全局⻚⽬录项 PGD(Page Global Directory);
  • 上层⻚⽬录项 PUD(Page Upper Directory);
  • 中间⻚⽬录项 PMD(Page Middle Directory);
  • ⻚表项 PTE(Page Table Entry);

在这里插入图片描述

4、TLB

多级⻚表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了⼏道转换的⼯序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。

程序是有局部性的,即在⼀段时间内,整个程序的执⾏仅限于程序中的某⼀部分。相应地,执⾏所访问的存储空间也局限于某个内存区域。

在这里插入图片描述

我们就可以利⽤这⼀特性,把最常访问的⼏个⻚表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯⽚中,加⼊了⼀个专⻔存放程序最常访问的⻚表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为⻚表缓存、转址旁路缓存、快表等。

在这里插入图片描述

在 CPU 芯⽚⾥⾯,封装了内存管理单元(Memory Management Unit)芯⽚,它⽤来完成地址转换和 TLB的访问与交互。

有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的⻚表。TLB 的命中率其实是很⾼的,因为程序最常访问的⻚就那么⼏个。

四、段页式内存管理

内存分段和内存分⻚并不是对⽴的,它们是可以组合起来在同⼀个系统中使⽤的,那么组合起来后,通常称为段⻚式内存管理。

在这里插入图片描述

段⻚式内存管理实现的⽅式:

  • 先将程序划分为多个有逻辑意义的段,也就是前⾯提到的分段机制;
  • 接着再把每个段划分为多个⻚,也就是对分段划分出来的连续空间,再划分固定⼤⼩的⻚;

这样,地址结构就由段号、段内⻚号和⻚内位移三部分组成。

⽤于段⻚式地址变换的数据结构是每⼀个程序⼀张段表,每个段⼜建⽴⼀张⻚表,段表中的地址是⻚表的起始地址,⽽⻚表中的地址则为某⻚的物理⻚号,如图所示:

在这里插入图片描述

段⻚式地址变换中要得到物理地址须经过三次内存访问:

  • 第⼀次访问段表,得到⻚表起始地址;
  • 第⼆次访问⻚表,得到物理⻚号;
  • 第三次将物理⻚号与⻚内偏移量组合,得到物理地址。

可⽤软、硬件相结合的⽅法实现段⻚式地址变换,这样虽然增加了硬件成本和系统开销,但提⾼了内存的利⽤率。

整理自小林coding所著的《图解系统》,仅做学习用,侵删

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值