Linux 虚拟内存系统
Linux 为每个进程维护了一个单独的虚拟地址空间。如图所示,包括代码段、数据段、堆、共享库和栈段。
内核为系统中的每个进程维护一个单独的任务结构(task_struct),任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(PID、指向用户栈的指针、可执行目标文件的名字,以及程序计数器)。
任务结构中的一个条目指向 mm_struct,它描述了虚拟内存的当前状态。我们感兴趣的两个字段是 pgd 和 mmap,其中 pgd 指向第一级页表(页全局目录)的基址,而 mmap 指向一个 vm_area_structs(区域结构)的链表,其中每个 vm_area_structs 都描述了当前虚拟地址空间的一个区域。当内核运行这个进程时,就将 pgd 存放在 CR3 控制寄存器中。
一个具体区域的区域结构包含下面的字段:
- vm_start:指向这个区域的起始处。
- vm_end:指向这个区域的结束处。
- vm_prot:描述这个区域内包含的所有页的读写许可权限。
- vm_flags:描述这个区域内的页面是与其他进程共享的,还是这个进程私有的(还描述了其他一些信息)。
- vm_next:指向链表中下—区域结构。
内存映射
内存映射(memory mapping):Linux 通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,初始化这个虚拟内存区域的内容。
虚拟内存区域可以映射到两种类型:
- Linux 文件系统的中的普通文件
- 匿名文件
进程这一抽象能够为每个进程提供私有的虚拟地址空间,可以免受其它进程的错误读写。不过许多进程有同样的只读代码区域,而且,许多程序需要访问只读运行时库代码的相同副本。如果每个进程都在物理内存中保持这些常用代码的副本,那就是极端的浪费了。内存映射可以用来控制多个进程如何共享对象。
一个对象可以被映射到虚拟内存的一个区域,要么作为共享对象,要么作为私有对象。如果一个进程将一个共享对象映射到它的虚拟地址空间的一个区域内,那么这个进程对这个区域的任何写操作,对于那些也把这个共享对象映射到它们虚拟内存的其他进程而言,也是可见的。而且,这些变化也会反映在磁盘上的原始对象中。
另一方面,对于一个映射到私有对象的区域做的改变,对于其他进程来说是不可见的,并且进程对这个区域所做的任何写操作都不会反映在磁盘上的对象中。一个映射到共享对象的虚拟内存区域叫做共享区域。类似地,也有私有区域。
假设进程1将一个共享对象映射到它的虚拟内存的一个区域中,现在假设进程2将同一个共享对象映射到它的地址空间。
因为每个对象都一个唯一的文件名,内核可以迅速地判定进程 1 已经映射了这个对象,而且可以使进程2中的页面条目指向相应的物理页面。关键点在于即使对象被映射到了多个共享区域,物理内存中也只需要存放共享对象的一个副本。
私有对象使用一种叫做写时复制(copy-on-write)的巧妙技术被映射到虚拟内存中。一个私有对象开始生命周期的方式基本上与共享对象的一样,在物理内存中只保存有私有对象的一份副本。其中两个进程将一个私有对象映射到它们虚拟内存的不同区域,但是共享这个对象同一个物理副本。对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时复制。只要没有进程试图写它自己的私有区域,它们就可以继续共享物理内存中对象的一个单独副本。然而,只要有一个进程试图写私有区域内的某个页面,那么这个写操作就会触发一个保护故障。
当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域中的一个页面而引起的,它就会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限,如图 9-30b 所示。当故障处理程序返回时,CPU 重新执行这个写操作,现在在新创建的页面上这个写操作就可以正常执行了。
mmap 相关函数
mmap 函数原型
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot,int flags, int fd, off_t offset);
参数:
-
addr: 映射区开始地址,一般传NULL
-
len: 映射区的长度
-
prot: 内存映射区的权限,可选值见下表
prot | 说明 | | ---------- | ------ | | PROT_READ | 数据可读 | | PROT_WRITE | 数据可写 | | PROT_EXEC | 数据可执行 | | PROT_NONE | 数据不可访问 |
-
lags: 指定映射对象的类型,可以是一个或多个值得组合体 如:MAP_SHARED | MAP_FIXED 值 | 含义 | | -------------- | ------------------------------------------------------------------------ | | MAP_FIXED | 如果addr参数所指的地址无法建立映射,则放弃映射,不对地址做修改,不建议使用此标志。 | | MAP_SHARED | 与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。 | | MAP_PRIVATE | 建立一个写时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和
MAP_SHARED标志是互斥的,只能选其一。 | | MAP_DENYWRITE | 这个标志被忽略。 | | MAP_EXECUTABLE | 这个标志被忽略。 | | MAP_NORESERVE | 不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。 | | MAP_LOCKED | 锁定映射区的页面,从而防止页面被交换出内存。 | | MAP_GROWSDOWN | 用于堆栈,告诉内核VM系统,映射区可以向下扩展。 | | MAP_ANONYMOUS | 匿名映射,映射区不与任何文件关联。 | | MAP_ANON | MAP_ANONYMOUS的别称,不再被使用。 | | MAP_FILE | 兼容标志,被忽略。 | | MAP_32BIT | 将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。 | | MAP_POPULATE | 为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。 | | MAP_NONBLOCK | 仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。 -
fd: 有效的文件描述符,一般是open()函数返回的描述符。
-
offset: 文件映射的偏移量,通常设置为0,代表从文件的最前方开始,
返回说明:
- 成功: mmap返回被映射区域的指针
- 失败: 返回 MAP_FALIED,错误原因在errno中。
错误码:
- EBADF 参数fd不是有效的文件描述符
- EACCES 存取权限有误,如果flags设置了MAP_PRIVATE,则文件必须有可读权限,flags设置了MAP_SHARED,则port参数必须设置PROT_WRITE,且文件必须可写。
- EINVAL 参数addr、length、offset三个参数中有不合法参数。
- EAGAIN 文件被锁,或者太多内存被锁.
- ENOMEM 内存不足。
munmap函数
从某个进程的地址空间删除一个映射关系
#include <sys/mman.h>
int munmap(void *addr, size_t len);
//返回: 若成功则为0,若出错则为-1
参数:
- addr: 由mmap函数返回的地址。
- len: 映射区的大小
如果调用了这个函数删除了进程空间的映射关系,那么进程中再次访问这些地址,将导致进程产生一个SIGSEGV信号。
如果映射区使用了MAP_PRIVATE标识,那么进程对该映射区做的所有变动都会被丢弃。
msync函数
同步内存映射区的数据到文件中
#include <sys/mman.h>
int msync(void *addr, size_t len,int flags);
//返回: 若成功则为0,若出错则为-1
参数:
-
addr: 一般是指整个映射区,也就是mmap函数返回的值,不过也可以指定该内存区的一个子集。
-
len: 需要同步的数据大小,不能超过mmap中申请的len。
-
flags: 标志位
常量 说明 MS_ASYNC 执行异步写 MS_SYNC 执行同步写 MS_INVALIDATE 使高速缓存的数据失效
参考
理解内存映射mmap,一篇就够了
认真分析mmap:是什么 为什么 怎么用
《深入理解计算机系统》