虚拟内存
虚拟内存(VM,现代系统提供的一种对主存的抽象概念),是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,为每个进程提供了一个大的、一致的和私有的地址空间。
虚拟内存提供了三个重要的能力:
- 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据==>高效地使用了内存
- 它为每个进程提供了一致的地址空间==>简化了内存管理
- 它保护了每个进程的地址空间不被其它进程破坏==>保护私有空间
物理和虚拟寻址
主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址。
现代处理器使用虚拟寻址,CPU通过生成一个虚拟地址(VA)来访问主存,这个虚拟地址在被送到内存之前先转换成适当的物理地址。
地址翻译(将一个虚拟地址转换为物理地址的过程),需要用到CPU硬件和操作系统来实现。CPU芯片上叫做内存管理单元(Memory Management Unit,MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容由操作系统管理。
地址空间是一个非负整数地址的有序集合:{1,2,3……},一个地址空间的大小是由表示最大地址所需要的位数来描述的。
假设使用的是线性地址空间(地址空间中的整数是连续的)
- 虚拟地址空间:CPU从一个有N = 2n个地址的地址空间中生成虚拟地址:{1,2,3,……,N - 1}
- 物理地址空间:系统中物理内存的M个字节:{1,2,3,……,M - 1}
地址空间清楚地区分了数据对象(字节)和它们的属性(地址),将地址和字节联系起来。
虚拟内存的基本思想:允许每个数据对象(字节)有多个独立的地址,其中每个地址都选自一个不同的地址空间。主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。
虚拟内存作为缓存工具
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。
VM系统通过将虚拟内存分割为固定大小(P = 2p字节)的块,虚拟页(virtual page,VP);物理内存被分割为固定大小(P字节)的块,物理页(physical page,PP),也称为页帧(page frame)。
在任意时刻,虚拟页面的集合都分为三个不相交的子集:
- 未分配的:VM系统还未分配(或者创建)的页,未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。
- 缓存的:当前已经缓存在物理内存中的已分配的页。
- 未缓存的:未缓存在物理内存中的已分配的页。
SRAM缓存表示CPU和主存之间的L1、L2和L3高速缓存;DRAM缓存表示虚拟内存系统的缓存,它在主存中缓存虚拟页。
虚拟页通常是4KB~2MB(大的不命中处罚和访问第一个字节的开销);DRAM缓存是全相联的(大的不命中处罚),任意PP都可以包含任意VP;DRAM缓存采用写回(对磁盘的访问时间很长)。
页表
虚拟内存系统的功能是由软硬件联合提供的,包括操作系统软件、MMU中的地址翻译硬件和一个存放在物理内存中的叫做页表(page table)的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟内存转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。
页表就是一个页表条目(Page Table Entry,PTE)的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE(每个VP都对应一个PTE)。PTE由一个有效位和一个n位地址字段组成。
- 设置了有效位:地址字段就表示DRAM中相应的物理页的起始地址,这个物理页中缓存了该虚拟页==>缓存的
- 没设置有效位并且空地址:虚拟页还没未被分配==>未分配的
- 没设置有效位并且有地址:地址指向该虚拟也在磁盘上的起始位置==>未缓存的
地址翻译硬件将虚拟地址作为一个索引来定位PTE,并从内存中读取。页命中:
缺页(DRAM缓存不命中)会触发一个缺页异常,缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页并修改此牺牲页的PTE,如果此牺牲页已经被修改了,那么内核会将牺牲页复制回磁盘。
内核会将缺页的页复制到内存中,并且修改PTE。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。
分配页面:例如调用malloc
的结果
抖动:页面不断地换进换出,可能是工作集(程序主要运转的页集合)的大小超出了物理内存的大小。
统计缺页次数:
#include <sys/resource.h>
int getrusage(int who, struct rusage *usage);
struct rusage {
struct timeval ru_utime; /* 用户CPU时间 */
struct timeval ru_stime; /* 系统CPU时间 */
long ru_maxrss; /* 最大驻留集大小 */
long ru_ixrss; /* 集成共享内存大小 */
long ru_idrss; /* 集成未共享内存大小 */
long ru_isrss; /* 集成堆栈大小 */
long ru_minflt; /* 页面重用次数 */
long ru_majflt; /* 页面错误次数 */
long ru_nswap; /* 换出次数 */
long ru_inblock; /* 块输入操作的次数 */
long ru_oublock; /* 块输出操作的次数 */
long ru_msgsnd; /* 发送消息次数 */
long ru_msgrcv; /* 接收消息次数 */
long ru_nsignals; /* 接收信号次数 */
long ru_nvcsw; /* 自愿上下文切换次数 */
long ru_nivcsw; /* 非自愿上下文切换次数 */
};
虚拟内存作为内存管理的工具
多个虚拟页面可以映射到同一个共享物理页面上。
按需页面调度和独立的虚拟地址空间的结合,VM实现了下面功能:
- 简化链接:独立的地址空间允许每个进程的内存映像使用相同的基本格式,而不管代码和数据实际存放在物理内存的何处。这样的一致性简化了链接器的设计和实现,允许链接器生成完全链接的可执行文件,这些可执行文件是独立于物理内存中代码和数据的最终位置的。
- 简化加载:VM使得容易向内存中加载可执行文件和共享对象文件。要把目标文件中.text和.data节加载到一个新创建的进程中,Linux加载器为代码和数据段分配虚拟页,把它们标记为无效的(即未被分配的),将PTE指向目标文件中适当的位置。加载器从不从磁盘到内存实际复制任何数据。在每个页初次被引用时,VM系统会按照需要自动的调入数据页。
- 简化共享:独立的地址空间为OS提供了一个管理用户进程和OS自身之间共享的一致机制。每个进程都有自己私有的不共享的段,这种情况下,OS创建页表,将相应的虚拟页映射到不连续的物理页面。OS通过将不同进程中适当的虚拟页面也射到相同的物理页面,从而安排共享代码和数据。
- 简化内存分配:VM为向用户进程提供一个简单的分配额外内存的机制。当一个运行在用户进程中的程序要求额外的堆空间时(如调用malloc的结果),OS分配一个适当数字(例如k)个连续的虚拟内存页面,并且将它们映射到物理内存中任意位置的k个任意的物理页面。由于页表工作的方式,OS没有必要分配k个连续的物理内存页面,页面可以随机地分散在物理内存中。
虚拟内存作为内存保护的工具
可以通过在PTE上添加一些额外的许可位来控制对一个虚拟页面内容的访问(每次CPU生成一个地址时,地址翻译硬件都会读一个PTE)。
SUP位表示进程是否必须运行在内核模式下才能访问该页,运行在内核模式中的进程可以访问任何页面,运行在用户模式中的进程只允许访问SUP=0的页面。如果一条指令违反了这些许可条件,那么CPU就触发一个一般保护故障,将控制传递给内核中的异常处理程序——>段错误(segmentation fault)。
地址翻译
地址翻译:一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素之间的映射。
MMU利用页表来实现这种映射。CPU中的页表基址寄存器(Page table Base register,PTBR,存储的是物理地址)指向当前页表。MMU利用VPN来选择合适的PTE。进程切换时,更新PTBR。
页命中时,CPU硬件执行步骤:页命中完全是由硬件处理的
- 处理器生成一个虚拟地址,并把它传送给MMU
- MMU生成PTE地址,并从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE
- MMU构造物理地址,并把它传送给高速缓存/主存
- 高速缓存/主存返回所请求的数据字给处理器
处理缺页要求硬件和OS内核协作完成:
- 处理器生成一个虚拟地址,并把它传送给MMU
- MMU生成PTE地址,并从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE
- PTE中的有效位是0,所以MMU触发了一次异常,传递CPU中的控制到OS内核中的缺页异常处理程序
- 缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘,更新PTE
- 缺页处理程序调入新的页面,更新内存中的PTE
- 缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU,此次命中,主存就会将所请求字返回给处理程序
大多数OS使用物理地址来访问SRAM高速缓存,使用物理寻址,多个进程同时在高速缓存中有存储块和共享来自相同虚拟页面的块变得容易。高速缓存无需处理保护问题,因为访问权限的检查是地址翻译过程的一部分。地址翻译发生在高速缓存查找之前。页表条目也可以缓存在高速缓存中。
翻译后备缓冲器(Translation Lookaside Buffer,TLB),在MMU中一个关于PTE的缓存。TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。TLB可以将不同层次上页表的PTE缓存起来。
所有的地址翻译过程都是在芯片上的MMU中执行的:
- CPU产生一个虚拟地址
- MMU从TLB中取出相应的PTE
- MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存
- 高速缓存/主存将所请求的数据字返回给CPU
- 当TLB不命中时,MMU必须从高速缓存中取出相应的PTE,新的PTE存放在TLB中,可能会覆盖一个已经存在的条目
多级页表
用来压缩页表的常用方法是使用层次结构的页表。
从两个方面减少了内存要求:
- 如果一级页表中的一个PTE是空的,那么相应的二级页表根本不会存在
- 只有一级页表才需要总是在主存中;虚拟内存系统可以在需要时创建、调入或调出二级页表;只有最经常使用的二级页表才需要缓存在主存中
带多级页表的地址翻译并不比单级页表慢得多。
Q:p575中读出偏移量CO处的数据字节后,需要返回给MMU吗?不是直接将数据字节给CPU吗?
Linux内存系统
每个进程有它私有的页表层次结构,当MMU翻译每一个虚拟地址时,它还会更新引用位(实现页面替换算法)和修改位(脏位,是否写回牺牲页),内核可以通过一条特殊的内核模式指令来清除引用位或修改位。
Linux为每个进程维护了一个单独的虚拟地址空间:
Linux将虚拟内存组织成一些区域(段)的集合。一个区域就是已经存在着的(已分配的)虚拟内存的连续片段(chunk),这些页是是以某种方式相关联的。每个存在的虚拟页面都保存在某个区域中,而不属于某个区域的虚拟页是不存在的,并且不能被进程引用。区域允许虚拟地址空间有间隙,内核不用记录那些不存在的虚拟页,不存在的虚拟页也不占用内存、磁盘或者内核本身中的任何额外资源。
内核为系统中的每个进程维护一个单独的任务结构(task_struct),任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(PID、指向用户栈的指针、可执行目标文件的名字、程序计数器)。下图是记录一个进程中虚拟内存区域的内核数据结构:
task_struct
中的一个条目指向mm_struct
(描述虚拟内存的当前状态),其中pgd
字段指向第一级页表(页全局目录)的基址,mmap
字段指向一个vm_area_structs
(区域结构)的链表,其中每个vm_area_structs
都描述了当前虚拟地址空间的一个区域。当内核运行这个进程时,将就pgd存放在CR3
控制寄存器中。
一个具体区域的区域结构包含下面的字段:
vm_start
:指向这个区域的起始处vm_end
:指向这个区域的结束处vm_port
:描述这个区域内包含的所有页的读写许可权限vm_flags
:描述这个区域内的页面是进程共享的还是私有的vm_next
:指向链表中下一个区域结构
MMU在试图翻译某个VM A时,触发了缺页,这个异常导致控制转移到内核的缺页处理程序,处理程序随后执行下面的步骤:
- A在某个区域结构定义的区域内吗?缺页处理程序搜索区域结构的链表,把A和每个区域结构中的
vm_start
和vm_end
做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止该进程。 - 试图进行的内存访问是否合法?(进程是否有读、写、执行这个区域内页面的权限?)如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止该进程。
- 此时内核知道此缺页是由于对合法的虚拟地址进行合法的操作造成的。正常缺页–>页面替换。
内存映射
内存映射(将一组连续的虚拟页映射到任意一个文件中的任意位置的表示法)
Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。虚拟内存区域可以映射到两种类型的对象中的一种:
- Linux文件系统中的普通文件。一个区域可以映射到一个普通磁盘文件的连续部分。文件区被分成页大小的片,每一片包含一个虚拟页面的初始内容。因为按需进行页面调度,所以这些虚拟页面没有实际交换进入物理内存,直到CPU第一次引用到页面。如果区域比文件区要大,那么就用零填充这个区域的余下部分。
- 匿名文件。一个区域也可以映射到一个匿名文件(由内核创建的,包含的全是二进制零)。CPU第一次引用这样一个区域内的虚拟页面时,内核就在物理内存中找到一个合适的牺牲页面,用二进制的零覆盖牺牲页面并更新页表,将这个页面标记为驻留在内存中的。磁盘和内存之间并没有实际的数据传送==>映射到匿名文件的区域中的页面有时也叫做请求二进制零的页。
一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件(交换空间、交换区域)之间换来换去。在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。
内存映射提供了一种用来控制多个进程如何共享对象的机制。
进程试图写被标记为私有的写时复制的区域结构时,写操作会触发一个保护故障,故障处理程序在物理内存中创建这个页面的新副本,更新PTE,然后恢复这个页面的可写权限。
fork创建带有自己独立虚拟地址空间的新进程的过程:当fork
函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct
、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此也就为每个进程保持了私有地址空间的抽象概念。
execve加载并运行a.out步骤:
- 删除当前进程虚拟地址的用户部分中的已存在的区域结构
- 映射私有区域,为新程序的代码、数据、bss和栈区域创建新的、私有的、写时复制的区域结构。.bss区域大小包含在a.out中,栈和堆区初始长度为0
- 映射共享区域,如果a.out程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内
- execve设置当前进程上下文中的程序计数器,指向代码区域的入口点
使用mmap函数的用户级内存映射:Linux进程可以使用mmap函数来创建新的虚拟内存区域,并将对象映射到这些区域中。
#include <unistd.h>
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
成功:指向映射区域的指针
出错:MAP_FAILED(-1)
port描述新映射的虚拟内存区域的访问权限,vm_port
int munmap(void *start, size_t length);
成功:0
出错:-1
mmap
函数要求内核创建一个新的虚拟内存区域,最好是从地址start开始的一个区域(只是一个暗示,通常为NULL),并将文件描述符fd指定的对象的一个连续的片映射到这个新的区域。
munmap
函数删除从虚拟地址start开始的,长为length字节组成的区域。接下来对已删除区域的引用会导致段错误。
工具
-
vmstat:命令用于报告虚拟内存统计信息,包括系统的运行状态、进程、内存、磁盘、陷阱和CPU活动。
root@china:~/csapp# vmstat -s | grep "page" 843850 K paged in 198100 K paged out 0 pages swapped in 0 pages swapped out root@china:~/csapp# vmstat procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu------- r b swpd free buff cache si so bi bo in cs us sy id wa st gu 1 0 0 2990956 1436 231120 0 0 88 20 386 1 0 0 99 0 0 0 root@china:~/csapp# vmstat -s 3948580 K total memory 953212 K used memory 70800 K active memory 508520 K inactive memory 2993504 K free memory 1444 K buffer memory 231196 K swap cache 1048576 K total swap 0 K used swap 1048576 K free swap 24873 non-nice user cpu ticks 245 nice user cpu ticks 26096 system cpu ticks 7542309 idle cpu ticks 3233 IO-wait cpu ticks 0 IRQ cpu ticks 8855 softirq cpu ticks 0 stolen cpu ticks 0 non-nice guest cpu ticks 0 nice guest cpu ticks 843850 K paged in 198268 K paged out 0 pages swapped in 0 pages swapped out 3668250 interrupts 10019654 CPU context switches 1718615184 boot time 29176 forks
问题自检
- 计算机寻址方式有哪些?现在CPU使用什么寻址?
- 什么是虚拟内存?
- 如何地址翻译?什么是页表?如何通过页表进行地址翻译?
- 主存是如何当磁盘的缓存的?页命中和缺页?如何分配一个页(malloc)?
- 虚拟内存如何管理内存?os为每个进程一个PT,不同进程的VP可以映射到相同的PP;简化了链接、加载、共享、内存分配
- 分别说明使用单级页表、单级页表+高速缓存、单级页表+高速缓存+TLB、多级页表的地址翻译过程?命中+不命中
- Linux的虚拟内存如何组织的?数据结构
- Linux缺页异常处理流程?
- 内存映射如何实现私有和共享?