Linux 进程虚拟地址空间与虚拟内存
本文主要介绍Linux进程虚拟地址空间和虚拟内存的概念,学习可用物理内存中的页帧与所有的进程虚拟地址空间中的页之间的关联:
逆向映射(reverse mapping) 技术有助于从虚拟内存页追踪到对应的物理内存页
缺页处理(page fault handliing 则允许从块设备按需读取数据填充虚拟地址空间。
1. 简介
无论是何种体系结构,虚拟地址空间的分布方式都有以下几个成分
- 当前运行的二进制代码
- 程序使用的动态链接库的代码
- 存储全局变量和动态产生的数据的堆
- 用于保存局部变量和实现函数/过程调用的栈
- 环境变量和命令行参数的栈
- 将文件内容映射到虚拟地址空间中的内存映射
系统中的各个进程都具有一个struct mm_struct的实例,可以通过task_struct访问,这个实例保存了进程的内存管理信息
struct mm_struct {
unsigned long (*get_unmapped_area) (struct file* filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
...
unsigned long mmap_base; /* mmap 区域的基地址 */
unsigned long task_size; /* 进程虚拟内存空间的长度 */
...
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
...
}
可执行代码占用的虚拟地址空间区域,其开始和结束分别通过start_code和end_code标记,类似的,start_data和end_data标记了包含已初始化数据的区域。在ELF二进制文件映射到地址空间后,这些区域的长度不再改变。
堆的起始地址保存在start_brk,brk表示堆区域当前的结束地址,尽管堆的起始地址在进程生命周期中保持不变,但是堆的长度会发生变化,因此brk也会发生变化。
参数列表和环境变量的位置分别由arg_start和arg_end、env_start和env_end描述。两个区域都位于栈中最高区域。
mmap_base表示虚拟地址空间中用于内存映射的起始地址,可调用get_unmapped_area在mmap区域中为新映射区为新映射找到适当的位置。
task_size顾名思义,存储了对应进程的地址空间长度。
test段如何映射到虚拟地址空间中由ELF标准确定,例如IA-32系统起始于0x0804800,AMD64则使用0x000000000400000。堆紧接着text段开始,向上生长。栈起始于STACK_TOP
,如果设置PF_RANDOMIZE,则起始点会减少一个小的随机值。每个体系结构都必须定义STACK_TOP
,大多数都设置为TASK_SIZE,即户地址空间中的最高可用地址。进程的参数列表和环境变量都是栈的初始数据。
用于内存映射区域起始于mm_stuct->mm_base,通常设置为TASK_UNMAPPED_BASE,每个体系结构都需定义,几乎所有情况下,都为TASK_SIZE/3。
2. 内存映射原理
由于所有的用户进程总的虚拟地址空间比可用的物理内存大得多,因此只有最常用的部分才与物理页帧关联。内核利用address_space数据结构,提供一组方法从后备存储器(例如文件系统功能)读取数据。因此,address_space形成了一个辅助层,将映射的数据表示为连续的线性区域,提供给内存管理子系统。
- 试图访问用户地址空间中的一个内存地址,但是用页表无法确定物理地址
- 处理器接下来会触发一个缺页异常,发送到内核
- 内核会检查负责缺页区域的进程地址空间的数据结构,找到适当的后备存储器,或者确认该访问实际上是不正确的
- 分配物理内存页,并从后备存储器读取所需数据填充
- 借助页表将物理内存页并入到用户进程的地址空间,应用恢复执行。
struct mm_struct出了前面提到的成员,还包括下列成员,用于管理用户进程在虚拟地址空间中的所有内存区域
struct mm_struct {
struct vm_area_struct * mmap; /* 虚拟内存区域列表 */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache; /* 上次find_vma的结果 */
...
}
每个区域都通过一个vm_area_struct实例描述,进程的各区域按两种方法排序
- 在一个单链表上(开始于mm_struct -> mmap)
- 在一个红黑树中,根结点位于mm_rb
内存映射也分好几种类型
-
按照物理页的类型来分
-
文件映射
把文件的一个区间映射到进程的虚拟地址空间,数据源是存储设备上的文件
-
匿名映射
没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间,没有数据源
-
通常把文件映射的物理页称为文件页,把匿名映射的物理页称为匿名页。
-
按照对其他进程是否可见来分
-
共享映射
修改数据时映射相同区域的其他进程可以看见,如果是文件支持的映射,修改会传递到底层文件
-
私有映射
第一次修改数据时会从数据源复制一个副本,然后修改副本,其他进程不可见,不影响数据源
-
两个进程可以使用共享的文件映射实现共享内存,匿名映射通常为私有映射,共享的匿名映射只可能出现在父进程和子进程之间,在进程的虚拟地址空间中,代码段和数据段是私有的文件映射,未初始化的数据段、堆栈是私有的匿名映射。
虚拟内存区域的表示
每个内存区域表示为vm_area_struct的一个实例,简化定义如下:
struct vm_area_struct {
struct mm_struct * vmm; /* 所属地址空间 */
unsigned long vm_start; /* vm_mm内的起始地址 */
unsigned long vm_end; /* 在vm_mm内结束地址后的第一个字节地址 */
/* 各进程的虚拟内存区域链表,按地址排序 */
struct vm_area_struct * vm_next;
pgprot_t vm_page_prot; /* 该虚拟内存区域的访问权限 */
unsigned long vm_flags; /* 标志,如下列出 */
struct rb_node vm_rb;
/*
* 对于有地址空间和后备存储器的区域来说
* shared连接到address_space->i_mmap优先树
* 或连接到悬挂在优先树节点外、类似的一组虚拟内存区域的链表
* 或连接到address_space->i_mmap_nonlinear链表中的虚拟内存区域 */
union {
struct {
struct list_head list;
void *parent; /* 与prio_tree_node的parent成员在内存中位于同一位置 */
struct vm_area_struct *head;
} vm_set;
struct raw_prio_tree_node prio_tree_node;
} shared;
/*
* 在文件的某一页经过写时复制之后,文件的MAP_PRIVATE虚拟内存区域可能同时在i_mmap树和
* anon_vma链表中,MAP_SHARED虚拟内存区域只能在i_mmap树中,匿名的MAP_PRIVATE、栈
* 或者brk虚拟内存区域(file指针为NULL)只能处于anon_vma链表中 */
struct list_head anon_vma_node; /* 对该成员的访问通过anno_vma->lock串行化 */
struct anon_vma *anon_vma; /* 对该成员的访问通过page_table_lock串行化 */
/* 用于处理该结构的各个函数指针 */
struct vm_operation_struct *vm_ops;
/* 后备存储器的相关信息 */
unsigned vm_pgoff; /* 文件映射的偏移量,以页为单位 */
struct file *vmfile; /* 所映射到的文件,也有可能为null */
void * vm_privat_data; /* vm_pte即共享内存 */
};
vm_ops是一个指针,指向许多方法的集合,这些方法用于在区域上执行各种标准操作
struct vm_operation_struct {
void (*open) (struct vm_area_struct *area);
void (*close) (struct vm_area_struct *area);
int (*fault) (struct vm_area_struct *vma, struct vm_fault *vmf);
struct page * (*nopage) (struct vm_area_struct *area, unsigned long
addresss, int * type);
}
在创建和删除区域时,分别调用open和close,这两个接口通常不使用,设置为NULL指针。
但是fault是非常重要的,如果地址空间中某个虚拟内存页不在物理内存中,自动触发的缺页异常处理程序会调用该函数,将对应的数据读取到一个映射在用户地址空间的物理内存页中。
nopage是内核原来用于响应缺页异常的办法,不如fault灵活,出于兼容考虑,该成员依然保留,但不应该用于新的代码。
优先查找树
优先查找树(priority search tree)用于建立文件中的一个区域与该区域映射到的所有虚拟地址空间之间的关联。
struct address_space {
struct inode *host;
...
struct prio_tree_root i_mmap;
struct list_head i_mmap_nonlinear;
}
struct file {
...
struct address_space *f_mapping;
...
}
struct inode {
...
struct address_space *i_mapping;
...
}
3. 对区域的操作
将虚拟地址关联到区域
通过虚拟地址,find_vma可以查找到用户地址空间中结束地址在给定地址后的第一个区域,即满足addr < vm_area_struct->vm_end
条件的第一个区域
struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
/* 首先检查缓存 */
/* 缓存命中概率通常为35% */
vma = mm->mmap_cache;
if (!(vma && vma->end > addr && vma->vm_struct <= addr)) {
struct rb_node *rb_node;
rb_node = mm->mm_rb.rb_node;
vma = NULL;
while (rb_node) {
struct vm_area_struct * vma_tmp;
vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
if (vma_tmp -> vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
break;
rb_node = rb_node -> rb_left;
} else
rb_node = rb_node -> rb_right;
}
if (vma)
mm->mmap_cache = vma;
}
}
return vma;
}
添加区域
删除区域
插入区域
4. 内存映射
C语言标准库提供了mmap函数建立映射,在内核测,提供了两个系统调用mmap和mmap2
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
mmap在调用进程的虚拟地址空间中创建一个新的映射,新映射的起始地址为addr,映射长度为length
prot定义了映射的内存保护类型
-
PROT_EXEC:Pages may be executed.
-
PROT_READ:Pages may be read.
-
PROT_WRITE:Pages may be written.
-
PROT_NONE:Pages may not be accessed.
最重要的几个标志:
- MAP_FIXED除了给定地址之外,不能将其他地址用于映射,如果没有设置该标志,内核可以在受阻时随意改变目标地址
- 如果一个对象(通常是文件)在几个进程之间共享时,则必须使用MAP_SHARED
- MAP_PRIVATE创建一个与数据源分离的私有映射,对映射区域的写操作不影响文件源数据
- MAP_ANONYMOUS创建与任何数据都不相关的匿名映射,内容被初始化为0,fd和off参数被忽略
通过前面的介绍,可以建立虚拟地址 和物理地址 之间的联系(通过页表),也可以建立一个进程的内存区域 和其虚拟内存页地址 之间的联系,但是仍然缺乏物理内存页和该页所属进程(更精确的说,所有使用该页的进程的对应页表项)之间的联系。
逆向映射
5. 缺页异常
6. 虚拟地址空间与虚拟内存的联系与不同
很多人都以为虚拟地址空间和虚拟内存是一个概念,但严格意义上来讲,二者的差别就像Java和Javascript
虚拟地址空间指的是操作系统给每个进程准备的地址空间,上面解释的,包括代码段、数据段、堆、栈等,是进程逻辑上使用的地址空间。虚拟地址空间的大小并不等于物理内存的大小,而是由操作系统总线宽度决定,32位操作系统那一半就是4G大小,操作系统会为每个进程分配特定的地址空间,并维护映射关系,将虚拟地址映射到物理地址上。当进程访问虚拟地址空间时,操作系统会通过MMU(可以去看我文件系统那篇博客)将虚拟地址转换为对应的物理地址,并进行访问。
而虚拟内存是指操作系统提供的一种机制,使得进程能够访问超出物理内存大小的地址空间。虚拟内存的实现方式是将物理内存分为许多大小相等的页(Page),并将虚拟地址空间也分为大小相等的页。当进程访问一个虚拟页时,操作系统会将该页从磁盘中加载到物理内存中的一个页框(Page Frame)中,并将虚拟地址映射到这个页框上。如果物理内存中的页框数量不足,操作系统会进行页面置换(Page Replacement),将不再需要或者使用频率较低的页从物理内存中移除,并将新的页加载到空出的页框中。
虚拟地址空间是进程在逻辑上使用的地址空间,而虚拟内存则是操作系统在物理内存和磁盘之间建立的一层抽象,使得进程能够访问超出物理内存大小的地址空间。虚拟地址空间则是通过对地址空间进行划分和映射来实现的,而虚拟内存是通过将虚拟地址转换为物理地址来实现的。没有虚拟内存这层抽象,虚拟地址空间根本无从发力。