VM是面向对象的方法设计的, 这里的对象是指内存对象: 内存对象是一个软件抽象的概念, 它描述内存区与后备存储之间的映射. 系统可以使用多种类型的后备存储, 比如交换空间, 本地或者远程文件以及帧缓存等等. VM系统对它们统一处理, 采用同一操作集操作, 比如读取页面或者回写页面等. 每种不同的后备存储都可以用不同的方法实现这些操作. 这样, 系统定义了一套统一的接口, 每种后备存储给出自己的实现方法. 这样, 进程的地址空间就被视为一组映射到不同数据对象上的的映射组成. 所有的有效地址就是那些映射到数据对象上的地址. 这些对象为映射它的页面提供了持久性的后备存储. 映射使得用户可以直接寻址这些对象.
值得提出的是, VM体系结构独立于Unix系统, 所有的Unix系统语义, 如正文, 数据及堆栈区都可以建构在基本VM系统之上. 同时, VM体系结构也是独立于存储管理的, 存储管理是由操作系统实施的, 如: 究竟采取什么样的对换和请求调页算法, 究竟是采取分段还是分页机制进行存储管理, 究竟是如何将虚拟地址转换成为物理地址等等(Linux中是一种叫Three Level Page Table的机制), 这些都与内存对象的概念无关.
下面介绍Linux中 VM的实现.
一个进程应该包括一个mm_struct(memory manage struct), 该结构是进程虚拟地址空间的抽象描述, 里面包括了进程虚拟空间的一些管理信息: start_code, end_code, start_data, end_data, start_brk, end_brk等等信息. 另外, 也有一个指向进程虚存区表(vm_area_struct: virtual memory area)的指针, 该链是按照虚拟地址的增长顺序排列的. 在Linux进程的地址空间被分作许多区(vma), 每个区(vma)都对应虚拟地址空间上一段连续的区域, vma是可以被共享和保护的独立实体, 这里的vma就是前面提到的内存对象. 下面是vm_area_struct的结构, 其中, 前半部分是公共的, 与类型无关的一些数据成员, 如: 指向mm_struct的指针, 地址范围等等, 后半部分则是与类型相关的成员, 其中最重要的是一个指向vm_operation_struct向量表的指针vm_ops, vm_pos向量表是一组虚函数, 定义了与vma类型无关的接口. 每一个特定的子类, 即每种vma类型都必须在向量表中实现这些操作. 这里包括了: open, close, unmap, protect, sync, nopage, wppage, swapout这些操作.
struct vm_area_struct {
/*公共的, 与vma类型无关的 */
struct mm_struct * vm_mm;
unsigned long vm_start;
unsigned long vm_end;
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned long vm_flags;
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
struct vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
/* 与类型相关的 */
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff;
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data;
};
vm_ops: open, close, no_page, swapin, swapout……
介绍完VM的基本概念后, 我们可以讲述mmap和munmap系统调用了. mmap调用实际上就是一个内存对象vma的创建过程, mmap的调用格式是:
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);
其中start是映射地址, length是映射长度, 如果flags的MAP_FIXED不被置位, 则该参数通常被忽略, 而查找进程地址空间中第一个长度符合的空闲区域;Fd是映射文件的文件句柄, offset是映射文件中的偏移地址;prot是映射保护权限, 可以是PROT_EXEC, PROT_READ, PROT_WRITE, PROT_NONE, flags则是指映射类型, 可以是MAP_FIXED, MAP_PRIVATE, MAP_SHARED, 该参数必须被指定为MAP_PRIVATE和MAP_SHARED其中之一, MAP_PRIVATE 是创建一个写时拷贝映射(copy-on-write), 也就是说如果有多个进程同时映射到一个文件上, 映射建立时只是共享同样的存储页面, 但是某进程企图修改页面内容, 则复制一个副本给该进程私用, 它的任何修改对其它进程都不可见. 而MAP_SHARED则无论修改与否都使用同一副本, 任何进程对页面的修改对其它进程都是可见的.
mmap系统调用的实现过程是:
1.先通过文件系统定位要映射的文件;
2.权限检查, 映射的权限不会超过文件打开的方式, 也就是说如果文件是以只读方式打开, 那么则不允许建立一个可写映射;
3.创建一个vma对象, 并对之进行初始化;
4.调用映射文件的mmap函数, 其主要工作是给vm_ops向量表赋值;
5.把该vma链入该进程的vma链表中, 如果可以和前后的vma合并则合并;
6.如果是要求VM_LOCKED(映射区不被换出)方式映射, 则发出缺页请求, 把映射页面读入内存中.
munmap(void * start, size_t length):
该调用可以看作是 mmap的一个逆过程. 它将进程中从start开始length长度的一段区域的映射关闭, 如果该区域不是恰好对应一个vma, 则有可能会分割几个或几个vma.
msync(void * start, size_t length, int flags):
把映射区域的修改回写到后备存储中. 因为munmap时并不保证页面回写, 如果不调用msync, 那么有可能在munmap后丢失对映射区的修改. 其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATE, MS_SYNC要求回写完成后才返回, MS_ASYNC发出回写请求后立即返回, MS_INVALIDATE使用回写的内容更新该文件的其它映射. 该系统调用是通过调用映射文件的sync函数来完成工作的.
brk(void * end_data_segement):
将进程的数据段扩展到 end_data_segement指定的地址, 该系统调用和mmap的实现方式十分相似, 同样是产生一个vma, 然后指定其属性. 不过在此之前需要做一些合法性检查, 比如该地址是否大于mm->end_code, end_data_segement和mm->brk之间是否还存在其它vma等等. 通过brk产生的vma映射的文件为空, 这和匿名映射产生的vma相似, 关于匿名映射不做进一步介绍. 库函数malloc就是通过brk实现的.