首先内核通过映射即值把进程的虚拟地址映像到物理地址,在进程运行时,如果内核发现进程要访问的页没有在物理内存时(却页异常),就发出了请页要求;如果有空闲的内存可供分配,就要请求分配内存(于是用到了内存的分配和回收),并把正在使用的物理页记录在页缓存(使用了缓存机制);如果没有足够的内存可供分配,那么就调用交换机制,腾出一部分内存;另外在地址映像重要通过TLB(旁路转换缓冲,存放了一些页表文件)来寻找物理页;交换机制中也要用到交换缓存,并把物理页内容交换到交换文件中后也要修改页表来映射文件地址。
由图,堆栈段安排在用户空间的顶部,运行时由顶向下延伸;代码段和数据段则在顶部,运行时并不向上延伸。从数据段的顶部到堆栈段地址下沿这个区间存在一个巨大的空洞,这就是进程在运行时调用malloc()可以动态分配的空间,也叫动态内存或堆。BSS时未初始化的数据段。
尽管每个进程拥有3GB的用户空间,但是其中的地址都是虚地址。因此,用户空间在这个虚拟内存中并不能真正的运行,必须把用户空间中的虚地址最终映射到物理存储空间才行,而这正是内核完成的。所谓内核申请一块空间,实际上是请求内核分配一块虚存区间和相应的物理页面,并建立映射关系。内核在创建进程时并不是为了整个用户空间都分配好相应的物理地址,而时根据需要才真正分配一些物理页面并建立映射。而且,系统还会用请页机制来避免对物理内存的过分使用。因为进程访问的用户空间中的页可能当前不再物理内存中,这时,操作系统通过请页机制把和苏剧从磁盘装入到物理内存中。为此,系统需要修改进程的页表,以便标志用户空间中的页已经装到物理页面中。Linux采用了比较复杂的数据结构跟踪进程用户空间的用户地址空间。
1.进程用户空间描述
一个进程的用户地址空间主要由两个数据结构来描述。一个是mm_struct他对进程的整个用户空间进行描述,简称内存描述符;另一个是vm_area_structs,它对用户空间中各个区间(虚存区,如代码段,数据段,BSS,堆栈)进行描述。
<1>mm_struct结构
mm_struct 存放了与地址空间有关的全部信息。
struct mm_struct {
struct vm_area_struct *mmap; //指向线性区对象的链表头(当虚拟区间较少时采用单链表,由mmap指针指向这个链表)
rb_root_t mm_rb; //指向线性区对象的红黑数的根(当虚拟区间较多时采用树结构)
struct vm_area_struct *mmap_cahce; //最近一次用到的虚存区很可能下一次还要用到,因此,把最近用到的虚存区结构体放入高速缓存,这个虚存区由mmap_cache指向
pgd_t *pgd; //进程的页目录基址,当调度程序调度一个进程运行时,就将这个地址专成物理地址,并写入控制寄存器(CR3)
atomic_t mm_users; //表示共享地址空间的进程数目(如子进程父进程共享地址空间)
atmoic_t mm_count; //对mm_struct 结构的引用计数。线程和进程共享一个用户空间,即mm_struct 结构,派生后系统会累加到计数引用上
int map_count; //在进程的整个用户空间中虚存区的个数
struct rw_semaphone mmap_sem; //线性区的读写信号量(由于进程的虚拟地址区间有可能在不同的上下文中受到访问,而这些访问又必须互斥)
spinlock_t page_table_lock; //线性区的自旋锁和页表的自旋锁(同上类似)
struct list_head mmlist //所有mm_struct通过mmlist域链接成双项链表,链表的第一个元素是idle进程(0号进程)的mm_struct;
.......
};
每个进程只有一个mm_struct 结构,在每个进程的 task_struct 结构中,由一个指向该结构的指针。
<2>vm_area_structs结构用来描述进程用户空间的一个虚拟内存区。相当于class了一个对象表示虚内存,有属性,有操作。
为什么把进程的用户空间划分为一个个区间?这是因为虚存区了能的来源不同,有的来自可执行映像,有的来自共享库,而有的来自动态分配的内存区,对不同的区间具有不同的访问权限,有可能会有不同的操作,所以需要把进程的用户空间分割管理,并用虚存区处理函数(vm_ops就像当了对象的操作,其他的域相当于对象的属性)。
struct vm_area_struct {
struct mm_struct *vm_mm; //指向虚存区所在的mm_struct结构的指针
unsigned long vm_start; //虚存区的起始地址和终止地址
unsigned long vm_end;
struct vm_area_struct *vm_next; //构成线性链表的指针,按虚存区基址由小到大排序
pgprot_t vm_page_prot; //虚存区的保护权限
unsigned long vm_flags; //虚存区的标志(指出虚存区域的操作特性,如虚存区域允许读取虚存区域允许写,虚存区域允许执行)
struct rb_node vm_rb; //用于红黑数结构
struct vm_operations_struct *vm_ops; //对虚存区的操作函数。这些给出了可以对虚存区中的页进行操作的函数。
//eg: 虚存区的操作函数open(),close(),nopage();open的目的在于文件属性和磁盘地址表装入内存,一边后续调用的快速存取,打开的文件并不在内存中。
unsigned long vm_pgoff; //映射文件的偏移量。对匿名页,为0,vm_struct或PAGE_OFFSET
struct file *vm_file; //指向映射文件的文件对象
//由两种情况下虚存区的页(或区间)会和磁盘发生关系。一种是磁盘交换区,当内存不够分配时,一些久未使用的页面可以交换到磁盘的交换区,腾出物理页面以供急需的进程使用,这就是内存的交换机制。另一种情况是,将一个磁盘文件映射到进程的用户空间中,,mmap()系统调用即也可以见一个打开的文件映射到进程的用户空间,此后可以如同访问内存一般访问该文件,就不必通过read,write这些函数来进行文件操作了。
void * vm_private_data; //指向内存区的私有数据
......
};
2.相关数据结构间的关系
进程控制块是内核中的核心数据结构。在进程的task_struct 结构包含一个mm域,他是指向mm_struct结构的指针。而进程的mm_struct结构则包含进程的可执行影响信息以及进程的页目录指针PGD。如图,使这几个结构之间的关系。
二.虚存映射
当调用exec系统调用开始执行一个进程时,进程的可执行映像(代码区,数据段等),必须装入到进程的用户空间。
Linux并非将映射装入物理内存,相反可执行文件只是被链接到进程的用户空间中。而随着进程的运行,被引用的程序部分会由操作系统装入物理内存,将映射链接到进程用户空间的方法叫做“虚存映射”,也就是把文件从此盘映射到进程的用户空间,这样吧对文件的访问转化位对虚存区的访问(对文件的读写不必再进行I/O操作,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用)。而虚存映射页分为两种:
(1)共享的:多个进程共享这一映射,也就是说,如果一个进程对该映射执行写的操作,将会改变该映射所对应的磁盘上的文件,而其他的进程也将感觉到改动。
(2)私有的:进程创建的这种映射只允许读文件,所以对虚存区的写操作不会修改磁盘上的文件。
如果映像与文件无关则称为匿名映像。
当可执行映像到进程的用户空间时将产生一组vm_area_struct 结构来描述各个虚拟区间的起始点和终止点,每个vm_area_struct 代表可执行映像的一部分,可能是可执行代码,可能是初始化变量或未初始化的数据,也可能是港大开的一个文件,这些映射都是通过mmap系统调用对应的do_mmap内核函数来实现的。随着vm_area_struct结构的生成,这些结构所描述的虚拟内存区间上的标准操作函数由Linux初始化。但在这一不还没有建立从虚拟内存到物理内存的映射。
do_mmap主要是建立了文件到虚存区的映射,而没有建立虚存页面到物理页面的映射。而关于页的的映射就比较复杂了。
三.与用户空间相关的系统调用
fork 创建具有新的用户空间的进程,用户空间中的所有页被标记为“写时复制,由父进程和子进程共享,当其中的一个进程所访问的页不再内存时,这个页就被复制一份。
mmap 在进程的用户空间内创建一个新的虚存区。
munmap销毁一个完整的虚存区或其中的一部分,如果要取消的虚存区为与某个虚存区中间,则这个虚存区划分为两个虚存区。
exec 装入新的可执行文件以代替当前用户空间的内容。
Exit 销毁进程的用户空间即其所有的虚存区。