1、地址空间

进程的地址空间由进程可寻址的虚拟内存组成。每个进程都有自己独立的地址空间,两个进程之间没有关系,互不影响。这些虚拟内存区域包含各种对象,常见的有如下:

代码段:可执行文件代码的内存映射

数据段:可执行文件的已初始化全局变量的内存映射

Bss段:包含未初始化的全局变量,也称bss段零页的内存映射

用户进程用户空间栈的零页内存映射,栈大小编译时指定

每个共享库的代码段、数据段和bss段也会被载入进程的地址空间

任何内存映射文件

任何共享内存段

任何匿名的内存映射,比如malloc

进程地址空间中任何有效地址都只能位于唯一的区域,这些内存区域不能相互覆盖。

2、内存描述符

(1)数据结构

内核使用内存描述符结构体标识进程的地址空间,该数据结构包含了和进行地址空间相关的全部信息。

struct task_struct {

struct mm_struct *mm,*active_mm;    //进程任务描述符指向内存描述符,mm为进程地址空间所有信息,active_mm则为当前执行的地址空间信息

}

struct mm_struct {

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

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

struct vm_area_struct * mmap_cache; //最近使用的内存区域

#ifdef CONFIG_MMU

unsigned long (*get_unmapped_area) (struct file *filp,

unsigned long addr,unsigned long len,

unsigned long pgoff, unsignedlong flags);

void (*unmap_area) (struct mm_struct *mm, unsigned long addr);

#endif

unsigned long mmap_base;        /*base of mmap area */

unsigned long task_size;        /*size of task vm space */

unsigned long cached_hole_size;    /* if non-zero, the largest hole below free_area_cache */

unsigned long free_area_cache;     /* first hole of size cached_hole_size or larger */

unsigned long highest_vm_end;      /* highest vma end address */

pgd_t * pgd;

atomic_t mm_users;          /* Howmany users with user space? */

atomic_t mm_count;          /* Howmany references to "struct mm_struct" (users count as 1) */

int map_count;              /*number of VMAs */

spinlock_t page_table_lock;     /*Protects page tables and some counters */

struct rw_semaphore mmap_sem;

struct list_headmmlist;        /* List of maybe swappedmm's.  These are globally strung

* together off init_mm.mmlist, andare protected

* by mmlist_lock

*/

unsigned long hiwater_rss;  /*High-watermark of RSS usage */

unsigned long hiwater_vm;   /*High-water virtual memory usage */

unsigned long total_vm;     /*Total pages mapped */

unsigned long locked_vm;    /*Pages that have PG_mlocked set */

unsigned long pinned_vm;    /*Refcount permanently increased */

unsigned long shared_vm;    /*Shared pages (files) */

unsigned long exec_vm;      /*VM_EXEC & ~VM_WRITE */

unsigned long stack_vm;     /*VM_GROWSUP/DOWN */

unsigned long def_flags;

unsigned long nr_ptes;      /*Page table pages */

unsigned long start_code, end_code, start_data, end_data;

unsigned long start_brk, brk, start_stack;

unsigned long arg_start, arg_end, env_start, env_end;

unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

/*

* Special counters, in some configurations protected by the

* page_table_lock, in other configurations by being atomic.

*/

struct mm_rss_stat rss_stat;

structlinux_binfmt *binfmt;

cpumask_var_t cpu_vm_mask_var;

/* Architecture-specific MM context */

mm_context_t context;

unsigned long flags; /* Must use atomic bitops to access the bits */

structcore_state *core_state; /* coredumping support */

}

mmap和mm_rb指向的是相同数据对象(vm_area_stuct虚拟内存区域),只是组织方式不一样。mmap以链表形式存放,利于简单、高效的遍历所有元素;mm_rb以红黑树的形式存放,更适合搜索指定元素。

pgd为页全局目录,实现虚拟地址和物理内存的映射,在switch_mm时将pgd首地址放入CPU寄存器供mmu(硬件实现)使用。TLB则是pgd的一个子集,存放已经使用过的映射,加速地址映射查找。

mm_users和mm_count:mm_users记录正在使用该内存的进程数目;mm_count域是mm_struct结构体的主引用计数。区别:mm_users一个进程创建多个线程时会增加,mm_count当内核线程共享进程地址空间时增加,初始化是1。

所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表进程0的地址空间。

见后面关联图说明。

(2)分配与撤销

A.分配

在进程创建一节提到过,do_fork的copy_process()完成大部分工作。copy_process()中的copy_mm()实现复制父进程的内存描述符,源码解析如下:

static intcopy_mm(unsigned long clone_flags, struct task_struct *tsk)

{

struct mm_struct *mm, *oldmm;

int retval;

tsk->min_flt = tsk->maj_flt = 0;

tsk->nvcsw = tsk->nivcsw = 0;

/* 把子进程初始化成NULL */

tsk->mm = NULL;

tsk->active_mm = NULL;

/* 由于内核线程没有独立的地址空间mm=NULL,所以当前current(即父进程)的mm为NULL,则表示父进程是内核线程,子进程上面已经初始化为NULL,这里直接返回 */

oldmm = current->mm;

if (!oldmm)

return 0;

/* 如果设置了CLONE_VM共享地址空间标识,则子进程直接指向父进程的内存描述符,使用计数+1,一般用户创建线程使用 */

if (clone_flags & CLONE_VM) {

atomic_inc(&oldmm->mm_users);

mm = oldmm;

goto good_mm;

}

retval = -ENOMEM;

/* 其他情况则是通过allocate_mm()申请自己的mm_struct,拷贝父进程的内容,再进行自己独立的初始化(包括pgd页表申请等)

allocate_mm()是kmem_cache_alloc的宏,从mm_cashep slab缓存中分配得到 */

mm = dup_mm(tsk);

if (!mm)

goto fail_nomem;

good_mm:

tsk->mm = mm;

tsk->active_mm = mm;

return 0;

fail_nomem:

return retval;

}

B.撤销

当进程退出时,内核会调用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域为空,即使访问内核内存,内核线程也还是需要使用一些数据的。为了避免内核线程为内存描述符和页表浪费内存,避免向新地址空间进程切换浪费处理器周期,内核线程将直接使用前一个进程的内存描述符,进程切换源码如下:

static inline void context_switch(struct rq*rq, struct task_struct *prev,

struct task_struct *next)

{

struct mm_struct *mm, *oldmm;

mm = next->mm;     //下一个将执行进程的mm

oldmm = prev->active_mm;    //当前执行进程的active_mm

/*mm=NULL则为内核线程,内核线程的active_mm指向当前执行进程的active_mm,同时mm_count+1

mm != NULL则为用户进程,通过switch_mm实现内存描述符的切换,主要完成将pgd首地址放入CPU寄存器供mmu(硬件实现)使用

*/

if (!mm) {

next->active_mm = oldmm;

atomic_inc(&oldmm->mm_count);

enter_lazy_tlb(oldmm, next);

}else

switch_mm(oldmm, mm, next);

/*如果当前的进程为内核线程,则将active_mm=NULL,下次运行再重新赋值*/

if (!prev->mm) {

prev->active_mm = NULL;

rq->prev_mm = oldmm;

}

}

3、虚拟内存区域

(1)数据结构

虚拟内存区域(VMA)由vm_area_struct结构体描述,指定地址空间内连续区间上的一个独立内存范围。每一个VMA作为一个单独的内存对象管理,具有一致的属性(比如访问权限等),因此,一个VMA就代表了一种类型的内存区域(如内存映射文件、进程用户空间栈等)。结构体如下:

struct vm_area_struct {

unsignedlong vm_start;     //内存区间的收地址

unsigned longvm_end;      //内存区间的尾地址

//VMA双向链表,按照地址顺序排序

struct vm_area_struct *vm_next, *vm_prev;

struct rb_nodevm_rb;  //放在红黑树上的节点

unsigned longrb_subtree_gap;

struct mm_struct *vm_mm;    //指向内存描述符

pgprot_t vm_page_prot;     //访问权限

unsigned long vm_flags;     //标志

union {

struct {

struct rb_node rb;

unsigned long rb_subtree_last;

} linear;

struct list_head nonlinear;

const char __user *anon_name;

} shared;

struct list_head anon_vma_chain;  //匿名VMA对象链表

struct anon_vma *anon_vma;  //匿名VMA对象

const structvm_operations_struct *vm_ops;        //相关的操作表

unsigned long vm_pgoff;     //文件中的偏移量

struct file * vm_file;      //被映射的文件

void * vm_private_data;     //私有数据

#ifndefCONFIG_MMU

struct vm_region *vm_region;    /* NOMMU mapping region */

#endif

};

在同一个地址空间内的不同内存区间不能重叠。

struct vm_operations_struct {

void(*open)(struct vm_area_struct * area);

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

void(*close)(struct vm_area_struct * area);

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

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

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

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

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

int (*access)(struct vm_area_struct *vma, unsigned long addr,

void *buf, int len, int write);

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

int(*remap_pages)(struct vm_area_struct *vma, unsigned long addr,

unsigned long size, pgoff_tpgoff);

};

(2)相关操作

内核时常需要在某个内存区域上执行一些操作,比如某个指定地址是否包含在某个内存区域中。这类操作非常频繁,也是mmap例程的基础。为了方便执行这类对内存区域的操作,内核定义了许多的辅助函数。

A. find_vma

structvm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)

为了找到一个给定的内存地址属于哪一个内存区域。该函数在指定的地址空间搜索第一个vm_end大于addr的内存区域。如果没有发现这样的区域,返回NULL,否则指向匹配的内存区域vm_area_struct结构体指针。返回的结果被缓存在内存描述符mmap_cache域中,有相当好的命中率,提高速度。该函数通过红黑树查找。

B.find_vma_prev

和find_vma类似,但是它返回第一个小于addr的VMA。

C.find_vma_intersection

返回第一个和指定地址区间相交的VMA,也是调用find_vma

D.mmap

创建一个新的线性地址区间。如果新创建的VMA和已经存在的地址区间相邻,并且相同的访问权限,两个区域将合并为一个;如果不能合并就算是一个新的VMA。创建之后,会将该地址空间加入到进程的地址空间中。

Mmap系统调用对应的内核函数do_mmap_pgoff:

unsigned long do_mmap_pgoff(struct file*file, unsigned long addr,

unsigned long len, unsigned long prot,

unsigned long flags, unsigned long pgoff,

unsigned long *populate)

file为NULL且pgoff为0,代表没有和文件相关,称为匿名映射

如果指定了文件名和偏移量,称为文件映射

E.munmap

从特定的进程地址空间中删除指定地址空间,munmap系统调用对应的内核函数do_munmap:

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

4、缺页异常处理

Linux缺页异常处理程序必须区分两种情况:

由编程错误引起的异常

由引用属于进程地址空间但还尚未分配物理页框的页所引起的异常。

内核由static int __kprobes do_page_fault(unsigned long addr, unsigned intfsr, struct pt_regs *regs)实现。

总体方案如下图:

5、关联图

进程、线程之间内存描述符关系

进程A和B是独立进程,进程B由A创建,T1是进程A的一个线程。上图展示说明如下:

A.进程A fork进程B,进程B会复制父进程A的task_struct和mm_struct,进行相应的初始化,有各自的页表目录。

B.进程A pthread_create 线程T1,复制父进程A的task_struct,但是共享mm_struct,使用同一个页表目录

C.进程A和进程B的虚拟地址通过mm_struct的pgd页表映射到实际物理内存,对于可读页(如C库等),两个进程是共享实际内存,对于可写页(如堆栈、变量等)则有各自的物理内存

D.进程A和线程T1共享mm_struct,因此也共享pgd页表,当线程需要自己的可写页时,就会加入到进程A的共享页表里。

进程的虚拟内存管理

VMA:虚拟内存区域

mm_rb:为红黑树,图中用链表示意,表示mmap和mm_rb都指向虚拟内存区域

每个进程都有一个task_struct,所有进程通过链表连接起来,链表头为init_task。每个task_struct又都有一个mm_struct结构,所有的mm_struct通过mmlist连接在一个双向链表中,该链表的首元素是init_mm。每个进程的各个虚拟内存区域通过mmap和mm_rb组织起来,然后通过pgd页表,映射到实际的物理内存。

linux内核之进程地址空间相关推荐

  1. 【Linux 内核】进程管理 ( 进程与操作系统 | 进程与程序 | 进程与线程 | 虚拟地址空间 )

    文章目录 一.进程与操作系统 二.进程与程序 三.进程与线程 四.虚拟地址空间 一.进程与操作系统 操作系统与硬件的关系 : 操作系统 使用 硬件 提供的资源 , 如 CPU , 内存 , 磁盘 , ...

  2. 【Linux 内核】进程管理 ( 内核线程概念 | 内核线程、普通进程、用户线程 | 内核线程与普通进程区别 | 内核线程主要用途 | 内核线程创建函数 kernel_thread 源码 )

    文章目录 一.内核线程概念 二.内核线程.普通进程.用户线程 三.内核线程.普通进程区别 四.内核线程主要用途 五.内核线程创建函数 kernel_thread 源码 一.内核线程概念 直接 由 Li ...

  3. 【Linux 内核】进程管理 ( 进程特殊形式 | 内核线程 | 用户线程 | C 标准库与 Linux 内核中进程相关概念 | Linux 查看进程命令及输出字段解析 )

    文章目录 一.进程特殊形式 ( 内核线程 | 用户线程 ) 二.C 标准库与 Linux 内核中进程相关概念 三.Linux 查看进程命令及输出字段解析 一.进程特殊形式 ( 内核线程 | 用户线程 ...

  4. 【Linux 内核】进程管理 ( 进程状态 | 进程创建 | 进程终止 | 调用 exit 系统调用函数主动退出 | main 函数返回自动退出 | kill 杀死进程 | 执行异常退出 )

    文章目录 一.进程状态 二.进程创建 三.进程终止 ( 调用 exit 系统调用函数主动退出 | main 函数返回自动退出 | kill 杀死进程 | 执行异常退出 ) 一.进程状态 Linux 进 ...

  5. 【Linux 内核】进程管理 task_struct 结构体 ⑤ ( files 字段 | nsproxy 字段 | 信号处理相关字段 | 信号量和共享内存相关字段 )

    文章目录 一.task_struct 结构体字段分析 1.files 字段 2.nsproxy 字段 3.信号处理相关字段 4.信号量和共享内存相关字段 在 Linux 内核 中 , " 进 ...

  6. 【Linux 内核】进程管理 task_struct 结构体 ④ ( comm 字段 | 进程优先级字段 | cpus_ptr 字段 | mm、active_mm 字段 | fs 字段 )

    文章目录 一.task_struct 结构体字段分析 1.comm 字段 2.进程优先级字段 3.cpus_ptr 字段 4.mm.active_mm 字段 5. fs 字段 在 Linux 内核 中 ...

  7. 【Linux 内核】进程管理 task_struct 结构体 ③ ( real_parent 字段 | parent 字段 | group_leader 字段 | real_cred、cred字段 )

    文章目录 一.task_struct 结构体字段分析 1.real_parent 字段 2.parent 字段 3.group_leader 字段 4.real_cred 字段 5.cred 字段 在 ...

  8. 【Linux 内核】进程管理 task_struct 结构体 ② ( state 字段 | stack 字段 | pid 字段 | tgid 字段 | pid_links 字段 )

    文章目录 一.task_struct 结构体字段分析 1.state 字段 2.stack 字段 3.pid字段 4.tgid 字段 5.pid_links 字段 在 Linux 内核 中 , &qu ...

  9. 【Linux 内核】进程管理 task_struct 结构体 ① ( task_struct 结构体引入 | task_struct 代码示例 )

    文章目录 一.task_struct 结构体 二.task_struct 结构体代码示例 一.task_struct 结构体 在 Linux 操作系统 中 , 进程 作为 调度的实体 , 需要将其抽象 ...

  10. 【Linux 内核】进程管理 ( Linux 内核中的进程状态 | TASK_RUNNING | TASK_INTERRUPTIBLE | __TASK_STOPPED | EXIT_ZOMBIE )

    文章目录 一.Linux 内核中的进程状态 二.TASK_RUNNING 状态 三.TASK_RUNNING 状态 四.TASK_UNINTERRUPTIBLE 状态 五.__TASK_STOPPED ...

最新文章

  1. 【微信小程序】:小程序,新场景
  2. 恩智浦AI视觉组之逐飞岁末彩蛋
  3. python【力扣LeetCode算法题库】面试题 01.06-字符串压缩
  4. python编程设计大学ppt_Python程序设计-清华大学出版社-董付国第5章-函数的设计和使用PPT...
  5. 让你了解什么是内存屏障
  6. Yii 框架里数据库操作详解-[增加、查询、更新、删除的方法](转)
  7. HierarchicalBeanFactory接口
  8. 启动spark shell
  9. python 装饰器
  10. 编辑距离Edit distance
  11. linux docker 安装sql,CentOS7使用Docker安装SQL Server 2017
  12. 协作多智能体强化学习中的回报函数设计
  13. Android 加载天地图
  14. mac删除默认ABC输入法,mac删除自带ABC输入法
  15. 深度学习在音乐信息检索(MIR)方向的应用介绍(上)
  16. 3Dtouch开发内容
  17. 最全解读】各种金融机构的产品分析(银行、证券、基金、信托...)
  18. 0528班宋ww:回顾刚来的那一天还历历在目,不禁感概一番
  19. IT外包:中国跃跃欲试,准备一鸣惊人——2012国际外包峰会所见所感(下)
  20. 汽车估损师跟二手车评估师的区别及鉴定方法

热门文章

  1. When Startup Disk is Full
  2. [转]printf 函数实现的深入剖析
  3. math ceil函数python_Python3 ceil() 函数
  4. python怎么画图表_Python 使用pycha画图表
  5. csrf跨站请求伪造问题解决
  6. C#操作十六进制数据以及十进制与十六进制互相转换
  7. redux之createStore
  8. python基础学习
  9. ButterKnife的安装与使用以及ButterKnife右键不显示的大坑
  10. 2015年度夏季假期学习内容