之六:虚存管理中的抽象

在软件设计时,我们一般要从需求中提取出抽象(类或者数据结构),然后围绕这些抽象设计相关的算法。内存管理自然也不能例外,这一节我们来看看为了管理为了内存以及整个虚存空间,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了)全部建立好了,这个映射的“效果”是:

①直接映射区的大小由物理内存中ZONE_DMA和ZONE_NORMAL这两个zone的大小决定,为这两个管理区的大小之和;

②物理内存的0地址,对应直接映射区的起始地址(内核中用常量PAGE_OFFSET来表示这个起始地址,当然,该值为3G);

③直接映射区中的虚拟地址到物理地址的映射为线性关系,即用虚拟地址减去PAGE_OFFSET即可得到对应的物理地址。内核中专门为此定义了宏__pa,相关的宏定义如下:

<page.h>
 
#define __PAGE_OFFSET    (0xC0000000)
......
#define PAGE_OFFSET      ((unsignedlong)__PAGE_OFFSET)
#define __pa(x)       ((unsignedlong)(x)-PAGE_OFFSET)
#define __va(x)       ((void *)((unsigned long)(x)+PAGE_OFFSET))

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

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

<processor.h>
/*
 * User spaceprocess 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 ofmemory

ZONE_NORMAL —— 16MiB -896MiB

ZONE_HIGHMEM —— 896 MiB -End

问题2:为什么要有vmalloc区?

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

①在VMALLOC区分配出一段连续的虚拟内存区域;

②通过伙伴系统获取物理页;

③通过页表的操作将步骤①中虚拟内存映射到步骤②中获得的物理页面上。

问题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.进程用户空间的布局

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 {
   structvm_area_struct * mmap;     /* list of VMAs*/
   structvm_area_struct * mmap_avl; /* tree of VMAs*/
   structvm_area_struct * mmap_cache;  /* lastfind_vma result */
   pgd_t * pgd;
   atomic_tmm_users;       /* How many users withuser space? */
   atomic_tmm_count;       /* How many references to"struct mm_struct"
                                   (users    count as 1) */
   intmap_count;           /* number of VMAs */
   structsemaphore mmap_sem;
   spinlock_tpage_table_lock;
 
   structlist_head mmlist;    /* List of all activemm's */
 
   unsignedlong start_code, end_code, start_data, end_data;
   unsignedlong start_brk, brk, start_stack;
   unsignedlong arg_start, arg_end, env_start, env_end;
   unsignedlong rss, total_vm, locked_vm;
   unsignedlong def_flags;
   unsignedlong cpu_vm_mask;
   unsignedlong swap_cnt;  /* number of pages to swapon next pass */
   unsignedlong swap_address;
 
   /*Architecture-specific MM context */
   mm_context_tcontext;
};

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,
                                structtask_struct *tsk, unsigned cpu)
{
    ...
      /*Re-load page tables */
      asmvolatile("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  * Thisis 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>中:

/*
 * Thisstruct defines a memory VMM memory area. There is one of these
 * perVM-area/task.  A VM area is any part ofthe process virtual memory
 * space thathas a special rule for the page-fault handlers (ie a shared
 * library,the executable area etc).
 */
struct vm_area_struct {
   structmm_struct * vm_mm;   /* VM area parameters*/
   unsignedlong vm_start;
   unsignedlong vm_end;
 
   /* linkedlist of VM areas per task, sorted by address */
   structvm_area_struct *vm_next;
 
   pgprot_tvm_page_prot;
   unsignedlong vm_flags;
 
   /* AVL treeof VM areas per task, sorted by address */
   shortvm_avl_height;
   structvm_area_struct * vm_avl_left;
   structvm_area_struct * vm_avl_right;
 
   /* Forareas 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.
    */
   structvm_area_struct *vm_next_share;
   structvm_area_struct **vm_pprev_share;
 
   structvm_operations_struct * vm_ops;
   unsignedlong vm_pgoff;     /* offset in PAGE_SIZEunits,
                                      *not*PAGE_CACHE_SIZE */
   struct file* vm_file;
   unsignedlong vm_raend;
   void *vm_private_data;     /* was vm_pte (sharedmem) */
};

[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 arethe virtual MM functions - opening of an area, closing and
 * unmappingit (needed to keep files on disk up-to-date etc), pointer
 * to thefunctions called when a no-page or a wp-page exception occurs.
 */
struct vm_operations_struct {
   void (*open)(structvm_area_struct * area);
   void(*close)(struct vm_area_struct * area);
   struct page* (*nopage)(struct vm_area_struct * area,
                              unsigned longaddress, int write_access);
};

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

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


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值