进程地址空间(Process Address Space)指进程可寻址空间。在64位处理器中,进程地址空间直接指可寻址256TB的用户态地址空间,每个进程都可以拥有一个完全独立的进程地址空间。在这个空间中,进程可以利用虚拟地址完成程序的段代码映射、数据段映射、用户进程栈、mmap映射、堆映射的数据结构初始化,并将程序在进程地址空间中设置好后,由CPU进行执行。
进程空间页表管理器mm_struct
每个进程都会在创建时,都会在内核态中留下一系列的数据结构,比如task_struct(进程本体数据结构)、mm_struct(内存管理数据结构)、vm_area_struct(虚拟地址空间数据结构)、files_struct(文件系统数据结构)、signal_struct(信号数据结构)、sched_entity(调度数据结构)、nsproxy(命名空间数据结构)等。这些数据结构从kernel的方方面面补齐了进程执行时可以与内核之间进行的操作。
这里,首先讨论的是进程在创建时,建立属于这个进程本身的进程地址空间的事情。由之前有关页表映射的文章讨论的结论,CPU在识别一个虚拟地址并将其翻译为物理地址时,需要两个输入:
1、需要一个页表基地址(一般保存在当前CPU的TTBR寄存器中)
2、需要一个待寻址的虚拟地址输入
所以,从MMU的页表映射的角度看,只要输入的页表基向量地址不一样,即便是同样的虚拟地址,也一样可以映射到完全不同的真实物理地址中去,看起来就像随着页表基地址的变化,虚拟地址空间也变化了一样。这正是kernel中,每个进程都可以拥有独立的用户地址空间的原因(独立的意思是,进程1和进程2中同一个虚拟地址,其对应的真实物理地址是不同的,互不影响的)。
kernel使用mm_struct(定义在/include/linux/mm_types.h)来管理VMA以及标识这个进程对应的PGD页表的基地址(由于缺页异常向量表的存在,只需要指定一个没用过的PGD基地址,并保留一个合适的内核态中的虚拟地址长度用来放后续PGD缺页时补充的实例化的页表描述符,在这个全空的页表中遇到缺页时会自动根据缺页异常动作申请新的物理地址并创建新的页表,就可以说,指定一个新的PGD就是创建了一个新的页表了)。可以说,mm_struct中完整描述了这个进程空间中各个VMA片段的总体结构。
mm_struct源代码(部分精简):
struct mm_struct {
struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
unsigned long mmap_base; /* base of mmap area */
pgd_t * pgd;
atomic_t mm_users;
atomic_t mm_count;
spinlock_t page_table_lock; /* Protects page tables and some counters */
struct rw_semaphore mmap_lock;
struct list_head mmlist;
unsigned long total_vm;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
...
}
};主要成员含义:
1、mmap:进程所有的VMA形成一个单独的链表,这是链表头
2、mm_rb:VMA红黑树的根节点
3、get_unmapped_area:用来判断虚拟内存空间是否有足够的空间,返回一段没有映射过的空间的起始地址。
4、mmap_base:指向mmap空间的起始地址。
5、pgd:指向进程的pgd一级页表基地址
6、mm_users:记录正在使用该进程空间的进程数目
7、mm_count:mm_struct结构体的主引用计数
8、mmap_sem:保护VMA的一个读写信号量
9、mmlist:所有的mm_struct数据结构都连接到一个双向链表中,链表头为init_mm内存描述符,init进程的地址空间
10、start_code,end_code:代码段的起始地址和结束地址
11、start_data,end_data:数据段的起始地址和结束地址
12、start_brk:堆空间的起始地址
13、brk:当前堆中的VMA的结束地址
14、total_vm:已经使用的进程地址空间总和
站在mm_struct的角度上看,堆就是由start_brk、brk两个指针管理的区域,由低往高增长的一个特殊的VMA;栈就是由start_stack与VMA内部的vm_start指针管理的,本身一定位于空间最高处,从上往下长的一种VMA。
具体来说,根据mm_struct的描述,进程地址空间从高地址到低地址的顺序排列,长下面这个样子:
1、用户栈,通常位于用户空间的最高地址(并不是固定最高地址,而是由start_stack指针指定的一个地址),从上往下延伸。它包含栈帧,里面包含了局部变量和函数调用参数等。进程的内核栈独立存在并由内核维护,主要用于上下文切换,用户的进程栈用于进程的调用传参。
2、mmap映射区域,位于用户进程站下面,主要用于mmap系统调用,如映射一个文件的内容到进程地址空间等。(这里是从上向下长的边界)
3、堆映射区域,malloc()函数分配的进程虚拟地址就是这段区域。(这里是从下向上长的边界)
4、数据段映射,可执行文件报含可读/可写的程序头,如数据段和未初始化数据段等。
5、代码段映射,可执行文件包含只读并可执行的程序头,如代码段、init段等(从start_code位置开始,并不是从0开始的,这可以防止空指针引用错误,并且可以通过动态挪动start_code位置,增加安全性)。
虚拟内存区域VMA
虚拟内存区域(Virtual Memory Area)是一种kernel中描述进程地址空间具体虚拟地址内存片段的数据结构(VMA结构)。进程可以通过内核的内存管理机制动态地添加和删除这些内存区域(即VMA),每个区域具有相关的权限,如可读、可写或可执行权限。一系列的VMA就构成了一个完整的进程地址空间。
VMA具体在kernel中定义在/include/linux/mm_types.h中(有所精简):
struct vm_area_struct {
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm;
pgprot_t vm_page_prot;
unsigned long vm_flags;
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops;
unsigned long vm_pgoff;
struct file * vm_file;
void * vm_private_data;
struct mempolicy *vm_policy;
};VMA数据结构中各成员含义如下:
1、vm_start和vm_end:指定VMA在进程地址空间的起始地址和结束地址
2、vm_next和vm_prev:进程的VMA形成的链表
3、vm_rb:VMA作为一个节点加入红黑树(用于快速查表的数据结构),每个进程的mm_struct数据结构中都有一颗红黑树,树中各节点即为这里的这个变量,意为在mm_struct的红黑树中代表一个VMA的节点
4、vm_mm:指向该VMA所属进程的mm_struct结构
5、vm_page_prot:VMA的访问权限
6、vm_flags:描述该VMA的一组标志位
7、anon_vma_chain和anon_vma:用于管理反向映射(Reverse Mapping, RMAP)
8、vm_ops:指向许多方法的集合,这些方法用于在VMA中执行各种操作,通常用于文件映射
9、vm_pgoff:指定文件映射的偏移量,这个变量的单位不是字节,而是页的大小
10、vm_file:指向file实例,被描述为一个被映射的文件
在mm_struct中,所有的VMA实例构成链表(由mm_struct中的mmap指针作为头,这个mmap头跟进程地址空间中的mmap映射没有鸡毛关系的),VMA的节点构成红黑树(运行在内核态的一个快速检索算法程序)。
在mm_struct中,栈之下还存在一个mmap映射的区域,主要用于内核态向进程地址空间的映射或文件向进程地址空间的映射,可以用于函数的动态链接库加载等。
栈结构本身的变化,可以参考ARMv8芯片的函数调用标准来学习。栈结构本身是一种CPU运行寄存器状态的内存拷贝。堆则是一种函数运行过程中,由函数自己生成而非文件加载的,可以支持函数长期调用的一种动态的数据结构。