linux内核--进程空间(二)

    内核处理管理本身的内存外,还必须管理用户空间进程的内存。我们称这个内存为进程地址空间,也就是系统中每个用户空间进程所看到的内存。linux操作系统采用虚拟内存技术,因此,系统中的所有进程之间虚拟方式共享内存。对一个进程而言,它好像都可以访问整个系统的所有物理内存。即使单独一个进程,它拥有的地址空间也可以远远大于系统物理内存。

一、地址空间

    每个进程都有一个32位或64位的平坦地址空间,空间的具体大小取决于体系结构。术语“平坦”指的是地址空间范围是一个独立的连续区间(比如,地址从0扩展到4294967295的32位地址空间)。一些操作系统提供了段地址空间,这种地址空间并非是一个独立的线性区域,而是被分段的,但现代采用虚拟内存的操作系统通常都是使用平坦地址空间而不是分段式的内存模式。一个进程的地址空间与另一个进程的地址空间即使有相同的内存地址,实际上也彼此互不相干。

    进程只能访问有效内存区域内的内存地址。内存区域可以包含各种内存对象:代码段、数据段、bss段、栈、堆。

二、内存描述符

    内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。内存描述符由mm_struct结构体表示,定义在文件<linux/shed.h>。在上一篇文章中介绍了这个结构体。

  1)分配内存描述符

    在进程的进程描述符(task_struct结构体就表示进程描述符)中,mm域存放着该进程使用的内存描述符,所以current->mm便指向当前进程的内存描述符。fork()函数利用copy_mm()函数复制父进程的内存描述符,也就是current->mm域给其子进程,而子进程中的mm_struct结构体实际是通过文件kernel/fork.c中的allocate_mm()宏从mm_cachep_slab缓存中分配得到的。通常,每个进程都有唯一的mm_struct结构体,既唯一的进程地址空间。

  2)撤销内存描述符

    当进程退出时,内核会调用定义在kernel/exit.c中的exit_mm()函数,该函数执行一些常规的撤销工作,同时更新一些统计量。其中,该函数会调用mmput()函数减少内存描述符中的mm_users用户计数,如果用户计数为0,调用mmdrop()函数,减少mm_count使用计数。如果使用计数也等于0,说明该内存描述符不再有任何使用者了,那么调用free_mm()宏通过kmem_cache_free()函数将mm_struct结构体归还给mm_cachep_slab缓存中。

  3)mm_struct与内核线程

    内核线程没有进程地址空间,也没有相关的内存描述符。所以内核线程对应的进程描述符中mm域为空。因为内核线程并不需要访问任何用户空间的内存而且因为内核线程在用户空间中没有任何页,所以实际上它们并不需要有自己的内存描述符和页表。尽管如此,即使访问内核内存,内核线程也还是需要使用一些数据的,比如页表。

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

三、虚拟内存区域

    内存区域由vm_area_struct结构体描述,定义在文件<linux/mm_types.h>中。内存区域在linux内核中也经常称作虚拟内存区域(virtual memory Areas,VMAs)。

    vm_area_struct结构体描述了指定地址空间内连续区间上的一个独立内存范围。内核将每个内存区域作为一个单独的内存对象管理,每个内存区域都拥有一致的属性,比如访问权限等,另外,相应的操作也都一致。在上一篇文章中有结构体的定义。

    每个内存描述符都对应于进程地址空间中的唯一区间。vm_start域指向区间的首地址,vm_end是内存区间的结束地址,vm_mm域指向和VMA相关的mm_struct结构体,每个VMA对其相关的mm_struct结构体来说都是唯一的,所以即使两个独立的进程将同一个文件映射到各自的地址空间,他们分别都会有一个vm_area_struct结构体来标志自己的内存区域;反过来,如果两个线程共享一个地址空间,那么他们也同时共享其中的所有vm_area_struct结构体。

  1)VMA操作

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

    操作函数表由vm_operations_struct结构体表示,定义在文件<linux/mm.h>

struct vm_operations_struct{

    void (*open)(struct vm_area_struct *);

    void (*close)(struct vm_area_struct *);

    int (*fault)(struct vm_area_struct *,struct vm_fault *);

    int (*page_mkwrite) (struct vm_area_struct *vma,struct vm_fault *vmf);

    int (*access) (struct vm_area_struct *,unsigned long,void *,int int );

};

下面介绍具体方法:

*void open (struct vm_area_struct *area)

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

*void close(struct vm_area_struct *area)

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

*int fault(struct vm_area_struct *area,struct vm_fault *vmf)

当没有出现在物理内存中的页面被访问时,该函数被页面故障处理调用。

*int page_mkwrite(struct vm_area_struct  *vmf)

当某个页面为只读页面时,该函数被页面故障处理调用。

*int access(struct vm_area_struct *vma,unsigned long address,void *buf,int len,int write)

当get_user_page()函数调用失败时,该函数被access_process_vm()函数调用。

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

    通过内存描述符中的mmap()和mm_rb域之一访问内存区域。这两个域各自独立地指向与内存描述符相关的全体内存区域对象。其实,他们包含完全相同的vm_area-struct结构体的指针,仅仅组织方法不同。mmap域使用单独链表连接所有的内存区域对象。每一个vm_area_struct结构体通过自身的vm_next域被连入链表,所有的区域按地址增长的方向排序,mmap域指向链表中第一个内存区域,链中最后一个结构体指针指向空。

    mm_rb域使用红-黑树连接所有的内存区域对象。mm_rb域指向宏-黑树的根节点,地址空间中每一个vm_area_struct结构体通过自身的vm_rb域连接到树中。

    红-黑树是一种二叉树,树中的每一个元素称为一个节点,最初的节点称为树根。红-黑树的多数节点由两个子节点:一个左子节点和一个右子节点,不过也有节点只有一个子节点的情况。红-黑树中的所有节点都遵从:左边节点值小于右边节点值;另外每个节点都被配以红色或黑色。分配的规则为:红节点的子节点为黑色,并且树中的任何一条从节点到叶子的路径必须包含同样数目的黑色节点。根节点总为红色。红-黑树的搜索、插入、删除等操作的复杂度都为O(logn)。

    链表用于需要遍历全部节点的时候,而红-黑树使用于在地址空间定位特定内存区域的时候。内核为了内存区域上的各种不同操作都获得高性能,所以同时使用了这两种数据结构。

  3)实际使用中的内存区域

    可以使用/proc文件系统和pmap(1)工具查看给定进程的内存空间和其中所含的内存区域。

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

四、操作内存区域

    内核和时常需要在某个内存区域上执行一些操作,比如某个指定的地址是否包含在某个内存区域中。这类操作非常频繁,另外它们也是mmap()例程的基础。

  1)find_vma()

    为了找到一个给定的内存地址属于哪一个内存区域,内核提供了find_vma()函数,该函数在文件<mm/mmap.c>中:

    struct vm_area_struct *find_vma(struct mm_struct *mm,unsigned long addr);

    该函数在指定的地址空间中搜索第一个vm_end大于addr的内存区域。


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

    内核使用do_mmap()创建一个新的线性地址区间。但是说该函数创建了一个新VMA并不非常准确。

unsigned long do_map(struct file *file,unsigned long addr,unsigned long len,unsigned long port,unsigned long flag,unsigned long offset);

    该函数映射由file指定的文件,具体映射的是文件中从偏移offset处开始,长度为len字节的范围内的数据。

六、mummap()和do_mummap():删除地址区间

    do_mummap()函数从特定的进程地址空间中删除指定地址区间,该函数定义在文件<linux/mm.h>

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

    第一个参数指定要删除区域所在的地址空间,删除从地址start开始,长度为len字节的地址区间。

    系统调用munmap()给用户空间程序提供了一种从自身地址空间中删除指定地址区间的方法,它和系统调用mmap()的作用相反:

    int munmap(void *start,size_t length);

七、页表

    虽然应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的却是物理内存。所以当用程序访问一个虚拟地址时,首先必须将虚拟地址转化成物理地址,然后处理器才能解析地址访问请求。地址的转换工作需要通过查询页表才能完成,概括地将,地址转换需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,而页表则指向下一级别的页表或者指向最终的物理页面。

    linux中使用三级页表完成地址转换。利用多级页表能够节约地址转换需占用的存放空间。如果利用三级页表转换地址,即使64位机器,占用的空间也很有限。linux使用的机制:

    顶级页表示页全局目录(PGD),它包含一个pgd_t类型数组,多数体系结构中pgd_t类型等同于无符号长整型。PGD中的表项指向二级页目录中的表项:PMD

    二级页表是中间页目录(PMD),它是个pmd_t类型数据,其中的表项指向PTE中的表项。

    最后一级的页表简称页表,其中包含了pte_t类型的页表项,该页表项指向物理页面。多数体系结构中,搜索页表的工作由硬件完成。每个进程都有自己的页表,内存描述符的pgd域指向的就是进程的页全局目录。

    由于几乎每次对虚拟内存中的页面访问都必须先解析它,从而得到物理内存中的对应地址,所以页表操作的性能非常关键。搜索内存中的物理地址速度很有限,因此为了加快搜索,多数体系结构都实现了一个翻译后缓冲器(TLB)。TLB作为一个将虚拟地址映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器首先检查TLB是否缓存了该虚拟地址到物理地址的映射。

 

posted on 2013-09-26 22:43  you Richer  阅读(447)  评论(0编辑  收藏  举报