首先内核通过映射即值把进程的虚拟地址映像到物理地址,在进程运行时,如果内核发现进程要访问的页没有在物理内存时(却页异常),就发出了请页要求;如果有空闲的内存可供分配,就要请求分配内存(于是用到了内存的分配和回收),并把正在使用的物理页记录在页缓存(使用了缓存机制);如果没有足够的内存可供分配,那么就调用交换机制,腾出一部分内存;另外在地址映像重要通过TLB(旁路转换缓冲,存放了一些页表文件)来寻找物理页;交换机制中也要用到交换缓存,并把物理页内容交换到交换文件中后也要修改页表来映射文件地址。
由图,堆栈段安排在用户空间的顶部,运行时由顶向下延伸;代码段和数据段则在顶部,运行时并不向上延伸。从数据段的顶部到堆栈段地址下沿这个区间存在一个巨大的空洞,这就是进程在运行时调用malloc()可以动态分配的空间,也叫动态内存或堆。BSS时未初始化的数据段。
尽管每个进程拥有3GB的用户空间,但是其中的地址都是虚地址。因此,用户空间在这个虚拟内存中并不能真正的运行,必须把用户空间中的虚地址最终映射到物理存储空间才行,而这正是内核完成的。所谓内核申请一块空间,实际上是请求内核分配一块虚存区间和相应的物理页面,并建立映射关系。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结构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 销毁进程的用户空间即其所有的虚存区。