虚拟存储器
定义:
对主存的抽象机制,是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互。
功能:
1. 将主存看成是一个存储在磁盘上的地址空间的高速缓存,在内存中只保存活动区域,并根据需要在磁盘和内存之间来回传送数据。
2. 为进程提供了一致的地址空间,从而简化了存储器管理。
3. 保护了每个进程的地址空间不被其他进程所破坏。
9.1 物理和虚拟地址
CPU通过生成一个虚拟地址(Virtual address,VA)来访问主存。将虚拟地址转换为物理地址叫做地址翻译(address translation)。地址翻译也需要CPU硬件和操作系统之间的紧密结合。
CPU芯片上有叫做存储器管理单元(Memory Management Unit,MMU)的专用硬件。利用存储在主存中的查询表来动态翻译虚拟地址。 查询表由操作系统管理。
9.2 地址空间
9.3 虚拟存储器作为缓存的工具
概念上而言,虚拟存储器(VM)被组织为一个存放在磁盘上的N个连续字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,这个唯一的虚拟地址作为到数组的索引。磁盘上数组的内容被缓存到主存中。VM系统通过将虚拟存储器分割为称为虚拟页的大小固定的块来处理磁盘和主存信息交互问题。
任何时候,虚拟页的集合都被分为3个不相交的子集。
1、[未分配的] VM系统还未分配(或者创建)的页。未分配的块没有任何数据与之相关联。不占用磁盘空间
2、[缓存的] 当前缓存在物理存储器的已分配页。
3、[未缓存的] 没有缓存在物理页面存储器中的已分配页。
9.3.2 页表
9.4 虚拟存储器作为存储器管理的工具
操作系统为每个进程提供一个独立的页表,VM简化了链接和加载,代码和数据共享,以及应用程序的存储器分配。
1.简化链接
独立的空间地址意味着每个进程的存储器映像使用相同的格式。文本节总是从0x08048000(32位)处
或0x400000(64位)处开始。然后是数据,bss节,栈。一致性极大简化了链接器的设计和实现。
2.简化加载
在ELF可执行文件中.text和.data节是连续的。要把这些节加载到一个新创建的进程中,linux加载器.
分配虚拟页的一个连续的片,从地址0x08048000处(32bit)开始, 或者从0x400000(64bit),
[把这些虚拟页标记为无效,将页表条目指向目标文件中适当的位置,加载器从不实际拷贝任何数据从磁盘到存储器.]
3.简化共享
4.简化存储器分配
malloc在堆空间分配一个适当数字(例如k)个连续的虚拟存储器页面,并且将他们映射到物理存储器中任意
位置的k个任意(不一定连续)的物理页面。
9.5 虚拟存储器作为存储器保护的工具
9.7.2 Linux虚拟存储器系统
Linux为每个进程维持一个单独的虚拟地址空间:内核虚拟存储器和进程虚拟存储器。
内核虚拟存储器包含内核中的代码和数据。
1、内核虚拟存储器的某些区域被映射到所有进程共享的物理页面.如:内核代码,全局数据结构。
2、Linux将一组连续的虚拟页面(大小等同于系统DRAM总量)映射到相应的一组物理页面。[直接映射,不使用页表]
3、内核虚拟存储器包含每个进程不相同的数据。页表,内核在进程上下文中时使用的栈等。
1.Linux 虚拟存储器区域
Linux将虚拟存储器组织成一些区域(也叫做段)的集合。一个区域
就是已经存在着的(已分配的) 虚拟存储器的连续片,这些片/页已某种形式相关联。
如:代码段,数据段,堆,共享库段,用户栈。
所有存在的虚拟页都保存在某个区域,允许虚拟地址空间有间隙。
虚拟存储器区域的内核数据结构
task_struct
mm_struct: 描述了虚拟存储器的当前状态。
pgd: 指向第一级页表的基址。当进程运行时,内核将pgd存放在CR3控制寄存器
mmap: 指向vm_area_structs的链表
vm_area_structs描述了当前虚拟地址空间的一个区域(area).
vm_start:指向这个区域的起始处。
vm_end:指向这个区域的结束处。
vm_port:描述这个区域内包含的所有页的读写许可权限。
vm_flags:描述这个区域页面是否与其他进程共享,还是私有等
vm_next: 指向链表的下一个区域。
2.Linux缺页异常处理
MMU在试图翻译虚拟地址A时,触发缺页。这个异常导致控制转移到缺页处理程序,执行如下步骤:
1、虚拟地址A是合法的吗? A在某个区域结构定义的区域内吗?
解决方法: 缺页处理程序搜索区域结构链表。把A和每个区域的vm_start和vm_end做比较。
如果不合法,触发段错误。
2、试图访问的存储器是否合法? 即:是否有读,写,执行这个页面的权限?
如果不合法,触发保护异常,终止进程。一切正常的话
3、若不存在以上情况,则选择牺牲页,替换,重新执行指令
9.8 存储器映射
定义:
Linux 通过将一个虚拟存储器区域与一个磁盘上的对象关联,以初始化这个虚拟存储器区域的内容。
虚拟存储器区域可以映射到以下两种类型文件:
1、Unix文件系统中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分。
例如,一个可执行文件。文件区(section)被分成页大小的片,每一片包含一个虚拟页面的初始化内容。
仅仅是初始化,虚拟页面此时还并未进入物理存储器,直到CPU第一次引用这个页面。
2、匿名文件
匿名文件由内核创建,包含的全是二进制零。CPU第一次引用这样区域(匿名文件)的虚拟页面时,
将存储器中牺牲页面全部用二进制零覆盖。并将虚拟页面标记为驻留在存储器中。
注意在磁盘和存储器之间并没有实际的数据传送。又叫请求二进制零的页(demand-zero page)。
注意: 一个虚拟页被初始化了,它就在一个有内核维护的专门的交换文件(交换空间)之间切换。
在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。
9.8.1 再看共享对象
一个对象可以被映像到虚拟存储器的一个区域,要么作为共享对象,要么作为私有对象.
私有对象的写时拷贝
9.8.2 再看fork函数
当fork函数被当前进程调用时:
1、内核为新进程创建内核数据结构,并分配给它唯一一个PID。
2、为了给新进程创建虚拟存储器[创建页目录]。
3、创建了当前进程的mm_struct,区域结构和页表的原样拷贝。
4、将两个进程的每个页面都标记为[只读]。并给两个区域进程的每个区域结构都标记为[私有的写时拷贝]。
注意:[没有对物理存储器进行拷贝,利用的是私有对象的写时拷贝技术。]
9.8.3 再看execve函数
假设运行在当前的进程中的程序执行了如下的调用:
execve("a.out",NULL,NULL);
execve函数在当前进程加载并执行目标文件a.out中的程序,用a.out代替当前程序。
加载并运行需要以下几个步骤。
1、删除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构。
2、映射私有区域:为新程序的文本,数据,bss和栈区域创建新的区域结构。所有新的区域结构都是私有的,写时拷贝的。
文本和数据区域被映射到a.out文件中的文件和数据区。bss区域是请求二进制零,映射到匿名文件。
3、映射共享区域
4、设置程序计数器
5、execve最后一件事设置PC指向文本区域的入口点。
9.8.4 使用mmap函数的用户级存储器映射
Unix进程可以使用mmap函数来创建新的虚拟存储器区域,并将对象映射到这些区域中
#include <unistd.h>
#include <sys/mman.h>
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset);
返回:若成功时则为指向映射区域的指正,若出错则为MAP_FAILED(-1).
munmap函数删除虚拟存储器的区域
#include <unistd.h>
#include <sys/mman.h>
void *munmap(void *start,size_t length);
返回:若成功则为0,若出错则为-1
练习题 9.5代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include "csapp.h" // 自己写的
#include <errno.h>
#include <fcntl.h>
int main(int argc, char* argv[]){
if (argc < 2)
csapp_error("lack of filename.");
int fd;
if ((fd = open(argv[1], O_RDWR)) < 0)
csapp_error2("open error",errno);
off_t length;
if ((length = lseek(fd, 0, SEEK_END)) < 0)
csapp_error2("lseek error.",errno);
char* bufptr = (char*)mmap(NULL,length,PROT_READ,MAP_PRIVATE,fd,0);
if (!bufptr)
csapp_error2("mmap error.", errno);
fprintf(stdout,"%s",bufptr);
exit(0);
}
mmap为什么比传统的读写速度要快
mmap : 将文件内容直接映射到进程的地址空间,通过对这段内存的读写,来达到对文件的读写目的;
read,write : 每次调用都需要从用户态到内核态的切换,且数据需要从用户态拷贝到内核态,然后再写入磁盘,增加了中间步骤
mmap的缺点 : 不能改变文件长度,无法写入多余的字符。
9.9 动态存储器分配
malloc通过调用sbrk函数来实现内存的分配,且在在sbrk之上加了一层对所分配的内存的管理,
而sbrk以及brk是实现从虚拟内存到内存的映射的
Linux内存分配小结--malloc、brk、mmap
动态存储器分配器维护着一个进程的虚拟存储区域,称为堆(heap)。
系统之间细节不同,但是不失通用型。
1、堆是一个请求二进制零的区域。
2、紧接着未初始化的bss区域,并向上生长(向更高的地址)。
3、对于每个进程,内核维护一个变量[brk],指向堆顶,当堆空间不足时,利用sbrk函数修改该变量。
4、分配器将堆视为一组不同大小的块block的集合来维护。
每个块就是一个连续的虚拟存储器片,要么是已分配,要么是空闲。