内存管理之6:虚存管理中的抽象

date: 2014-09-20 19:09linux

在软件设计时,咱们通常要从需求中提取出抽象(类或者数据结构),而后围绕这些抽象设计相关的算法。内存管理天然也不能例外,这一节咱们来看看为了管理为了内存以及整个虚存空间,linux提取哪些抽象,提取这些抽象背后的动机是什么?这些抽象之间的关联是什么?算法

注:本文展现的结构体定义来自2.4.0版本的内核。数组

1. 4G虚存空间的划分

前面讲过,linux的页式存储管理为虚存地址空间设置了两种权限(段描述符中的DPL字段):最高级(0级)为内核所使用,最低级(3级)为用户空间所使用。换句话说,linux区分两种虚拟地址:系统空间的地址和用户空间的地址,用户空间无权访问系统空间的地址,从而实现对系统空间的保护。另外,从linux设置的内核空间和用户空间的段描述来看,内核空间和用户空间均可以访问0~4G的空间,但若是任凭内核空间与用户空间在4G空间上随意散布、交织,能够想象一下,光是管理这些空间都很费劲,更遑论地址空间保护了。缓存

将复杂的事情简单化,linux将4G的虚存空间划分为两块,高端的1G归内核,低端的3G归用户空间。这个分界线由常量TASK_SIZE表示(内核中,task表示进程,TASK_SIZE能够理解为进程用户空间的大小)。每一个用户进程都有独立的3G用户空间(说独立,是说进程有本身的mm_struct结构,也有本身的目录表pgd),全部进程共同拥有内核的1G空间。从用户进程的视角来看,每一个进程都有4G的虚存空间,示意以下:安全

内核空间&&用户空间

TASK_SIZE就像一道自然屏障,有了这个屏障,用户空间和内核空间就能够“井水不犯河水”了。借由这个屏障,内核中相关的实现也简单化了。最大的利好应该是内核空间与物理内存之间的映射关系简单化了(下节将会讲到)。随着研究的深刻,你可能愈来愈体会到这一“将复杂问题简单化”带来的好处。数据结构

2. 内核空间的布局

根据虚存空间与物理内存的映射关系的不一样,内核空间还能够细分,以下图(偷图自《深刻linux驱动程序内核机制》,我从新画了下,加入4G之下的Gap):架构

内核空间按照映射的不一样进行划分

其中ZONE_DMA和ZONE_NORMAL这两个zone中的物理内存直接映射到内核虚存空间中的“物理页面直接映射区”。为何叫直接映射区呢?这是由于在系统初始化时,已经将该区域到物理内存的映射页表(固然包括对应的PGD和PMD了)所有创建好了,这个映射的“效果”是:app

  1. 直接映射区的大小由物理内存中ZONE_DMA和ZONE_NORMAL这两个zone的大小决定,为这两个管理区的大小之和;
  2. 物理内存的0地址,对应直接映射区的起始地址(内核中用常量PAGE_OFFSET来表示这个起始地址,固然,该值为3G);
  3. 直接映射区中的虚拟地址到物理地址的映射为线性关系,即用虚拟地址减去PAGE_OFFSET便可获得对应的物理地址。内核中专门为此定义了宏__pa,相关的宏定义以下:
<page.h>
        
        #define __PAGE_OFFSET		(0xC0000000)
        ......
        #define PAGE_OFFSET		((unsigned long)__PAGE_OFFSET)
        #define __pa(x)			((unsigned long)(x)-PAGE_OFFSET)
        #define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))

  __pa将虚拟地址转换对应的物理地址,而__va则恰好相反。框架

前面提到用户空间大小TASK_SIZE为3G,这里的PAGE_OFFSET也为3G,其实TASK_SIZE是由PAGE_OFFSET来定义的:ide

<processor.h>
    /*
     * User space process size: 3GB (default).
     */
    #define TASK_SIZE	(PAGE_OFFSET)

千万不要小瞧内核在系统初始化期间为“物理页面直接映射起始区”所创建的映射,有了这个映射,将会带来不少好处。首先这个区间的虚拟地址到物理地址的映射是“一步到位”的,在ZONE_DMA或ZONE_NORMAL区间分配物理页面时,能够直接获得页面对应的虚拟地址,不用再去操做页表了,大大提升了效率。而用户空间中堆栈的扩展则是借由“页面异常”一步步创建页表,一步步进行扩张的。

图中的其余信息咱们经过下面的几个问题来分析。

问题1:为何物理内存要存在一个高端内存区ZONE_HIGHMEM?

虽然内核只有1G的虚存空间,但做为操做系统的核心,它应该能管理到全部的物理内存。当物理内存超过1G时,显然没法将整个内存都作线性映射到内核的1G空间中。因此就将超出的部分单独出来,经过其余的方式去映射。这种映射方式就是:当内核要访问ZONE_HIGHMEM中的一个物理页时,先从动态映射区或者固定映射区临时分配一个虚存页,并经过设置页表为两者创建页面映射,这样,内核就能够临时借用动态映射区或者固定映射区的虚拟地址去访问ZONE_HIGHMEM中的内存了。访问结束,再将虚拟地址归还给动态映射区或者固定映射区。正由于动态映射区或者固定映射区的存在,内核不可能将整个1G虚存空间多做为“直接映射区”,而高端内存ZONE_HIGHMEM也不只仅是物理内存中超出1G的部分。前面提到过的,物理内存区域的划分以下:

  • ZONE_DMA First —— 16MiB of memory
  • ZONE_NORMAL —— 16MiB - 896MiB
  • ZONE_HIGHMEM —— 896 MiB - End

问题2:为何要有vmalloc区?

与问题1的情形相反,若是系统的物理内存比较紧缺,好比嵌入式领域,物理内存一般都比较小,从“物理页面直接映射区”中没法得到连续的物理内存区域,那么就能够利用vmalloc函数来将不连续的物理内存拼凑出一块连续(虚拟地址空间连续,最终仍是靠页表来建议页面映射)的内存区域。与问题1相似,vmalloc函数的实现原理分为以下三步:

  1. 在VMALLOC区分配出一段连续的虚拟内存区域;
  2. 经过伙伴系统获取物理页;
  3. 经过页表的操做将步骤①中虚拟内存映射到步骤②中得到的物理页面上。

问题3:“物理页面直接映射区”与vmalloc区之间、vmalloc区域动态映射区之间、以及4G空间之下都有一个Gap,是作什么用的?

Gap区就像一个空洞,内核不会在空洞进行任何的地址映射,这主要用做安全保护,防止越界访问。因为这些区间没有作地址映射,那么访问这些区间的地址时处理器将触发页面异常,内核捕获到这个异常后就可进行相应的处理。Gap就像是内核故意设置的陷阱,而后内核说“够胆你就踩吧,反正我已经严阵以待了”。

除此之外,Gap还有其余的妙用,好比内核中ERR_PTR、PTR_ER和IS_ERR函数就是利用了4G之下的空洞来实现“将错误码地址化”,并据此来判断“究竟是错误码仍是正常地址”。详情请参考另外一篇文章《也谈ERR_PTR、PTR_ERR和IS_ERR》。

问题4:若是物理内存小于1G的话将不存在高端内存区ZONE_HIGHMEM,此时整个物理内存都将会被映射到内核空间中的“物理页面直接映射区”,那么进程的用户空间岂不是没有物理内存可用了?

咱们知道,内核的镜像也就几十个MB,内核确定用不完整个物理内存的。咱们要明确一点,内核使用内存也是要向伙伴系统申请的,“物理页面直接映射区”的存在并非说将整个物理内存都分配给内核使用。这里的“物理页面直接映射区”至关于给内核开了个“绿色通道”,或者说是伙伴系统给内核“开后门”。当内核申请内存时,固然是须要返回内存的虚拟地址了,伙伴系统从ZONE_DMA或ZONE_NORMAL区间分配物理内存页面以后,借由这个“绿色通道”,就能迅速的从“物理地址”转换到“虚拟地址”了,再将虚拟地址提供给内核使用。

物理内存是“供应方”,虚拟内存是“需求方”。“物理页面直接映射区”是“供应方”到“需求方”的“绿色通道”,这个绿色通道很宽,可是内核不见得就要占尽整个绿色通道,内核没使用那部分通道,其对应的物理内存仍能够被进程的用户空间使用到。

3 进程用户空间mm_struct

x86架构下,进程用户空间的典型布局以下:

进程用户空间的布局

  • 进程的命令行参数和环境变量存储在0XC0000000下方的第一个区域,以后才是堆栈区。
  • 在某些场合下,咱们认为堆栈从0XC0000000地址开始,这固然是在不影响讨论内容状况下的一种粗略的描述。堆栈的增加方向是往下增加,意味着每执行一次入栈操做(push),栈顶指针esp将减4。 堆Heap的增加方向为往上增加。在传统布局中,Heap的上限为0X40000000,意味着堆的大小不可能超过1G。从2.6版本之后的内核中,引入了新式布局,使得堆能够突破1G。(后面可能会讲到,还不肯定)。
  • 程序的代码段(text段,也叫正文段)从0x08048000地址开始,0地址到0x08048000之间的区域保留不用,这固然也是出于安全保护的目的。

在内核中,用mm_struct来描述进程的用户空间,结构的定义在<include/linux/sched.h>文件中:

struct mm_struct {
    	struct vm_area_struct * mmap;		/* list of VMAs */
    	struct vm_area_struct * mmap_avl;	/* tree of VMAs */
    	struct vm_area_struct * mmap_cache;	/* last find_vma result */
    	pgd_t * pgd;
    	atomic_t mm_users;			/* How many users with user space? */
    	atomic_t mm_count;			/* How many references to "struct mm_struct" 
                                       (users    count as 1) */
    	int map_count;				/* number of VMAs */
    	struct semaphore mmap_sem;
    	spinlock_t page_table_lock;
    
    	struct list_head mmlist;		/* List of all active mm's */
    
    	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 rss, total_vm, locked_vm;
    	unsigned long def_flags;
    	unsigned long cpu_vm_mask;
    	unsigned long swap_cnt;	/* number of pages to swap on next pass */
    	unsigned long swap_address;
    
    	/* Architecture-specific MM context */
    	mm_context_t context;
    };
  • mm_struct结构是对进程整个用户空间的抽象。每一个进程都有一个mm_struct结构,在每一个进程的进程控制块即task_struct结构体中,有一个指针(mm)指向该进程的mm_struct结构。

  • 虽然用户空间多达3G,但若是不认真组织打理,最终也会混乱不堪。就像用户空间布局图中展现的同样,内核将3G的空间分红更小粒度的虚存区间(对应结构体vm_area_struct)来管理。成员mmap用来将用户空间中全部的虚存区间组成一个单链表,mmap做为链表头;成员mmap_avl用来将全部的虚存区间作成一个AVL树,mmap_avl做为树的根节点;成员mmap_cache用来缓存上一次查找获得的vm_area_struct结构,以便下一次查找时提升效率。

  • pgd成员引领用户空间的页面映射。每当调度一个进程进入运行的时候(意味着即将进入进程的用户空间去运行程序),内核都要为即将运行的进程设置好控制寄存器CR3,而MMU的硬件老是从CR3中取得当前页面目录表的指针。不过CPU在执行代码是使用的是虚拟地址,而MMU硬件在进行映射时使用的是物理地址,所以须要一个从虚拟地址到物理地址的转换。还记得__pa这个宏,这里就要用到它了。对应的代码在<asm/ mmu_context.h >的switch_mm函数中:

static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, 
                                        struct task_struct *tsk, unsigned cpu)
        {
              ...
	    	  /* Re-load page tables */
	    	  asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));
             ...
        }

 问题:在即将离开内核空间要进入到进程的用户空间以前须要将CR3设置为进程的pgd,那么反过来,在从用户空间陷入内核空间时,是否也须要将内核的pgd设置进CR3寄存器中呢?

 答案是不须要设置。mm_struct结构中的pgd表明着整个的4G空间,页面目录表其实分为两部分:一部分表明着内核空间的虚存区间,一部分表明着用户空间的虚存期间。对于不一样的进程,页面目录表中表明内核空间的目录项是一致的(意味着其下属的页面表也是一致的),其与物理内存的映射是在系统初始化阶段创建的(初始化期间存储在swapper_pg_dir表中);而表明用户空间的那部分则各自为政。不信?有代码为证。第4章讲execve系统调用时,在为当前进程构建新的用户空间时,会依次调用mm_alloc()-->mm_init()-->pgd_alloc()-->get_pgd_fast()-->get_pgd_slow()。咱们来看看这段代码:

<arch/i386/kernel/head.s>
    383 /*
    384  * This is initialized to create an identity-mapping at 0-8M (for bootup
    385  * purposes) and another mapping of the 0-8M area at virtual address
    386  * PAGE_OFFSET.
    387  */
    388 .org 0x1000
    389 ENTRY(swapper_pg_dir)
    390         .long 0x00102007
    391         .long 0x00103007
    392         .fill BOOT_USER_PGD_PTRS-2,4,0
    393         /* default: 766 entries */
    394         .long 0x00102007
    395         .long 0x00103007
    396         /* default: 254 entries */
    397         .fill BOOT_KERNEL_PGD_PTRS-2,4,0
    398
    
    <include/asm/pgtable.h>
    /*TASK_SIZE为3G, PGDIR_SIZE为4M,所以USER_PTRS_PER_PGD为768,
      表示目录表中前768个目录项表明着进程的用户空间*/
    #define USER_PTRS_PER_PGD	 (TASK_SIZE/PGDIR_SIZE)  
    
    <include/asm/pgalloc.h>
    extern __inline__ pgd_t *get_pgd_slow(void)
    {
        //分配一个页面即4K
    	pgd_t *ret = (pgd_t *)__get_free_page(GFP_KERNEL);
    
    	if (ret) {
           //将页面的内容置0
    		memset(ret, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));
           //从swapper_pg_dir中拷贝表明着内核空间的目录项(共256个)
    		memcpy(ret + USER_PTRS_PER_PGD, 
                  swapper_pg_dir + USER_PTRS_PER_PGD, 
    				(PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
    	}
    	return ret;
    }

swapper_pg_dir即初始化期间使用的页面目录表。第392行利用汇编语言提供的功能在当前位置填入766个目录项,每一个目录项的大小为4字节,内容为0;一样第397行业填入254个这样的目录项。可见swapper_pg_dir有1024个目录项,是一个真正的页面目录表。固然,该表的内容在初始化期间会逐渐被更新。

由此,咱们得出结论,将进程mm_struct结构中的pgd设置进CR3,内核空间、进程的用户空间均可以正常进行页面映射了。

  • 一个进程对应一个mm_struct结构,但反过来却不成立。一个mm_struct结构可能被多个进程所共享。好比当一个进程建立(vfork或者clone)一个子进程时,其子进程就可能和父进程共享一个mm_struct结构。因此mm_struct结构体中有mm_users成员表示用户数,mm_count成员表示引用计数。
  • start_code、 end_code等成员请参考用户空间布局图。

4. 虚存区间

虚存区间对应的结构体为vm_area_struct,定义在<linux/mm.h>中:

/*
     * This struct defines a memory VMM memory area. There is one of these
     * per VM-area/task.  A VM area is any part of the process virtual memory
     * space that has a special rule for the page-fault handlers (ie a shared
     * library, the executable area etc).
     */
    struct vm_area_struct {
    	struct mm_struct * vm_mm;	/* VM area parameters */
    	unsigned long vm_start;
    	unsigned long vm_end;
    
    	/* linked list of VM areas per task, sorted by address */
    	struct vm_area_struct *vm_next;
    
    	pgprot_t vm_page_prot;
    	unsigned long vm_flags;
    
    	/* AVL tree of VM areas per task, sorted by address */
    	short vm_avl_height;
    	struct vm_area_struct * vm_avl_left;
    	struct vm_area_struct * vm_avl_right;
    
    	/* For areas with an address space and backing store,
    	 * one of the address_space->i_mmap{,shared} lists,
    	 * for shm areas, the list of attaches, otherwise unused.
    	 */
    	struct vm_area_struct *vm_next_share;
    	struct vm_area_struct **vm_pprev_share;
    
    	struct vm_operations_struct * vm_ops;
    	unsigned long vm_pgoff;		/* offset in PAGE_SIZE units, 
                                          *not* PAGE_CACHE_SIZE */
    	struct file * vm_file;
    	unsigned long vm_raend;
    	void * vm_private_data;		/* was vm_pte (shared mem) */
    };

[vm_start, vm_end)定义了一个虚存区间的范围,这是一个前闭后开的区间。

区间的划分并不只仅取决于地址的连续性,还有地址的访问权限等其余因素。因此包含在同一个虚存区间中全部页面具备相同的访问权限和其余属性,这些由成员vm_page_prot和vm_flags来表示。

属于同一个进程的全部区间都要按照起始地址从低到高连接在一块儿,这就是vm_next的做用。

给定一个虚拟地址,找到它所属的虚存区间是个频繁调用的动做,为了提升效率,进程的全部区间构成了一棵AVL树,这就是vm_avl_height、vm_avl_left和vm_avl_right的做用。

在两种状况下,虚存页面会和磁盘发生关联。一种是盘区交换:将久未使用的页面交换到磁盘上,从而腾出物理页面供更急需的进程使用。另外一种是mmap系统调用,将磁盘上的文件映射到进程的虚拟地址空间中,此后就能够像访问内存中的字符数组同样来访问文件的内容,而没必要使用read、lseek和write这些费时的操做。vm_next_share、vm_pprev_share和vm_file等就是用来记录和管理这种联系。(后面可能会讲到)。

区间结构体中另外一重要的成员是vm_operations_struct 类型的指针vm_ops,表明区间上的相关操做,vm_operations_struct定义在同一个文件中:

/*
     * These are the virtual MM functions - opening of an area, closing and
     * unmapping it (needed to keep files on disk up-to-date etc), pointer
     * to the functions called when a no-page or a wp-page exception occurs. 
     */
    struct vm_operations_struct {
    	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 write_access);
    };

这里定义了一组函数指针,这些函数与文件操做有关。为何要有这些函数呢?这是由于对不一样的虚存区间可能须要一些不一样的附加操做。其中,nopage指定了当该区间发生了页面异常时应该执行的操做,该函数一般会尝试申请物理内存页面,并设置页面表项来修复“异常页面”。物理内存页面的分配为何和文件操做有关呢?首先考虑文件共享的情形,当多个进程将同一个文件映射到各自的虚存空间时,内存中只须要保留一份物理页面便可,只有当某个进程须要写入时,才有必要另外复制一份独立的副本,此即Copy On Wrtie。这种状况下,物理页面的分配(是否不须要从新分配,用以前的只读副本就能够了?仍是说有进程要进行写操做,必需要分配新物理页面?)显然与文件有关。其次,进程经过mmap将文件映射到虚存区间中,当在用户空间像读写内存同样读写文件时,必然致使虚存区间的扩展(好比从文件头读到文件尾),伴随着虚存区间的扩展,其底层必然伴随着分配物理页面并将文件内容读入物理页面的操做。此外,内存页面与磁盘页面的交换显然也是和文件操做相关的。

mm_struct结构及其旗下的各个vm_area_struct结构只是代表了对虚存空间的需求。一个虚拟地址存在于某个虚存区间中,并保证该地址所在的虚存页面已经映射到一个物理(内存或磁盘)页面上,更不保证该页面就在内存中。当访问一个未经映射的页面时,将触发Page Falut(也成缺页异常),那时Page Fault的异常服务程序会来处理该问题。因此,从这个意义上讲,mm_struct以及vm_area_struct结构说明了对页面的需求,前面的zone和page则说明了页面的供应,而页面目录、中间目录和页面表则是两者之间的桥梁。这种关系能够描述以下:

进程虚拟内存管理框架图(结构体之间的联系)

相关文章
相关标签/搜索