explorer

万丈高楼平地起,勿在浮沙筑高台

0%

[What]Complete Virtual Memory Systems

现在再来以全局的方式看虚拟内存空间。

VAX/VMS 的虚拟内存

内存管理的硬件

虚拟内存与物理内存的关系

VAX-11 架构使用 32 位虚拟地址空间,页大小为 512 字节,也就是说地址的低 9 位是页内偏移。

23 位中的高两位用于将地址空间平均分为 4 段,用于表示每一段中是否有驻留内存,这是一种节省页表项占用空间的策略。

虚拟内存的划分

VAX 将虚拟内存划分为两半:

  • 第一半是进程空间(process space),当然每一个进程的这部分内容是不一样的,这部分又分为两半
    • 低半用于存放只读的用户代码和堆,堆是向下增长的。
    • 高半是用于存放栈,栈是向上增长的
  • 第二半是系统空间,存放系统的代码和数据,这部分对于各个进程是共享的

VMS 的处理

由于硬件页大小仅仅 512 字节,假设只有一级页表的话,每个进程对应的页表项需要 32MB,这实在是太大了。

VMS 对此使用了以下两种方式:

  • 将用户空间分为两段,每段单独使用一个页表。硬件提供了页表基地址和页表的大小,那么在栈和堆中没有被用到的部分就不用为它分配页表项了,也就节约了内存。
  • 将用户空间的两段页表存放于内核空间中,当页表过大时,内核将其置换到硬盘以保证足够的物理内存空间。

由于用户空间的页表存放在内核空间中,虚拟地址到物理地址的转换变得更为繁琐:

  • 当用户空间(虚拟地址)发起内存访问时,MMU 首先需要获取内核空间的页表基地址(物理地址)
  • 根据内核页表基地址(物理地址)和用户空间的页表存放地址(虚拟地址),得到用户空间页表基地址(物理地址)
  • 再根据用户空间页表基地址(物理地址)和用户空间的访问地址(虚拟地址)得到这段物理地址
  • 根据得到的物理地址进行访问

也就是说上面这种转换比平常的转换多了一级转换:从内核页表得到用户页表的基地址

真实的地址空间

mempic/vmc/vms_space.jpg

途中的 page 0 被设置为 invalid,这是为了调试空指针问题。

页置换

VAX 的页表项包含以下这些位:

  • valid bit: 1 位
  • protection field bits: 4 位
  • dirty bit:1 位
  • reserved for OS bits : 5 位
  • 物理内存块位

但是并没有 reference bit,也就无法通过硬件参考完成 LRU 算法。

对于此,VAX 使用 FIFO 置换策略:

  • 为进程规定一个最大的驻留内存(resident set size,RSS)
  • 将这些内存页项存放于一个 FIFO 中
  • 当有超过该 RSS 的内存被申请时,那边在 FIFO 头的页面将会被置换出去

在此基础之上,VMS 提供了一个干净(未被修改过)列表和脏(已经被修改过)列表。

当一个进程需要申请空闲内存时,那么首先从干净列表中取出内存页,这种情况下不会触发页面置换。 只有当需要占用脏列表时,才会将内容写入硬盘,这样会减轻 IO 负担。

  • 并且在系统底层是会收集多个页面,然后一次性写入以提高 IO 效率

其他的技巧

lazy 机制

当用户空间申请一段内存时,系统并没有真正的搜寻那么多物理内存然后映射。 而是将这些页面映射到一个只读的,内容为 0 的页上。

当代码真正写内存时,就会触发异常而陷入内核态,这时系统会根据该页表项判断此页面是否具有写权限,如果有才是真正的寻找物理页完成映射。

这种方式即可以节省内存又可以提升系统的处理效率。

cow(copy on write)

当执行内存复制时,系统并没有真正的搬移数据,而是将目标虚拟地址也映射在了原来同样的物理地址上,并且将源地址和目标地址的页表项都改为只读。

当源地址和目标地址中任何一个写操作时,同样也会触发异常而陷入内核,内核判断这个是 COW 页面,此时才会真正的为发出该请求的虚拟地址映射对应的物理内存页。

并且原来的页表项也会变得可读可写了。

这种方式即可以节省内存又可以提升系统的处理效率。

Linux 的虚拟内存

linux 地址空间

mempic/vmc/linux_space.jpg

很多地方和 VMS 类似,page 0 依然是 invalid,并且内核空间对多个进程是共享的。

在 32 位架构中,虚拟地址 0x0 ~ 0xBFFFFFFF 用于用户空间,0xC0000000 ~ 0xFFFFFFFF 用于内核空间。

内核空间被分为了两部分:

逻辑地址空间

一部分是内核逻辑地址空间(kernel logical addresses),这部分就是普通的虚拟地址空间。

内核数据结构、页表、每个进程在内核中的栈等都位于这部分,并且这部分是不能被置换到硬盘的。

这部分还有一个很重要的特点,就是其虚拟地址是 线程映射 到物理地址空间的,也就是说连续的虚拟地址映射到了连续的物理地址空间。

  • 比如 0xC0000000 对应物理地址 0x00000000,0xC0000FFF 对应物理地址 0x00000FFF。

这种特点有两个优点:

  1. 当内核需要通过虚拟地址求物理地址时,代码变得非常简单,就仅仅是减去一个偏移即可
  2. 由于物理地址连续,对应像 DMA 这种需要连续物理地址的硬件,内核就可以很容易的为其分配

在这部分申请内存,内核调用 kmalloc 接口。

虚拟地址空间

另一部分是内核虚拟地址空间(kernel virtual address),这部分对应的物理地址空间是非连续的。

  • 内核通过 vmalloc 接口申请内存。

既然这部分空间不是线性映射的,那么就不使用于 DMA 这种需要连续物理内存的硬件来申请。

这部分存在的原因是为了在 32 位系统中,内核可以映射超过 1GB 的内存。

  • 而在 64 位系统中,这部分则没有存在的必要。

x86 页表的结构

x86 具有 hardware managed MMU(硬件来完成 TLB 刷新),并且支持多级页表。

操作系统在建立好一个进程的页表后,仅需要将一级页表的首地址存入 MMU 即可。

64 位系统使用的是 4 级页表,并且目前 64 位中仅使用到了 48 位(毕竟 48 位可以寻址 256T 的地址空间啊!):

mempic/vmc/64bit_tb.jpg

可以看到,即使是 64 位,其内存块大小依然是 4KB。

大页的支持

除了标准的 4KB 页大小,x86 还支持设置更大的页,比如 2MB,甚至是 1GB 页大小。

使用大页有以下两个好处:

  1. 页表的的占用更小了,节省了内存空间
  2. TLB 的命中率提高了,提升了系统性能
    • 即使出现了 TLB miss,由于页表层级更少,那么搜寻页面的时间也更短了

当然,页太大也有如下缺点:

  1. 如果申请一页仅使用很少的部分,那么就会造成页内内存浪费
  2. 当需要进行置换时,写入硬盘的内容就较多,时间较长

page cache

page cache 用于将硬盘中的内存缓存一部分到内存以提升系统效率。

具体点讲,将下面 3 类内容进行缓存:

  • memory-mapped files:硬盘的文件以及其元数据,通常是通过 read(),write() 等系统调用产生的内容
  • anonymous memory:进程的堆栈所占用的内存块,当内存吃紧时,这些内存块将被置换到硬盘
  • page cache hash table:用于快速的查询到页内容

最开始的页面都是 clean 状态(只用于读,还未被修改过),当页被修改过其状态就是 dirty(或 modified)。 Linux 的后台线程周期性的将 dirty 写回到硬盘,当内存吃紧时,还会将匿名页置换到置换空间。

Last Updated 2020-06-28 Sun 11:59.
Render by hexo-renderer-org with Emacs 26.3 (Org mode 9.3.7)