介绍
计算机的主存(内存条)被组织成一个由M个连续的字节大小的单元组成的数组,每一个byte都有一个唯一的物理地址。
早期的计算机寻址使用物理寻址的方式,也就是cpu需要某一个地址的值,那么久直接从内存中去找这个地址并且取值。
而虚拟内存:虚拟虚拟,也就是实际不存在与内存中的地址,cpu生成一个虚拟地址,这个地址在物理内存中是不存在的!那么要如何寻求实际需要的地址呢?cpu芯片中有一个叫MMU(memory management Unit)内存管理单元的硬件,这个硬件的作用是将cpu生成的虚拟地址翻译成实际存在物理内存中的地址。具体细节下面讨论。
页表的概念
页表是存在物理内存中的结构体数组,每一个页表条目(page table entry,简称PTE)中的成员有2个:
有效位地址 当有效位为1时,表示cpu想要的地址实际存在于物理内存中,而结构体中的地址值就是物理内存中的一个地址;这种情况就是页命中 当有效位为0时,表示cpu想要的地址实际不存在于物理内中,也就是这个虚拟页还没有被分配,这个结构体中的地址指向存在于磁盘上的资源开始的地址;这种情况就是缺页缺页的处理: 当mmu寻得的PTE中有效位为0时,也就是需要的页还没被读到磁盘上,那么就需要将磁盘上的页读到内存中的页表中,相应的,内存中的页表就要牺牲一个PTE来替换成需要的PTE,在磁盘和内存之间传送也的活动叫做交换(swapping)或页面调度(paging),例如,当使用malloc()之后,就会在磁盘,而不是内存中创建PTE并且更新相应页表中的条目。
局部性、抖动的概念 局部性也就是当内存足够程序所需,那么程序所需要的页都已经被加载到内存中了,只需要调用就ok了,不会再和磁盘有交互。 抖动也就是当内存不够用了,程序需要的页一部分必须存在磁盘中,当用到的时候,需要不断的在内存和磁盘两者间抽抽插插= =。因为页面调度很耗时,所以这个时候程序就跑得很慢了!
虚拟内存实现的细节: CPU中有一个寄存器,叫页表基寄存器(Page Table Base Register,PTBR)指向当前页表。进程是CPU分配资源的单位,而每个进程都有一个逻辑页表基址,对应于task_struct的一个叫mm_struct的结构体,这个mm_struct结构体中有一个叫pgd的字段,指向了第一级页表的基址,当进程被调用,那么这个pgd的值就会被放到页表寄存器CR3中~
类似与进程的逻辑pc和cpu中的物理pc,当进程被cpu调度的时候,物理ptbr指向了进程的逻辑btbr,这个时候cpu就可以访问这个进程的页表了。
过程
1.cpu先生成一个n位虚拟地址,这个地址由两部分组成:1个p位的虚拟页面偏移(Virtual Page Offset,VPO)和一个(n-p)位的虚拟页号(Virtual Page Number,VPN)
2.MMU利用vpn在页表中查询相应的ppn,再结合vpo,MMU就将虚拟地址翻译成了PTE条目的物理地址,那么就可以根据这个地址去内存中找了!
3.1若MMU得到的PTE有效位为1,也就是内存中有这个值,那么就返回资源地址,MMU再向内存中读取资源返回给处理器;
3.2若MMU得到的PTE有效位位0,那么就会触发异常,将控制暂时交给内核,触发处理缺页的内核程序,也就是上面说到的牺牲页表中的一个页(对应于物理内存中的一个资源块),如果这个牺牲页被修改了,则把它换出到磁盘。最后把磁盘中的资源读到这个牺牲页并且更新相应的PTE。 于是乎,当cpu下次再请求这个虚拟地址的时候,需要的资源就已经加载到内存中了,也就能页命中了!~
简化链接 虚拟内存要做的并不是直接映射直接的物理地址,而是映射页表的页面偏移量和页条目偏移量,在Linux中,对于64位地址空间,每个进程的代码段总是从虚拟地址0x400000开始的(位于每个进程的起点),也就是说每个进程的代码段除了段号是不同的,起始的页号和页内偏移都是相同的。那么虚拟地址就都可以使用同一个(也就是使用相同的页面偏移或页条目偏移)
简化加载 也就像一些web框架中的一个叫**“懒加载”**的概念,也就是等到资源要被调用的时候我才加载到内存里用,只要我cpu不用,那资源就肯定保存在硬盘,等到用到了,我才调用缺页异常把硬盘的资源读到内存里调用。
使内存分配更灵活 例如,当连续调用malloc函数分配用户栈资源时,如果使用物理内存,那么这个栈区域(快)肯定要使用物理内存中连续的字节来表示,但是有了虚拟内存,每一个需要分配的资源都会通过 缺页异常 在物理内存中分配,并且,这个分配不需要是连续的!它可以随机在物理内存中某个空的地方划一块页面给你用。
给内存增加访问权限检查 当cpu通过虚拟地址想读取一个存在PTE中的物理资源地址的时候,只要在PTE中增加如SUP、READ、WRITE权限标识位,那么就可以在这一步检查是否访问满足这些权限,如果不满足的话,可能会抛出一个叫段错误的异常。
Linux下的内存区域
Linux中内存被分成许许多多的区域(段),一个区域也就是已经分配的虚拟内存的连续片(chunk),注意,这里的连续片是针对虚拟地址来讲的!。例如,对于每一个进程,代码段、数据段、堆、共享库段、以及用户栈都是不同的区域,不属于某个区域的虚拟页也是不存在的,并且不能被进程引用,否则会触发如段错误这样的异常在Linux中,每一个进程的各个区域是由一个链表连接起来的(详细见p581),这个链表位于task_struct中的mm_struct下的mmap字段,mm_struct下还有一个叫pgd的字段,表示这个进程的页表基址地址,当被cpu调度时会被放至CR3寄存器.
在当前进程调用execve函数会将目标文件替代当前文件,也就是加载新鲜的程序~ 具体执行步骤如下:
删除已经存在的用户区域映射新程序的各个区域到PTE映射共享区域(DLL,例如.so文件)更新pc的值为新程序