虚拟内存

1️⃣ 什么是虚拟地址?

操作系统 为每个进程都分配了独立的 虚拟地址。并且,操作系统提供一种机制,将不同进程的虚拟地址和不同的物理内存地址映射起来。如果程序要访问虚拟地址,就由操作系统转换成不同的物理内存地址。

之后,进程持有的虚拟内存地址会通过 CPU 芯片中的 MMU 的映射关系,将其转换成物理内存地址。

作用:

  • 虚拟内存可以使得进程对运行内存超过物理内存大小:
    • 因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性
    • 对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
  • 由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的
  • 页表里的页表项中除了物理地址之外,还有对页的标志位

2️⃣ 操作系统是如何管理虚拟地址与物理地址之间的关系?

主要有两种方式,分别是 内存分段 和 内存分页

3️⃣ 分段机制下,虚拟地址和物理地址是如何映射的?

分段机制下的虚拟地址由两部分组成,段选择因子 和 段内偏移量。

段选择因子 最重要的就是 段号,用作 段表 的索引。其中 段表 保存了 段的基地址 以及 段的界限地址。通过 段基地址 加上 段内偏移量 得到 物理内存地址。

4️⃣ 分页机制下,虚拟地址和物理地址是如何映射的?

分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。 这样一个连续并且尺寸固定的内存空间,叫做页 Page。

分页机制下的虚拟地址分为两部分,页号 和 页内偏移。页号 作为 页表 的索引,页表 包含物理页在物理内存的 基地址,这个 基地址 加上 页内偏移 就形成了 物理内存地址。

缺点

在 32 位计算机系统下,虚拟地址空间大小共有 4GB。假设一个页的大小位 4KB,那么就需要 (4GB/4KB) 的页的数量 2^20,假设 每个页表项 用 4B 来存储,那么那么 4GB 空间的映射就需要有 4MB 的内存来存储页表。

但是每个进程的都有自己的虚拟地址空间:拥有自己的页表。这就造成了额外的内存开销

5️⃣ 多级页表机制下,虚拟地址和物理地址是如何映射的?

多级页表机制下的虚拟地址分为五部分,全局页目录项 PGD , 上层页目录项 PUD ,中间页目录项 PMD,页表项,页内偏移。

多级页表虽然解决了空间上的问题,但是 虚拟地址 到 物理地址 的转换带来了 时间开销。

因此,提出了 TLB 的概念:在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache。

6️⃣ 段页页表机制下,虚拟地址和物理地址是如何映射的?

段页式机制下的虚拟地址分为三部分,段号,页号 和 页内偏移。

每一个程序一张 段表,每个段又建立一张 页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号。

7️⃣ 内核态与用户态的区别

  • 进程在用户态,只能访问用户空间内存
  • 进入内核态后,才可以访问内核空间的内存

虽然每个进程都各自有 独立的虚拟内存,但是 每个虚拟内存中的内核地址,其实关联的都是 相同的物理内存。

内存分配

1️⃣ malloc 是如何分配内存的?

malloc() 并不是 系统调用,而是 C 库里的函数,用于动态分配内存。

  • 通过 brk() 系统调用从 堆 分配内存
  • 通过 mmap() 系统调用在文件映射区域分配内存

2️⃣ malloc() 会通过 brk() 分配内存?又是什么场景下通过 mmap() 分配内存?

malloc() 源码里定义了一个阈值:

  • 用户分配的内存小于 128 KB,则通过 brk() 申请内存
  • 用户分配的内存大于 128 KB,则通过 mmap() 申请内存

3️⃣ malloc() 分配的是物理内存吗?

malloc() 分配的是虚拟内存。

  • 如果分配后的虚拟内存没有被访问,虚拟内存 是 不会映射 到 物理内存 的,这样就不会占用物理内存了。
  • 如果分配后的虚拟内存有被访问,操作系统 通过 查找页表,发现 虚拟内存 对应的页没有在物理内存中,就会 触发缺页中断,然后操作系统会建立 虚拟内存 和 物理内存 之间的映射关系。

4️⃣ malloc(1) 会分配多大的虚拟内存?

malloc() 在分配内存的时候,并不是按用户预期申请的字节数来分配内存空间大小,而是会 预分配更大的空间 作为内存池。

5️⃣ free 释放内存,会归还给操作系统吗?

  • malloc 通过 brk() 方式申请的内存的情况,free 释放内存后会给 内存池。
  • malloc 通过 mmap() 方式申请的内存的情况,free 释放内存后就会归归还给 操作系统。

6️⃣ 为什么不全部使用 mmap 来分配内存?

malloc 的智慧就在于 “看人下菜碟”:

  • 小内存、频繁请求: 主要靠 brk 管理的堆区。速度快。
  • 大内存: 直接用 mmap 单独映射一块。这样避免了在堆区造成 外部碎片 ,释放时也能干净利落地立刻归还给系统,不拖累堆区。

7️⃣ free() 函数只传入一个内存地址,为什么能知道要释放多大的内存?

malloc 返回给 用户态的内存起始地址 比 进程的堆空间起始地址 要更多。多出来的字节 表示 该内存块的信息 。

8️⃣ 内存分配的过程

应用程序通过 malloc 函数申请内存的时候,实际上申请的是 虚拟内存,此时并不会 分配物理内存。

当程序 读写了这块虚拟内存,CPU 就会去访问这个 虚拟内存。如果 虚拟内存 没有映射到 物理内存,CPU 就会产生 缺页中断,此时,进程陷入到 内核态,由内核来对 缺页中断 进行处理,并且检查是否有 空闲内存。

  • 如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
  • 如果没有,内核 就会开始进行 回收内存 的工作。

9️⃣ 回收内存方式

  • 后台内存回收: 唤醒 kswapd 内核线程来回收内存,这个回收内存的过程 异步 的,不会阻塞进程的执行。
  • 直接内存回收: 后台异步回收 跟不上 进程内存申请 的速度,会阻塞进程的执行。
  • OOM: 如果 直接内存回收 后,空闲的物理内存 仍然无法满足 此次物理内存的申请,就会执行 OOM。

    根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。

1️⃣0️⃣ 申请的虚拟内存超过物理内存会怎么样?

  • 在 32 位操作系统,因为进程最大只能申请 3 GB 大小的虚拟内存,所以直接申请超过 3GB 的内存时,会直接失败。
  • 在 64 位操作系统,因为进程最大只能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请超过 4GB 是可以的,因为申请的是 虚拟内存。

程序申请的虚拟内存,如果没有被使用,它是不会占用物理空间的。当访问这块虚拟内存后,操作系统才会进行物理内存分配。

1️⃣1️⃣ 申请物理内存大小超过了空闲物理内存大小会怎么办?

  • 如果没有开启 Swap 机制,程序就会直接 OOM;
  • 如果有开启 Swap 机制,程序可以正常运行;

1️⃣2️⃣ 什么是 Swap 机制?

系统的物理内存不够用 的时候,就需要将 物理内存 中的 一部分空间释放 出来,以供当前运行的程序使用。被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间会被 临时保存到磁盘,等到那些程序要运行时,再从磁盘中恢复保存的数据到内存中。

另外,当 内存使用存在压力 的时候,会开始 触发内存回收 行为,会把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。