每个用户进程都提供了一个虚拟地址空间,虚拟地址空间其上是内核地址空间。Linux中,线性的虚拟地址空间由一些区域(段)组成,区域的构成是许多连续虚拟页面。
这并不是一个经典的虚拟地址空间布局,布局的方式特定于体系结构。
虽然有差异,但是他们都有下列的共同成分。
Linux里的task_struct
中有一个指向mm_struct
结构体的指针,mm_struct
这个结构体里面保存了进程的内存信息。
其部分成员变量如下:
//定义在文件
struct mm_struct
{
……
unsigned long mmap_base; //mmp区域的基地址
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
标记了虚拟地址空间中,已初始化数据所占区域的开始和结束地址。
start_brk
标记了当前运行时堆的起始地址,brk
标记了当前堆的结束地址,堆是动态分配的,也就是堆区域的长- 度会发生变化,所以brk
的值可以改变。【brk
读作“break”】
arg_start
和arg_end
标记了虚拟地址空间中,参数列表所占区域的起始地址和结束地址。
env_start
和env_end
标记了虚拟地址空间中,环境变量所占区域的起始地址和结束地址
参数列表和环境变量所占的区域位于栈中最高的位置,是栈的初始化数据
mmap_base
标记了虚拟地址空间中,用于内存映射的起始地址。
task_size
标记了进程的地址空间长度。
还需要考虑进程标志位PF_RANDOMIZE
,如果设置了,内核将不会为栈和内存映射的起点选择固定位置。
栈的起始地址为STACK_TOP,STACK_TOP是一个宏,如果设置了进程标志位PF_RANDOMIZE,则起始地址会减少一个小的随机量,起始地址向下偏移。每个体系结构都必须定义STACK_TOP,多数设置为TASK_SIZE
在代码段的起始地址和最低可用地址之间还有一定的间距,用于捕获NULL指针。
内存映射的区域起始地址为mm_struct->mmap_base
,通常设置成TASK_UMMAPPED_BASE
,值被定义为TASK_SIZE / 3
。
地址空间如果要求随机化机制,那么mmap的起始位置可以减去一个随机的偏移量,但是体系结构要求必须确保对齐到物理页面(页帧)。
mm_struct
这个结构体还包含了下面的成员。
struct mm_struct
{
struct vm_area_struct *mmap;
struct rb_root mm_rb;
struct vm_area_struct *mmap_cache;
}
vm_area_struct是一个结构体类型,这个结构体描述了一个区域(段)的相关信息。
struct vm_area_struct
{
struct mm_struct *vm_mm; //
unsigned long vm_start;
unsigned long vm_end;
struct vm_area_struct *vm_next;
pgport_t vm_page_prot;
unsigned long vm_flags; //
struct rb_node vm_rb;
}
vm_start:这个区域的起始地址
vm_end:这个区域的结束地址
vm_next:指向单链表的下一个结点。
vm_page_port: 这个区域的所有页的读写执行权限
vm_flags:判断这个页面是和其他进程共享的还是私有的。
每个段都有一个vm_area_struct
用于描述的结构体,那么一个进程就有许多的vm_area_struct,所以需要管理,将这些vm_area-struct组织起来。
两种组织的方式:
内存映射:将虚拟内存的一个区域和一个磁盘上的文件关联起来,用以初始化这段区域的过程,该过程称内存映射。
内存映射的文件类型:
文件数据在硬盘上的存储实际上并不是连续的,这里是为了方便理解才假设是连续的,文件数据在硬盘的存储是分布在若干的区域里。
如果一个虚拟页面被初始化,页面的调动则需要通过交换空间(又可称交换区域、交换文件),交换空间可以限制当前运行的进程能够分配的虚拟页面的数量。
一个文件可以称为一个对象。
一个对象映射到虚拟内存中,可以作为共享对象、或者私有对象。
引入共享对象的作用:
每个进程都提供自己私有的虚拟地址空间,可以免受其他进程的影响。然而许多进程有同样的只读文本区域,意味着一份同样的数据要驻留在物理内存多份,这样真的是太过奢侈!内存映射给我们一个清晰的机制可以改善这种浪费——>>许多的进程可以共享对象。
因为文件名是唯一的。所以当进程2要映射这个对象时,内核可以判定出这个对象已经被其他进程映射了,所以进程2直接把页表条目指向对应的物理页面就行。
映射到共享对象的虚拟内存的区域称为共享区域。
映射到私有对象的虚拟内存的区域称为私有区域。
系统提供了一种叫做写时拷贝的机制,开始生命周期的时候,私有对象的方式基本和共享对象是一样的。因为要维持进程的独立性,当修改数据的时候,会影响到其他的进程,所以只要有一个进程尝试去修改私有区域时,会触发一个保护故障,调用故障处理程序,会在物理内存中创建这个页面的新的拷贝,更新页表条目指向这个新的拷贝,然后恢复这个页面的可写权限。这个过程实际上是在"赌",堵它会不会被修改,即使赌错了也没有任何的损失,大不了拷贝一份就可以。
当发生缺页时,会触发一个缺页异常,缺页异常使得控制转移至内核中的缺页处理程序。