非连续内存区管理

来源:互联网 发布:安卓旗舰机 知乎 编辑:程序博客网 时间:2024/05/20 02:30

在内核态申请内存比在用户态申请内存要更为直接,它没有采用用户态那种延迟分配内存技术。内核认为一旦有内核函数申请内存,那么就必须立刻满足该申请内存的请求,并且这个请求一定是正确合理的。相反,对于用户态申请内存的请求,内核总是尽量延后分配物理内存,用户进程总是先获得一个虚拟内存区的使用权,最终通过缺页异常获得一块真正的物理内存。

1.物理内存的内核映射

IA32架构中内核虚拟地址空间只有1GB大小(从3GB到4GB),因此可以直接将1GB大小的物理内存(即常规内存)映射到内核地址空间,但超出1GB大小的物理内存(即高端内存)就不能映射到内核空间。为此,内核采取了下面的方法使得内核可以使用所有的物理内存。

1.高端内存不能全部映射到内核空间,也就是说这些物理内存没有对应的线性地址。不过,内核为每个物理页框都分配了对应的页框描述符,所有的页框描述符都保存在mem_map数组中,因此每个页框描述符的线性地址都是固定存在的。内核此时可以使用alloc_pages()和alloc_page()来分配高端内存,因为这些函数返回页框描述符的线性地址。

2.内核地址空间的后128MB专门用于映射高端内存,否则,没有线性地址的高端内存不能被内核所访问。这些高端内存的内核映射显然是暂时映射的,否则也只能映射128MB的高端内存。当内核需要访问高端内存时就临时在这个区域进行地址映射,使用完毕之后再用来进行其他高端内存的映射。

由于要进行高端内存的内核映射,因此直接能够映射的物理内存大小只有896MB,该值保存在high_memory中。内核地址空间的线性地址区间如下图所示:

从图中可以看出,内核采用了三种机制将高端内存映射到内核空间:永久内核映射,固定映射和vmalloc机制。

2.物理内存管理机制

基于物理内存在内核空间中的映射原理,物理内存的管理方式也有所不同。内核中物理内存的管理机制主要有伙伴算法,slab高速缓存和vmalloc机制。其中伙伴算法和slab高速缓存都在物理内存映射区分配物理内存,而vmalloc机制则在高端内存映射区分配物理内存。

伙伴算法

伙伴算法负责大块连续物理内存的分配和释放,以页框为基本单位。该机制可以避免外部碎片。

per-CPU页框高速缓存

内核经常请求和释放单个页框,该缓存包含预先分配的页框,用于满足本地CPU发出的单一页框请求。

slab缓存

slab缓存负责小块物理内存的分配,并且它也作为高速缓存,主要针对内核中经常分配并释放的对象。

vmalloc机制

vmalloc机制使得内核通过连续的线性地址来访问非连续的物理页框,这样可以最大限度的使用高端物理内存。

3.物理内存的分配

内核发出内存申请的请求时,根据内核函数调用接口将启用不同的内存分配器。

3.1 分区页框分配器

分区页框分配器 (zoned page frame allocator) ,处理对连续页框的内存分配请求。分区页框管理器分为两大部分:前端的管理区分配器和伙伴系统,如下图:

管理区分配器负责搜索一个能满足请求页框块大小的管理区。在每个管理区中,具体的页框分配工作由伙伴系统负责。为了达到更好的系统性能,单个页框的申请工作直接通过per-CPU页框高速缓存完成。

该分配器通过几个函数和宏来请求页框,它们之间的封装关系如下图所示。

这些函数和宏将核心的分配函数__alloc_pages_nodemask()封装,形成满足不同分配需求的分配函数。其中,alloc_pages()系列函数返回物理内存首页框描述符,__get_free_pages()系列函数返回内存的线性地址。

3.2 slab分配器

slab 分配器最初是为了解决物理内存的内部碎片而提出的,它将内核中常用的数据结构看做对象。slab分配器为每一种对象建立高速缓存。内核对该对象的分配和释放均是在这块高速缓存中操作。一种对象的slab分配器结构图如下:

可以看到每种对象的高速缓存是由若干个slab组成,每个slab是由若干个页框组成的。虽然slab分配器可以分配比单个页框更小的内存块,但它所需的所有内存都是通过伙伴算法分配的。

slab高速缓存分专用缓存和通用缓存。专用缓存是对特定的对象,比如为内存描述符创建高速缓存。通用缓存则是针对一般情况,适合分配任意大小的物理内存,其接口即为kmalloc()。

3.3 非连续内存区内存的分配

内核通过vmalloc()来申请非连续的物理内存,若申请成功,该函数返回连续内存区的起始地址,否则,返回NULL。vmalloc()和kmalloc()申请的内存有所不同,kmalloc()所申请内存的线性地址与物理地址都是连续的,而vmalloc()所申请的内存线性地址连续而物理地址则是离散的,两个地址之间通过内核页表进行映射。

vmalloc()的工作方式理解起来很简单:

1.寻找一个新的连续线性地址空间;

2.依次分配一组非连续的页框;

3.为线性地址空间和非连续页框建立映射关系,即修改内核页表;

vmalloc()的内存分配原理与用户态的内存分配相似,都是通过连续的虚拟内存来访问离散的物理内存,并且虚拟地址和物理地址之间是通过页表进行连接的,通过这种方式可以有效的使用物理内存。但是应该注意的是,vmalloc()申请物理内存时是立即分配的,因为内核认为这种内存分配请求是正当而且紧急的;相反,用户态有内存请求时,内核总是尽可能的延后,毕竟用户态跟内核态不在一个特权级。

后记:本文将Linux内核中物理内存管理这部分内容进行框架性总结,对内存管理感兴趣的同学可以从伙伴算法,slab和vmalloc()三个角度去了解和学习物理内存管理。



为什么需要高端内存

目前来说32位x86架构的计算机体系结构是最为流行的。在这种体系结构中,Linux内核把4GB的虚拟内存地址空间划分为3GB大小的用户空间和1GB大小的内核空间。

在传统理论上的32位系统中,内核可以将所有的物理内存映射到地址空间中去,这样做有几点好处:

※ 内核可以直接存取任何物理内存,这可能简化处理和加快速度。

※ 可以使用指针直接存取内存中的对象。另一种做法就是内核跟踪物理内存的使用状态,映射物理内存,并在操作对象时先对虚拟地址进行转换,最后再取消映射这块物理地址。

※ 使用更大的分页尺寸,以提高TLB(译著:即快表)覆盖率和性能。

在一些其他的体系架构上,比如MIPS处理器,硬件本身只能够存取一部分物理内存。在一个32位的MIPS上操作系统只能运行在内存的前512MB,这部分内存有直接的虚拟地址到物理地址的映射。其他的内存必须通过查询TLB缓存才能够存取,并且不能够同时映射4GB的内存。鉴于此,MIPS需要的对高端内存的支持比x86更加迫切。

应对高端内存

然而,许多人仍然坚持在32位的架构上使用大于1GB的内存,这就促使Linux内核使用千方百计…

Linux基本上使用如下策略:

※ 物理地址大于896MB的内存仅当需要时被临时映射到虚拟内存中。(见临时内存映射一节。)

※ 内核需要频繁存取的数据被分配在低于896MB的内存中(ZONE_NORMAL),这样内核就能随时存取这些数据了。

※ 内核偶尔需要存取的数据,包括页缓存、进程内存和页表等,倾向于被分配到高端内存中(ZONE_HIGHMEN)。

※ 还有一块小的内存管理区用来执行DMA设备请求,成为ZONE_DMA和ZONE_DMA32。

※ 各个内存管理区的分配和缺页的压力需要处理均衡。(见内存均衡一节。)

临时内存映射

从高端内存到内核虚拟内存的映射是由函数kmap(), kunmap(), kmap_atomic() 以及kunmap_atomic()来完成的。

kmap()创建一个持久的映射,这种映射在调用schedule()或者切换CPU后都是一直可用的。然而完成这种类型的映射需要获取到一把全局的锁,这在SMP系统下可能会成为瓶颈。不推荐使用此方法。

kmap_atomic()可以在SMP系统下获得更好的可伸缩性,它不需要锁机制。之所以不需要用到锁,是因为这种内存页到固定地址的转换仅对当前运行的CPU有效。当然这就意味着你在建立这个映射和使用它之前不能调用schedule(),因为同一个CPU上的另一个进程可能会用到相同的虚拟地址!这种方法在2.6的内核中使用最为广泛。

内存均衡

在一个具有2GB内存的系统中,一半多一点的内存属于高端内存(ZONE_HIGHMEN),其他不到一半的内存属于低端内存(ZONE_NORMAL 和 ZOME_DMA)。内核必须都要从它们中分配进程空间和页缓存,并且使这两个区域的页面循环数量和这两个区域的大小成比例。

这样做的原因是应用程序可能期望使用所有的2GB内存,如果内核总是在高端内存中分配和回收内存,那么应用程序的部分数据就会被换出,而不是存放在低端内存中。内核对高端内存和低端内存的分配和循环越均衡,应用程序在内存中的大小就越接近于2GB.

系统管理者的考虑

在32位系统上使用大量高端内存(大于8GB)的危险是系统可能会在ZONE_NORMAL管理区内分配更多的内存(译注:比例),这就意味着可能会造成在许多高端内存仍然未用的情况下出现内存不足。

另一个问题是系统会更加肆无忌惮地回收可以提高系统性能的内核数据结构(如cached inodes, buffer heads或者其他缓存类型)。

第三个问题是,在一个32位的系统上,没有一个进程可以有效地使用大于3GB的内存,这就意味着仅仅在“没有需要所有内存的进程”的情况下,买一个4GB大的内存才是有意义的。

基于以上原因,如果您要买一个大于4GB内存的计算机,您应该考虑使用64位的CPU并安装64位的操作系统。(译著:最后几句比较了几年前32位和64位电脑的价格,就不翻译了。)



内存映射结构:

1.32位地址线寻址4G的内存空间,其中0-3G为用户程序所独有,3G-4G为内核占有。

2.struct page:整个物理内存在初始化时,每个4kb页面生成一个对应的struct page结构,这个page结构就独一无二的代表这个物理内存页面,并存放在mem_map全局数组中。

3.段式映射:首先根据代码段选择子cs为索引,以GDT值为起始地址的段描述表中选择出对应的段描述符,随后根据段描述符的基址,本段长度,权限信息等进行校验,校验成功后。cs:offset中的32位偏移量直接与本段基址相累加,得出最终访问地址。

0-3G与mem_map的映射方式:
因linux中采用的段式映射为flat模式,所以从逻辑地址到线性地址没有变化。从段式出来进入页式,每个用户进程都独自拥有一个页目录表(pdt),运行时存放于CR3。  CR3(页目录) + 前10位 =>  页面表基址 + 中10位 => 页表项 + 后12位 => 物理页面地址

3G-4G与mem_map的映射方式:
分为三种类型:低端内存/普通内存/高端内存。
低端内存:3G-3G+16M 用于DMA        __pa线性映射
普通内存:3G+16M-3G+896M          __pa线性映射 (若物理内存<896M,则分界点就在3G+实际内存)
高端内存:3G+896-4G               采用动态的分配方式

4.高端内存(假设3G+896为高端内存起址)
作用:访问到1G以外的物理内存空间。
线性地址共分为三段:vmalloc段/kmap段/kmap_atomic段(针对与不同的内存分配方式)


从内存分配函数的结构来看主要分为下面几个部分:
a.伙伴算法(最原始的面向页的分配方式)
alloc_pages 接口:
    struct page * alloc_page(unsigned int gfp_mask)——分配一页物理内存并返回该页物理内存的page结构指针。
    struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)——分配 个连续的物理页并返回分配的第一个物理页的page结构指针。
    <释放函数:__free_page(s)>
    
    内核中定义:#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)   
    最终都是调用 __alloc_pages.
    其中MAX_ORDER 11,及最大分配到到页面个数为2^10(即4M)。
    分配页后还不能直接用,需要得到该页对应的虚拟地址:
    void *page_address(struct page *page);
    低端内存的映射方式:__va((unsigned long)(page  -  mem_map)  <<  12)
    高端内存到映射方式:struct page_address_map分配一个动态结构来管理高端内存。(内核是访问不到vma的3G以下的虚拟地址的) 具体映射由kmap / kmap_atomic执行。
    
get_free_page接口:(alloc_pages接口两步的替代函数)
    unsigned long get_free_page(unsigned int gfp_mask) 
    unsigned long __get_free_page(unsigned int gfp_mask) 
    Unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order)
    <释放函数:free_page>
    与alloc_page(s)系列最大的区别是无法申请高端内存,因为它返回到是一个线性地址,而高端内存是需要额外映射才可以访问的。

b.slab高速缓存(反复分配很多同一大小内存)   注:使用较少
    kmem_cache_t* xx_cache;
    创建: xx_cache = kmem_cache_create("name", sizeof(struct xx), SLAB_HWCACHE_ALIGN, NULL, NULL);
    分配: kmem_cache_alloc(xx_cache, GFP_KERNEL);
    释放: kmem_cache_free(xx_cache, addr);
  内存池
      mempool 不使用。
  
c.kmalloc(最常用的分配接口)         注:必须小于128KB
    GFP_ATOMIC 不休眠,用于中断处理等情况
    GFP_KERNEL 会休眠,一般状况使用此标记
    GFP_USER   会休眠
    __GFP_DMA  分配DMA内存
    kmalloc/kfree
    
d.vmalloc/vfree
    vmalloc采用高端内存预留的虚拟空间来收集内存碎片引起的不连续的物理内存页,是用于非连续物理内存分配。
当kmalloc分配不到内存且无物理内存连续的需求时,可以使用。(优先从高端内存中查找)
    
e.ioremap()/iounmap()
  ioremap()的作用是把device寄存器和内存的物理地址区域映射到内核虚拟区域,返回值为内核的虚拟地址。使用的线性地址区间也在vmmlloc段
注:
vmalloc()与 alloc_pages(_GFP_HIGHMEM)+kmap();前者不连续,后者只能映射一个高端内存页面
__get_free_pages与alloc_pages(NORMAL)+page_address(); 两者完全等同
内核地址通过 __va/__pa进行中低内存的直接映射
高端内存采用kmap/kmap_atomic的方式来映射

    
个人总结如下:
a.在<128kB的一般内存分配时,使用kmalloc
    允许睡眠:GFP_KERNEL
    不允许睡眠:GFP_ATOMIC
b.在>128kB的内存分配时,使用get_free_pages,获取成片页面,直接返回虚拟地址(<4M)(或alloc_pages + page_address)
c.b失败,
    如果要求分配高端内存:alloc_pages(_GFP_HIGHMEM)+kmap(仅能映射一个页面)
    如果不要求内存连续: 则使用vmalloc进行分配逻辑连续的大块页面.(不建议)/分配速度较慢,访问速率较慢。
d.频繁创建和销毁很多较大数据结构,使用slab.
e.高端内存映射:
    允许睡眠:kmap              (永久映射)
    不允许睡眠:kmap_atomic      (临时映射)会覆盖以前到映射(不建议)


这篇讲述linux内存寻址与内存管理的文章,讲解的非常好。

1.     x86的物理地址空间布局

 

以x86_32,4G RAM为例:

物理地址空间的顶部以下一段空间,被PCI设备的I/O内存映射占据,它们的大小和布局由PCI规范所决定。640K~1M这段地址空间被BIOS和VGA适配器所占据。

由于这两段地址空间的存在,导致相应的RAM空间不能被CPU所寻址(当CPU访问该段地址时,北桥会自动将目的物理地址“路由”到相应的I/O设备上,不会发送给RAM),从而形成RAM空洞。

 

当开启分段分页机制时,典型的x86寻址过程为

 

内存寻址的工作是由Linux内核和MMU共同完成的,其中Linux内核负责cr3,gdtr等寄存器的设置,页表的维护,页面的管理,MMU则进行具体的映射工作。

 

2.     Linux的内存管理

Linux采用了分页的内存管理机制。由于x86体系的分页机制是基于分段机制的,因此,为了使用分页机制,分段机制是无法避免的。为了降低复杂性,Linux内核将所有段的基址都设为0,段限长设为4G,只是在段类型和段访问权限上有所区分,并且Linux内核和所有进程共享1个GDT,不使用LDT(即系统中所有的段描述符都保存在同一个GDT中),这是为了应付CPU的分段机制所能做的最少工作。

Linux内存管理机制可以分为3个层次,从下而上依次为物理内存的管理、页表的管理、虚拟内存的管理。

 

3.     页表管理

为了保持兼容性,Linux最多支持4级页表,而在x86上,实际只用了其中的2级页表,即PGD(页全局目录表)和PT(页表),中间的PUD和PMD所占的位长都是0,因此对于x86的MMU是不可见的。

 

在内核源码中,分别为PGD,PUD,PMD,PT定义了相应的页表项,即

(定义在include/asm-generic/page.h中)

typedef struct {unsigned long pgd;} pgd_t;

typedef struct {unsigned long pud;} pud_t;

typedef struct {unsigned long pmd;} pmd_t;

typedef struct {unsigned long pte;} pte_t;

为了方便的操作页表项,还定义了以下宏:

(定义在arch/x86/include/asm/pgtable.h中)

mk_pte

pgd_page/pud_page/pmd_page/pte_page

pgd_alloc/pud_alloc/pmd_alloc/pte_alloc

pgd_free/pud_free/pmd_free/pte_free

set_pgd/ set_pud/ set_pmd/ set_pte

 

4.     物理内存管理

Linux内核是以物理页面(也称为page frame)为单位管理物理内存的,为了方便的记录每个物理页面的信息,Linux定义了page结构体:

(位于include/linux/mm_types.h)

struct page {

      unsigned long flags;         

      atomic_t _count;       

      union {

             atomic_t _mapcount;      

             struct {          /* SLUB */

                    u16 inuse;

                    u16 objects;

             };

      };

      union {

          struct {

             unsigned long private;            

             struct address_space *mapping;   

          };

          struct kmem_cache *slab;      /* SLUB: Pointer to slab */

          struct page *first_page;  /* Compound tail pages */

      };

      union {

             pgoff_t index;             /* Our offset within mapping. */

             void *freelist;             /* SLUB: freelist req. slab lock */

      };

      struct list_head lru;          

};

Linux系统在初始化时,会根据实际的物理内存的大小,为每个物理页面创建一个page对象,所有的page对象构成一个mem_map数组。

进一步,针对不同的用途,Linux内核将所有的物理页面划分到3类内存管理区中,如图,分别为ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。

 

  • ZONE_DMA的范围是0~16M,该区域的物理页面专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。
  • ZONE_NORMAL的范围是16M~896M,该区域的物理页面是内核能够直接使用的。
  • ZONE_HIGHMEM的范围是896M~结束,该区域即为高端内存,内核不能直接使用。

 

内存管理区

内核源码中,内存管理区的结构体定义为

struct zone {

...

       struct free_area  free_area[MAX_ORDER];

...

       spinlock_t            lru_lock;      

       struct zone_lru {

              struct list_head list;

       } lru[NR_LRU_LISTS];

       struct zone_reclaim_stat reclaim_stat;

       unsigned long             pages_scanned;     /* since last reclaim */

       unsigned long             flags;               /* zone flags, see below */

       atomic_long_t            vm_stat[NR_VM_ZONE_STAT_ITEMS];

       unsigned int inactive_ratio;

...

       wait_queue_head_t   * wait_table;

       unsigned long             wait_table_hash_nr_entries;

       unsigned long             wait_table_bits;

...

       struct pglist_data       *zone_pgdat;

       unsigned long             zone_start_pfn;

...

};

  • 其中zone_start_pfn表示该内存管理区在mem_map数组中的索引。
  • 内核在分配物理页面时,通常是一次性分配物理上连续的多个页面,为了便于快速的管理,内核将连续的空闲页面组成空闲区段,大小是2、4、8、16…等,然后将空闲区段按大小放在不同队列里,这样就构成了MAX_ORDER个队列,也就是zone里的free_area数组。这样在分配物理页面时,可以快速的定位刚好满足需求的空闲区段。这一机制称为buddy system。
  • 当释放不用的物理页面时,内核并不会立即将其放入空闲队列(free_area),而是将其插入非活动队列lru,便于再次时能够快速的得到。每个内存管理区都有1个inacitive_clean_list。另外,内核中还有3个全局的LRU队列,分别为active_list,inactive_dirty_list和swapper_space。其中active_list用于记录所有被映射了的物理页面,inactive_dirty_list用于记录所有断开了映射且未被同步到磁盘交换文件中的物理页面,swapper_space则用于记录换入/换出到磁盘交换文件中的物理页面。

 

物理页面分配

分配物理内存的函数主要有

  • struct page * __alloc_pages(zonelist_t *zonelist, unsigned long order);

参数zonelist即从哪个内存管理区中分配物理页面,参数order即分配的内存大小。

  • __get_free_pages(unsigned int flags,unsigned int order);

参数flags可选GFP_KERNEL或__GFP_DMA等,参数order同上。

该函数能够分配物理上连续的内存区域,得到的虚拟地址与物理地址是一一对应的。

  • void * kmalloc(size_t size,int flags);

该函数能够分配物理上连续的内存区域,得到的虚拟地址与物理地址是一一对应的。

 

物理页面回收

当空闲物理页面不足时,就需要从inactive_clean_list队列中选择某些物理页面插入空闲队列中,如果仍然不足,就需要把某些物理页面里的内容写回到磁盘交换文件里,腾出物理页面,为此内核源码中为磁盘交换文件定义了:

(位于include/linux/swap.h)

struct swap_info_struct {

       unsigned long      flags;            /* SWP_USED etc: see above */

       signed short prio;              /* swap priority of this type */

       signed char  type;             /* strange name for an index */

       signed char  next;             /* next type on the swap list */

       unsigned char *swap_map;     /* vmalloc'ed array of usage counts */

       struct block_device *bdev;      /* swap device or bdev of swap file */

       struct file *swap_file;              /* seldom referenced */

};

其中swap_map数组每个元素代表磁盘交换文件中的一个页面,它记录相应磁盘交换页面的信息(如页面基址、所属的磁盘交换文件),跟页表项的作用类似。

回收物理页面的过程由内核中的两个线程专门负责,kswapd和kreclaimd,它们定期的被内核唤醒。kswapd主要通过3个步骤回收物理页面:

  • 调用shrink_inactive_list ()扫描inacive_dirty_pages队列,将非活跃队列里的页面写回到交换文件中,并转移到inactive_clean_pages队列里。
  • 调用shrink_slab ()回收slab机制保留的空闲页面。
  • 调用shrink_active_list ()扫描active_list队列,将活跃队列里可转入非活跃队列的页面转移到inactive_dirty_list。

 

5.     虚拟内存管理

Linux虚拟地址空间布局如下

 

Linux将4G的线性地址空间分为2部分,0~3G为user space,3G~4G为kernel space。

由于开启了分页机制,内核想要访问物理地址空间的话,必须先建立映射关系,然后通过虚拟地址来访问。为了能够访问所有的物理地址空间,就要将全部物理地址空间映射到1G的内核线性空间中,这显然不可能。于是,内核将0~896M的物理地址空间一对一映射到自己的线性地址空间中,这样它便可以随时访问ZONE_DMA和ZONE_NORMAL里的物理页面;此时内核剩下的128M线性地址空间不足以完全映射所有的ZONE_HIGHMEM,Linux采取了动态映射的方法,即按需的将ZONE_HIGHMEM里的物理页面映射到kernel space的最后128M线性地址空间里,使用完之后释放映射关系,以供其它物理页面映射。虽然这样存在效率的问题,但是内核毕竟可以正常的访问所有的物理地址空间了。

 

内核空间布局

下面是内核空间布局的详细内容,

 

在kernel image下面有16M的内核空间用于DMA操作。位于内核空间高端的128M地址主要由3部分组成,分别为vmalloc area,持久化内核映射区,临时内核映射区。

由于ZONE_NORMAL和内核线性空间存在直接映射关系,所以内核会将频繁使用的数据如kernel代码、GDT、IDT、PGD、mem_map数组等放在ZONE_NORMAL里。而将用户数据、页表(PT)等不常用数据放在ZONE_ HIGHMEM里,只在要访问这些数据时才建立映射关系(kmap())。比如,当内核要访问I/O设备存储空间时,就使用ioremap()将位于物理地址高端的mmio区内存映射到内核空间的vmalloc area中,在使用完之后便断开映射关系。

 

用户空间布局

在用户空间中,虚拟内存和物理内存可能的映射关系如下图

 

当RAM足够多时,内核会将用户数据保存在ZONE_ HIGHMEM,从而为内核腾出内存空间。

下面是用户空间布局的详细内容,

 

用户进程的代码区一般从虚拟地址空间的0x08048000开始,这是为了便于检查空指针。代码区之上便是数据区,未初始化数据区,堆区,栈区,以及参数、全局环境变量。

 

虚拟内存区段

为了管理不同的虚拟内存区段,Linux代码中定义了

(位于include/linux/mm_types.h)

struct vm_area_struct {

       struct mm_struct * vm_mm;   /* The address space we belong to. */

       unsigned long vm_start;          /* Our start address within vm_mm. */

       unsigned long vm_end;            /* The first byte after our end address

                                      within vm_mm. */

       /* linked list of VM areas per task, sorted by address */

       struct vm_area_struct *vm_next, *vm_prev;

       pgprot_t vm_page_prot;         /* Access permissions of this VMA. */

       unsigned long vm_flags;          /* Flags, see mm.h. */

};

其中vm_start,vm_end定义了虚拟内存区段的起始位置,vm_page_prot和vm_flags定义了访问权限等。

  • vm_next构成一个链表,保存同一个进程的所有虚拟内存区段。
  • vm_mm指向进程的mm_struct结构体,它的定义为

(位于include/linux/mm_types.h)

struct mm_struct {

       struct vm_area_struct * mmap;            /* list of VMAs */

       struct rb_root mm_rb;

       struct vm_area_struct * mmap_cache;       /* last find_vma result */

       unsigned long mmap_base;            /* base of mmap area */

       unsigned long task_size;          /* size of task vm space */

       unsigned long cached_hole_size;

       unsigned long free_area_cache;          

       pgd_t * pgd;

       atomic_t mm_users;                /* How many users with user space? */

       atomic_t mm_count;              

};

每个进程只有1个mm_struct结构,保存在task_struct结构体中。

与虚拟内存管理相关的结构体关系图如下

 

虚拟内存相关函数

  • 创建一个内存区段可以用

unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags);

  • 当给定一个虚拟地址时,可以查找它所属的虚拟内存区段:

struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr);

由于所有的vm_area_struct组成了一个RB树,所以查找的速度很快。

  • 向用户空间中插入一个内存区段可以用

void insert_vm_struct (struct mm_struct *mm, struct vm_area_struct *vmp);

  • 使用以下函数可以在内核空间分配一段连续的内存(但在物理地址空间上不一定连续):

void *vmalloc(unsigned long size);

  • 使用以下函数可以将ZONE_HIGHMEM里的物理页面映射到内核空间:

static inline void *kmap(struct page*page);

 

6.     内存管理3个层次的关系

 

下面以扩展用户堆栈为例,解释3个层次的关系。

调用函数时,会涉及堆栈的操作,当访问地址超过堆栈的边界时,便引起page fault,内核处理页面失效的过程中,涉及到内存管理的3个层次。

Ø 调用expand_stack()修改vm_area_struct结构,即扩展堆栈区的虚拟地址空间;

Ø 创建空白页表项,这一过程会利用mm_struct中的pgd(页全局目录表基址)得到页目录表项(pgd_offset()),然后计算得到相应的页表项(pte_alloc())地址;

Ø 调用alloc_page()分配物理页面,它会从指定内存管理区的buddy system中查找一块合适的free_area,进而得到一个物理页面;

Ø 创建映射关系,先调用mk_pte()产生页表项内容,然后调用set_pte()写入页表项。

Ø 至此,扩展堆栈基本完成,用户进程重新访问堆栈便可以成功。

可以认为,结构体pgd和vm_area_struct,函数alloc_page()和mk_pte()是连接三者的桥梁。


0 0
原创粉丝点击