两个不同的进程可以在它们各自地址空间相同地址内存存放不同的数据.但是进程之间也可以选择共享地址空间,这样的进程我们称之为线程.
内存区域:
内存地址是一个给定的值,它要在地址空间范围之内的,比如4021f000.这个值表示的是进程32位地址空间中的一个特定的字节.在地址空间中,我们更关心的是进程有权访问的虚拟内存地址区间,比如08048000-0804c000.这些可被访问的合法地址区间谓之内存区域(memory area).
内存区域的内存对象:
.可执行文件代码的内存映射,谓之代码段;
.可执行文件的已初始化全局变量的内存映射,谓之数据段;
.包含未初始化全局变量,也就是bss段的零页的内存映射;
.用于进程用户空间栈(不要和进程内核栈混淆,进程的内核栈独立存在并由内核维护)的零页的内存映射;
.每一个诸如C库或动态连接程序等共享库的代码段、数据段和bss也会被载入进程的地址空间;
.任何内存映射文件;
.任何共享内存段;
.任何匿名的内存映射,比如由mallock()分配的内存.
14.1 内存描述符
内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息.用mm_struct表示,定义在文件<linux/sched.h>.一些重要的域如下:
Mm_users域:记录正在使用该地址的进程数目;
Mm_count域:mm_struct结构体的主引用计数,只要有进程在使用该地址(mm_user域不为0),那么此域就等于1;没有进程使用该地址(mm_user=0时)此域才为0;
Mmap域:以链表形式存放该地址空间中的全部内存区域;
Mm_rb域:以红-黑树的形式存放该地址空间中的全部内存区域;
Mmlist域:所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表init进程的地址空间.
14.1.1 分配内存描述符
Copy_mm()和allocate_mm()函数:
在进程描述符中,mm域存放着该进程的内存描述符,current->mm便指向当前进程的内存描述符.fork()函数利用copy_mm()函数复制父进程的内存描述符,而子进程的mm_struct结构通过allocate_mm()宏从mm_cachep slab缓存中摘取.因此,每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间.
CLONE_VM标志:
如果父进程希望和其子进程共享地址空间,可以调用clone()时,设置CLONE_VM标志.这样创建出来的进程我们称之为线程.
当CLONE_VM指定后,内核不再需要调用allocate_mm()函数了,而仅需要调用copy_mm()函数中将mm域指向其父进程的内存描述符即可.
示意代码如下:
14.1.2 销毁内存描述符
当里程退出时,内核会调用exit_mm()函数进行一些进程退出的扫除工作.
14.1.3 mm_struct与内核线程
内核线程没有进程地址空间,也没有相关的内存描述符.内核线程对应的进程描述符中的mm域为空.
内核线程去访问内核内存时,往往通过页表来实现.
14.2 内存区域
内存区域由vm_area_struct结构体描述,定义在文件<linux/mm.h>中.内存区域在内核中被称为虚拟内存区域或VMA.
Vm_area_struct结构体描述了指定地址空间内连续区间上一个独立的内存范围.内核将每个内存区域作为一个单独的内存对象管理.
Vm_area_struct结构体中比较重要的域有:
Vm_start:指向区间的首地址;
Vm_end:指向区间的末尾.
一个内存区域位于[vm_start,vm_end]这个区间当中.
Vm_mm域:指向和VMA相关的mm_struct.其功能和上述的mm_struct类似.也是每个进程唯一的.
14.2.1 VMA标志
VMA标志包含在vm_flags域内,标志了内存区域所包含的页面的行为和信息.
常用标志意义如下:
VM_SHARED:指明了内存区域包含的映射是否可以在多进程间共享;
VM_IO:标志内存区域中包含对设备I/O空间的映射.该标志通常在设备驱动程序执行mmap()函数进行I/O空间映射时才被设置;
VM_RESERVED:标志内存区域不能被换出,也是设备驱动进行映射时被设置;
VM_SEQ_READ:标志暗示内核应用程序对映射内容执行有序(线性连续的)读操作;
VM_RAND_READ:和VM_SEQ_READ相反,暗示应用程序对映射执行随机(非有序)读操作;
14.2.2 VMA操作
vm_ops域:指向与指定内存区域相关的操作函数集.如下:
下面介绍具体方法:
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()来为将要发生的缺页中断预映射一个新映射.
14.2.3 内存区域的树型结构和内存区域的链表结构
上述中的mm_struct描述符中的mmap和mm_rb域之一访问内存区域.这两个域各自独立地指向内存描述符相关的全体内存区域对象.vm_area_struct结构体里面的域vm_mm是mm_struct类型,里面也必然封装了mmap和mm_rb域.
Vm_next域:连入链表,便于内核管理.
14.2.4 实际使用中的内存区域
可以使用/proc文件系统和pamp工具查看给定的进程的内存空间和其中所含的内存区域.内存区域的区域比较抽象,下面以鲜明的例子来解析,达到形象的理解.
Int main(int argc,char *argv[])
{
Return 0;
}
下面列出该进程地址空间中包含的内存区域.其中有有代码段、数据段和bss段等.假设该进程与C库动态连接,那么地址空间中还将分别包含libc.so和ld.so对应的上述三种内存区域.
/proc/<pid>/map的输出显示了该进程地址空间中的全部内存区域:
每行数据格式如下:
开始-结束 访问权限 偏移 主设备号:次设备号 i节点 文件
用pmap工具将上述信息以更方便阅读的形式输出:
前三行分别对应C库中libc.so的代码段、数据段和bss段;
接下来两行是可执行对象的代码段和数据段;
再接下三行是动态连接程序ld.so的代码段,数据段和bss段;
最后一行是进程的栈.
小结:
上述主要讲述了内存、内存区域、进程空间地址、内核线程地址空间四个概念.其中内存是后面三者的关于内存的基本单元元素.
内存的结构体:
Mm_struct;
内存区域的结构体:
struct vm_area_struct {
struct mm_struct * vm_mm; /* The address space we belong to. */
... ...;
}
进程:
struct task_struct {
... ...;
struct mm_struct *mm, *active_mm;
... ...;
}
内核线程:
LINUX中内核线程也叫进程,它和普通的进程间的区别在于内核线程没有独立的地址空间,则它的mm指针被设置为NULL.例如内核的进程0唯一静态分配的.如下:
#define INIT_TASK(tsk) \
{ \
... ...;
.mm= NULL,\
.active_mm = &init_mm, \
... ...; \
}
14.3 操作内存区域
内核时常需要判断进程地址空间中的内存区域是否满足某些条件,比如某个指定地址是否包含在某个内存区域中.它们也是mmap()例程的基础.
14.3.1 find_vma()
Find_vma()函数定义在文件<mm/mmap.c>中.
函数作用:
在指定的地址空间中搜索第一个vm_end大于addr的内存区域.该函数寻找第一个包含addr或首地址大于addr的内存区域,如果没有发现这样的区域,返回NULL;否则返回指向匹配的内存区域的vm_area_struct结构指针.此函数返回的结果被缓存在内存描述符的mmap_cache域中.
14.3.2 find_vma_prev()
工作方式和上述的find_vma()函数相同.返回第一个小于addr的VMA.
14.3.3
Find_vma_intersection()函数返回第一个和指定地址区间相关的VMA.如下:
第一个参数mm是要搜索的地址空间,要搜索的地址空间范围是[start_addr,end_addr]
14.4 mmap()和do_mmap():创建地址区间
内核使用do_mmap()函数创建一个新的线性地址区间.它会将一个地址区间加入到进程的地址空间中.定义在<linux/mm.h>中.
Unsigned long do_mmap(struct file *file,unsigned long addr,unsigned long len,unsigned long prot,unsigned long flag,unsigned long offset)
各参数意义如下:
File:指定要映射的文件;
Offset:指定要映射的文件的偏移量;
Len:映射的长度,以字节为单位;
Addr:指定搜索空闲区域的起始位置;
Prot:指定内存区域中页面的访问权限,如可读、可写、可执行和禁止访问等;
Mmap()系统调用
用户空间可以通过mmap()系统调用获取内核do_mmap()的功能.
14.5 munmap()和do_munmap():删除地址区间
Do_munmap()函数从特定的进程地址空间中删除指定地址区间,定义于<linux/mm.h>:
Int do_munmap(struct mm_struct *mm,unsigned long start,size_t len)
各参数的意义如下:
Mm:指定要删除区域所在的地址空间;
Start:要删除的地址空间的起始位置;
Len:删除的地址空间的长度.
Munmap()系统调用
是用户空间程序提供了一种从自身地址空间中删除指定地址区间的方法,它和系统调用mmap()作用相反.如下:
Asmlinkage ling sys_munmap(unsigned long addr,size_t len)
{
Int ret;
Struct mm_struct *mm;
Mm = current->mm;
Down_write(&mm->mmap_sen);
Ret = do_munmap(mm,addr,len);
Up_write(&mm->mmap_sen);
Return ret;
}
14.6 页表
应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的却是物理内存.所以当应用程序访问一个虚拟内存地址时,首先必须将虚拟地址转化为物理地址,然后处理器才能解析地址访问请求.地址的转换工作需要通过查询页表才能完成.简单来说,地址转换需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,而页表项则指向下一级别的页表或指向最终的物理页面.
LINUX中使用三级页表完成地址转换.
一级页表:PGD.PGD包含了一个pgd_t类型数组.PGD中的表项指向二级目录中的表项:PMD.
二级页表:PMD.PMD是pmd_t类型数组.其中的表项指向PTE中的表项;
最后一级页表简称页表,包含了pte_t类型的页表项,该页表项指向物理页面.内存描述符的pgd域指向的就是进程的页全局目录.这三级页表关系如下图所示:
TLB:每次对虚拟内存中的页面访问都必须先解析它,从而得到物理内存中的对应地址,所以页表操作的性能非常关键.但是搜索内存中的物理地址速度比较有限,因此为了加快搜索,多数体系结构都实现了一个翻译后缓冲器(TLB).TLB作为一个将虚拟地址映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器将首先检查TLB中是否缓存了该虚拟地址到物理地址的映射.如果缓存命中,物理地址立刻返回,否则再路由页表去搜索.