十五、进程地址空间-创新互联

15.1 地址空间十五、进程地址空间

进程地址空间由进程可寻址的虚拟内存组成。每个进程都有一个32位或64位的平坦地址空间,空间的大小取决于体系结构。术语“平坦”指的是地址空间范围是一个独立的连续区间。一些操作系统提供了段地址空间,这种地址空间并非一个独立的线性区域,而是被分段的,现代采用虚拟内存的操作系统通常都使用平坦地址空间而不是分段式的内存模式。通常,每个进程都有唯一的这种平坦地址空间,一个进程的地址空间与另一进城的地址空间即使有相同的内存地址,实际上也互不相干,我们称这样的进程为线程。

创新互联专注于企业成都营销网站建设、网站重做改版、蟠龙网站定制设计、自适应品牌网站建设、HTML5电子商务商城网站建设、集团公司官网建设、外贸营销网站建设、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为蟠龙等各大城市提供网站开发制作服务。

尽管一个进程可以寻址4GB的虚拟内存(32位),并不表示它有权访问所有的虚拟地址。可以被合法访问的地址空间称为内存区域。通过内核,进程可以给自己的地址空间动态的添加或减少内存区域。

进程只能访问有效区域的内存地址。每个内存区域也具有相关权限如对相关进程有可读、可写、可执行属性。如果一个进程访问了不在有效范围中的内存区域,或以不正确的方式访问了有效地址,那么内核就会终止该进程,并返回段错误信息。

内存区域包含各种内存对象:

1、可执行文件代码的内存映射,称为代码段;

2、可执行文件的已初始化全局变量的内存映射,称为数据段;

3、未初始化全局变量,bss段的零页的内存映射

4、用于进程用户空间栈的零页内存映射

5、每一个诸如C库或动态连接程序等共享库的代码段、数据段和bss也会被载入进程的地址空间

6、任何内存映射文件

7、任何共享内存段

8、任何匿名映射,比如由malloc()分配的内存。

进程地址空间中的任何有效地址都只能位于唯一的区域,这些内存区域不能相互覆盖。在执行的进程中,每个不同的内存片段都对应一个独立的内存区域:栈、对象代码、全局变量、被映射的文件等。

15.2 内存描述符

内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。由mm_struct结构体表示

struct mm_struct{

struct vm_area_struct  *mmap; //内存区域链表

struct rb_root      mm_rb;//内存区域形成的红黑树

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;  //懒惰TLB交换掩码

mm_context_t     context;   //体系结构特殊数据

unsigned long      flags;//状态标识

int            core_waiters;//内核转存储等待线程

struct core_state    *core_state;//核心转储的支持

spinlock_t        ioctx_lock;//AIO IO链表锁

struct hlist_head     ioctx_list;//AIO IO链表

};

15.2.1 分配内存描述符

在进程的进程描述符中,mm域存放着该进程使用的内存描述符,所以current->mm便指向当前进程的内存描述符。fork()函数利用copy_mm()函数复制父进程的内存描述符,而子进程中的mm_struct结构体实际是通过allocate_mm()宏从mm_cachep_slab缓存中分配得到的。

如果父进程希望和其子进程共享地址空间,可以在调用clone()时,设置CLONE_VM标志。我们把这样的进程称作线程。当CLONE_VM被指定后,内核就不再需要调用allocate_mm()宏了,而仅仅需要在调用copy_mm()函数中将mm域指向其父进程的内存描述符即可:

if(clone_flags&CLONE_VM){

atomic_inc(¤t->mm->mm_users);

tsk->mm = current->mm;

}

15.2.2 撤销内存描述符

进程退出->exit_mm()->mmput()减少mm_users用户计数,如果为0则调用mmdrop()函数,减少mm_count计数,如果mm_count为0 ->free_mm()通过kmem_cache_free()函数将mm_struct结构体归还到mm_cache_slab缓存中。

15.2.3 mm_struct与内核线程

内核线程没有进程地址空间,也没有相关的内存描述符。所以内核线程对应的进程描述符中mm域为空。

内核线程访问内核内存需要使用一些数据,比如页表。为了避免内核线程为内存描述符和页表浪费内存,也为了当新内核线程运行时,避免浪费处理器周期向新的地址空间进行切换,内核将直接使用前一个进程的内存描述符。内核线程对应的进程描述符中的active_mm域使其指向前一个进程的内存描述符。所以在需要时,内核线程便可以使用前一个进程的页表。因为内核线程不访问用户空间的内存,所以他们仅仅使用地址空间中和内核内存相关的信息,这些信息的含义和普通进程完全相同。

15.3 虚拟内存区域

内存区域由vm_area_struct结构体描述。

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;//树上该VM节点

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;//私有数据

};

15.3.1 VMA标志

15.3.2 VMA操作

struct vm_operations_struct{

void (*open)(struct vm_area_struct *);当指定的内存区域被加入到一个进程地址空间时,该函数被调用

void (*close)(struct vm_area_struct *);当指定的内存区域被从进程地址空间删除的时候,该函数被调用

int (*fault)(struct vm_area_struct *);当没有出现在物理内存中的某个页面被访问时,该函数被页面故障处理调用

int (*page_mkwrite)(struct vm_area_struct *);当某个页面为只读页面时,该函数被页面故障处理调用

int (*access)(struct vm_area_struct *,unsigned long,void *,int,int);当get_user_pages()函数调用失败时,该函数被access_process_vm()调用。

15.3.3 内存区域的树形结构和链表结构

链表适用于需要遍历全部节点的时候,而红黑树适用于在地址空间中定位特定内存区域。

15.3.4 实际使用中的内存区域

如果一片内存范围是共享的或不可写的,那么内核只需要在内存中为文件保留一份映射。如果考虑到映射区域不可写意味着该区域不可被改变,就应该清楚只把该映像读入一次是很安全的。所以C库在物理内存中仅仅需要占用1212k空间,而不需要为每个使用C库的进程在内存中都保存一个1212k的空间。

15.4 操作内存区域

15.4.1 find_vma();

该函数在指定的地址空间中搜索第一个vm_end大于addr的内存区域即该函数寻找第一个包含addr或首地址大于addr的内存区域。

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;

if((!vma&&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,struct vm_area_struct,vm_rb);

      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;

}

15.4.2 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);

pprev参数存放指向先于addr的VMA指针。

15.4.3 find_vma_intersection()

返回第一个和指定地址区间相交的VMA

static inline struct vm_area_struct *

find_vma_intersection(struct mm_struct *mm,unsigned long start_addr,unsigned long end_addr){

struct vm_area_struct *vma;

vma = find_vma(mm,start_addr);

if(vma&&end_addr<=vma->vm_start)

  vma = NULL;

return vma;

}

15.5 mmap()和do_mmap():创建地址区间

内核使用do_mmap()函数创建一个新的线性地址区间。如果创建的地址区间和一个已经存在的地址区间相邻,并且他们具有相同的访问权限,则这两个区间合并为一个。do_mmap()会将一个地址区间加入到进程的地址空间中。

如果系统调用do_mmap()的参数中有无效参数,那么它返回一个负值;否则,它会在虚拟内存中分配一个合适的新内存地址。如果可能,将新区域和邻近区域进行合并,否则内核从vm_area_cachep长字节(slab)缓存中分配一个vm_area_struct结构体,并且使用vma_link()函数将新分配的内存区域添加到进程地址空间的内存区域链表和红黑树中,随后还要更新内存描述符中的total_vm域,然后才返回新分配的地址空间的初始地址。

在用户空间可以通过mmap()系统调用获取内核函数do_mmap()的功能。

15.6 munmap()和do_munmap():删除地址区间

int do_munmap(struct mm_struct *mm,unsigned long start,size_t len);

系统调用munmap()给用户空间提供了一种从自身地址空间中删除指定地址区间的方法。

int munmap(void *start,size_t length);

15.7 页表

地址转换工作需要通过查询页表才能完成。多数体系结构实现了一个翻译后的缓冲器(translat lookaside buffer,TLB).TLB作为一个将虚拟地址映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器将首先检查TLB中是否缓存了改虚拟地址到物理地址的映射,如果在缓存中直接命中,物理地址立刻返回;否则,就需要再通过页表搜索需要的物理地址。


网站名称:十五、进程地址空间-创新互联
标题链接:http://myzitong.com/article/ccdehd.html