进程地址空间
前言
Linux操作系统采用虚拟内存技术,因此系统中的所有进程之间以虚拟方式共享内存,对一个进程而言,它好像都可以访问整个系统的所有物理内存,即使单独一个进程,它拥有的地址空间也可以远远大于系统物理内存。
地址空间概述
进程地址空间由进程可寻址的虚拟内存组成,内存地址的给定的一个值,它要在地址空间范围内,比4021f000。这个值表示的是进程32位地址空间中的一个特定的字节。尽管一个进程可以寻址4GB的虚拟内存(32位的地址空间中),但这并不代表它就有权访问所有的虚拟地址。这些可被访问的合法地址空间称为内存区域。进程只能访问有效内存区域的内存地址,每个内存区域也具有相关权限如相关进程有读、可写、可执行属性。如果一个进程访问了不在有效范围的内存区域,或以不正确的方式访问了一个有效地址,那么内核就会终止该进程。下图是32位的进程地址空间示意图。
每个段对应的内容如下:
多进程的地址空间就如下图所示:
内存描述符mm_struct
struct mm_struct{
struct vm_area_struct *mmap; /*内存区域链表*/
struct rb_root mm_rb; /*VMA形成的红黑树*/
struct vm_area_struct *mmap_cache; /*最近使用的内存区域*/
unsigned long free_area_cache; /*地址空间的第一个空洞*/
pgd_t *pgd; /*页全局目录*/
atomic_t mm_users; /*使用地址空间的用户数*/
atomic_t mm_count; /*主使用计数器*/
int map_count; /*内存区域的个数*/
struct rw_semaphore mmap_sem; /*内存区域的信号量*/
spinlock_t page_table_lock; /*页表锁*/
struct list_head mmlist; /*所有mm_struct形成的链表*/
unsigned long start_code; /*代码段的开始地址*/
unsigned long end_code; /*代码段的结束地址*/
unsigned long start_data; /*数据段的首地址*/
unsigned long end_data; /*数据段尾地址*/
unsigned long start_brk; /*堆的首地址*/
unsigned long brk; /*堆的尾地址*/
unsigned long start_stack; /*进程栈的首地址*/
unsigned long arg_start; /*命令行参数的首地址*/
unsigned long arg_end; /*命令行参数的尾地址*/
unsigned long env_start; /*环境变量的首地址*/
unsigned long env_end; /*环境变量的尾地址*/
unsigned long rss; /*所分配的物理页*/
unsigned long total_vm; /*全部页面数目*/
unsigned long locked_vm; /*全部上锁的页面数目*/
unsigned long saved_auxv[AT_VECTOR_SIZE]; /*保存的auxv*/
cpumask_t cpu_vm_mask; /*懒惰(lazy)TLB交换掩码*/
mm_context_t context; /*体系结构特殊数据*/
unsigned long flags; /*状态标志*/
int core_waiters; /*内核转储等待线程*/
struct core_state *core_state; /*核心转储的支持*/
spinlock_t ioctx_lock; /*ATO I/O 链表锁*/
struct hlist_head ioctx_list; /*ATO I/O 链表*/
};
mm_users与mm_count:
(1)mm_user指的就是所有共享此mm_struct描述的进程地址空间的线程数量,即:一个(进程中)线程组中的线程个数,当本进程中的线程退出时,mm_user减1,但只有当所有共享此进程空间的线程退出时,会对mm_count减1,否则不减。
(2)mm_count指的就是对mm_struct本身此结构体的**引用次数。**不管本进程中有多少线程,在没有其他进程或线程引用的情况下,mm_struct为1,因为本进程中的所有线程共享一个进程地址空间,就是创建线程,fork()时,直接将主线程task_struct中的mm域之间给了被fork出来的task_struct的mm域。当有其他进程或线程(除本进程中的线程)引用此mm_struct时,则mm_struct加1.内核线程在运行时会借用其他进程的mm_struct,这样的线程叫"anonymous users",因为他们不关心mm_struct指向的用户空间,也不会去访问这个用户空间.他们只是临时借用.mm_count记录这样的线程.
(3)当本进程中的线程退出时,mm_user会减1,如果mm_user减到0了,则会对mm_count减1,如果此时mm_count也为0了,说明该进程空间没有任何使用者了,则会归还此进程地址空间占的内存给系统。
mmap与mm_rb
mmap以链表的形式存放地址空间中的全部内存区域,mm_rb以红黑树存放。使用的用途不同:
1、mmap结构体作为链表,利于简单、高效地遍历所有元素
2、mm_rb结构作为红-黑树,适合搜索指定元素。
mmlist
所有的mm_struct结构体通过mmlist域连接在一个双链表中,链表的首元素是init_mm内存描述符,代表init进程的地址空间。
虚拟内存区域vm_area_struct
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; /*VMA链表*/
pgprot_t vm_page_prot;/*访问控制权限*/
unsigned long vm_flags; /*标志*/
struct rb_node vm_rb; /*树上该VMA的节点*/
union{
struct{
struct list_head list;
void *parent;
struct vm_area_struct *head;
}vm_set;
struct prio_tree_node prio_tree_node;
}shared;
struct list_head anon_vma_node; /*anon_vma项*/
struct anon_vma *anon_vma; /*匿名VMA对象*/
struct vm_operations_struct *vm_ops; /*相关的操作表*/
unsigned long vm_pgoff; /*文件中的偏移量*/
struct file *vm_file; /*被映射的文件*/
void *vm_private_data; /*私有数据*/
};
如上述结构体,vm_area_struct结构体描述了指定地址空间内连续区间上的一个独立内存范围,每个内存区域都拥有一致的属性,比如访问权限,每一个VMA代表不同类型的内存区域,内存区域的范围是[vm_start,vm_end].
task_struct、mm_struct、vm_area_strcut的关系
一个进程的虚拟地址空间主要由两个数据结来描述:
一个是最高层次的:mm_struct,描述了一个进程的整个虚拟地址空间
一个是较高层次的:vm_area_structs,描述了虚拟地址空间的一个区间(简称虚拟区)
每个进程只有一个mm_struct结构,在每个进程的task_struct结构中,有一个指向该进程的结构。可以说,mm_struct结构是对整个用户空间的描述。
虚拟内存区域的操作
find_vma():寻找一个针对于指定地址的vma,该vma要么包含了指定的地址,要么位于该地址之后并且离该地址最近。
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
vma = mm->mmap_cache; //首先尝试mmap_cache中缓存的vma
/*如果不满足下列条件中的任意一个则从红黑树中查找合适的vma
1.缓存vma不存在
2.缓存vma的结束地址小于给定的地址
3.缓存vma的起始地址大于给定的地址*/
if (!(vma && vma->vm_end > addr && vma->vm_start <= 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, //获取节点对应的vma
struct vm_area_struct, vm_rb);
/*首先确定vma的结束地址是否大于给定地址,如果是的话,再确定
vma的起始地址是否小于给定地址,也就是优先保证给定的地址是
处于vma的范围之内的,如果无法保证这点,则只能找到一个距离
给定地址最近的vma并且该vma的结束地址要大于给定地址*/
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;
}
**find_vma_prev():**和find_vma()相同,但是返回的是第一个小于addr的VMA
struct vm_area_struct *find_vma_prev(struct mm_struct *mm,unsigned long addr,
struct vm_area_struct **pprev)
**mmap()和do_mmap():**创建地址区间
创建分区的两种情况:
1、创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限,则合并为一个
2、不能合并,这创建的是一个新的VMA
do_mmap()函数的定义如下:
unsigned long do_map(struct *file,unsigned long addr,
unsigned long len,unsigned long prot,
unsigned long flag,unsigned long offset)
file:指定文件
addr:可选参数,指定搜索空闲区域的起始位置
prot:指定内存区域中页面的访问权限
flag:指定VMA标志
offset与length:指定映射的数据,即文件中从偏移offset处开始,长度为len字节的范围内,如果file参数为NULL并且offset参数也为空,则代表映射和文件没有关系,称为匿名映射。
**文件映射:**文件映射给用户提供了一组措施,好似用户将文件映射到自己地址空间的某个部分,**使用简单的内存访问指令读写文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Gl5IIl8-1603372818075)(6.jpg)]
在用户空间可以通过mmap()系统调用获取内核函数do_mmap()的功能,mmap()系统调用如下:
void *mmap2(void *start,
size_t length,
int port,
int flag,
int fd,
off_t pgoff)