Linux虚拟内存-创新互联

问题
  1. 什么是虚拟内存地址 ?
  2. Linux 内核为啥要引入虚拟内存而不直接使用物理内存 ?
  3. 虚拟内存空间到底长啥样?
  4. 内核如何管理虚拟内存?
  5. 什么又是物理内存地址 ?如何访问物理内存?
什么是虚拟内存地址

举一个生活中的例子,比如大家在日常生活中给亲朋好友邮寄一些本地特产时,都会填写收件人地址以及寄件人地址。以及在日常网上购物时,都会在相应电商 APP 中填写自己的收获地址。

创新互联成立于2013年,是专业互联网技术服务公司,拥有项目网站制作、成都网站建设网站策划,项目实施与项目整合能力。我们以让每一个梦想脱颖而出为使命,1280元西充做网站,已为上家服务,为西充各地企业和个人服务,联系电话:13518219792

随后快递小哥就会根据我们填写的收货地址找到我们的真实住所,将我们网购的商品送达到我们的手里。

收货地址是用来定位我们在现实世界中真实住所地理位置的,而现实世界中我们所在的城市,街道,小区,房屋都是一砖一瓦,一草一木真实存在的。但收货地址这个概念模型在现实世界中并不真实存在,它只是人们提出的一个虚拟概念,通过收货地址这个虚拟概念将它和现实世界真实存在的城市,小区,街道的地理位置一一映射起来,这样我们就可以通过这个虚拟概念来找到现实世界中的具体地理位置。

综上所述,收货地址是一个虚拟地址,它是人为定义的,而我们的城市,小区,街道是真实存在的,他们的地理位置就是物理地址。

再比如今天北京叫北京,古代北京可能叫其他名字,所以虚拟地址是可变的,物理地址是永远不变的

我们还是以日常生活中的收货地址为例做出类比,我们都很熟悉收货地址的格式:xx省xx市xx区xx街道xx小区xx室,它是按照地区层次递进的。同样,在计算机世界中的虚拟内存地址也有这样的递进关系。
这里我们以 Intel Core i7 处理器为例,64 位虚拟地址的格式为:全局页目录项(9位)+ 上层页目录项(9位)+ 中间页目录项(9位)+ 页表项(9位)+ 页内偏移(12位)。共 48 位组成的虚拟内存地址。

在这里插入图片描述

32 位虚拟地址的格式为:页目录项(10位)+ 页表项(10位) + 页内偏移(12位)。共 32 位组成的虚拟内存地址。
在这里插入图片描述

进程虚拟内存空间中的每一个字节都有与其对应的虚拟内存地址,一个虚拟内存地址表示进程虚拟内存空间中的一个特定的字节。

为什么要使用虚拟地址访问内存

既然物理内存地址可以直接定位到数据在内存中的存储位置,那为什么我们不直接使用物理内存地址去访问内存而是选择用虚拟内存地址去访问内存呢?

假设现在没有虚拟内存地址,我们在程序中对内存的操作全都都是使用物理内存地址,在这种情况下,程序员就需要精确的知道每一个变量在内存中的具体位置,我们需要手动对物理内存进行布局,明确哪些数据存储在内存的哪些位置,除此之外我们还需要考虑为每个进程究竟要分配多少内存?内存紧张的时候该怎么办?如何避免进程与进程之间的地址冲突?等等一系列复杂且琐碎的细节。

如果我们在单进程系统中比如嵌入式设备上开发应用程序,系统中只有一个进程,这单个进程独享所有的物理资源包括内存资源。在这种情况下,上述提到的这些直接使用物理内存的问题可能还好处理一些,但是仍然具有很高的开发门槛。

然而在现代操作系统中往往支持多个进程,需要处理多进程之间的协同问题,在多进程系统中直接使用物理内存地址操作内存所带来的上述问题就变得非常复杂了。

比如我们现在有这样一个简单的 Java 程序。

public static void main(String[] args) throws Exception {
        string i = args[0];
        ..........
    }

在程序代码相同的情况下,我们用这份代码同时启动三个 JVM 进程,我们暂时将进程依次命名为 a , b , c 。
这三个进程用到的代码是一样的,都是我们提前写好的,可以被多次运行。由于我们是直接操作物理内存地址,假设变量 i 保存在 0x354 这个物理地址上。这三个进程运行起来之后,同时操作这个 0x354 物理地址,这样这个变量 i 的值不就混乱了吗? 三个进程就会出现变量的地址冲突。
在这里插入图片描述

所以在直接操作物理内存的情况下,我们需要知道每一个变量的位置都被安排在了哪里,而且还要注意和多个进程同时运行的时候,不能共用同一个地址,否则就会造成地址冲突。
现实中一个程序会有很多的变量和函数,这样一来我们给它们都需要计算一个合理的位置,还不能与其他进程冲突,这就很复杂了。

而虚拟内存的引入正是要解决上述的问题,虚拟内存引入之后,进程的视角就会变得非常开阔,每个进程都拥有自己独立的虚拟地址空间,进程与进程之间的虚拟内存地址空间是相互隔离,互不干扰的。每个进程都认为自己独占所有内存空间,自己想干什么就干什么。

系统上还运行了哪些进程和我没有任何关系。这样一来我们就可以将多进程之间协同的相关复杂细节统统交给内核中的内存管理模块来处理,极大地解放了程序员的心智负担。这一切都是因为虚拟内存能够提供内存地址空间的隔离,极大地扩展了可用空间。

在这里插入图片描述

这样进程就以为自己独占了整个内存空间资源,给进程产生了所有内存资源都属于它自己的幻觉,这其实是 CPU 和操作系统使用的一个障眼法罢了,任何一个虚拟内存里所存储的数据,本质上还是保存在真实的物理内存里的。只不过内核帮我们做了虚拟内存到物理内存的这一层映射,将不同进程的虚拟地址和不同内存的物理地址映射起来。
当 CPU 访问进程的虚拟地址时,经过地址翻译硬件将虚拟地址转换成不同的物理地址,这样不同的进程运行的时候,虽然操作的是同一虚拟地址,但其实背后写入的是不同的物理地址,这样就不会冲突了。

程序局部性原理表现为:时间局部性和空间局部性。时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某块数据被访问,则不久之后该数据可能再次被访问。空间局部性是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。
从程序局部性原理的描述中我们可以得出这样一个结论:进程在运行之后,对于内存的访问不会一下子就要访问全部的内存,相反进程对于内存的访问会表现出明显的倾向性,更加倾向于访问最近访问过的数据以及热点数据附近的数据。
根据这个结论我们就清楚了,无论一个进程实际可以占用的内存资源有多大,根据程序局部性原理,在某一段时间内,进程真正需要的物理内存其实是很少的一部分,我们只需要为每个进程分配很少的物理内存就可以保证进程的正常执行运转。

进程虚拟内存空间

上小节中,我们介绍了为了防止多进程运行时造成的内存地址冲突,内核引入了虚拟内存地址,为每个进程提供了一个独立的虚拟内存空间,使得进程以为自己独占全部内存资源。
那么这个进程独占的虚拟内存空间到底是什么样子呢?

本小节我们只讨论进程用户态虚拟内存空间的布局,我们先把内核态的虚拟内存空间当做一个黑盒来看待,在后面的小节中再来详细介绍内核态相关内容。

首先我们会想到的是一个进程运行起来是为了执行我们交代给进程的工作,执行这些工作的步骤我们通过程序代码事先编写好,然后编译成二进制文件存放在磁盘中,CPU 会执行二进制文件中的机器码来驱动进程的运行。所以在进程运行之前,这些存放在二进制文件中的机器码需要被加载进内存中,而用于存放这些机器码的虚拟内存空间叫做代码段。
在这里插入图片描述
在程序运行起来之后,总要操作变量吧,在程序代码中我们通常会定义大量的全局变量和静态变量,这些全局变量在程序编译之后也会存储在二进制文件中,在程序运行之前,这些全局变量也需要被加载进内存中供程序访问。所以在虚拟内存空间中也需要一段区域来存储这些全局变量。

  • 那些在代码中被我们指定了初始值的全局变量和静态变量在虚拟内存空间中的存储区域我们叫做数据段
  • 那些没有指定初始值的全局变量和静态变量在虚拟内存空间中的存储区域我们叫做 BSS 段。这些未初始化的全局变量被加载进内存之后会被初始化为 0 值
    在这里插入图片描述
    上面介绍的这些全局变量和静态变量都是在编译期间就确定的,但是我们程序在运行期间往往需要动态的申请内存,所以在虚拟内存空间中也需要一块区域来存放这些动态申请的内存,这块区域就叫做堆。注意这里的堆指的是 OS 堆并不是 JVM 中的堆。
    在这里插入图片描述
    除此之外,我们的程序在运行过程中还需要依赖动态链接库,这些动态链接库以 .so 文件的形式存放在磁盘中,比如 C 程序中的 glibc,里边对系统调用进行了封装。glibc 库里提供的用于动态申请堆内存的 malloc 函数就是对系统调用 sbrk 和 mmap 的封装。这些动态链接库也有自己的对应的代码段,数据段,BSS 段,也需要一起被加载进内存中。
    还有用于内存文件映射的系统调用 mmap,会将文件与内存进行映射,那么映射的这块内存(虚拟内存)也需要在虚拟地址空间中有一块区域存储。
    这些动态链接库中的代码段,数据段,BSS 段,以及通过 mmap 系统调用映射的共享内存区,在虚拟内存空间的存储区域叫做文件映射与匿名映射区。
    在这里插入图片描述
    最后我们在程序运行的时候总该要调用各种函数吧,那么调用函数过程中使用到的局部变量和函数参数也需要一块内存区域来保存。这一块区域在虚拟内存空间中叫做栈。
    在这里插入图片描述

总结:内核根据进程运行的过程中所需要不同种类的数据而为其开辟了对应的地址空间。分别为:

  • 用于存放进程程序二进制文件中的机器指令的代码段
  • 用于存放程序二进制文件中定义的全局变量和静态变量的数据段和 BSS 段。
  • 用于在程序运行过程中动态申请内存的堆。
  • 用于存放动态链接库以及内存映射区域的文件映射与匿名映射区。
  • 用于存放函数调用过程中的局部变量和函数参数的栈。

以上就是我们通过一个程序在运行过程中所需要的数据所规划出的虚拟内存空间的分布,这些只是一个大概的规划,那么在真实的 Linux 系统中,进程的虚拟内存空间的具体规划又是如何的呢?

Linux 进程虚拟内存空间 32 位机器上进程虚拟内存空间分布

在 32 位机器上,指针的寻址范围为 2^32,所能表达的虚拟内存空间为 4 GB。所以在 32 位机器上进程的虚拟内存地址范围为:0x0000 0000 - 0xFFFF FFFF。
其中用户态虚拟内存空间为 3 GB,虚拟内存地址范围为:0x0000 0000 - 0xC000 000 。
内核态虚拟内存空间为 1 GB,虚拟内存地址范围为:0xC000 000 - 0xFFFF FFFF。
在这里插入图片描述
但是用户态虚拟内存空间中的代码段并不是从 0x0000 0000 地址开始的,而是从 0x0804 8000 地址开始。
0x0000 0000 到 0x0804 8000 这段虚拟内存地址是一段不可访问的保留区,因为在大多数操作系统中,数值比较小的地址通常被认为不是一个合法的地址,这块小地址是不允许访问的。比如在 C 语言中我们通常会将一些无效的指针设置为 NULL,指向这块不允许访问的地址。

BSS 段的上边就是我们经常使用到的堆空间,从图中的红色箭头我们可以知道在堆空间中地址的增长方向是从低地址到高地址增长。
内核中使用 start_brk 标识堆的起始位置,brk 标识堆当前的结束位置。当堆申请新的内存空间时,只需要将 brk 指针增加对应的大小,回收地址时减少对应的大小即可。比如当我们通过 malloc 向内核申请很小的一块内存时(128K 之内),就是通过改变 brk 位置实现的。

堆空间的上边是一段待分配区域,用于扩展堆空间的使用。接下来就来到了文件映射与匿名映射区域。注意:在文件映射与匿名映射区的地址增长方向是从高地址向低地址增长。

接下来用户态虚拟内存空间的最后一块区域就是栈空间了,在这里会保存函数运行过程所需要的局部变量以及函数参数等函数调用信息。栈空间中的地址增长方向是从高地址向低地址增长。每次进程申请新的栈地址时,其地址值是在减少的。

在栈空间的上边就是内核空间了,进程虽然可以看到这段内核空间地址,**但是就是不能访问。**这就好比我们在饭店里虽然可以看到厨房在哪里,但是厨房门上写着 “厨房重地,闲人免进” ,我们就是进不去。

64 位机器上进程虚拟内存空间分布

我们知道在 32 位机器上,指针的寻址范围为 2^32,所能表达的虚拟内存空间为 4 GB。
那么我们理所应当的会认为在 64 位机器上,指针的寻址范围为 2^64,所能表达的虚拟内存空间为 16 EB 。虚拟内存地址范围为:0x0000 0000 0000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。
好家伙 !!! 16 EB 的内存空间,笔者都没见过这么大的磁盘,在现实情况中根本不会用到这么大范围的内存空间,
事实上在目前的 64 位系统下只使用了 48 位来描述虚拟内存空间,寻址范围为 2^48 ,所能表达的虚拟内存空间为 256TB。

其中低 128 T 表示用户态虚拟内存空间,虚拟内存地址范围为:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 。
高 128 T 表示内核态虚拟内存空间,虚拟内存地址范围为:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。
这样一来就在用户态虚拟内存空间与内核态虚拟内存空间之间形成了一段 0x0000 7FFF FFFF F000 - 0xFFFF 8000 0000 0000 的地址空洞,我们把这个空洞叫做 canonical address 空洞。
在这里插入图片描述
大家注意到在低 128T 的用户态地址空间:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 范围中,所以虚拟内存地址的高 16 位全部为 0 。
如果一个虚拟内存地址的高 16 位全部为 0 ,那么我们就可以直接判断出这是一个用户空间的虚拟内存地址。
同样的道理,在高 128T 的内核态虚拟内存空间:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 范围中,所以虚拟内存地址的高 16 位全部为 1 。
也就是说内核态的虚拟内存地址的高 16 位全部为 1 ,如果一个试图访问内核的虚拟地址的高 16 位不全为 1 ,则可以快速判断这个访问是非法的。

那么处于 canonical address 空洞 :0x0000 7FFF FFFF F000 - 0xFFFF 8000 0000 0000 范围内的地址的高 16 位 不全为 0 也不全为 1 。如果某个虚拟地址落在这段 canonical address 空洞区域中,那就是既不在用户空间,也不在内核空间,肯定是非法访问了。
未来我们也可以利用这块 canonical address 空洞,来扩展虚拟内存地址的范围,比如扩展到 56 位。
在这里插入图片描述
从上图中我们可以看出 64 位系统中的虚拟内存布局和 32 位系统中的虚拟内存布局大体上是差不多的。主要不同的地方有2点:

  • 就是前边提到的由高 16 位空闲地址造成的 canonical address 空洞。在这段范围内的虚拟内存地址是不合法的,因为它的高 16 位既不全为 0 也不全为 1
  • 在代码段跟数据段的中间还有一段不可以读写的保护段,它的作用是防止程序在读写数据段的时候越界访问到代码段,这个保护段可以让越界访问行为直接崩溃,防止它继续往下运行。

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


分享文章:Linux虚拟内存-创新互联
标题链接:http://pwwzsj.com/article/cdcsep.html