概述:虚拟内存主要有三个能力:
- 1 将主存看成是磁盘的高速缓存,在主存中保留活动区域,因而高效使用了内存
- 2 为每个进程提供了一致的进程空间,简化内存管理
- 3 保护每个进程的地址空间不被其他进程破坏
下文内容就围绕这几个来讲
物理和虚拟寻址
物理寻址
计算机的主存组织是一个(由M个连续字节大小的单元组成的)数组,每一个字节独有一个唯一的物理地址,从0开始,以此类推!!!
上图读取物理地址4处内容
CPU执行指令,生成有效物理地址->通过内存总线传到主存->主存取出,返回CPU.
虚拟寻址
CPU生成虚拟地址->MMU通过页表转换->其余同上
地址空间
地址空间是一个非负整数地址的有序集合{0,1,2,……}
一个32位的系统中有:2^32 = 4 294 967 296B(4GB)个有效地址。
地址空间的概念很重要,我们必须要清楚数据对象(字节)和它的属性(地址)的区别, 允许数据对象有多个独立的地址,每个地址可以选自不同的地址空间(物理和虚拟地址空间)
.这就是虚拟存储器的基本思想是:主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。
虚拟内存作为缓存
虚拟内存被组织为一个(存放在磁盘上的N个连续字节大小的单元组成的)数组,磁盘上数组的内容被缓存到主存.(看到这里还不清楚是什么意思???往下看)
虚拟存储器的主要思想就是:在主存中缓存硬盘上的虚拟页(pagefile.sys),虚拟页有三个状态:未分配、缓存的、未缓存的。
主存作为磁盘缓存的组织结构
DRAM全相联的(页框与页),使用写回策略
页表
虚拟内存系统必须得判定一个虚拟页是否在DRAM中的某个地方,如果是,还必须确定这个虚拟页存放在拿个物理页中,如果没在,那么还必须在主存中选择一个页进行置换
页表在主存中,页表将虚拟页映射到物理页.每次MMU翻译时,都需要读取页表.页表由OS维护每个进程一个页表
页命中
虚拟地址做索引->由于有效位为1,被缓存在主存->直接使用页表中的地址
缺页(其实就是缓存不命中,想想学过的缓存知识)
CPU使用VP3->查出未被缓存,触发缺页异常->调用内核的缺页异常处理程序->(在这里假设选择了VP4(如果被修改了,就直接写回
磁盘)->从磁盘复制VP3到原来VP4的位置,更新页表并返回)->异常处理程序返回,重新启动导致缺页的指令,重新MMU
按需页面调度:不命中发生时,才进行换入页面的操作
分配页面:程序中malloc是如何工作的?
很简单,就是分配了一块虚拟内存,然后系统只不过创建了一个页表项而已.
这就是你循环malloc很快,但是malloc并write的花就会很慢的原因(因为会触发很多很多次缺页异常)
局部性(页的不命中,开销真的很大)
抖动
其实就是前面说的缓存不命中的一种情况:容量不命中:就是 k 层放不下
工作集的大小超过物理内存大小,不断换入换出
虚拟内存作为内存管理的工具
多个虚拟页面可以映射到同一个共享物理页面上
简化链接:
独立的空间地址意味着每个进程的存储器映像使用相同的格式
。文本节总是从0x08048000(32位)处或0x400000(64位)处开始。然后是数据,bss节,栈。一致性极大简化了链接器的设计和实现。
简化加载:
在硬盘中双击一个图标,启动一个应用程序时,实际上你都不需要将这个程序从硬盘给加载到内存,只需要建个页表,然后页表里的编号指向的是硬盘,然后CPU访问到具体代码的时候,再按照上一节的寻址的方式,按需的将硬盘上的东东加载到内存。加载过程及其简单了。
简化共享:
如果进程用到 printf 函数,则建一个页表项即可,不需要可再每一个进程copy 一份.
会将连续的虚拟页对应到不连续的物理页
简化内存分配:
malloc(100)->连续的虚拟内存页面->映射到不连续的100个物理页面
虚拟内存作为内存保护的工具
通过为PTE添加额外的标识位提供对存储器的保护。
对于标志位我觉得看这个比较舒服一点:
整体举例还是看csapp的:
具体详细的过程可以看我的另一篇文章:Linux中CPU是如何访问到内存的?–MMU最基本原理(转)
加入高速缓存后的结构
TLB PTE的缓存,放在CPU里面,很快,全相联高速缓存(只有一个组)
Linux虚拟内存系统
Linux 为每一个进程读维护了一个单独的虚拟地址空间,如上图
task_struct
:
pgd 指向第一级页表的基址,mmap指向的是一个vm_area_structs的链表,每个该链表中的一个元素描述的是当前虚拟地址空间的一个段(text、data、bss等)
,当内核运行该进程的时候CR3寄存器就被放入了pgd。
如果页面没有被修改过,就直接覆盖!!!!,不需要置换了
内核 内存映射的原理
Linux通过将一个虚拟内存区域(也在磁盘)与一个硬盘上的文件关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。这种将虚拟内存系统集成到文件系统的方法可以简单而高效地把程序和数据加载到内存中。虚拟内存区域额可以关联的文件有:
- 普通文件:用普通文件初始化虚拟内存区域,这样,
程序对该区域操作就相当于在文件中操作了
- 匿名文件:内核使用匿名文件来初始化虚拟内存区域.匿名文件中全是二进制0,由内核创建.置换与缓存置换算法等一样
无论那种情况,只要虚拟页被初始化了,它就在一个由内核维护的交换文件(swap file)之间换来换去。交换文件又称为交换空间(swap space)或交换区域(swap area)。swap区域不止用于页交换,在物理内存不够的情况下,还会将部分内存数据交换到swap区域(使用硬盘来扩展内存)。
内存映射与共享对象
一个对象被映射到虚拟内存的一个区域,要么是作为共享对象,要么是作为私有对象的。
共享对象
如果一个进程将一个共享对象映射到它的虚拟地址空间的一个区域内,那么这个进程对这个区域的任何写操作,对于那些也把这个共享对象映射到它们虚拟内存的其他进程而言,也是可见的。
相对的,对一个映射到私有对象的区域的任何写操作,对于其他进程来说是不可见的。一个映射到共享对象的虚拟内存区域叫做共享区域,类似地,也有私有区域。
写时复制与私有对象
为了节约内存,私有对象开始的生命周期与共享对象基本上是一致的(在物理内存中只保存私有对象的一份副本),并使用写时复制的技术来应对多个进程的写冲突。
对于每一个映射了私有对象的进程,相应私有区域的页表条目被标记为只读,其实就是只要不写他自己的私有区域,就一直共享,写的话就复制一份就行了,如图:
再看fork函数
当fork()函数被当前进程调用时,内核会为新进程创建各种必要的数据结构,并分配给它一个唯一的PID。为了给新进程创建虚拟内存,它复制了当前进程的mm_struct、vm_area_struct和页表的原样副本。并将两个进程的每个页面都标为只读,两个进程中的每个区域都标记为私有区域(写时复制)。
这样,父进程和子进程的虚拟内存空间完全一致,只有当这两个进程中的任一个进行写操作时,再使用写时复制来保证每个进程的虚拟地址空间私有的抽象概念。
用户级别的内存映射-mmap函数
#include <sys/mman.h>
void *mmap(void *start/addr, size_t length, int prot, int flags,
int fd, off_t offset);
mmap
创建新的虚拟内存区域(最好从start开始)->将 fd 指向的对象映射到该区域
上面的这个虚拟内存其实也在磁盘上
prot
参数代表访问权限:
- PROT_READ 这个区域内的页面可读
- PROT_WRITE 这个区域内的页面可写
- PROT_EXEC 这个区域内的页面可执行
- PROT_NONE 这个区域内的页面不能被访问
flags
表示是共享对象还是匿名对象还是私有对象:
- MAP_SHARED 共享对象,提供了POSIX共享内存
- MAP_PRIVATE 所私有对象。对该内存段的修改不会反映到映射文件
- MAP_ANNO 匿名对象
实例:使用共享对象来进行进程间通信
// writer.c
int main(void)
{
// 1.打开映射文件
int fd = open("test.shm", O_RDWR | O_CREAT, 0666);
// 2. 让其在磁盘上存在实际的页面
ftruncate(fd, sizeof(int));
//3.将文件映射到自己的虚拟地址空间中
int *p = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
print_err("mmapp函数");
//4.关闭映射文件,不影响
close(fd);
// 5.产生随机数并存入共享内存中,使用sleep为了便于观察
for (srand(7777);; sleep(1))
{
int v = rand();
*p = v;
printf("produce: %d\n", v);
}
//6.解除内存映射
munmap(p, sizeof(int));
return 0;
}
// reader.c
int main(void)
{
// 1.打开映射文件
int fd = open("test.shm", O_RDWR, 0666);
//3.将文件映射到自己的虚拟地址空间中
int *p = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
print_err("mmapp函数");
//4.关闭映射文件,不影响
close(fd);
// 5.读取随机数
for (;; sleep(1))
{
printf("%d\n", *p);
}
//6.解除内存映射
munmap(p, sizeof(int));
return 0;
}