操作系统中的 CPU 和主内存(Main memory)都是稀缺资源,所有运行在当前操作系统的进程会共享系统中的 CPU 和内存资源,操作系统会使用 CPU 调度器分配 CPU 时间1并引入虚拟内存系统以管理物理内存,本文会分析操作系统为什么需要虚拟内存。

在回答虚拟内存存在的必要性之前,我们需要理解操作系统中的虚拟内存是什么,它在操作系统中起到什么样的作用。正如软件工程中的其他抽象,虚拟内存是操作系统物理内存和进程之间的中间层,它为进程隐藏了物理内存这一概念,为进程提供了更加简洁和易用的接口以及更加复杂的功能。

现代的操作系统都引入了虚拟内存,进程持有的虚拟地址(Virtual Address)会经过内存管理单元(Memory Mangament Unit)的转换变成物理地址2,然后再通过物理地址访问内存:

操作系统以页为单位管理内存,当进程发现需要访问的数据不在内存时,操作系统可能会将数据以页的方式加载到内存中,这个过程是由上图中的内存管理单元(MMU)完成的。操作系统的虚拟内存作为一个抽象层,起到了以下三个非常关键的作用:

  • 虚拟内存可以利用内存起到缓存的作用,提高进程访问磁盘的速度;
  • 虚拟内存可以为进程提供独立的内存空间,简化程序的链接、加载过程并通过动态库共享内存;
  • 虚拟内存可以控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性;

缓存

我们可以将虚拟内存看作是在磁盘上一片空间,当这片空间中的一部分访问比较频繁时,该部分数据会以页为单位被缓存到主存中以加速 CPU 访问数据的性能,虚拟内存利用空间较大的磁盘存储作为『内存』并使用主存储缓存进行加速,让上层认为操作系统的内存很大而且很快,然而区域很大的磁盘并不快,而很快的内存也并不大

虚拟内存中的虚拟页(Virtual Page,VP)可能处于以下的三种状态 :

  • 未分配(Unallocated)
  • 未缓存(Uncached)
  • 已缓存(Cached)

其中未分配的内存页是没有被进程申请使用的,也就是空闲的虚拟内存,不占用虚拟内存磁盘的任何空间。未缓存和已缓存的内存页分别表示仅加载到磁盘中的内存页和已经加载到主存中的内存页。如上图所示,图中绿色的虚拟内存页由主存中的物理内存页(Physical Page,PP)支撑,所以它是已经缓存过的,而黄色的虚拟内存页仅在磁盘中,所以没有被物理内存缓存。

当用户程序访问未被缓存的虚拟页时,硬件就会触发缺页中断(Page Fault,PF),在部分情况下,被访问的页面已经加载到了物理内存中,但是用户程序的页表(Page Table)并不存在该对应关系,这时我们只需要在页表中建立虚拟内存到物理内存的关系;在其他情况下,操作系统需要将磁盘上未被缓存的虚拟页加载到物理内存中4

因为主内存的空间是有限的,当主内存中不包含可以使用的空间时,操作系统会从选择合适的物理内存页驱逐回磁盘,为新的内存页让出位置,选择待驱逐页的过程在操作系统中叫做页面替换(Page Replacement)。缺页中断和页面替换技术都是操作系统调页算法(Paging)的一部分,该算法的目的就是充分利用内存资源作为磁盘的缓存以提高程序的运行效率。

内存管理

虚拟内存可以为正在运行的进程提供独立的内存空间,制造一种每个进程的内存都是独立的假象,在 64 位的操作系统上,每个进程都会拥有 256 TiB 的内存空间,内核空间和用户空间分别占 128 TiB5,部分操作系统使用 57 位虚拟地址以提供 128 PiB 的寻址空间6。因为每个进程的虚拟内存空间是完全独立的,所以它们都可以完整的使用 0x0000000000000000 到 0x00007FFFFFFFFFFF 的全部内存。

 

虚拟内存空间只是操作系统中的逻辑结构,就像我们上面说的,应用程序最终还是需要访问物理内存或者磁盘上的内容。因为操作系统加了一个虚拟内存的中间层,所以我们也需要为进程实现地址翻译器,实现从虚拟地址到物理地址的转换,页表是虚拟内存系统中的重要数据结构,每一个进程的页表中都存储了从虚拟内存到物理内存页的映射关系,为了存储 64 位操作系统中 128 TiB 虚拟内存的映射数据,Linux 在 2.6.10 中引入了四层的页表辅助虚拟地址的转换7,在 4.11 中引入了五层的页表结构8,在未来还可能会引入更多层的页表结构以支持 64 位的虚拟地址。

 

在如上图所示的四层页表结构中,操作系统会使用最低的 12 位作为页面的偏移量,剩下的 36 位会分四组分别表示当前层级在上一层中的索引,所有的虚拟地址都可以用上述的多层页表查找到对应的物理地址。

因为有多层的页表结构可以用来转换虚拟地址,所以多个进程可以通过虚拟内存共享物理内存。我们在 为什么 Redis 快照使用子进程 一文中介绍的写时复制就利用了虚拟内存的这个特性,当我们在 Linux 中调用 fork 创建子进程时,实际上只复制了父进程的页表。如下图所示,父子进程会通过不同的页表指向相同的物理内存:

虚拟内存不仅可以在 fork 时用于共享进程的物理内存,提供写时复制的机制,还能共享一些常见的动态库减少物理内存的占用,所有的进程都可能调用相同的操作系统内核代码,而 C 语言程序也会调用相同的标准库。

除了能够共享内存之外,独立的虚拟内存空间也会简化内存的分配过程,当用户程序向操作系统申请堆内存时,操作系统可以分配几个连续的虚拟页,但是这些虚拟页可以对应到物理内存中不连续的页中。

内存保护

操作系统中的用户程序不应该修改只读的代码段,也不应该读取或者修改内核中的代码和数据结构或者访问私有的以及其他的进程的内存,如果无法对用户进程的内存访问进行限制,攻击者就可以访问和修改其他进程的内存影响系统的安全。

如果每一个进程都持有独立的虚拟内存空间,那么虚拟内存中页表可以理解成进程和物理页的『连接表』,其中可以存储进程和物理页之间的访问关系,包括读权限、写权限和执行权限:

总结

虚拟内存的设计方法可以说是软件工程中的常见手段,通过结合磁盘和内存各自的优势,利用中间层对资源进行更合理地调度充分提高资源的利用率并提供和谐以及统一的抽象,而在实际的业务场景中,类似的缓存逻辑也比较常见。

操作系统的虚拟内存是非常复杂的组件,没有工程师能够了解其中的全部细节,不过了解虚拟内存的整体设计也很有价值,我们能够从中找到很多软件设计的方法。我们重新回到今天的问题 — Linux 操作系统中为什么需要虚拟内存:

  • 虚拟内存可以结合磁盘和物理内存的优势为进程提供看起来速度足够快并且容量足够大的存储;
  • 虚拟内存可以为进程提供独立的内存空间并引入多层的页表结构将虚拟内存翻译成物理内存,进程之间可以共享物理内存减少开销,也能简化程序的链接、装载以及内存分配过程;
  • 虚拟内存可以控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性;

推荐阅读

转载自 为什么 Linux 需要虚拟内存 - 面向信仰编程 (draveness.me)

补充1:fork与内存页表

1. 首先我们可以确定父子进程的代码段是相同的,所以代码段是没必要复制的,因此内核将代码段标记为只读,这样父子进程就可以安全的共享此代码段了。fork之后在进程创建代码段时,新子进程的进程级页表项都指向和父进程相同的物理页帧

2. 而对于父进程的数据段,堆段,栈段中的各页,由于父子进程要相互独立,所以我们采用写实复制的技术,来最大化的提高内存以及内核的利用率。刚开始,内核做了一些设置,令这些段的页表项指向父进程相同的物理内存页(即子进程完整复制父进程的页表)。调用fork之后,内核会捕获所有父进程或子进程针对这些页面的修改企图(说明此时还没修改)并为将要修改的页面创建拷贝。系统将新的页面拷贝分配给被内核捕获的进程,还会对子进程的相应页表项做适当的调整,现在父子进程就可以分别修改各自的上述段,不再互相影响了

写实复制前:

写实复制后:

参考: 解析Linux进程的创建函数fork()及其fork内核实现 - 知乎 (zhihu.com)

补充2:brk 与 memmap

1. 当要分配的内存为小块内存时(4K内存页时<=128K),Linux内核使用brk分配内存,即在堆区域分配,并修改堆顶指针。此时缓存利用率较高。

2. 当要分配的内存为大块内存时,Linux内核使用mmap分配内存,即在mmap区域直接映射一块内存分配给用户,并在释放时归还给系统。这会造成每次申请时都会产生缺页中断,所以分配耗时比较长。

补充3:Cacheable Memory / Non-Cacheable Memory

各页表项还存有页面的访问权限、是否允许cache的标记。如果不允许cache,就跳过CPU Cahce,直接访问主存。这在异构系统(如带有NPU的处理器系统)里有体现。

补充阅读:CPU TLB原理 [转载好文] - vaughn.huang - 博客园 (cnblogs.com)

为什么Linux需要虚拟内存 [转载好文]的更多相关文章

  1. [转载]linux+nginx+python+mysql安装文档

    原文地址:linux+nginx+python+mysql安装文档作者:oracletom # 开发包(如果centos没有安装数据库服务,那么要安装下面的mysql开发包) MySQL-devel- ...

  2. linux 命令总结(转载)

    linux 命令总结(转载) 1. 永久更改ip ifconfig eth0 新ip 然后编辑/etc/sysconfig/network-scripts/ifcfg-eth0,修改ip 2.从Lin ...

  3. 理解 Linux 的虚拟内存

    前言 前不久组内又有一次我比较期待的分享:”Linux 的虚拟内存”.是某天晚上加班时,我们讨论虚拟内存的概念时,leader 发现几位同事对虚拟内存认识不清后,特意给这位同学挑选的主题(笑). 我之 ...

  4. 【转帖】理解 Linux 的虚拟内存

    理解 Linux 的虚拟内存 https://www.cnblogs.com/zhenbianshu/p/10300769.html 段页式内存 文章了里面讲了 页表 没讲段表 记得最开始的时候 学习 ...

  5. 为什么Linux需要虚拟内存

    本文转载自为什么 Linux 需要虚拟内存 导语 操作系统中的 CPU 和主内存(Main memory)都是稀缺资源,所有运行在当前操作系统的进程会共享系统中的 CPU 和内存资源,操作系统会使用 ...

  6. linux的虚拟内存是4G,而每个进程都有自己独立的4G内存空间,怎么理解?

    问: linux的虚拟内存是4G,而每个进程都有自己独立的4G内存空间,怎么理解? 每个进程所拥有的4G独立的虚拟内存空间是什么意思?linux系统的虚拟4G空间中,高位的1G是用于系统内核运行的,那 ...

  7. Linux计划任务(转载)

    Linux计划任务(转载) Linux的计划任务是系统管理方面的一个重要内容,是系统自动完成工作的一种实现方式,正因为有了计划任务,我们才可以完全实现系统管理的脚本化和自动化. 关于计划任务,Linu ...

  8. Linux(Ubuntu)使用日记------markdown文档转化为word文档

    Linux(Ubuntu)使用日记------markdown文档转化为word文档

  9. Linux的虚拟内存管理-如何分配和释放内存,以提高服务器在高并发情况下的性能,从而降低了系统的负载

    Linux的虚拟内存管理有几个关键概念: Linux 虚拟地址空间如何分布?malloc和free是如何分配和释放内存?如何查看堆内内存的碎片情况?既然堆内内存brk和sbrk不能直接释放,为什么不全 ...

  10. Linux添加虚拟内存 && 修改Linux系统语言

    Linux添加虚拟内存 首先执行free -h查看内存状况: total used free shared buff/cache available Mem: 1.8G 570M 76M 8.4M 1 ...

随机推荐

  1. 【读书笔记】C#高级编程 第十四章 内存管理和指针

    (一)后台内存管理 1.值数据类型 Windows使用一个虚拟寻址系统,该系统把程序可用的内存地址映射到硬件内存中的实际地址,该任务由Windows在后台管理(32位每个进程可使用4GB虚拟内存,64 ...

  2. JavaWeb核心篇(2)——Request和Response

    JavaWeb核心篇(2)--Request和Response 上篇文章中提及到了Servlet,在Servlet中我们主要继承了HTTPServlet类,在HTTPServlet类中我们会接触到两个 ...

  3. JS作用域、变量提升和闭包

    作用域 作用域可以理解为JS引擎执行代码的时候,查找变量的规则. 从确定变量访问范围的阶段的角度,可以分为2类,词法作用域和动态作用域.js是词法作用域. 从变量查找的范围的角度,可以分为3类,全局作 ...

  4. 命令行配置Windows高级防火墙

    今天正好看到个帖子,询问如何通过命令行配置防火墙策略中远程IP的地址,特别是添加新的地址. 就是图中Scope里Remote IP address的地址. 第一反应就是用netsh firewall来 ...

  5. Elastic Stack 8.0 再次创建enrollment token

    enrollment token 在第一个 Elasticsearch 启动后的有效时间为30分钟.超过30分钟的时间上述 token 将会无效. enrollment token分两个,一个是kib ...

  6. Kibana插件

    附加的功能在 Kibana 中是以插件的形式提供的.您可以利用 bin/kibana-plugin 命令来管理这些模块.您也可以手动安装这些插件,只需要将这些插件包放到 plugins 目录并解压到新 ...

  7. Beats在Kibana中的集中管理

    前提条件: 1.es版本是白金版 2.es开启安全设置,kibana访问es需要密码 操作步骤汇总: 1-3步是基础环境配置 4-9步是注册beats到集中管理平台,然后启动beats,只是单纯启动b ...

  8. cf1082 A. Vasya and Book

    中文题意: 思路:我们先看看能不能直接从x翻到y,abs(y-x)%d==0,可以就直接输出abs(y-x)/d咯,不行的话之后有2种操作 1.先翻回到第一页,从第一页看看能不能范到y,不能的话翻到最 ...

  9. 洛谷P1962 斐波那契数列 (矩阵快速幂)

    学了矩阵,练一下手... 1 #include<bits/stdc++.h> 2 typedef long long ll; 3 const ll mod=1e9+7; 4 using n ...

  10. NOI2015 洛谷P1955 程序自动分析(并查集+离散化)

    这可能是我目前做过的最简单的一道noi题目了...... 先对e=1的处理,用并查集:再对e=0查询,如果这两个在同一集合中,则为""NO",最后都满足的话输出" ...