Linux系统内存管理系列之四,linux内存管理系列


前面3节重点讲解了在内核如何分配内核空间内存,而进程的用户空间的内存同样受到内核的控制,用户空间的地址内存称为进程地址空间。Linux采用虚拟内存技术使得系统可以同时运行多个内存,而且每一个进程的地址空间都为整个物理内存的大小。这一部分重点讲解内核如何管理进程地址空间。

一 地址空间

进程地址空间由进程可寻址的虚拟内存组成,内核允许进程使用这种虚拟内存的地址,通常情况下各个进程之间的内存地址相互独立,当进程之间的内存可以相互访问时这种进程又称为线程。尽管一个进程的虚拟地址寻址区域可以达到4GB,但并非该进程有权限访问的地址可以达到4GB。我们通常将进程有权限访问的地址空间称为内存区域,通过内核的操作进程可以对自己的内存区域进行增加或减少操作。

一般情况下,进程访问到非法地址时都会返回“段错误”信息,并且内核会终止进程的运行。内存区域可以包含,代码段,数据段,bss段,进程用户空间栈,任何内存映射文件,任何共享内存段,任何匿名的内存段。

二 内存描述符

内核中定义了一种结构体mm_struct内存描述符,该结构包含了和进程地址有关的全部信息。

其中的mm_users代表该进程中的线程个数,mm_count代表进程的个数。只有当mm_count为0时,说明没有任何进程使用该内存描述符,这时内核可以撤销该结构体。Mmap与mm_rb这两个数据结构都表示虚拟内存区域对象,只是mmap以链表形式存放利于元素遍历,mm_rb以红黑树形式存放利于元素搜索。

1 分配内存描述符

在进程描述符task_struct结构体中mm域存放着该进程的内存描述符mm_struct,fork()函数利用copy_mm()函数复制父进程的mm_struct,子进程的mm_struct是通过mm_cachep slab缓存中分配得到的。通常每个进程都有唯一的mm_struct。如果是希望创建线程,即在调用clone()时,设置CLONE_VM标致。所以在内核的角度并不做进程与线程的区分,他们唯一的区别就是能否共享地址空间。

2 撤销内存描述符

当线程退出时,内核会减少mm_struct中的mm_users的用户计数,当进程退出时mm_count为0,同时回收mm_struct,调用kmem_cache_free()函数将mm_struct回收到mm_cachep slab缓存中。

3 内核线程的mm_struct

内核线程也是一种特有的进程,因为它一直处于内核空间执行,没有进程地址空间。同时也没有用户上下文。它的mm域为空,同时在内核线程访问内核空间时同样需要页表转化虚拟地址,这种情况下内核线程直接使用CPU上前一个执行进程的内存描述符。

三 虚拟内存区域

虚拟内存区域由结构体vm_area_struct描述,vm_area_struct描述了指定地址空间内存连续区间上的一个独立内存范围,内核将每个内存区域当做一个对象来进行管理。

vm_area_struct结构体如下所示:

 

\

 

每个内存描述符都代表进程地址空间的一段区间,其中vm_start指向区间的首地址,vm_end指向区间的尾地址的下一个地址。每个mm_struct均对应一个唯一的vm_area_struct,两个进程在做共享内存同时映射一个文件时都分别有自己的vm_area_struct,两个线程能够共享一个vm_area_struct,这是因为它们共享一个mm_struct。

vm_flags域表示当前内存区域的权限,常用的几种标志包括:VM_READ、VM_WRITE和VM_EXEC分别代表该内存区域中页面的读,写与执行权限。VM_SHARD指明了内存区域包含的映射是否可以在多进程间共享。

vm_ops域代表该内存区域的相关操作函数表,一般的操作包括:增加进程内存区域,删除进程内存区域,页面故障处理等。

四 实际进程的地址空间

首先运行一个守护进程sleep 3000&。然后查看/proc/[pid]/maps,结果如下所示:

 

\

 

接着执行pmap [pid],可以看到如下内容:

 

\

 

这其中主要包括:C库中的代码段,数据段和bss段,动态链接程序的代码段,数据段和bss段。进程的栈与可执行对象的代码段与数据段。

五 内存区域操作

为了找到给定内存地址属于哪一块内存区域,内核提供了find_vmal()函数。

struct vm_area_struct * find_vmal(struct mm_struct *mm,unsigned long addr)

该函数根据给定的内存描述符,与虚拟地址参数。然后在指定的地址空间中搜索第一个vm_end大于addr的内存区域,如果不存在该区域则返回null,否则返回的vm_area_struct指针被缓存到最近使用内存区域mmap_cache域中。同理内核函数find_vma_prev(),用于返回给定地址的前一块内存区域。

do_mmap()函数可以将一个线性地址空间加入到指定进程的地址空间中,其函数原型如下:do_mmap(struct file*file,unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset),file指定文件指针,addr指空闲区间开始地址,len指文件内存区间映射长度,prot指页的访问权限,flag指线性区的其他标志,offset指文件偏移长度。对应的撤销地址空间映射函数do_unmmap()。

六 页表

进程是无法操作内存物理地址的,不论是内核线程还是用户进程。进程只能操作线性地址,所以内核需要为每个进程维护一个页表来做地址映射。Linux一般采用三级页表,顶级页表是页全局目录(PGD)长10位,二级页表是页中间目录(PMD) 长10位,最后一级页表又称页内偏移量长12位。页表项与页表的个数一致,通过页表项可以固定到某一页,通过页内偏移可以固定到具体的线性地址。

进程在执行过程中,由于每次的内存访问都会访问页表,为了提高线性地址解析效率多数体系结构实现了块表TLB缓存,保留进程线性地址与物理地址的映射关系。所以一般情况下,要解析线性地址可以先访问TLB,加入TLB失效我们再去访问进程页表。

相关内容