Table of Contents

为什么 Linux 需要虚拟内存?

缓存

内存管理

内存保护

总结

推荐阅读

为什么 Linux 默认页大小是 4KB?

页表项

碎片化

总结

推荐阅读


为什么 Linux 需要虚拟内存?

https://draveness.me/whys-the-design-os-virtual-memory/


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

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

图 1 - 进程和操作系统的中间层

如果需要我们从头设计一个操作系统,让系统中的进程直接访问主内存中的物理地址应该是非常自然的决定,早期的操作系统确实也都是这么实现的,进程会使用目标内存的物理地址(Physical Address)直接访问内存中的内容,然而现代的操作系统都引入了虚拟内存,进程持有的虚拟地址(Virtual Address)会经过内存管理单元(Memory Mangament Unit)的转换变成物理地址2,然后再通过物理地址访问内存:

图 2 - 虚拟内存系统

主存储是相对比较稀缺的资源,虽然顺序读取只比磁盘快 1 个数量级,但是它能提供极快的随机访问速度,从内存上随机读取数据是磁盘的 100,000 倍3,充分利用内存的随机访问速度是改善程序执行效率的有效方式。

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

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

缓存

《Linux内存管理:转换后备缓冲区(TLB)原理》

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

图 3 - 虚拟内存、主存和磁盘

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

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

图 4 - 虚拟内存的缺页中断

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

内存管理

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

图 5 - 操作系统的虚拟内存空间

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

图 6 - 四层页表结构

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

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

图 7 - 进程间共享内存

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

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

内存保护

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

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

图 8 - 读权限、写权限和执行权限

内存管理单元可以决定当前进程是否有权限访问目标的物理内存,这样我们就最终将权限管理的功能全部收敛到虚拟内存系统中,减少了可能出现风险的代码路径。

总结

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

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

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

到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细思考一下下面的问题:

  • 为什么每层的页表结构只能够负责 9 位虚拟地址的寻址?
  • 64 位的虚拟内存在操作系统中需要多少层的页表结构才能寻址?

如果对文章中的内容有疑问或者想要了解更多软件工程上一些设计决策背后的原因,可以在博客下面留言,作者会及时回复本文相关的疑问并选择其中合适的主题作为后续的内容。

推荐阅读

  • Virtual Memory and Linux
  • Difference Swapping and Paging
  • Intel® 64 and IA-32 Architectures Software Developer’s Manual

  • 调度系统设计精要 https://draveness.me/system-design-scheduler ↩︎

  • Bryant & O’Hallaron. “Computer Systems: A Programmer’s Perspective” 3rd Edition. Chapter 9. Virtual Memory. ↩︎

  • Adam Jacobs. July 6, 2009. “The Pathologies of Big Data” https://queue.acm.org/detail.cfm?id=1563874 ↩︎

  • Wikipedia: Page fault https://en.wikipedia.org/wiki/Page_fault ↩︎

  • Wikipedia: x86-64 https://zh.wikipedia.org/wiki/X86-64 ↩︎

  • 5-Level Paging and 5-Level EPT https://software.intel.com/sites/default/files/managed/2b/80/5-level_paging_white_paper.pdf ↩︎

  • Four-level page tables merged https://lwn.net/Articles/117749/ ↩︎

  • Five-level page tables https://lwn.net/Articles/717293/ ↩︎

为什么 Linux 默认页大小是 4KB?

https://draveness.me/whys-the-design-linux-default-page/


我们都知道 Linux 会以页为单位管理内存,无论是将磁盘中的数据加载到内存中,还是将内存中的数据写回磁盘,操作系统都会以页面为单位进行操作,哪怕我们只向磁盘中写入一个字节的数据,我们也需要将整个页面中的全部数据刷入磁盘中。

Linux 同时支持正常大小的内存页和大内存页(Huge Page)1,绝大多数处理器上的内存页的默认大小都是 4KB,虽然部分处理器会使用 8KB、16KB 或者 64KB 作为默认的页面大小,但是 4KB 的页面仍然是操作系统默认内存页配置的主流;除了正常的内存页大小之外,不同的处理器上也包含不同大小的大页面,我们在 x86 处理器上就可以使用 2MB 的内存页。

4KB 的内存页其实是一个历史遗留问题,在上个世纪 80 年代确定的 4KB 一直保留到了今天。虽然今天的硬件比过去丰富了很多,但是我们仍然沿用了过去主流的内存页大小。如下图所示,装过机的人应该对这里的内存条非常熟悉:

图 1 - 随机存取内存

在今天,4KB 的内存页大小可能不是最佳的选择,8KB 或者 16KB 说不定是更好的选择,但是这是过去在特定场景下做出的权衡。我们在这篇文章中不要过于纠结于 4KB 这个数字,应该更重视决定这个结果的几个因素,这样当我们在遇到类似场景时才可以从这些方面考虑当下最佳的选择,我们在这篇文章中会介绍以下两个影响内存页大小的因素,它们分别是:

  • 过小的页面大小会带来较大的页表项增加寻址时 TLB(Translation lookaside buffer)的查找速度和额外开销;
  • 过大的页面大小会浪费内存空间,造成内存碎片,降低内存的利用率;

上个世纪在设计内存页大小时充分考虑了上述的两个因素,最终选择了 4KB 的内存页作为操作系统最常见的页大小,我们接下来将详细介绍以上它们对操作系统性能的影响。

页表项

我们在 为什么 Linux 需要虚拟内存 一文中曾经介绍过 Linux 中的虚拟内存,每个进程能够看到的都是独立的虚拟内存空间,虚拟内存空间只是逻辑上的概念,进程仍然需要访问虚拟内存对应的物理内存,从虚拟内存到物理内存的转换就需要使用每个进程持有页表。

为了存储 64 位操作系统中 128 TiB 虚拟内存的映射数据,Linux 在 2.6.10 中引入了四层的页表辅助虚拟地址的转换2,在 4.11 中引入了五层的页表结构3,在未来还可能会引入更多层的页表结构以支持 64 位的虚拟地址。

图 2 - 四层页表结构

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

因为操作系统的虚拟地址空间大小都是一定的,整片虚拟地址空间被均匀分成了 N 个大小相同的内存页,所以内存页的大小最终会决定每个进程中页表项的层级结构和具体数量,虚拟页的大小越小,单个进程中的页表项和虚拟页也就越多。

因为目前的虚拟页大小为 4096 字节,所以虚拟地址末尾的 12 位可以表示虚拟页中的地址,如果虚拟页的大小降到了 512 字节,那么原本的四层页表结构或者五层页表结构会变成五层或者六层,这不仅会增加内存访问的额外开销,还会增加每个进程中页表项占用的内存大小。

碎片化

因为内存映射设备会在内存页的层面工作,所以操作系统认为内存分配的最小单元就是虚拟页。哪怕用户程序只是申请了 1 字节的内存,操作系统也会为它申请一个虚拟页,如下图所示,如果内存页的大小为 24KB,那么申请 1 字节的内存会浪费 ~99.9939% 的空间。

图 3 - 大内存的碎片化

随着内存页大小的增加,内存的碎片化严情况会越来越严重,小的内存页会减少内存空间中的内存碎片,提高内存的利用率。上个世纪的内存资源还没有像今天这么丰富,在大多数情况下,内存都不是限制程序运行的资源,多数的在线服务都需要更多的CPU,而不是更多的内存。不过在上个世纪内存其实也是稀缺资源,所以提高稀缺资源的利用率是我们不得不考虑的事情:

图 4 - 内存的价格

上个世纪八九十年代的内存条只有 512KB 或者 2MB,价格也贵得离谱,但是几 GB 的内存在今天却非常常见5,所以虽然内存的利用率仍然十分重要,但是在内存的价格大幅降低的今天,碎片化的内存不再是需要解决的关键问题了。

除了内存的利用率之外,较大的内存页也会增加内存拷贝时的额外开销,因为 Linux 上的写时拷贝机制,在多个进程共享同一块内存时,当其中的一个进程修改了共享的虚拟内存会触发内存页的拷贝,这时操作系统的内存页越小,写时拷贝带来的额外开销也就越小。

总结

就像我们在上面提到的,4KB 的内存页是上个世纪决定的默认设置,从今天的角度来看,这很可能已经是错误的选择了,arm64、ia64 等架构已经可以支持 8KB、16KB 等大小的内存页,随着内存的价格变得越来越低、系统的内存变得越来越大,更大的内存可能是操作系统更好的选择,我们重新回顾一下两个决定内存页大小的要素:

  • 过小的页面大小会带来较大的页表项增加寻址时 TLB(Translation lookaside buffer)的查找速度和额外开销,但是也会减少程序中的内存碎片,提高内存的利用率;
  • 过大的页面大小会浪费内存空间,造成内存碎片,降低内存的利用率,但是可以较少进程中的页表项以及 TLB 的寻址时间;

这种类似的场景在我们做系统设计时也比较常见,举一个不是特别恰当的例子,当我们想要在集群上部署服务时,每个节点上的资源是有限的,单个服务占用的资源可能会影响集群的资源利用率或者系统的额外开销。如果我们在集群中部署 32 个占用 1 CPU 的服务,那么可以充分利用集群中的资源,但是如此多的实例数会带来较大的额外开销;如果我们在集群中部署 4 个占用 8 CPU 的服务,那么这些服务的额外开销虽然很小,但是可能会在节点中留下很多空隙。到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细思考一下下面的问题:

  • Linux 中的扇区、块和页都有什么区别和联系?
  • Linux 中的块大小是如何决定的?常见的大小有哪些?

如果对文章中的内容有疑问或者想要了解更多软件工程上一些设计决策背后的原因,可以在博客下面留言,作者会及时回复本文相关的疑问并选择其中合适的主题作为后续的内容。

推荐阅读

  • 为什么 Linux 需要虚拟内存
  • 为什么系统调用会消耗较多资源

  1. Huge TLB Pages https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt ↩︎

  2. Four-level page tables merged https://lwn.net/Articles/117749/ ↩︎

  3. Five-level page tables https://lwn.net/Articles/717293/ ↩︎

  4. 为什么 Linux 需要虚拟内存 https://draveness.me/whys-the-design-os-virtual-memory/ ↩︎

  5. Memory Prices 1957+ https://jcmit.net/memoryprice.htm ↩︎

Linux内存管理:为什么 Linux 需要虚拟内存?为什么 Linux 默认页大小是 4KB?相关推荐

  1. linux内存管理的主要概念是虚拟内存,有关linux内存管理机制的相关内容,linux物理内存和虚拟内存,深入了解Linux内存运行 ......

    在linux中空闲内存很少,似乎所有的内存都被系统占用了,表面感觉是内存不够用了,其实不然. 这是Linux内存管理的一个优秀特性,区别于Windows的内存管理. 主要特点: 无论物理内存有多大,L ...

  2. Linux 内存管理 详解(虚拟内存、物理内存,进程地址空间)

    Linux -操作系统内存管理 存储系统 存储器的层次结构 Linux的内存管理 物理内存 物理内存管理 虚拟内存 虚拟地址空间 (写时拷贝) 和物理地址映射关系 页表 虚拟内存优缺点 「在 4GB ...

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

    文章目录 分段 分页 多级页表 快表(TLB) 段页式 Linux Linux 内存管理 | 物理内存管理:内存碎片.伙伴系统.slab分配器 Linux 内存管理 | 虚拟内存管理:虚拟内存空间.虚 ...

  4. linux内存管理_架构师必读:Linux 的内存分页管理

    内存是计算机的主存储器.内存为进程开辟出进程空间,让进程在其中保存数据.我将从内存的物理特性出发,深入到内存管理的细节,特别是了解虚拟内存和内存分页的概念. 内存 简单地说,内存就是一个数据货架.内存 ...

  5. Linux内存管理(四十):Linux PSI 详解

    源码基于:Linux 5.4 0. 前言 之前在<Linux PSI 指标>一文中简单的描述了 PSI 指标的意义,以及PSI 出现的历史过程. 在PSI 之前,Linux 也有一些资源压 ...

  6. 为什么 Linux 默认页大小是 4KB

    我们都知道 Linux 会以页为单位管理内存,无论是将磁盘中的数据加载到内存中,还是将内存中的数据写回磁盘,操作系统都会以页面为单位进行操作,哪怕我们只向磁盘中写入一个字节的数据,我们也需要将整个页面 ...

  7. linux空目录4kb,为什么 Linux 默认页大小是 4KB

    为什么这么设计(Why's THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点.对具体实现造成的影响 ...

  8. Linux内存管理之基本概念介绍(一)

    Linux内存管理之基本概念介绍(一) Linux内存管理之物理内存管理(二) Linux内存管理之内存管理单元(MMU)(三) Linux内存管理之分配掩码(四) Linux内存管理之伙伴系统(五) ...

  9. Linux内存管理:Fixmaps(固定映射地址)和ioremap

    目录 Fixmaps和ioremap 映射 ioremap工作原理 早期ioremap的使用 Links 相关阅读 Fix-Mapped地址是一组特殊的编译时地址,其对应的物理地址不必是线性地址减__ ...

最新文章

  1. Linux基础命令---diffstat
  2. ab压力 failed_Apache ab 压力测试
  3. 在C#中实现Socket端口复用
  4. python基本的信号与槽函数的使用 信号发射 槽函数接收
  5. qtreewidgetitem 文字内存太长换行_table文字溢出显示省略号问题
  6. 步步为营:Asp.Net序列化与反序列化
  7. 爷青结?诺基亚贝尔实验室官宣转让Plan 9版权!
  8. eclipse-memory-analyzer使用说明
  9. java参数可变方法
  10. 容器技术Docker K8s 16 容器服务ACK基础与进阶-容器网络管理
  11. 为什么使用了security 后台出不来了_C#实现使用MD5加密用户登录密码,System.Web.Security报错...
  12. 【Java练习题】Java 程序的输出 | 第十一套(含解析)
  13. 希沃集控系统流媒体服务器未开启,希沃集控,让教育信息化管理尽在“掌控”之中...
  14. selenium + 超级鹰 识别验证码自动登录
  15. 原创 C++应用程序在Windows下的编译、链接:第二部分COFF/PE文件结构
  16. 2022年二级建造师建设工程法规及相关知识考试每日一练及答案
  17. 华为ENSP(VRP)命令行
  18. toolbar wpf 按钮带文字_Tob设计:中台设计组件按钮
  19. Viewstate verification failed 解决办法
  20. 尚品汇VUE项目实战知识点总结

热门文章

  1. java 时间日期视频_40-日期和时间-JAVA和C#语法比较 - 视频教程 - 北盟网校 - 专注原创教学第一站...
  2. Java受检异常和非受检异常
  3. 看了一下lua的实现
  4. bean type not found
  5. backbone入门系列(4)集合
  6. .NET 对接JAVA 使用Modulus,Exponent RSA 加密
  7. window.open以post方式提交
  8. Siebel Admin: How to find the Component that associated with Application
  9. 查看linux代码版本,如何查看 Linux Mint 版本号和代号 | Linux 中国
  10. 传智php入学测试题,传智播客PHP 0912 基础班 入学测试题