【1】前言

内存管理是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。

内存管理是操作系统很重要的一部分。作为一个后端开发来说,了解操作系统是如何进行内存管理是必须要的。

【2】读前须知

在看正文之前,我们需要先了解一些相关的知识点帮助接下来的理解。

内存和外存

内存(RAM ) 可以大致理解为在市面上看到的内存条,通常有4G、16G等容量。有着易失性、容量较小、价格偏贵
读写很快得特点。这里需要和 CPU 的一二三级缓存分开理解哦。

外存 就大家所知的磁盘、硬盘等外存设备。有着容量大、价格便宜、随机读写较慢、顺序写速度接近内存、可持久化保持数据等特点。

物理内存

物理内存其实就是内存,但是不是内存的容量大小操作系统都能使用到。比如 32 位的操作系统寻找空间理论上最大只有4G, 除去系统所占可能能使用的只有3.75G。所以不要闹出在 32 位操作系统上使用大于 4G的笑话哦,不过钱多当我没说。

其对应的空间地址称之为:物理内存地址(Physical Memory Address)。

虚拟内存

实际上我们写的程序是没有直接使用到物理内存的,还是使用虚拟内存。操作系统为每个进程分配独立的一套虚拟地址,并提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。

对应的空间地址称之为:虚拟内存地址(Virtual Memory Address)。

为什么需要做一层中间映射是因为:

  1. 进程空间不隔离,没有权限保护。进程间可以相互读写一份地址,造成安全、无法运行等问题。
  2. 程序运行时候的地址不确定,随机分配。
  3. 内存利用率低,容易造成内存碎片。

进程持有的虚拟地址是通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存。

【3】正文

在了解上面的知识背景后,我们知道进程是需要通过虚拟内存来联系到物理内存。接下来我们需要开始了解虚拟内存和物理内存是怎么映射起来的。

一般会有分段、分页、段页混用三种方式,他们各有优劣。

分段

分段(Segmentation)中程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的。

所以说,若运行多个同一个进程它们是可以共用代码分段的,因为可以配置双基址:指令和数据各自配置基址。下面会在讲解。

首先我们先贴一张图,看着图讲解。

在这里插入图片描述

如上图所示,在分段模式下虚拟内存地址由段选择因子和段内偏移量构成。

段选择因子是存储在段寄存器中,其包含了段号(可以理解为段表的ID)。对应的段表中则保存了该虚拟地址对应的物理地址的基地址、偏移量和特权等级等。

段内偏移量可以取0和用户态最大可用段号之间,一个进程可使用的物理地址范围就是基地址 + 段内偏移量。

上面提到为什么使用虚拟内存技术的原因,结合上面的图文现在可以解决空间隔离和程序运行时地址不确定两点,但是分段还是比较大的一块块连续的地址,所以没有解决碎片化问题。

比如现在有 1G 可用的物理内存,这时候进程 A 使用了一半 512MB;进程 B 使用了 128MB; 进程 C 使用了 256MB, 三个进程所使用的内存段是连续的。现在还剩下 128 M 可用。

这时候进程 B 运行完毕了退出释放内存,这时候 A C之间空了一段 128MB 的内存。我们需要启动一个新的进程 D 它需要200 MB 的内存。现在不管是使用A C 或 C 后的内存都不够了。

这时候就会使用到一种技术 内存交换(Swap)。系统分配给程序的空间分段映射的是一段连续的物理内存,所以空间不够时可以将程序倒到外置存储中,再寻找足够的内存空间加载。

把进程 C 内存数据暂时复制到硬盘,给进程 D 分配好内存后在把 C 复制到内存中。这个复制不是一下子全部复制,是分段多次复制。

所以这种情况就会有这些问题:1,多次交换之后内存碎片会越来越多;2,交换过程涉及到外置存储,速度比内存低很多。3,单一程序不能超过物理内存空间。

所以为了解决内存分段的内存碎片和内存交换效率低的问题,就出现了内存分页方式。

分页

分页是为了解决内存分段的内存碎片和内存交换效率低的问题。

分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为 4KB(是不是以前听过4k对齐)。可使用 getconf PAGE_SIZE 看到你的操作系统当前页大小哦。

我们还是先看图。

在这里插入图片描述

如上图所示虚拟地址与物理地址之间通过页表来映射,页表存储在 CPU 的内存管理单元 (MMU) 中,于是 CPU 就可以直接通过 MMU,找出要实际要访问的物理内存地址。

当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。其实就是触发了一个中断。

如上图内存空间都是预先划分好的而且页大小默认都不大,释放的内存都是以页为单位释放。所有一定程度上减少了内存碎片。当内存不够时候也是安页为单位进行交换,所以相对于分段交换效率一定程度有提高。

这时候大家有没有想过,内存是按页划分且页表是每个进程都有一份自己的页表。那多进场下光页表占用的空间也是不小的花费。

所以这时候会采用多级页表(Multi-Level Page Table)和 TLB(Translation Lookaside Buffer) ,通常称为页表缓存、转址旁路缓存、快表等。这里大家有兴趣自行查阅了。

段页

其实有时候也会把分页和分段结合起来,通常称为段页式内存管理。实现方式大致是:

  1. 将程序划分为多个有逻辑意义的段,就是分段机制;
  2. 把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;

所以这时候虚拟内存地址就由段号、段内页号和页内位移三部分组成。下面的图我偷个懒大致画画。

在这里插入图片描述

所以变成来先得到页表起始地址,得到物理页号,最后将物理页号与页内位移组合,得到物理地址。

【4】Linux 内存管理

终于来到本文的最终目的,Linux是怎么实现内存管理的。其实这个和 CPU 的发展历程有关,感兴趣自行查阅。大致总结下来就是开始使用的是段式内存管理,后面由段式内存管理把逻辑地址映射成线性地址,然后再由页式内存管理将线性地址映射成物理地址。

所以Linux 内存主要采用的是页式内存管理,同时也使用了段机制。每个段都是从 0 地址开始的整个用户态最大可用,所有的段的起始地址都是一样的。

意味着,Linux 系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。

在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,上面的图中能够体现到。不同位数的系统,地址空间的范围也不同。

常见的 32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间;64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。

尽管每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。

【5】总结

1)操作系统并没有直接使用物理内存,还是实现了和物理内存映射的虚拟内存。
2)虚拟地址与物理地址的映射可分为分段、分页或段页等管理实现。
3)使用多级页表来避免页表过大的问题。
4)Linux 系统主要采用了分页管理,当然也有分段管理。
5)Linxu 系统中虚拟空间分布可分为用户态和内核态两部分,其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。