内存除了管理本身的内存(物理内存)外,还必须管理用户空间中的进程的内存(虚拟内存),这个内存叫进程地址空间。尽管一个进程可以寻址4GB的虚拟内存,但是这并不代表它有权访问所有的虚拟内存,这些可以被访问的地址空间称为内存区域。如果一个进程访问了不在有效范围内的内存区域,或者以不正确的方式访问了有效地址,那么内核会终止该进程,并返回段错误信息。

  内核使用内存描述符结构体表示进程的地址空间,内存描述符由struct mm_struct结构体表示。分配内存描述符有两种方法:其一,fork函数利用copy_mm函数复制父进程的内存描述符,也是current->mm域给其子进程,而子进程中的mm_struct结构体实际上是通过allocate_mm宏从mm_cachep_slab缓存中分配得到的。其二,当CLONE_VM被指定后,内核不需要调用allocate_mm函数,而仅仅需要在调用copy_mm函数中将mm域指向其父进程的内存描述符。

  内核线程与内存描述符

  内核线程没有进程地址空间,也没有自己的内存描述符,内核线程是没有用户上下文的。因为内核线程不需要访问用户空间内存,所以它们不需要有自己的内存描述符和页表。但是,当内核线程访问内核内存时,内核线程还是需要使用一些数据的,比如页表,为了避免内核线程为内核描述符呵呵页表浪费内存,内核线程直接使用前一个进程的内存描述符,而且仅仅使用前一个进程的内存描述符中和内核内存相关的信息,这些信息的含义和普通进程完全相同。

  内存区域由struct vm_area_struct结构体描述,在linux内核中,内存区域又叫虚拟内存区域(VMAs),内核将每一个内存区域作为一个单独的内存对象管理,内存区域的位置在[vm_start,vm_end]之间。在struct vm_area_struct中,我们有个flags域,这里面存放VMA标志,和物理页的访问权限不同,VMA标志反映了内核处理页面所需要遵守的行为准则,而不是硬件要求。另外提供了VMA操作来处理虚拟内存区域,包括open,close,fault,page_mkwrite,access函数。

  内存区域的树型结构和内存区域的链表结构

  红黑树的特点:第一,左边节点值小于右边节点值;第二,红节点的子节点为黑色;第三,树中的任何一条从节点到叶子的路径必须包括相同数量的黑色节点;第四,跟节点为红色;第五,红黑树搜索、插入、删除等操作的复杂度都是O(log(n))。链表用于需要遍历全部节点的时候,而红黑树适合在地址空间中定位特定内存区域的时候,内核为了内存区域上的各种不同的操作获得更高的性能,所以同时使用红黑树和链表这两种数据结构。

  创建和撤销内存区域

  如果创建内核区域,系统会调用do_mmap函数,在用户空间可以通过mmap函数获取内核函数do_mmap函数的功能。如果需要撤销内核区域,系统会调用do_munmap函数,在用户空间可以调用munmap函数获取内核do_munmap函数的功能。

  原则上用户空间不能访问设备空间,mmap函数能把用户空间和设备空间进行映射,使得对用户空间的访问等同于对设备空间的访问。mmap具体功能有两个:其一,将普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能。其二,为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。

  当用户调用mmap函数,内核会进行如下处理:第一,在进程的虚拟空间查找一块VMA;第二,将这块VMA进行映射;第三,如果设备驱动程序或者文件系统的file_operations定义了mmap操作,则调用它;第四,将这个VMA插入进程的VMA链表中。

  应用层测试mmap代码:


#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main()
{
  int fd;
  char *map;
  char buf[20];
  fd=open("mmap_test", O_RDWR);
//创建内存区域,映射的起始地址通常为NULL,由内核指定
  map=mmap(NULL,20,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
  strcpy(buf,map);  /*read date from map*/
  printf("%s",buf);
  strcpy(map,"This is a map test!");
  munmap(map,20);      //撤销内存区域
  close(fd);
  return 0;
}


  页表

  当应用程序访问一个虚拟地址时,首先必须将虚拟地址转化为物理地址,然后处理器才能解析地址访问请求,地址转换需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表。Linux使用三级页表完成地址转换,页表是页全局目录(PGD),它包含一个pgd_t类型数组,其中表项指向PMD中的表项,在内存描述符mm_struct结构体中包含pgd_t类型数据;二级页表是中间页目录(PMD),它是一个pmd_t类型数组,其中的表项指向PTE中的表项;后一级的页表简称页表,它是一个pte_t类型数组,该页指向物理页面。多数情况下搜素页表的工作是由硬件完成的,操作和检索页表时必须使用内存描述符中的page_table_lock锁。

  搜素内存中的物理地址速度很有限,为了加快搜素,多数体系结构都实现了一个TLB(转换旁路缓存),TLB作为一个将虚拟地址映射到物理地址的硬件缓存。当请求访问一个虚拟地址时,处理器将首先检查TLB中是否缓存了该虚拟地址到物理地址的映射,如果在缓存中直接命中,物理地址立刻返回,否则,需要再通过页表搜索需要的物理地址。2.6版本内核对页表管理的主要改进是从高端内存分配部分页表。未来改进的地方包括通过写时拷贝的方式共享页表,这样可以消除fork操作时页表拷贝所带来的消耗。