目录
1. 虚拟内存
虚拟内存的基本思想:每个程序都有自己的地址空间,这个空间被分割成多个块,每一个块被称作一页或页面。每一页有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。
1.1 linux进程内存布局
进程内存空间通过分段存储来管理
内核空间(kernel space)(1G)
内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。
用户空间(3G)
- 环境变量(environment variables)
- 命令行参数(command-line arguments)
- 栈(stack)
栈又称堆栈,由编译器自动分配释放,用来存储临时数据和栈帧。 - 内存映射段(memory mapping segment)
将硬盘文件的内容直接映射到内存,内存映射是一种方便高效的文件I/O方式, 因而被用于装载动态共享库。 - 堆(heap)
堆用于存放进程运行时动态分配的内存段。 - BSS段(bss segment)
静态内存分配,保存未初始化的全局及静态变量(皆为0),可读可写。 - 数据段(data segment)
静态内存分配,保存已初始化的全局及静态变量,可读可写。 - 代码段(text segment)
保存可执行机器码和常量,可读不可写可执行。 - 保留区(reserved)(32位cpu下占128M)
位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。
它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称。
在32位X86架构的Linux系统中,用户进程可执行程序一般从虚拟地址空间0x08048000开始加载。该加载地址由ELF文件头决定
交换分区:在物理内存满时, 如果还需要内存资源,内核则把物理内存中非活动的页面放到交换分区中。
1.2 分页
大部分虚拟内存系统中都使用一种称为分页的技术。
由程序产生的地址称为虚拟地址,它们构成了一个虚拟地址空间。在没有虚拟内存的计算机上,系统直接把虚拟地址送到内存总线上,读写操作使用具有相同地址的物理内存字;而在使用虚拟内存的情况下,虚拟地址不是被直接送到内存总线上,而是被送到内存管理单元(Memory Management Unit,MMU),MMU把虚拟地址映射为物理内存地址。
虚拟地址空间按照固定大小划分成被称为页面(page)的若干单元。在物理内存中对应的单元称为页框。页面和页框的大小通常是一样的。实际系统中的页面大小从512字节到1GB。
栗子:一页4KB,64KB的虚拟地址空间和32KB的物理内存可得到16个虚拟页面和8个页框。从虚拟地址到物理地址的流程:
在上图中,虚拟地址8196(二进制是0010 0000 0000 0100),输入的16位虚拟地址被分为4位的页号和12位的偏移量。4位的页号可以表示16个页面,12位的偏移量可以为一页内的全部4096个字节编址。
可用页号作为页表的索引,以得出对应于该虚拟页面的页框号。如果“在/不在”位是0,则引起缺页中断。如果该位是1,则将在页表中查到的页框号复制到输出寄存器的高3位中,再加上输入虚拟地址的低12位偏移量。如此就构成了15位的物理地址。输出寄存器的内容随即被作为物理地址送到内存总线。
此处讨论缺页中断(或称为缺页错误)发生的事:假如操作系统访问虚拟页面B产生缺页中断,决定放弃页框1(对应虚拟页面A),那么它将把产生缺页中断所对应的虚拟页面(页面B)装入页框1的起始物理地址,并对MMU映射做两处修改:首先,它要将原来页框1所对应的虚拟页面(页面A)的表项设为未映射,使以后任何对原来虚拟页面(页面A)的访问都导致陷阱。随后,把引起缺页中断(页面B)的的表项设为映射,因此在引起陷阱的指令重新启动时,它将虚拟页面B映射为物理地址页框1中的某个地址。
1.3 页表
虚拟地址被分成虚拟页号(高地址)和偏移量(低地址)两部分。不同的划分对应了不同的页面大小。
虚拟页号可作为页表的索引,以找到该虚拟页面对应的页表项。由页表项可以找到对应的页框。然后把页框号拼接到偏移量的高位端,以替换调虚拟页号,形成送往内存的物理地址。
页表的目的是把虚拟页面映射为页框,把虚拟地址中的虚拟页面域替换成页框域,从而形成物理地址(本篇博客讨论的情况均不涉及虚拟机,每个虚拟机都需要有自己的虚拟内存,因此页表组织变得很复杂,包括影子页表和嵌套页表)。
页表项的结构
- 页框号:最重要的就是页框号,页映射的目的就是找到这个值
- “在/不在”位:是1时表示该表项是有效的,可以使用;是0时则表示该表项对应的虚拟页面现在不在内存中,访问该页面会引起一个缺页中断。
- 保护位:指出一个页允许什么样的方式访问,最简单的形式是只有一位,0表示读/写,1表示只读;更先进的方式是使用三位,各位分别表示是否启用读、写、执行该页面。
- 修改位:记录页面的使用情况,在写入一个页时自动设置修改位。如果一个页面已经被修改过(即它是“脏”的),则必须把它写会磁盘。如果没有被修改过(即它是“干净”的),可以直接被丢弃,因为它在磁盘上的副本仍然是有效的。脏位,反映看该页面的状态。
- 访问位:不论是读还是写,系统都会在该页面被访问时设置访问位。用于页面置换算法中。
- 禁止该页面被高速缓存:对于映射到设备寄存器而不是常规内存的页面很重要。具有独立的I/O空间而不使用内存映射I/O的机器不需要这一位。
在任何分页系统中,都需要考虑两个主要问题:
- 虚拟地址到物理地址的映射必须非常快。
- 如果虚拟地址空间很大,页表也会很大。
1.4 加速分页过程
每次访问内存都需要进行虚拟地址到物理地址的映射,每条指令进行一两次或更多页表访问是必要的。如果执行一条指令需要1ns,页表查询必须在0.2ns之内完成,以避免映射成为一个主要瓶颈。
现代计算机使用至少32位的虚拟地址。假设页面大小为4KB,32位的地址空间将有100万页,那么页表必然有100万条表项,而且每个进程都需要自己的页表。64位地址空间简直多到超乎你的想象。
转换检测缓冲区
大多数程序总是对少量的页面进行多次的访问,只有很少的页表项会被反复读取,而其他大的页表项很少被访问。利用这种特性有一种解决方案:为计算机设计一个小型的硬件设备,将虚拟地址直接映射到物理地址,而不必再访问页表。这种设备称为转换检测缓冲区(Translation Lookaside Buffers,TLB),有时又称为相联存储器或快表。它通常在MMU中,包含少量的表项,在实际中很少会超过256个。每个表项记录了一个页面的相关信息,包括虚拟页号、页面的修改位、保护码和该页锁对应的物理页框,还有另外一位用来记录这个表项是否有效(即是否在使用)。
TLB的工作过程:将一个虚拟地址放入MMU中进行转换时,硬件首先通过将该虚拟页号与TLB中所有表项同时(并行)进行匹配,判断虚拟页面是否在其中。如果发现在,并且不违反保护码,则将页框号直接从TLB中取出而不必再访问页表。如果违反了保护码,则会产生一个保护错误,就像对页表进行非法操作一样。如果虚拟页号不在TLB中,此时就会去进行正常的页表查询。接着从TLB中淘汰掉一个表项,然后用找到的页表项代替它。当一个表项被清除除TLB时,将修改位复制到内存中的页表项,而除了访问位,其他的值不变。当页表项中从页表中装入TLB中时,所有的值都来自内存。
软件TLB管理
现代许多的机器,几乎所有的页面管理都在软件中实现。TLB被操作系统显示地加载,当发生TLB访问失效时,生成一个TLB失效并将问题交给操作系统解决。系统找到该页面,然后从TLB中删除一个项,接着装载一个新的项,最后再执行先前出错的指令。TLB失效比缺页中断更加频繁。
如果TLB大到(如64个表项)可以减少失效率时,TLB的软件管理就会变得足够有效。
在减少TLB失效的同时,又要在发生TLB失效时减少处理开销。有时候操作系统能用“直觉”指出哪些页面下一步可能会被用到并预先为它们在TLB中装载表项。
(软件、硬件)处理TLB失效常用的办法都是找到页表并执行索引操作以定位将要访问的页面。软件做这样的搜索时,通过在内存中的固定位置维护一个大的(如4KB)TLB表项的软件高速缓存(该高速缓存的页面一直保存在TLB中)来减少TLB失效。
两种不同的TLB失效:
- 软失效
一个页面访问在内存中而不在TLB中,此时要做的是更新TLB,不需要产生磁盘I/O。(10~20个机器指令,几纳秒) - 硬失效
页面本身不在内存中(当然也不TLB中),此时需要一次磁盘存取以装入该页面。(几毫秒,硬失效的处理时间往往是软失效的百万倍)
在也表中查找相应的映射被称为页表遍历。
假设页表遍历没有在进程的页表汇总找到需要的页,从而产生了一个缺页错误,此时有三种可能:
(1):所需要的页面就在内存中,但未记录在该进程的页表里。比如该页面可能已由其他其他进程从硬盘中调入内存,这种情况只需要把所需要的页面正确映射到页表中,而不是从磁盘调入。这是一种软失效,称为次要缺页错误。
(2):需要从硬盘重新调入页面,这就是严重缺页错误。
(3):程序访问了一个非法地址,根本不需要向TLB中新增映射。此时,操作系统一般会通过报告段错误来终止该程序。只有第三种缺页属于程序错误,其他缺页情况都会被硬件或操作系统以降低性能为代价而自动修复。
1.5 针对大内存的页表
怎样解决巨大的虚拟地址空间?两种办法
多级页表
32位的虚拟地址被划分为10位的PT1域,10位的PT2域和12位的Offset(偏移量)域。因为偏移量是12位,所以页面大小是4KB,共有220个页面。假设每个进程都占用了4G的线性地址空间,页表共含1M个表项,每个表项占4个字节,那么每个进程的页表要占据4M的内存空间。为了节省页表占用的空间,我们使用两级页表。每个进程都会被分配一个页目录,但是只有被实际使用页表才会被分配到内存里面。一级页表需要一次分配所有页表空间,两级页表则可以在需要的时候再分配页表空间。
引入多级页表的原因是避免把全部页表一直保存在内存中(特别是一些从不需要的页表)。
在左边的是顶级页表(页目录表),它有1024个表项,对应于10位的PT1域。当一个虚拟地址被送到MMU时,MMU首先提取PT1域并把该值作为访问顶级页表的索引。因为整个4GB(即32位)虚拟地址空间已经按4KB大小分块,所以顶级页表中这1024个表项的每一个都表示4M的块地址范围。二级页表的每一项都表示4KB的地址范围。Offset对4096个地址进行编址。
由索引顶级页表得到的表项中含有二级页表的地址或页框号。顶级页表的表项0指向程序正文的页表,表项1指向数据的页表,表项1023指向堆栈的页表,其他的表项未用,现在把PT2域作为访问选定的二级页表的索引,以便找到该虚拟页面的对应页框号。
虽然虚拟地址空间超过100万个页面,实际上只需要4个页表:顶级页表,0-4M(正文段),4M-8M(数据段)和顶端4M(堆栈段)的二级页表。顶级页表中1021个表项的“在/不在”都设成0,当访问他们时强制产生一个缺页中断。
二级页表可扩充为三级、四级或更多级。级数越多,灵活性就越大。
页目录指针表。每一级的页表项由32位扩展到了64位,这样处理器就能寻址到4GB以外的地址空间。