向下之旅(二十):进程地址空间(一)

  内核除了管理本身的内存外,还必须管理进程的地址空间——即系统中每个用户空间进程所看到的内存。Linux操作系统采用虚拟内存技术,因此,系统中的所有进程之间以虚拟方式共享内存。即使单独的一个进程,它拥有的地址空间也可以远远大于系统的物理内存。

  进程地址空间由每个进程中的线性地址区组成,更重要的特点是内核允许进程使用该空间中的地址。每个进程都有一个32或64位的平坦(falg)地址空间,空间的具体大小取决于体系结构。一些操作系统提供了段地址空间,但通常情况下都使用平坦地址空间而不是分段式的内存模式。

  内存地址是一个给定的值,它要在地址空间范围之内,比如4021f000。这个值表示的是进程32位地址空间中的一个特定的字节。而我们更关系的是进程有权访问的虚拟内存地址空间,比如08048000-0804c000。这些可被访问的合法地址区间被称为内存区域,通过内核,进程可以给自己的地址空间动态的添加或减少内存区域。

  进程只能访问有效范围内的内存地址。每个内存区域有具有特定的访问属性:只读、只写、可执行等属性。若一个进程访问了不在有效范围中的地址,或以不正确的方式访问有效地址,内核就会终止进程,并返回“段错误”信息。

  内存区域可以包含各种内存对象,比如:

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

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

  3.包含未初始化全局变量,也就是bss段的零页(页面的信息全部为0值,所以可以用于映射bss段等目的)的内存映射。

  4.用于进程用户空间栈(不要和进程内核栈混淆,进程的内核栈独立存在并由内核维护)的零页的内存映射。

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

  6.任何内存映射文件。

  7.任何共享内存段。

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

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

  内存描述符

  内核使用内存描述符结构体表示进程的地址空间,其结构如下:

  

  mm_users域表示正在使用该地址的进程数。若两个进程共享该地址空间,则mm_users就等于2,若mm_users等于0,则mm_count就等于0,表明已经没有任何指向该mm_struct结构体引用了,此时就会被销毁,反之,mm_count就等于1。

  所有的mm_struct结构体都通过自身的mmlist域链接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表init进程的地址空间,此外要注意,操作该链表的时候需要使用mmlist_lock锁来防止并发访问。内存描述符的总数存放在mmlist_nr全局变量中。

  分配内存描述符

  在进程的进程描述符中,mm域存放着该进程的内存描述符,所以current->mm便指向当前进程的内存描述符。

  有两种方式来分配内存描述符,第一种,子进程与父进程不共享地址空间(也就是并列的进程,若共享地址空间,则子进程与父进程为线程关系),子进程中的mm_struct结构体实际是通过文件kernel/forc.c中的allocate_mm()宏从mm_cachep slab缓存中分配得到的。第二种,通过调用clone()时,设置CLONE_VM标志,这种进程叫做线程,当CLONE_VM被指定后,内核就不在需要调用allocate_mm()函数,仅仅需要在调用copy_mm()函数中将mm域指向其父进程的内存描述符就可以了。通常,每个进程都有唯一的mm_struct结构体,即唯一进程地址空间。

  销毁内存描述符

  当进程退出时,内核会调用exit_mm()函数,该函数执行一些常规的销毁工作,同时更新一些统计量。其中该函数会调用mmput()函数减少内存描述符中的mm_users用户计数,如果用户计数降到零,继续调用mmdrop()函数,减少mm_count使用计数,如果mm_count也为零,说明该内存描述符不再有任何使用者了,那么调用free_mm()宏通过kmen_cache_free()函数将mm_struct结构体归还到mm_cachep slab缓存中。

  mm_struct与内核线程

  内核线程没有进程地址空间,也没有相关的内存描述符。所以内核进程的mm域为空。事实上,这也是内核进程的真实含义——它们没有用户上下文。因为内核线程并不需要访问用户空间的内存。

  当一个进程被调度时,该进程的mm域指向的地址空间被装载到内存,进程描述符中的active_mm域会被更新,指向新的地址空间。内核线程没有地址空间,所以mm域为NULL。当一个内核线程被调度时,内核发现它的mm域为NULL,就会保留一个进程的地址空间,随后内核更新内核线程对应的进程描述符中的active_mm域,使其指向前一个进程的内存描述符。所以在需要时,内核线程便可以使用前一个进程的页表。

  内存区域

  内存区域由vm_area_struct结构体描述,在内核中也经常被称为虚拟内存或VMA。它描述了指定地址空间内连续区间上的一个独立内存范围。内核将每个内存区域作为一个单独的内存对象管理,每个内存区域都拥有一致的属性,比如访问权限等,另外相应的操作都一致,这种管理方式类似于VFS,采用面向对象方法使VMA结构体可以代表多种类型的内存区域——比如内存映射文件或进程的用户空间栈等。该结构体如下:

  

  vm_mm域指向和VMA相关的mm_struct结构体,每个VMA对齐相关的mm_struct结构体来说都是唯一的,所以即使两个独立的进程将同一个文件映射到各自的地址弓箭,它们分别都会有一个vm_area_struct结构体来标志自己的内存区域,若两进程共享一个地址空间,那么它们也同时共享其中的所有的vm_area_struct结构体。

  VMA标志

  VMA标志包含在vm_flags域内,标志了内存区域所包含的页面的行为和信息。VMA标志反应了内核处理页面所需要遵循的行为准则,而不是硬件要求,同时包含了内存区域中页面的信息,或内存区域的整体信息。表格如下:

  

  VMA操作

  vm_area_struct结构体中的vm_ops域指向与指定内存区域相关的操作函数表,内核使用表中的方法操作VMA。vm_area_struct作为通用对象代表了任何类型的内存区域,而操作表描述针对特定的对象实例的特定方法。操作函数表由vm_operations_struct结构体表示,结构体如下:

  void open (struct vm_area_struct *area)

  当指定的内存区域被加入到一个地址空间时,该函数被调用。

  void close (struct vm_area_struct *area)

  当指定的内存区域从地址空间删除时,该函数被调用。

  struct page *nopage (struct vm_area_struct *area,unsigned long address, int unused)

  当要访问的页不存在物理内存中时,该函数被页错误处理程序调用。

  int populate(struct vm_area_struct *area,unsigned long address,unsigned long len,pgprot_t prot, unsigned long pgoff, int nonblock)

  该函数被系统调用remap_pages()调用来为将要发生的缺页中断预映射一个新映射。

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

  可以通过内存描述符中的mmap和mm_rb域之一访问内存区域,这两个域都各自独立的指向与内存描述符想换的全体内存区域对象。它们包含完全相同的vm_area_struct结构体的指针,仅仅是组织方法不同。前者使用单独链表链接所有的内存区域对象。后者使用红-黑树链接所有的内存区域对象。

  每个和进程相关的内存区域都对应一个vm_area_struct结构体。另外进程不同于线程,进程结构体task_strct包含唯一的mm_struct结构体引用。

  

  参考自:《Linux Kernel Development》.

posted on 2016-03-30 14:18  画家丶  阅读(207)  评论(0编辑  收藏  举报