Linux 内存管理 | 地址映射:分段、分页、段页


Linux 内存管理 | 物理内存管理:内存碎片、伙伴系统、slab分配器
Linux 内存管理 | 虚拟内存管理:虚拟内存空间、虚拟内存分配

在前两篇博客中,我介绍了虚拟内存与物理内存的管理方式,那么对于操作系统来说,它是如何管理它们两个之间的关系的呢?如何进行地址的映射呢?

在早期的计算机中,程序是直接运行在物理内存上的,所以其通常都会面临以下几种问题

  1. 地址空间不隔离
  2. 内存使用效率低
  3. 程序运行的地址不确定

为了解决这些问题,我们又引入了虚拟内存这个概念,但是虚拟内存是如何解决这个问题的呢?它与物理内存又是如何进行映射的呢?这就是本篇博客所要讲的内容。

对于操作系统来说,通常解决这个问题的方式有三种,一种是内存分页,另一种是内存分段,以及两者相结合的段页式


分段

在最开始时,人们采用的是分段的方法,为了简化地址管理,所以将虚拟内存空间中的虚拟内存按照其逻辑划分为代码段、数据段、堆段、栈段几部分
在这里插入图片描述
通过段寄存器中的段表,来将虚拟地址与物理地址进行映射。段表中存储了每一个逻辑段的段号对应的物理内存的起始地址
在这里插入图片描述
对于每一个在虚拟内存中存储的数据,其虚拟地址都以其所在的段号以及段内偏移组成。

因此虚拟地址与物理地址的转换方式如下

  1. 根据虚拟地址中的段号查询段表
  2. 根据段表查询到对应的段的物理内存起始地址
  3. 物理内存起始地址加上段内偏移,即为其对应的物理地址

在这里插入图片描述
如上图,例如变量A段号为2,段内偏移为500。首先根据段号查询段表,得知物理内存起始地址位于3000的位置,接着找到对应的起始地址,加上段内偏移500,此时3500的位置即为其对应的物理地址。

通过分段的方式,我们解决了上面所说的问题1和问题3,但是对于内存的使用效率,分段仍然存在以下两个问题

  1. 内存碎片
  2. 内存交换的效率低

为什么会存在内存碎片的问题呢?

在上面的讲解中可以看出,在分段存储中,一个段内可能保存有多个变量,而这些变量都是从同一个物理地址起始位置开始偏移。因此在物理内存中,同一个段中的数据使用了连续的地址空间

例如我们有1G的物理内存,倘若我们运行了512M的程序A,接着运行了128M的程序B,128M的程序C。剩余内存为256M
在这里插入图片描述
倘若我们此时结束程序B,释放内存,此时总剩余空间为384M
在这里插入图片描述
倘若我们此时需要运行300M的进程D,但是这时候就会因为剩余空间不连续,导致我们的程序无法运行,这也就是我们常说的内存外碎片问题。

那么如何解决这个问题呢?这就会使用到内存交换。例如上面那种情况,我们就会将程序C写入硬盘的SWAP分区(交换分区,用于内存和硬盘的空间交换)。紧接着再将其从硬盘中读取回来,让其紧挨着程序A的那块内存,这样就能保证后面的空闲内存都是连续的了。
在这里插入图片描述

为什么内存交换的效率低呢?

由于分段对物理内存的映射是以程序为单位,按照其逻辑进行分段映射,如果我们的内存不足,那么被换入换出到硬盘中的都是整个程序,这样就必然会造成大量的磁盘访问操作,总所周知,磁盘IO的速度特别慢,因此就会严重影响我们的访问速度。

根据程序的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁地用到了一小部分数据,也就是说程序中的很多数据其实在一个时间段内都是不会被用到的。

而我们分段的最大问题就在于其以程序为单位进行映射,因此我们只需要使用更小粒度的存储单位,就可以解决这个问题,大大的提升内存的使用率。因此在后续的设计中,就以作为基本单位,这也就是分页机制的由来


分页

分页就是将内存空间人为地划分成固定大小的页,每一页地大小由硬件决定,在Linux中,一页是4KB

与段表类似,虚拟地址与物理地址的映射是通过MMU(内存管理单元)中的页表来完成的。
在这里插入图片描述
页表中不仅保存了页号,物理内存地址,还保留了该物理页的访问权限,用以实现对页的访问控制

在分页机制下,虚拟地址由页号以及页内偏移组成

因此在分页机制下,虚拟地址与物理地址的转换方式如下

  1. 根据虚拟地址中的页号查询页表
  2. 根据页表查询到对应的页的物理内存起始地址
  3. 物理内存起始地址加上页内偏移,即为其对应的物理地址

如下图
在这里插入图片描述
当进程需要访问物理地址时,此时CPU就会通过MMU中的页表,来找到对应的物理地址。

讲了这么多,再次回到之前的问题,分页是如何解决分段的内存利用率低的问题的呢?

主要就是依靠以下两方面来完成的

1、使用更低粒度的内存单位
分段所面临的最大问题,无非就是内存碎片以及交换效率低。

导致内存碎片最大的原因就是各个逻辑段的数据需要连续存储,而逻辑段又过大,导致我们需求大量的连续空间。而当我们所有的内存分配释放都以页为单位时,就能够很好的解决这个问题了。

而当内存空间不够时,我们需要进行将内存中的数据暂时写入到硬盘中,之后再重新写回来这样的换入换出操作。而使用页为单位后,即使我们还是需要进行磁盘IO,但是由于我们交换的容量仅仅只有几个页,所以也不会花费过多的时间。

2、不需要将程序一次性加载进内存,什么时候需要,什么时候加载。
按照前面说的,为了满足程序的局部性原理。所以为了能够尽可能提高内存的利用率,在建立了虚拟内存空间后并不会直接分配物理内存,而是在我们程序运行中需要用到的时候,再将其加载进内存中。

所以如果在页表中查找不到时,此时就会由内核的请求分页机制产生缺页中断,然后进入内核态中分配物理内存、更新进程页表,最后再返回用户态,恢复进程的运行。


在上面所介绍的页表中,有一个非常致命的缺点,就是空间占用大

在 Linux中,可以并发的执行多个进程,而每个进程都有其自己的虚拟内存空间,那么也自然都有自己独有的页表。在32位Linux系统下,我们的虚拟内存空间的大小为4G,而每页的大小为4K,这也就意味着我们至少有2^20个内存页,倘若每个页表项为4Byte,那么每个页表大小也至少为4M

倘若我们此时并发了两百个进程,那么占用则高达800M,即使是在现在,这个数字也是非常庞大的,因为并发数百个进程是非常常见的情况,更别提64位的操作系统,随着寻址范围的增加,页表将更为庞大。

为了解决这个问题,就引入了多级页表


多级页表

我们将一级页表再进行分页,分成1024个二级页表,并且每个二级页表中存有1024个页表项,形成如下的二级分页的结构。
在这里插入图片描述
虽然分级乍一看花费的物理内存变多了,但是实际上对于大多数程序来说,其使用到的空间远未达到 4G,所以会存在部分对应的页表项都是空的,根本没有分配。而对于已分配的页表项,如果存在最近一定时间未访问的页表,在物理内存紧张的情况下,操作系统会将页面换出到硬盘,也就是说不会占用物理内存。

如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。假设每个二级页表大小为4M(1024 * 4K),而我们用到的一级页表只有20%

在这种情况下,页表所占用的物理内存就只有4K + 20% * 4M,即0.804M,比起只用了一级页表的4M,大大的节约了内存。

而在64位系统中,两级页表是肯定不够用的,因此又演变成了四级目录

  • 全局页目录项 PGD
  • 上层页目录项 PUD
  • 中间页目录项 PMD
  • 页表项 PTE

结构如下图所示
在这里插入图片描述


快表(TLB)

多级页表虽然解决了空间占用大的问题,但是由于其复杂化了地址的转换,因此也带来了大量的时间开销,使得地址转换速度减慢。

如果要解决这个问题,那么最简单的方式就是降低查询页表的频率,那么如何实现呢?这时候就需要用到缓存的技术

与我之前在Redis系列博客中所提到的,对于热点资源,我们可以将其提前缓存下来,到以后使用时就可以直接到缓存中查找。对于操作系统来说,也是这么一个道理。

在操作系统中,这个缓存就是CPU中的TLB,也就是我们通常所说的快表。我们将最常访问的几个页表项存储到TLB中,在之后进行寻址时,CPU就会先到TLB中进行查找,如果没有找到,这时才会去查询页表。


段页式

虽然分段和分页各有优缺点,但他们直接并不是对立的,所以如今大部分的内存管理方式,都是将分段与分页相结合,也就是我们常说的段页式

它的原理非常简单,就是先对虚拟内存空间进行分段管理,然后再对每一个段进行分页管理。如下图
在这里插入图片描述
所以此时的虚拟地址结构,就由段号、段内页号、页内偏移所组成。此时对于每个进程来说,都会建立一个段表,而对于段表中的每一个段,又会再分别建立一个页表,如下图
在这里插入图片描述
所以此时的虚拟地址转换为物理地址,就需要以下三个步骤

  1. 访问段表,得到页表的起始地址
  2. 访问页表,得到物理页的起始地址
  3. 访问物理页,加上页内偏移,得到实际的物理地址

这种方法虽然增加了系统开销以及硬件成本,但是内存的利用率得到了巨大的提升。


Linux

由于硬件问题的限制,Linux 内存主要采用的是页式内存管理,但同时也不可避免地涉及了段机制。

在往常的机制中,地址的转换流程如下
在这里插入图片描述
但是在Linux中,并没有逻辑地址这一说(所有段起始地址相同),因为其将段机制进行了弱化,此时段只用于进行访问控制以及内存保护

Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下),也就是所有的段的起始地址都是一样的。
这意味着,Linux系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。

猜你喜欢

转载自blog.csdn.net/qq_35423154/article/details/111084633
今日推荐