为什么需要虚拟内存?

  1. 内存隔离,使每个运行的进程都认为它拥有整个系统的内存。在多进程系统中直接操作物理内存地址的话,我们需要精确地知道每一个变量的位置都被安排在了哪里,而且还要注意当前进程在和多个进程同时运行的时候,不能共用同一个地址,否则就会造成地址冲突。
  2. 延迟内存分配,只有当页面真正被访问时,才需要从磁盘加载到物理内存。
  3. 页式内存管理技术。
  4. 通过与磁盘空间的交换实现扩展内存容量。

虚拟地址怎么认?

在32位机器上,指针的寻址范围为 $2^{32}$,所能表达的虚拟内存空间为4GB($2^{30}$是从B到GB)。在目前的64位系统下只使用了48位来描述虚拟内存空间,寻址范围为 $2^{48}$,所能表达的虚拟内存空间为256TB(字节可寻址,一个地址的大小是Byte)。

  • 低128T表示用户态虚拟内存空间,虚拟内存地址范围为:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 。

1左移47位(二进制到16进制4位一组)得到的地址是 0x0000800000000000,然后减去一个 PAGE_SIZE(默认为 4K),就是0x00007FFFFFFFF000,共 128T。

  • 高128T表示内核态虚拟内存空间,虚拟内存地址范围为:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。

0x[用户/内核态位][47位偏移量][保留位][12位页内偏移].用户空间的虚拟内存地址的高16位全部为0.内核空间的虚拟内存地址的高16位全部为1.

这样一来就在用户态虚拟内存空间与内核态虚拟内存空间之间形成了一段0x0000 7FFF FFFF F000 - 0xFFFF 8000 0000 0000 的地址空洞,我们把这个空洞叫做canonical address空洞。高16位不全为0也不全为1.

64位Linux系统下,进程虚拟内存空间布局是怎样的?

在进程的地址空间中,包括heap, stack和通过mmap匿名映射的区域,这些区域在建立的时候只有虚拟地址,当它们真正被访问到的时候,内核才会为其分配物理页面,这种方式被称作demand allocation(按需分配),它和page cache的demand paging的概念是对应的这部分物理页面由于没有对应外部存储介质上的文件,因此被统称为anonymous pages。

  1. 准备运行的程序编译成二进制文件存放在磁盘中,CPU会执行二进制文件中的机器码来驱动进程的运行。所以在进程运行之前,这些存放在二进制文件中的机器码需要被加载进内存中,而用于存放这些机器码的虚拟内存空间叫做代码段
  2. 在代码段跟数据段的中间还有一段不可以读写的保护段,它的作用是防止程序在读写数据段的时候越界访问到代码段,这个保护段可以让越界访问行为直接崩溃,防止它继续往下运行。
  3. 程序代码中我们通常会定义大量的全局变量和静态变量,这些全局变量在程序编译之后也会存储在二进制文件中,在程序运行之前,这些全局变量也需要被加载进内存中供程序访问。指定了初始值的全局变量和静态变量在虚拟内存空间中的存储区域我们叫做数据段。没有指定初始值的全局变量和静态变量在虚拟内存空间中的存储区域叫做BSS段。因为内核知道这些数据是没有初值的,所以在二进制文件中只会记录BSS段的大小,在加载进内存时会生成一段0填充的内存空间。
  4. 程序在运行期间往往需要动态的申请内存,所以在虚拟内存空间中也需要一块区域来存放这些动态申请的内存,这块区域就叫做堆。堆空间中地址的增长方向是从低地址到高地址增长。内核中使用start_brk标识堆的起始位置,brk标识堆当前的结束位置。当堆申请新的内存空间时,只需要将brk指针增加对应的大小,回收地址时减少对应的大小即可。比如当我们通过malloc向内核申请很小的一块内存时(128K之内),就是通过改变brk位置实现的。
  5. 程序在运行过程中还需要依赖动态链接库,这些动态链接库以.so文件的形式存放在磁盘中,比如C程序中的glibc,里边对系统调用进行了封装。glibc库里提供的用于动态申请堆内存的malloc函数就是对系统调用sbrk和mmap的封装。这些动态链接库也有自己的对应的代码段,数据段,BSS段,也需要一起被加载进内存中。还有用于内存文件映射的系统调用mmap,会将文件与内存进行映射,那么映射的这块内存(虚拟内存)也需要在虚拟地址空间中有一块区域存储。这些都被放在文件映射与匿名映射区
  6. 在程序运行的时候总该要调用各种函数吧,那么调用函数过程中使用到的局部变量和函数参数也需要一块内存区域来保存。这一块区域在虚拟内存空间中叫做

虚拟内存是怎么被组织的?

可以通过cat /proc/pid/maps或者pmap pid来查看某个进程的实际虚拟内存布局。

这涉及到具体的数据结构和红黑树,不是这篇blog重点,下面放一个概要图。

ZONE类型与内核空间

  1. 内核空间是所有进程共享的,不同进程进入内核态之后看到的虚拟内存空间全部是一样的。
  2. 内核态虚拟内存空间的前896M区域是直接映射到物理内存中的前896M区域中的,直接映射区中的映射关系是一比一映射。映射关系是固定的不会改变。直接映射区的前16M专门让内核用来为DMA分配内存,这块16M大小的内存区域我们称之为ZONE_DMA。 X86体系结构下ISA总线的直接内存存取控制器叫DMA。16M到896M(不包含896M)这段区域,我们称之为 ZONE_NORMAL
  3. 物理内存896M以上的区域被内核划分为ZONE_HIGHMEM区域,我们称之为高端内存。这块动态映射区内核是使用 vmalloc 进行内存分配,将不连续的物理内存映射到连续的虚拟内存上。

内核页面属于逻辑zone的那个不可移动类的。

与用户态内核态有什么联系?

  • 内核空间用于存放操作系统内核的代码和数据,具有高特权级别,只能由内核态的代码访问。
  • 用户空间用于存放用户应用程序的代码和数据,具有较低的特权级别,只能由用户态的代码访问。

这种分离允许操作系统保护其核心功能并确保系统的稳定性和安全性。用户应用程序需要通过系统调用来与内核进行通信和访问系统资源。

页表是物理页和虚拟页的纽带

页表也是放在物理页中的。因为页表也是可以被放在LLC之类的靠近CPU的地方缓存的,大内存使用后page walk的开销对DRAM5-6次的访问发生次数到底是不是瓶颈?加上页面被动态迁移,就算是再大的内存,热的部分在DRAM里了,LLC可以存那个区域的PDE,这样更减少了page walk。

page fault的发生是建立物理页和虚拟页对应的重要途径。

分配虚拟页就是vma是malloc和mmap在做,但是他们区别是什么呢?

PS. 文章根据公众号@bin的技术小屋做了一定增删。


文章作者: 易百分
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 易百分 !
  目录