进程地址空间
内核除了管理本身的内存外,还必须管理用户空间中进程的内存,我们称这个内存为进程地址空间。linux操作系统采用虚拟内存技术
,所以,系统中所有的进程之间以虚拟方式共享内存,对一个进程而言,它好像可以访问整个系统中所有的物理内存。更重要的是,即使是一个进程,它访问的地址空间也可以远远大于系统物理内存。
地址空间
进程的地址空间由进程可寻址的虚拟内存组成,每个进程都有一个32位或64位的平坦地址空间,空间的大小取决于体系结构。内存区域包含各种内存对象,比如:
- 可执行文件代码的内存映射,称为代码段(text section)
- 可执行文件的已初始化全局变量的内存映射,称为数据段(data section)
- 未初始化全局变量,bss段,使用零页映射(页面信息全部为0)
- 任何匿名映射,通俗点就是堆
- 任何内存映射文件
- 共享库映射区(包括共享库的代码段、数据段、bbs)
- 进程用户空间栈(不要和进程内核栈混淆,内核栈独立存在并由内核维护)
内存描述符
内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息,定义在文件<linux/sched.h>中。内存描述符结构如下:
mm_struct和task_struct
在进程描述符task_struct中,mm域存放着该进程的内存描述符,所以current->mm便指向当前进程的内存描述符。
通常,子进程会复制父进程的内存描述符(通过从slab缓存中分配得到)。因此,每个进程都有自己唯一的进程地址空间
。
如果父进程希望子进程和其共享地址空间,可以在调用clone时,设置CLONE_VM标志,我们把这样的进程称为线程。在linux中,线程和进程本质的区别就是是否共享内存地址空间
。除此之外,内核并不会区别对待线程,线程对内核而言仅仅是一个共享特定资源的进程而已。
mm_struct与内核线程
值得注意的是,内核线程是在内核中运行的轻量级进程,与普通用户进程有所不同,内核线程通常没有属于自己独立的用户空间,它们共享内核的地址空间。因此内核线程没有进程地址空间,也没有相关的内存描述符,内核线程对应的进程描述符中mm域为空。这也是内核线程的真实含义,它们没有用户上下文
。
在需要的时候,内核线程是可以直接使用前一个进程的内存描述符的(避免内核线程为内存描述符和页表浪费内存),但是仅仅能使用地址空间中和内核内存相关的信息
。
虚拟内存区域
内存区域由vm_area_struct结构体描述,定义在文件<linux/mm_types.h>中,描述了指定内存连续区间上的内存范围(进程虚拟内存的区间)
。内存区域在linux内核中也被称为虚拟内存区域(VMA)。其结构定义如下:
虚拟内存区域VMA的理解
内存区域(vm_area_struct)指的的单个进程中不同的内存区域,例如,代码区、全局变量区等,具体见上面的地址空间结构。它们都拥有一致的属性和操作方法列表,不同之处在于不同的内存区域需要实现自己的方法和初始化自己的属性值。需要注意的是,同一地址空间中不同内存区域区间不能重叠
。
VMA操作
vm_area_struct结构体中的vm_ops域指向与指定内存区域相关的操作函数列表,vm_area_struct作为通用对象代表了任何类型的内存区域,而操作表针对特定的对象实现特定的方法。
查看内存区域
可以使用/proc文件系统(proc/pid/maps)和pmap工具查看给定进程的内存地址空间和其所包含的内存区域。
例如,查看/home/rlove/src目录下一个简单的example程序(程序什么也不做,直接返回):
上图中每列的含义如下:
[开始-结束] [访问权限] [偏移] [主设备号:次设备号] [i节点] [ 文件]
分析上图中每行所代表的内存区域,如下:
- 前三行分别是C库中lic.so的代码段、数据段和bbs段;
- 接下来两行是进程本身的代码段和数据段;
- 再下面三行是动态链接程序ld.so的代码段、数据段和bbs;
- 最后一行是进程的栈。
注意,设备标志为00:00,索引节点也为0的区域称为零页,零页的一个重要用处就是初始化bbs段。
操作内存区域
内核时常需要在某个内存区域执行一些操作,并且这类操作非常频繁,为了方便执行这类对内存区域的操作,内核定义了许多辅助函数。它们都声明在文件<linux/mm.h>中。
-
find_vma()
找到指定的内存地址属于那一个内存区域,并返回匹配内存区域的vm_area_struct结构体指针。struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr);
-
find_vma_prev()
找到第一个小于指定内存地址的VMA。struct vm_area_struct * find_vma_prev(struct mm_struct*mm, unsigned long addr, struct vm_area_struct **pprev);
-
find_vma_intersection()
找到第一个和指定内存地址相交的VMA。struct vm_area_struct * find_vma_intersection(struct mm_struct*mm, unsigned long start_addr, unsigned long end_addr);
-
mmap()和do_mmap()
创建一个新的线性地址空间,如果新创建的地址和已存在的地址区间相邻
,并且它们具有相同的访问权限
,两个区间会合并为一个
。否则,就会创建一个新的VMA,并将其加入到进程的地址空间中。mmap是用户空间使用,do_mmap内核使用,前者会调用后者。unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset);
-
mummap和do_mummap()
do_mummap函数从特定的进程地址空间中删除指定地址区间。mummap是对do_mummap的简单封装。这两个函数分别对应着前面两个函数。int do_mummap(struct mm_struct* mm, unsigned long start, size_t len);
页表
虽然应用程序操作的对象是虚拟内存,但是处理器直接操作的还是物理内存。所以应用程序访问一个虚拟内存时,首先必须要将虚拟内存转化为物理内存
,然后处理器才能解析地址访问请求。地址的转换工作是通过查询页表
来完成的,概括地讲,地址转换需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,而页表表项则指向下一级页表或是最终得物理页面。如下所示:
Linux中一般使用三级页表来完成地址转换。在多数体系结构中,搜索页表的工作是由硬件完成的,虽然通常操作中,很多使用页表的工作都可以通过硬件来执行,但是只有在内核正确设置页表的前提下,硬件才能方便地操作它们。
每个进程都有自己的页表(线程之间会共享页表),内存描述符的pgd域指向的就是进程的页全局目录。
在linux内核之内存寻址文章中,有对页面做较为详细的说明。
TLB
内存寻址缓冲器(TLB),TLB作为一个将虚拟地址映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器将首先检查TLB中是否缓存了该虚拟地址到物理地址的映射,如果在缓存直接命中,物理地址立即返回,否则,就需要再通过页表搜索需要的物理地址。