几个概念
- 程序
存储在磁盘上的文件,在执行的时候加载如内存 - 内存
分为DRAM,和SRAM
DRAM:可以就看成我们买的内存条
SRAM:可以就看是CPU中的一二三级缓存
虚拟内存
好处:
- 将主存(DRAM)进行抽象,通过分页机制,实现:主存中保存活动的区域,并根据需要(缺页异常和正常存取)在主存之间传送数据。高效的使用了主存
- 为进城提供一直的地址空间,简化内存管理,和程序的链接
- 保护每个进城的地址空间不被其他进程破坏。
程序和DRAM
DRAM可以看做是一个M个连续字节大小的数组,每个字节都有唯一的地址:物理地址。
CPU直接访问DRAM使用物理地址的方式成为:物理寻址。
早起程序使用物理寻址,但是目前大多使用虚拟寻址。目前仍有一些特殊的机器使用物理寻址。
虚拟寻址
指的是:CPU访问DRAM的地址不是物理地址,称为虚拟地址。需要由CPU中的内存管理单元(MMU)将虚拟地址转换为物理地址。然后MMU利用转换得到的物理地址发给DRAM,DRAM将数据直接送给CPU。
虚拟寻址主存虚拟内存VM
在物理内存上面抽象出的一层成为虚拟内存,主要是用来管理物理内存的。
虚拟内存对应一个虚拟地址空间,物理内存对应一个物理地址空间。也就是两个M字节的连续数组。分别两种寻址方式。
其中虚拟内存中的某个地址对应物理内存中的某个地址,可以使一对多的关系。
虚拟页
VM将内存又划分为很多的小块,每块大4K,不固定,可变,以后用P表示一个页的大小,每个单位成为虚拟页
同时物理内存页被分页,每个单位成为页帧
虚拟内存通过分页机制达到将将程序的活动部分加载进内存。
可以理解为,将程序也按照每块P大小分割,并不全部将程序加载进内存,而是在需要那一页的时候,将这一页加载进内存。(这里涉及到缺页异常)
每个页可能有如下三种不想交的状态,每个时刻只能有一种:
- 未分配
指的是,VM中的一页,并没有被使用。所以这一虚拟页没有对应的页帧(物理内存的一页)。 - 未缓存
指的是,虚拟页被内存使用,相当于一个变量被声明。但是还没有定义。所以,只是在VM中占用,但是还没有分配页帧。(没有分配页帧的意思是,对应的值没有加载进物理内存) - 以缓存
值得是,VM中的虚拟页,已经连接到页帧,同时也从磁盘中读取数据存储在对应的页帧。
图上说是虚拟页存储在磁盘上?大哥,主存是缓存,虚拟页怎么可能比缓存慢。
页表条目(Page Table Entry)维护了虚拟页到页帧的转换,由操作系统维护。
页表条目存储在物理内存中。
页表条目每一项由:有效位,和物理页号组成。
有效位指的是:1该虚拟页已经指向页帧(以缓存),0该虚拟页没有被使用或者已使用但是未指向页帧(未分配和未缓存)。
物理页号,指的是虚拟页指向的页帧地址。
指向物理内存的是以缓存,指向VP的是未缓存,NULL的代表未分配。
除了有效位以外,页表中还可以存储其他的信息,比如是否可读之类的。
页命中
命中可以理解成,当CPU取数据的时候,能够直接主存中获取,成为命中。如果数据还没有存进主存成为未命中。
在虚拟内存中,DRAM缓存不命中成为缺页。
当CPU读取数据的时候回发生两种情况:
1. 虚拟地址指向的表项PTE中有效位为0
可能是未缓存或者是未分配,这时候触发一个缺页异常!
linux下,MMU试图翻译一个虚拟地址A时,触发一个缺页。这个异常导致控制转移到内核的缺页处理程序::
- 确认A地址是合法。
缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start
和vm_end
做比较。如果指令不合法,发生一个段错误segmatation fault
,结束程序。
因为A地址可能是个函数,或是堆上或是栈上数据,所有要每个都搜索。 - 地址合法的情况下,确认是否有权限
也就是,进城是否又读写或执行该区域内页帧的权限。 - 权限合法的情况下,加载进主存
当物理内存未满的情况下,可能是直接找个空闲的地方加载页帧。(不知道啊。书上都没写。可能是这样)
否则,缺页处理程序在物理内存中选择一个牺牲页,然后,如果该牺牲也被修改过了,那么将页帧内容写会磁盘。然后将新的磁盘内容加载进页帧。同时更新页表。
当缺页处理程序返回的时候,CPU重新启动引起缺页的指令,这时候A已经是以缓存的。正常读取。
选择牺牲页,应该是又算法的,局部性原理。
其中在第一部搜索接受的时候,结构是这样的:
每个线程都有的结构缺页之前:
image.png缺页处理程序返回之后:
image.png当我工作集超出了物理内存的大小,会产生抖动,页面将不断的换进换出。
虚拟页与共享库
当多个程序调用同一个共享库时,程序没必要为每一个程序加载一次共享库。
而是不同程序的虚拟页指向同一页帧。
产生了多对一的情形:
不同程序的虚拟页对应同一页帧
页面调度和虚拟地址空间的结合有点:
- 简化程序链接
每个程序可以采用相同基本格式,从同一虚拟地址开始。 - 简化加载
当程序加载进内存的时候,只需要分配虚拟页,而不必加载进页帧。当需要的时候,发生缺页异常,然后加载。按需加载。 - 简化共享
方便共享库的加载。 - 简化内存分配
在虚拟地址空间中连续的地址,在物理地址空间中不一定连续。
也就是程序在堆栈中分配的地址,在物理空间中可能并不是连续的。
地址翻译过程
- 处理器生成虚拟地址传送给MMU
- MMU生成PTE地址,并从主存或高速缓存请求,去访问的是页表条目。应该是将整一项返回给MMU
- 高速缓存(查找页表条目)以后返回一项给MMU
命中情况下
- MMU根据返回的PTE(此时PTE的有效位为1)构造物理地址,再次发送给主存
- 主存直接读取数据,发送给CPU
缺页
- PTE有效位为0,MMU触发缺页异常。CPU中的控制转移到内核中的缺一处理程序
- 确定牺牲页,替换。缺页处理程序更新PTE
- 确立处理程序返回,CPU再次执行导致缺页的程序
TLB缓存
CPU每次请求,MMU都去查找一次过于浪费。
因此MMU中有一个翻译后背缓冲器(Tanslation Lookaside Buffer)
在上面的第2部时。变为:MMU去查找TLB,如果没查找到,再去主存中查找页表条目。返回后跟新TLB。
页表分级
image.png暂时不是很懂。不过有点理解吧
二. linux虚拟内存系统
execve函数
调用execve函数以后执行以下步骤
- 删除已存在的用户区域
删除当前进程虚拟地址中用户部分已存在的结构,.text,.data等 - 映射私有区域
为因程序的代码,数据,bss和站区域创建新的结构。
这些区域都是私有的,代码区和数据区映射对应文件的.text,.data。而.bss映射到匿名文件,其大小保存在要执行的文件中。 - 映射共享区域
如果要执行的程序调用了共享库之类的,那么需要将这些库映射到用户虚拟空间中的共享区域。 - 设置程序计数器(PC)
最后一件事情是设置进程上下文中的程序计数器,使其指向代码区域的入口点。
图中请求二进制0的地方,为映射到匿名文件
mmap()
。
动态内存分配
分配器指的是new
,delete
,malloc
,free
这些。
分为显示分配器,如上面的四个,也就是需要用户手动释放
隐式分配器也就是垃圾收集器,比如java等语言。
malloc实现
#include <stdlib.h>
void *malloc(size_t size);
返回一个void *
指针,可以隐式转化为任意类型。
其分配的块总是对其的。32下为8的倍数,64位下是16的倍数。
malloc
并不初始化其分配的内存,calloc
初始化。
malloc
的实现,基于mmap()
和sbrk()
。
#include <unistd.h>
void *sbrk(intptr_t incr);
以后补
网友评论