虚拟内存及页、区、页框的分配及其释放

引言

        本文是本系列博客的第一篇,本系列博客涉及的主题有:内存管理、内核映射、伙伴系统、slab告诉缓存、内存池、页缓存、cache等,凡是跟内存相关的都会记录在这个系列里面。

一、内存层次结构

        内存是操作系统的宝贵资源,管理内存是操作系统管理的核心功能,对内存的动态管理关系到系统的性能。在32位机器上,Linux内存一共4GB,其中0-3GB是进程地址空间,3GB-4GB是内核空间。先来看下Linux系统中的内存层次结构

         最底层的是硬件部分,底层的硬件决定着软件如何编写的问题。因此,先来介绍下虚拟内存部分--内存管理器(MMU)。

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
现代所有用于一般应用的操作系统都对普通的应用程序使用虚拟内存技术,老一些的操作系统,如DOS和1980年代的Windows,或者那些1960年代的大型机,一般都没有虚拟内存的功能
——维基百科

        维基百科上说的很清楚了,虚拟内存可以将物理不连续的内存在逻辑上连续起来。这会引起一个问题-物理地址和逻辑地址的转换,二者并非一一对应。下图是物理地址和逻辑地址地址转换结构图。

 目录、页面、页内偏移三者合在一起叫做页表,用32位数据表示,一共可寻址4GB。

页表的每项含义: 页内偏移:0--11,页面:12--21,目录:22--31。

其寻址过程如下:

        1)操作系统(OS)从寄存器CR3获得当前页面目录指针(基地址);
        2)基地址+页面目录偏移->页面表指针(基地址);
        3)页面表指针+页面表偏移->内存页基址;
        4)内存页基址+页内偏移->具体物理内存单元。

        页号=逻辑地址/页面大小;页面偏移量=逻辑地址%页面大小 。页目录保存页表项的地址,页表项保存物理地址,最后1项保存4k页内偏移。

        页表是系统为每个进程建立的页面映像表。在地址空间内的所有页(0~n),依次在页表中有一页表项,记录了相应页在内存块中对应的物理块号。

其中(0,2)、(1,3)叫做页表项。页表项长度指每个页表项占多大的内存空间。

页表在内存中占用的大小=页表长度×页表项长度;

内存大小=页面长度×页面大小(物理块大小)

二、页框及其分配、释放

1、页和页框的区别

        页框(物理块):将内存空间分成一个个大小相等的分区(页框号或物理块号从0开始)。

        页(页面):将用户进程的地址空间为与页框大小相等的一个个区域(页号一般也从0开始)。

        页框是实际的物理内存,页是对地址空间的划分。页可以装进页框中,这些页框不一定连续(物理上),但页框连续(逻辑上)。

2、页框描述符

        内核把物理页作为内存管理的最基本单元,尽管我们使用内存是以字节为单位,但MMU以页(Page)为单位管理内存,页大小4KB(大部分32位处理器),也可以位8KB(64位,其实不同的体系规定不一样,64位的英特尔处理器也有支持页大小为4MB,本文以32位处理器,页4KB为主)。Linux使用4KB作为标准,基于以下原因:

1)有分页单元引发的缺页异常很容易得到解释,或者是由于请求的页存在但不允许进程对其访问,或者是由于请求的页不存在。在第二种情况下,内存分配器必须找到一个4KB的空闲页框,并将其分配给进程。

2)虽然4KB和4MB都是磁盘块大小的倍数,但绝大多数情况下,当主存和磁盘之间传输小块数据时更高效。

——《深入理解Linux内核》

        内核使用struct page结构体描述每个物理页,记录每个页框的状态,该结构体位于<linux/mm_types.h>中。所有的页描述符存放在mem_map数组中,virt_to_page(addr)宏产生线性地址addr对应的页描述符地址,pfn_to_page(pfn)宏产生与页框号pfn对应的页描述符地址。

        内核需要区分哪些是进程的页框、哪些属于内核,还要确定动态内存中的页框是否空闲。在以下情况下页框是不空闲:包含用户态进程的数据、某个软件高速缓存的数据、动态分配的内核数据结构、设备驱动程序缓冲的数据、内核模块的代码等。struct page的结构体如下:

struct page {
	unsigned long flags; //描述页的状态,包括页脏、是否被锁在内存中	,每一位描述一个状态,原子操作 	
						 //<linux/page-flags.h>存放这些标志
	/*
		下面的联合体表示page用于何处,如页缓冲区等等,
		看对应的英文即可
	*/
	union {
		struct {	/* Page cache and anonymous pages */
			struct list_head lru;  //LRU算法指针
			struct address_space *mapping; //页高速缓存、匿名页的反向映射用到
			pgoff_t index;		//在不同内核成分中有不同含义
			unsigned long private; //用于正在使用页的内核成分
		};
		struct {	/* page_pool used by netstack */
		};
		struct {	/* slab, slob and slub */
		};
		struct {	/* Tail pages of compound page */
		};
		struct {	/* Second tail page of compound page */
		};
		struct {	/* Page table pages */
			};
		};
		struct {	/* ZONE_DEVICE pages */
		};

	};
    atomic_t _mapcount;  //页框中的页表项数目,没有则为-1
    atomic_t _refcount;  //引用次数,-1表示未被引用,可以在新的分配中使用
	void *virtual;		//虚拟地址,若为高端内存,其为NULL	

} ;

        中间有些不太重要的给删去了。flag用来描述页的状态,包括页是不是脏的、是不是被锁在内存,其每一位表示一个状态,标志定义在<linux/page-flags.h>中。_count存放页的引用计数,内核一般用page_count()函数判断是否有可用页。virtual是页的虚拟内存,高端动态内存描述该段再详细说明。page结构与物理页相关,而非虚拟页有关,内核仅仅用这个数据结构描述物理内存本身,而非包含在其中的数据。

 描述页框状态的标志

3、非一致内存访问

        我们希望内存是一种均匀、共享的资源,但由于CPU和内存之间传输速度的巨大差异,我们不得不在二者之间添加各种高速缓存(各种cache)来满足CPU的访问。Linux支持非一致内存访问(Non-Uniform Memory Access,NUMA)模型,在这种模型中,给定CPU对不同内存单元的访问时间可能不一样。系统的物理内存被划分为几个节点(node)。在一个单独的节点中,任一给定CPU访问页面所需的时间都是相同的。然而,对不同的CPU,这个时间不一定成立。对每个CPU而言,内核都试图把耗时节点的访问次数减到最少。

        每个节点的物理内存可以划分为几个管理区(Zone),每个节点都有一个类型为pg_data_t的描述符,它的第一个元素有pgdat_list变量指向,其结构体如下。

类型         参数 说明
  struct zone[]   node_zones 节点中管理区描述符的数组
  struct zonelist[]   node_zonelists 页分配器使用的  node_zonelists数据结构的数组
int                 nr_zones 节点中管理区的个数
struct page* node_mem_map 节点中页描述符的数组
struct bootmem_data* bdata 用在内核初始化阶段
unsigned long node_start_pfn 节点中第一个页框的下标
unsigned long node_present_pages 内存结点的大小(页框为单位)
unsigned long node_spanned_pages 节点的大小(页框为单位)
int node_id 节点标识符
pg_data_t * pgdat_next 内存节点链表中的下一项
wait_queue_head_t kswapd_wait kswapd也患处守护进程使用的等待队列
struct task_struct* kswapd 指针指向kswapd内核线程的进程描述符
int kswapd_max_order kswapd将要创建的空闲块大小取对数的值

4、内存管理区

        理想的模型中,一个页框就是一个内存储存单元,但实际上,受到硬件的约束,限制了页框的使用方式。在x86上,Linux必须两种硬件约束:

1)ISA总线的直接内存存取(DMA)处理器有一个严格的限制:只能访问RAM的前16MB。

2)在具有大容量的RAM中,CPU不能访问所有的物理内存(线性空间太小),一些内存无法永久的映射到内核空间上。

因此,Linux将内存划分为3个管理区(Zone),在x86中,管理区为:

ZONE_DMA:包含低于16MB的内存页框,用于DMA操作

ZONE_NORMAL:包含高于16MB、低于896MB的内存页框,正常映射的页

ZONE_HIGHMEM:包含高于896MB的内存页框,这些页并不能永久映射到内核空间。

这些划分和具体的体系有关,有的体系中,ZONE_DMA为空,ZONE_NORMAL就可直接用于分配。

        Linux将系统的页划分为区,形成不同的内存池,可以根据用途对其进行分配。区的划分没有任何物理意义,仅仅是为了管理页而进行的逻辑上的划分。有些特殊用途的页必须从特地区域分配,但也可以从其他区分配。如一般用途的内存既可以从ZONEZ-NORMAL中分配,也可以从ZONE_DMA中分配(ZONEZ-NORMAL资源不够时)。

管理区描述符的字段 

        当内核调用一个内存分配函数时,必须指明请求页框所在的管理区。为了在内存分配中指定首选管理区,内核使用zonelist数据结构管理区描述符指针数组。

5、分区页框分配器

        分区页框分配器(zoned page frame allocator)处理对连续页框组的内存分配请求,组成如图。

        管理区分配器接受动态内存的分配和释放。在请求分配的情况下,其搜索一个能满足要求的一组连续页框内存的管理区。在每个管理区内,使用伙伴算法来处理,为了更好的性能,一部分页框保留在高速缓存中用于快速地满足对单个页框的分配请求(在后面介绍伙伴系统中介绍该部分)。

6、请求页

        内核提供一组接口用于请求页框,以页为单位分配内存,接口在<linux/gfp.h>,分别介绍如下。

接口名         功能
struct page* alloc_pages(gfp_t gfp_mask, unsigned int order); 分配2order(1 << order)个连续物理页,并返回一个指针,指向第一个物理页的page结构体,错误返回NULL。
void* page_address(struct page*  page) 转化为逻辑地址,指定当前物理页的逻辑地址。
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order); 该函数作用与alloc_pages函数和相同,返回第一个物理页的逻辑地址。
struct page* alloc_page(gfp_t gfp_mask); 分配一页内存,相当于order参数为0。
unsigned long __get_free_page(gfp_t gfp_mask); 分配一页内存,相当于order参数为0。
unsigned long get_zeroed_page(gfp_t gfp_mask) 返回页的内容为0,该函数与__get_free_pages函数一样,只不过返回的内容被填充为0;分配的页返回给用户空间时非常有用。

请求页框用到的标志。

在实际中,Linux使用预定义标志值的组合,这些才是常用的标志。

什么时候用哪种标志位呢?这里进行以下总结。

标志 描述
GFP_AROMIC 可以用在中断处理程序、下半部、持有自旋锁以及其他不能睡眠的地方
GFP_NOWAIT 与GFP_AROMIC类似,不同之处在于调用故不会退给紧急内存池,这增加了内存分配失败的可能性。
GFP_NOIO 可以阻塞,但不会启动磁盘IO。
GFP_NOFS 在必要时可能引起阻塞,也可能启动磁盘IO,但不会启动文件系统操作。
GFP_KERNEL 常规的分配方式,可能会阻塞,在睡眠安全时用在进程上下文代码中。为了分配内存,内核尽力而为,该标志时首选标志。
GFP_USER 常规分配内存方式,可能会引起阻塞,用于为用户空间进程分配内存时。
GFP_HIGHUSER 从ZONE_HIGHMEM分配内存,可能回族赛,用于为用户空间进程分配内存时。
GFP_DMA 从ZONE_DMA分配内存,通常与其他标志一起使用。

大部分情况下,要么用GFP_KERNEL,要么用GFP_AROMIC。

GFP_KERNEL:进程上下文,可以休眠

GFP_AROMIC:进程上下文,不可以休眠;中断处理程序;软中断;tasklet;

在睡眠之前使用GFP_AROMIC:或者在睡眠以后使用GFP_KERNEL执行内存分配内存。

在需要用到DMA时且可以休眠:GFP_DMA  |  GFP_KERNEL

在需要用到DMA时且不可以休眠:GFP_DMA  |  GFP_AROMIC,或者在睡眠之前执行内存分配。

GFP_NOIO分配不会启动任何磁盘IO来帮助满足请求,GFP_NOFS可能会起到磁盘IO,但绝不会启动系统IO,二者分别用在某些低级块IO或者文件系统的代码中。

7、释放页

释放页相关接口如下,均用于释放页框。

接口名         功能
void __free_page(struct page* page, unsigned  int  order); 释放page对应的页框,一共释放2^order个连续页框。
void  free_pages(unsigned long adrr, unsigned int order); 参数为要释放的第一个页框的线性地址addr
void  free_page(unsigned long addr) 释放地址为addr的页框

三、总结

        内存管理是操作系统的一个核心部分,其牵扯到硬件相关东西,内存管理的好坏直接影响系统性能。抛开这些不谈,对内存的掌握也是技术大牛的必备了。

猜你喜欢

转载自blog.csdn.net/zichuanning520/article/details/130786364
今日推荐