update
2019-06-28 20:40
在 stack exchange 社区上看到一个通俗易懂的回答:https://superuser.com/a/1165426
在现代操作系统中,进程之间共享使用cpu和内存,但是内存资源有限,为了更加高效地使用内存,现代操作系统提供一个内存抽象—虚拟内存。虚拟内存巧妙地利用内存,地址转换,磁盘文件和操作系统内核来为每一个进程提供足够大的统一的私有地址空间。虚拟内存提供三个重要的能力:1)将内存当作磁盘的缓存,在内存中只保留常用数据,必要时从内存和磁盘之间交换数据。2)简化内存管理,为每个进程提供统一的地址空间。3)保护进程的内存空间不受其他进程影响。
虚拟内存是计算机系统里的一个很棒的方案。它的实现对于程序员来说是透明的,但是程序员为什么需要了解它的工作机制呢?1)虚拟内存的位置比较核心,它渗透到计算机系统的方方面面,比如硬件异常设计,汇编器,加载器,共享对象文件,文件和进程。2)虚拟内存功能非常强大,它能给应用程序提供创建销毁一块内存并将其映射到磁盘文件的能力,还能与其他进程共享内存。比如,你能通过读写特定内存地址来改变磁盘文件的内容,还可以在不需要copy操作的情况下,将磁盘文件的内容加载到内存里。
上图是cpu通过物理地址读取4个字节示意图。当cpu执行加载操作,它将有效的物理地址通过地址总线传给内存,内存从地址4开始读取4个字节,将这4个字节内容返回给cpu。
上图是cpu通过虚拟地址读取4个字节示意图。cpu在加载虚拟地址(VA)之前,MMU(地址管理单元)会将VA转换成PA(物理地址),然后在执行正常的读取操作。
从概念上来讲,虚拟内存一种存储在磁盘上的N个连续字节的数组。对应与数组中的每个字节都有其唯一的虚拟地址。数组中的部分字节内容缓存在内存中。内存与磁盘之间交换的时候以内存页(VPs)为最小单位,对应地,物理内存在分页,PPs。
在某个时刻,虚拟内存页(VPs)可以有一下三种:
VM system 必须确认某个虚拟页(VP) 是否存储在DRAM中. 如果在,还要确定存储在那个物理内存页中(PP).
这种情况会出发操作系统内核page fault exception,系统内核会选择一个内存页剔除,将存储在磁盘上对VP(图中就是VP3)加载到内存中。这里假设内核选择了将VP4剔除出去,这时如果VP4内容相比在磁盘中的VP4有变更,内核会将DRAM中的VP4拷贝到磁盘中,然后修改页表内对应的PTE的valid位为0,标明该VP没有在DRAM中。
操作系统通过系统调用malloc来分配一页虚拟内存。例如下图就是一个分配虚拟内存的例子。在磁盘上为虚拟内存页VP5分配空间,并且在页表上登记VP5。
上面讲了那么多都是关于VM作为缓存工具的内容,其实有意思的是,在很多计算机系统里虚拟内存比物理内存要小,这样看起来就没必要使用缓存了。但是VM另有大用,它可以作为有效的内存管理工具来保护内存。
上面的内容都是假设有一张页表,将一个虚拟内存空间映射到物理内存空间,其实,操作系统为每个进程提供来单独的页表和单独的虚拟内存空间。请看下面的示意图。
进程i的VP1映射到PP2,VP2映射到PP7,进程j的VP1映射到PP7,VP2映射到PP10,进程i和共享PP7.
VM能够简化链接、加载、代码与数据共享以及程序到内存分配。
.text
段和.data
段是连续相邻的,为了将其加载到内存中,加载器从0x08048000(32bit system)或者0x400000(64bit system)开始分配一块连续虚拟内存,并在页表中标记valid位为0(没有命中缓存,not cached),然后将对应到PTE执行可执行文件到对应位置(这里就是.text
段)。可以看出,这个过程加载器并不需要从磁盘copy内容到内存里,CPU加载或者执行指令时候返现相应到VP没有在缓存中,这时才会触发数据从磁盘copy内容到内存。操作系统将连续的虚拟内存映射到一个文件到任意位置的能力称作内存映射(memory mapping)。Unix操作系统的mmap
系统调用允许应用程序做内存映射操作。printf
函数。这时就要用到上图示意的共享PP的场景了。通过在页表上加上三个权限位(sup, write, read)来控制内存的访问权限来保存内存。
sup(supervisor)如果是yes,那么代表该VP只有在进程运行在kernal模式下才能访问,否则不能访问。read和write分别代表读写权限。
这里介绍一下地址转换的基础知识。
地址转换就是一个映射的过程(map)。将N大小的虚拟地址空间映射到M大小的物理地址空间。
MAP: VAS → PAS ∪ ∅
where
MAP(A) = A′ if data at virtual addr A is present at physical addr A′ in PAS,
= ∅ if data at virtual addr A is not present in physical memory.
先定义一些符号:
下图展示了MMU如何利用页表来完成映射的。CPU里有个寄存器(page table base register)指向当前页表。n-bit虚拟地址分成两部分表示,其中p-bit位表示虚页offset(virtual page offset (VPO)),剩下(n-p)bit表示虚页号(virtual page number (VPN))。MMU根据VPN找到对应的PTE,比如VPN1对应PTE1,VPN2对应PTE2,类似,VPNn对应PTEn。虚页个数总是与页表PTE的个数一致。通过PTE里记录的物理地址页号( physical page number (PPN))组合VPO就可以得到物理地址了。假设虚拟页和物理页的offset都是p-bit,那么VPO和PPO是相等的。
下面详细列举一下page hit的过程,如下图:
1. CPU将虚拟地址传给MMU,
2. MMU生成PTE页表地址,去内存里拿页表。
3. 内存返回页表给MMU,
4. MMU根据页表查表后得到物理地址,去请求内存
5. 根据物理地址,内存将数据返回给CPU
上面的步骤看出,page hit 这种情况可以在硬件基础上完成,可是 page fault 就需要操作系统内核配合完成了,下图是page fault 情况下的寻址:
1. 1,2,3步同上面相同。
4. 发现PTE的valid标志位是0,MMU将CPU的执行权交给操作系统内核的page fault 异常处理程序(page fault exception handler)。
5. 异常处理程序选中一个物理内存页(PP)剔除出去,如果该页内容相比之前有变动,同时将其内容写回磁盘。
6. 异常处理程序将需要的页从磁盘里读取到内存里,并且更新内存里的页表里的PTE。
7. 异常处理程序执行结束返回,CPU继续之前的寻址操作,现在一切情况都和 page hit 情况一样了。
不管是 page hit 还是 page fault,MMU想要查表就必须有表可查,表是放在内存中的,每次CPU寻址,都要去内存中去获取页表,貌似浪费很多个时钟周期啊。如果PTE缓存在CPU的L1缓存中,那么获取查表速度就提升很多了。目前translation lookaside buffer (TLB)就是干这个事儿的。
http://engineering.pivotal.io/post/virtual_memory_settings_in_linux_-_the_problem_with_overcommit/
https://manybutfinite.com/post/anatomy-of-a-program-in-memory/
http://www.science.unitn.it/~fiorella/guidelinux/tlk/node50.html
https://www.tldp.org/LDP/sag/html/vm-intro.html
https://stackoverflow.com/questions/4970421/whats-the-difference-between-virtual-memory-and-swap-space