内核处理管理本身的内存外,还必须管理用户空间进程的内存。我们称这个内存为进程地址空间,也就是系统中每个用户空间进程所看到的内存。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是否缓存了该虚拟地址到物理地址的映射。

linux内核--进程空间(二)的更多相关文章

  1. Linux内核学习笔记二——进程

    Linux内核学习笔记二——进程   一 进程与线程 进程就是处于执行期的程序,包含了独立地址空间,多个执行线程等资源. 线程是进程中活动的对象,每个线程都拥有独立的程序计数器.进程栈和一组进程寄存器 ...

  2. Linux内核分析(二)----内核模块简介|简单内核模块实现

    原文:Linux内核分析(二)----内核模块简介|简单内核模块实现 Linux内核分析(二) 昨天我们开始了内核的分析,网上有很多人是用用源码直接分析,这样造成的问题是,大家觉得很枯燥很难理解,从某 ...

  3. “Linux内核分析”实验二报告

    张文俊 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.第二周学习内 ...

  4. Linux内核 ——进程管理之进程诞生(基于版本4.x)

    <奔跑吧linux内核>3.1笔记,不足之处还望大家批评指正 进程是Linux内核最基本的抽象之一,它是处于执行期的程序.它不仅局限于一段可执行代码(代码段),还包括进程需要的其他资源.在 ...

  5. Linux内核——进程管理之CFS调度器(基于版本4.x)

    <奔跑吧linux内核>3.2笔记,不足之处还望大家批评指正 建议阅读博文https://www.cnblogs.com/openix/p/3262217.html理解linux cfs调 ...

  6. linux内核学习之二 一个精简内核的分析(基于时间片轮转)

    一   实验过程及效果 1.准备好相关的代码,分别是mymain.c,mypcb.h,myinterrupt.c ,如下图,make make成功: 在qemu创建的虚拟环境下的运行效果:(使用的命令 ...

  7. Linux内核分析作业二—操作系统是如何工作的

    一.实验:简单的时间片轮转多道程序内核代码运行与分析 my_start_kernel之前都是硬件初始化,它是操作系统的执行入口,每循环100000次就进行一次打印. 执行更加简单,每次时钟中断时都会调 ...

  8. linux内核--进程与线程

    http://blog.csdn.net/yusiguyuan/article/details/12154823 在<linux内核设计与实现>中第三章讲解了进程管理,在关于进程和线程的概 ...

  9. Linux内核——进程管理与调度

    进程的管理与调度 进程管理 进程描写叙述符及任务结构 进程存放在叫做任务队列(tasklist)的双向循环链表中.链表中的每一项包括一个详细进程的全部信息,类型为task_struct,称为进程描写叙 ...

随机推荐

  1. hdu 1874 畅通工程续(最短路)

    最短路问题! 最简单的最短路问题! 恩! #include<stdio.h> #define MAX 1000000 int map[500][500]; int n,m,start,en ...

  2. javaweb项目的优化

    简单地来看一个浏览器用户访问的流程: 浏览器->服务器->返回结果显示 这么简单地看,可能想得到的优化手段很少,常见的可能就是优化sql,加快数据库处理:加个缓存,加快返回:使用静态文件, ...

  3. js高级程序设计(第三版)学习笔记(第一版)

    ecma:欧洲计算机制造商协会iso/iec:国际标准化和国际电工委员会 dom级别(10*)文档对象模型1:DOM核心(映射基于xml文档)与dom html(在dom核心基础上)2:对鼠标,事件, ...

  4. python网络请求简洁之道--python requests简介

    #requests中文文档:http://cn.python-requests.org/en/latest/#学习出处:http://mp.weixin.qq.com/s?__biz=MjM5NzU0 ...

  5. J2EE基础总结(1)——J2EE入门

    J2EE诞生的背景 在传统的开发模式(单层应用结构)下.应用普遍存在下面致命缺点: - 数据.页面和业务逻辑在一个逻辑层次中.功能紧密耦合. - 代码重用性极低,可维护性差. - 应用耦合度高,全然没 ...

  6. UGUI 全方位了解

    随着 unity3d 4.6 ~ 5.x + 新 UI 系统最终与大家见面了.这篇文章将不会介绍怎样使用button.滚动栏之类的UI控件.这些内容能够參考Unity Manual:这篇文章的重点是. ...

  7. [RxJS] Wrap up

    Last thing to do is clean the score box and input, also auto foucs on input when click start. const ...

  8. [RxJS] Displaying Initial Data with StartWith

    You often need to render out data before you stream begins from a click or another user interaction. ...

  9. C#基础:集合

        C#中的数组实现为 System.Array 类的实例,它们只是集合类(Collection Classes)中的一种类型.集合类一般用于处理对象列表,其功能比简单数组要多,功能大多是通过实现 ...

  10. WCF websocket

    WebSocket, like as TCP, is a bi-directional, full-duplex communication channel over a single TCP con ...