内存管理(下)

5、物理内存的管理

在内核初始化完成后,内存管理的责任由伙伴系统(高效、高速)承担。前端

一、伙伴系统的结构

系统内存中的每一个物理内存页(页帧),都对应于一个struct page实例。每一个内存域都关联了一个struct zone的实例,其中保存了用于管理伙伴数据的主要数组。node

1 struct zone {
2 ...
3   struct free_area free_area[MAX_ORDER];    //不一样长度的空闲区域
4 ...
5 } ;

sruct free_area是一个辅助结构,以下所示。程序员

1 struct free_area {
2     struct list_head free_list[MIGRATE_TYPES];    //用于链接空闲页的链表
3     unsigned long nr_free;    //当前内存区中空闲页块的数目
4 };

 

阶(order)是伙伴系统中一个很是重要的术语。它描述了内存分配的数量单位内存区的管理单位,内存块的长度是2的order次方。图1是伙伴系统中相互链接的内存区,内存区中第1页内的链表元素,可用于将内存区维持在链表中。所以,也没必要引入新的数据结构来管理物理上连续的页,不然这些页不可能在同一内存区中,MAX_ORDER根据硬件不一样而设置不一样的值,表示一次分配能够请求的最大页数的以2为底的对数。算法

1 伙伴系统相互链接的内存区后端

伙伴没必要是彼此链接的。若是一个内存区在分配其间分解为两半,内核会自动将未用的一半加入到对应的链表中。若是在将来的某个时刻,因为内存释放的缘故,两个内存区都处于空闲状态,可经过其地址判断其是否为伙伴。数组

基于伙伴系统的内存管理专一于某个结点的某个内存域,例如,DMA或高端内存域。但全部内存域和结点的伙伴系统都经过备用分配列表链接起来。如图2所示。缓存

2 伙伴系统和内存域/结点之间的关系安全

二、避免碎片

Linux系统启动并长期运行后,物理内存会产生不少碎片。这对用户空间应用程序没有问题(其内存经过页表进行映射,物理内存分布与应用程序看到的内存无关),但对内核来讲,碎片是一个问题(大多数物理内存一致映射到地址空间内核部分)。网络

1)依据可移动性组织页数据结构

文件系统的碎片主要经过碎片合并工具解决,不一样于物理内存,许多物理内存页不能移动到任意位置,阻碍了该方法的实施。内核处理避免碎片的方法是反碎片(版本2.6.24),试图从最初开始尽量防止碎片。

内核将已分配页划分为如下3种不一样类型:

  • 不可移动页:在内存中有固定位置,不能移动到其余地方。核心内核分配的大多数内存属于该类别;
  • 可回收页:不能直接移动,但能够删除,其内容能够从某些源从新生成。例如,映射自文件的数据属于该类别,kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存;
  • 可移动页:能够随意地移动。属于用户空间应用程序的页属于该类别。它们是经过页表映射的。若是它们复制到新位置,页表项能够相应地更新,应用程序不会注意到任何事。

页的可移动性,依赖该页属于3种类别的哪种。内核使用的反碎片技术,将具备相同可移动性的页进行分组。根据页的可移动性,将其分配到不一样的列表中,防止不可移动的页位于可移动内存区中间的状况出现。这样对于不可移动页中仍然难以找到较大的连续空闲时间,但对可回收的页就相对容易了。

内核定义了一些宏来表示迁移类型:

1 #define MIGRATE_UNMOVABLE 0    //类型
2 #define MIGRATE_RECLAIMABLE 1    //类型
3 #define MIGRATE_MOVABLE 2        //类型
4 #define MIGRATE_RESERVE 3    //向具备特定可移动性的列表请求分配内存失败,从MIGRATE_RESERVE分配内存(紧急分配)
5 #define MIGRATE_ISOLATE 4 //不能从这里分配,特殊的虚拟区域,用于跨越NUMA结点移动物理内存页
6 #define MIGRATE_TYPES 5

对伙伴系统的主要数据结构影响是将空闲列表分解为MIGRATE_TYPE个列表,代码以下:

1 struct free_area {
2     struct list_head free_list[MIGRATE_TYPES];
3     unsigned long nr_free;    //全部列表上空闲页的数目
4 };

内核提供了一个备用列表,规定了在指定列表中没法知足分配请求时,接下来使用的迁移类型的种类。(在内核想要分配不可移动页时,若是对应链表为空,则后退到可回收页链表,接下来到可移动页链表,最后到紧急分配链表。)

页可移动性分组特性老是编译到内核中,但只有在系统中有足够内存能够分配到多个迁移类型对应的链表时,才会起做用。两个全局变量pageblock_order和pageblock_nr_pages提供每一个迁移链表对应的适当数量的内存。第一个表示内核认为是“大”的一个分配阶,pageblock_nr_pages则表示该分配阶对应的页数。若是体系结构提供了巨型页机制,则pageblock_order一般定义为巨型页对应的分配阶(IA-32巨型页长度是4MB),若是体系结构不支持巨型页,则将其定义为第二高的分配阶(MAX_ORDER-1)。若是各迁移类型的链表中没有一块较大的连续内存,那么页面迁移不会提供任何好处,所以在可用内存太少时内核会经过设置全局变量page_group_by_mobility为0关闭该特性(一旦停用了页面迁移特性,全部页都是不可移动的)。

在内存子系统初始化期间,memmap_init_zone负责处理内存域的page实例。它将全部的页最初都标记为可移动的,此时若是须要分配不可移动的内存,则必须“盗取”(见4分配API)。实际上,启动期间分配可移动内存区的状况较少,分配器有很高的概率分配长度最大的内存区,并将其从可移动列表转换到不可移动列表。因为分配的内存区长度是最大的,所以不会向可移动内存中引入碎片。这种作法避免了启动期间内核分配的内存(常常在系统的整个运行时间都不释放)散布到物理内存各处,从而使其余类型的内存分配免受碎片的干扰,这也是页可移动性分组框架的最重要的目标之一。

2)虚拟可移动内存域

依据可移动性组织页是防止物理内存碎片的一种可能方法,内核还提供了另外一种阻止该问题的手段:虚拟内存域ZONE_MOVABLE,其特性必须由管理员显示激活。

基本思想:可用的物理内存划分为两个内存域,一个用于可移动分配,一个用于不可移动分配。

kernelcore参数用来指定用于不可移动分配的内存数量(用于既不能回收也不能迁移的内存数量)。参数movablecore控制用于可移动内存分配的内存数量。若是同时指定两个参数,内核会按照必定的方法进行计算,取指定值与计算值中较大的一个。

ZONE_MOVABLE并不关联到任何硬件上有意义的内存范围,该内存域中的内存取自高端内存域或普通内存域,所以称虚拟内存域。

从物理内存域提取用于ZONE_MOVABLE的内存数量主要考虑如下两个因素:

  • 用于不可移动分配的内存会平均地分布到全部内存结点上;
  • 只使用来自最高内存域的内存(在内存较多的32位系统上,一般是ZONE_HIGHMEM,对于64位系统,使用ZONE_NORMAL或ZONE_DMA32)。

最后是计算结果,用于为虚拟内存域ZONE_MOVABLE提取内存页的物理内存域,保存在全局变量movable_zone中;对每一个结点来讲,zone_movable_pfn[node_id]表示ZONE_MOVABLE在movable_zone内存域中所取得内存的起始地址。

(虚拟内存域具体的实如今4分配API中)

三、初始化内存域和结点数据结构

在启动期间,各体系结构相关的代码须要确立系统中各内存域的页帧的边界(max_zone_pfn数组);肯定各结点页帧的分配状况(全局变量early_node_map)。

1)管理数据结构的建立

3概述了管理数据结构创建的过程。

3 管理数据结构构建过程示意图

 

4 free_area_init_nodes的代码流程图

free_area_init_nodes代码流程图如图4所示,完成如下工做:

  • 首先分析并改写特定于体系结构的代码提供的信息(对照在zone_max_pfn和zone_min_pfn中指定的内存域的边界,计算各个内存域可以使用的最低和最高的页帧编号);
  • 根据结点的第一个页帧start_pfn,对early_node_map中的各项进行排序;
  • 以[low, high]形式描述各个内存域的页帧区间,存储在对应的全局变量中;
  • 接下来构建其余内存域的页帧区间,方法很直接:第n个内存域的最小页帧,即前一个(第n-1个)内存域的最大页帧(当前内存域的最大页帧由max_zone_pfn给出);
  • 最后遍历全部活动结点,并分别对各个结点调用free_area_init_node创建数据结构。

2)对各个结点建立数据结构

在内存域边界已经肯定以后,free_area_init_nodes分别对各个内存域调用free_area_init_node建立数据结构。这涉及到几个辅助函数(见图4):

  • calculate_node_totalpages首先累计各个内存域的页数,计算结点中页的总数;
  • alloc_node_mem_map负责初始化一个简单但很是重要的数据结构(struct page);
  • free_area_init_core依次遍历结点的全部内存域,负责初始化内存域数据结构涉及的繁重工做(内存域的真实长度、系统中的页数、初始化zone结构中各个表头、将各个结构成员初始化为0)。

此时,空闲页的数目(nr_free)当前仍然规定为0,这显然没有反映真实状况。直至停用bootmem分配器、普通的伙伴分配器生效,才会设置正确的数值。

四、分配器API

伙伴系统接口对于NUMA和UMA体系结构没有差异,可是它只能分配2的整数幂个页(分配必须指定阶),内核中的细粒度分配只能借助于slab分配器(或者slub、slob分配器)。

  • alloc_pages(mask, order)分配2order页并返回一个struct page的实例,表示分配的内存块的起始页;
  • get_zeroed_page(mask)分配一页并返回一个page实例,页对应的内存填充0(全部其余函数,分配以后页的内容是未定义的);
  • __get_free_pages(mask, order)和__get_free_page(mask)的工做方式与上述函数相同,但返回分配内存块的虚拟地址,而不是page实例;
  • get_dma_pages(gfp_mask, order)用来得到适用于DMA的页。
  • 4个函数用于释放再也不使用的页:
  • free_page(struct page *)和free_pages(struct page *, order)用于将一个或2的order次幂的页返回给内存管理子系统,内存区的起始地址由指向该内存区的第一个page实例的指针表示;
  • __free_page(addr)和__free_pages(addr, order)的语义相似于前两个函数,但在表示须要释放的内存区时,使用了虚拟内存地址而不是page实例。

1)分配掩码

分配器API中的mask参数,称为掩码,它包含了图5所示的内容。 (GFP表示get free page)

5 GFP掩码布局

  • 内存域修饰符(最低4个比特位)用于指定从哪一个内存孕育分配所需的页;
  • 标志位在不限制从哪一个物理内存段分配内存的基础上,改变分配器的行为(好比查找空闲内存时的积极程度)。(具体含义及用法见源码及手册)

 (2)内存分配宏

经过使用标志、内存域修饰符和各个分配函数,内核提供了一种很是灵活的内存分配体系,全部接口函数均可以追溯到一个基本函数alloc_pages_node,如图6所示。

 

6 伙伴系统的各分配函数之间关系

  • 分配单页的函数alloc_page和__get_free_page是借助于宏定义的,alloc_pages也是一样;
  • get_zeroed_page的实现是对alloc_pages使用__GFP_ZERO标志,便可分配填充字节0的页;
  • __get_free_pages访问了alloc_pages,而alloc_pages又借助了alloc_pages_node。

相似地,内存释放函数也能够归约到一个主要的函数__free_pages,如图7所示(只是调用参数不一样)。

 

7 伙伴系统各内存释放函数之间关系

free_pages和__free_pages之间的关系经过函数而不是宏创建,由于首先必须将虚拟地址转换为指向struct page的指针。

五、分配页

内核源代码将__alloc_pages称之为“伙伴系统的心脏”,由于它处理的是实质性的内存分配。

1)选择页

内核定义了一些函数使用的标志,用于控制到达各水印指定的临界状态时的行为。

#define ALLOC_NO_WATERMARKS 0x01 /* 彻底不检查水印 */
#define ALLOC_WMARK_MIN 0x02 /* 使用pages_min水印 */
#define ALLOC_WMARK_LOW 0x04 /* 使用pages_low水印 */
#define ALLOC_WMARK_HIGH 0x08 /* 使用pages_high水印 */
#define ALLOC_HARDER 0x10 /* 试图更努力地分配,即放宽限制 */
#define ALLOC_HIGH 0x20 /* 设置了__GFP_HIGH */
#define ALLOC_CPUSET 0x40 /* 检查内存结点是否对应着指定的CPU集合 */

默认状况下(即没有因其余因素带来的压力而须要更多的内存),只有内存域包含页的数目至少为zone->pages_high时,才能分配页。这对应于ALLOC_WMARK_HIGH标志。若是要使用较低(zone->pages_low)或最低(zone->pages_min)设置,则必须相应地设置ALLOC_WMARK_MIN或ALLOC_WMARK_LOW。ALLOC_HARDER通知伙伴系统在急

需内存时放宽分配规则。在分配高端内存域的内存时,ALLOC_HIGH进一步放宽限制。最后,ALLOC_CPUSET告知内核,内存只能从当前进程容许运行的CPU相关联的内存结点分配,固然该选项只对NUMA系统有意义。

__alloc_pages是伙伴系统的主函数,函数比较复长,可用内存足够时必要工做很快完成,可用内存太少或逐渐用完时,函数就会变得比较复杂。

在最简单的情形中,分配空闲内存区只涉及调用一次get_page_from_freelist,而后返回所需数目的页(由标号got_pg处的代码处理)。

其余状况中,会进行屡次内存分配尝试:

  • 第一次内存分配尝试不会特别积极。若是在某个内存域中没法找到空闲内存,则意味着内存没剩下多少了,内核须要增长较多的工做量才能找到更多内存。内核再次遍历备用列表中的全部内存域,每次都会唤醒负责换出页的kswapd守护进程(任务见页面回收和页同步),此时,空闲内存能够经过缩减内核缓存和页面回收得到。
  • 此后,内核开始新的尝试,在内存域之一查找适当的内存块。这一次进行的搜索更为积极,对分配标志进行了调整,修改成一些在当前特定状况下更有可能分配成功的标志。同时,将水印下降到最小值。而后用修改的标志集,再一次调用get_page_from_freelist,试图得到所需的页。
  • 若是再次失败,若设置了PF_MEMALLOC或进程设置了TIF_MEMDIE标志,会再次调用get_page_from_freelist试图得到所需的页(彻底忽略水印);若没有设置PF_MEMALLOC,内核仍然还有一些选项能够尝试,进入一条低速路径,分配掩码中设置__GFP_WAIT标志,为使守护进程取得必定的进展,其余进程可能进入睡眠状态,而后使用辅助函数try_to_free_pages查找当前不急需的页,以便换出(若是须要分配多页,那么per-CPU缓存中的页也会被try_to_free_pages拿回到伙伴系统),最后内核再次调用get_page_from_freelist尝试分配内存。
  • 若是依然申请不到内存(会涉及到一些对VFS层的影响,此处不做介绍),内核只能放弃,并向用户返回NULL指针,并输出一条内存请求没法知足的警告消息。

2)移除选择的页

若是内核找到适当的内存域,具备足够的空闲页可供分配,那么还有两件事情须要完成。首先它必须检查这些页是不是连续的;其次,必须按伙伴系统的方式从free_lists移除这些页,这可能须要分解并重排内存区。

内核将工做委托给辅助函数buffered_rmqueue完成,其代码流程图如图8所示。

8 buffered_rmqueue代码流程图

首先,判断阶数,若为0,则表示只请求一页。此时,内核试图借助于per-CPU缓存加速请求的处理。若是缓存为空,内核可借机检查缓存填充水平。若是per-CPU缓存中没法找到适当的页,则向缓存添加一些符合当前要求迁移类型的页,而后从per-CPU列表移除一页,接下来进一步处理。

若不是0,则表示请求多页。内核调用__rmqueue(要求页连续)会从内存域的伙伴列表中选择适当的内存块。若有必要,该函数会自动分解大块内存,将未用的部分放回列表中。若分配失败,则会返回NULL指针。全部失败情形都跳转到标号failed处理,这能够确保内核到达当前点以后,page指向一系列有效的页。在返回指针以前,prep_new_page须要作一些准备工做,以便内核可以处理这些页(若是所选择的页出了问题,则该函数返回正值。在这种状况下,分配将从头从新开始)。

六、释放页

 __free_pages是一个基础函数,用于实现内核API中全部涉及内存释放的函数。其代码流程图如图9所示。

9 __free_pages代码流程图

__free_pages首先判断所需释放的内存是单页仍是较大的内存块?若是释放单页,则不还给伙伴系统,而是置于per-CPU缓存中,对极可能出如今CPU高速缓存的页,则放置到热页的列表中。出于该目的,内核提供了free_hot_page辅助函数,该函数只是做一下参数转换,接下来调用free_hot_cold_page。若是释放多个页,那么__free_pages将工做委托给__free_pages_ok,最后到__free_one_page。与其名称不一样,该函数不只处理单页的释放,也处理复合页释放。

七、内核中不连续页的分配

物理上连续的映射对内核是最优的,但不可能老是成功使用。对此,内核分配了其虚拟地址空间的一部分,用于创建连续映射。如图10所示,在IA-32系统中,紧随直接映射的前892 MiB物理内存,在插入的8 MiB安全隙以后,是一个用于管理不连续内存的区域。这一段具备线性地址空间的全部性质。经过修改负责该区域的内核页表,能够将其中的页映射到物理内存的任何地方。每一个vmalloc分配的子区域都是自包含的,与其余vmalloc子区域经过一个内存页分隔。相似于直接映射和vmalloc区域之间的边界,不一样vmalloc子区域之间的分隔也是为防止不正确的内存访问操做。

10 IA-32系统上内核的虚拟地址空间中的vmalloc区域

1)用vmalloc分配内存

vmalloc是一个接口函数,内核使用它来分配虚拟内存中连续但在物理内存中不必定连续的内存。

void *vmalloc(unsigned long size);

该函数只须要一个参数,用于指定所需内存区的长度(字节)。

内核对模块的实现中,有不少使用vmalloc的地方,由于函数可能在任什么时候候加载,若是模块数比较多,那么没法保证有足够的连续内存可用(尤为是系统已经运行了比较长时间的状况下)。由于用于vmalloc的内存页老是必须映射在内核地址空间中,所以使用ZONE_HIGHMEM内存域的页要优于其余内存域。这使得内核能够节省更宝贵的较低端内存域,而又不会带来额外的坏处。

vmalloc的代码流程图如图11所示。

11 vmalloc代码流程图

vmalloc的实现分为三个部分,首先,get_vm_area在vmalloc地址空间中找到一个适当的区域。接下来从物理内存分配各个页,最后将这些页连续地映射到vmalloc区域中,完成分配虚拟内存的工做。

2)备选映射方法

  • 除了vmalloc以外,还有其余方法能够建立虚拟连续映射:
  • vmalloc_32的工做方式与vmalloc相同,但会确保所使用的物理内存老是能够用普通32位指针寻址;
  • vmap使用一个page数组做为起点,来建立虚拟连续内存区;
  • 不一样于上述的全部映射方法,ioremap是一个特定于体系结构上的函数,它将取自物理地址空间、由系统总线用于I/O操做的一个内存块,映射到内核的地址空间中。

3)释放内存

有两个函数用于向内核释放内存,vfree用于释放vmalloc和vmalloc_32分配的区域,而vunmap用于释放由vmap或ioremap建立的映射。两个函数都会归结到__vunmap。其代码流程图如图12所示。

12 __vunmap代码流程图

  • __vunmap首先在__remove_vm_area(由remove_vm_area在完成锁定以后调用)中扫描该链表,以找到相关项;
  • 而后使用找到的vm_area实例,从页表删除再也不须要的项;
  • 若是__vunmap的参数deallocate_pages设置为1(在vfree中),内核会遍历指向所涉及的物理内存页的page实例的指针,而后对每一项调用__free_page,将页释放到伙伴系统;
  • 最后释放用于管理该内存区的内核数据结构。

八、内核映射

 

尽管vmalloc函数族可用于从高端内存域向内核映射页帧,但这并非这些函数的实际用途。内核提供了其余函数用于将ZONE_HIGHMEM页帧显式映射到内核空间。

1)持久内核映射

若是须要将高端页帧长期映射(做为持久映射)到内核地址空间中,必须使用kmap函数。须要映射的页用指向page的指针指定,做为该函数的参数。若是没有启用高端支持,该函数只须要返回页的地址;若是启用了高端支持,则相似于vmalloc,内核首先必须创建高端页和所映射到的地址之间的关联,在虚拟地址空间中分配一个区域以映射页帧,最后,内核必须记录该虚拟区域的哪些部分在使用中,哪些仍然是空闲的。

内核在IA-32平台上vmalloc区域以后分配了一个区域,从PKMAP_BASE到FIXADDR_START,该区域用于持久映射,不一样体系结构使用的方案是相似的。

pkmap_count是一容量为LAST_PKMAP的整数数组,其中每一个元素都对应于一个持久映射页。它其实是被映射页的一个使用计数器,0意味着相关的页没有使用,1有特殊语义,n表明内核中有n-1处使用该页(n≥2)。)

kmap映射的页,若是再也不须要,必须用kunmap解除映射。

2)临时内核映射

kmap函数不能用于中断处理程序,由于它可能进入睡眠状态(pkmap数组中没有空闲位置时)。内核提供了kmap_atomic,该函数执行是原子的,比普通的kmap快速,不能用于可能进入睡眠的代码,对于很快就须要一个临时页的简短代码是很是理想的。

kmap_atomic的定义在IA-3二、PPC、Sparc32上是特定于体系结构的,但这3种实现只有很是细微的差异,其原型是相同的。

void *kmap_atomic(struct page *page, enum km_type type)       //page是一个指向高端内存页的管理结构的指针,type定义了所需的映射类型

(内核的固定映射机制,使之能够在内核地址空间中访问用于创建原子映射的内存。能够在FIX_KMAP_BEGIN和FIX_KMAP_END之间创建一个用于映射高端内存页的区域,该区域位于fixed_addresses数组中,准确的位置须要根据当前活动的CPU和所需映射类型计算。)

在使用kmap_atomic时不会阻塞。若是发生阻塞,那么另外一个进程可能创建一样类型的映射,覆盖现存的项。

kunmap_atomic函数从虚拟内存解除一个现存的原子映射,该函数根据映射类型和虚拟地址,从页表删除对应的项。

3)没有高端内存的计算机上的映射函数

许多体系结构不须要支持高端内存(好比AMD64),为了避免须要老是区分高端内存和非高端内存体系结构,内核定义了几个在普通内存实现兼容函数的宏(在支持高端内存的计算机上,若是停用了高端内存,也会使用这些宏)。

 1 #ifdef CONFIG_HIGHMEM
 2 ...
 3 #else
 4 static inline void *kmap(struct page *page)
 5 {
 6     might_sleep();
 7     return page_address(page);
 8 }
 9 #define kunmap(page) do { (void) (page); } while (0)
10 #define kmap_atomic(page, idx) page_address(page)
11 #define kunmap_atomic(addr, idx) do { } while (0)
12 #endif

 

6、slab分配器

相似于C语言中的malloc,slab分配器提供小块内存,同时它也用做一个缓存,主要针对常常分配并释放的对象。slab分配器将释放内存块保存在一个内部列表中,并不立刻返回给伙伴系统,以便下一次高速的内存分配。这样内核没必要使用伙伴系统算法,处理时间会变短,同时该内存块仍然驻留在CPU告诉缓存的几率较高。

slab分配器有两大好处:

  • 调用伙伴系统的操做对系统的数据和指令高速缓存有至关的影响。内核越浪费这些资源,这些资源对用户空间进程就越不可用。更轻量级的slab分配器在可能的状况下减小了对伙伴系统的调用。
  • 若是数据存储在伙伴系统直接提供的页中,那么其地址老是出如今2的幂次的整数倍附近(许多将页划分为更小块的其余分配方法,也有一样的特征)。这对CPU高速缓存的利用有负面影响,因为这种地址分布,使得某些缓存行过分使用,而其余的则几乎为空。多处理器系统可能会加重这种不利状况,由于不一样的内存地址可能在不一样的总线上传输,上述状况会致使某些总线拥塞,而其余总线则几乎没有使用。经过slab着色(slab coloring),slab分配器可以均匀地分布对象,以实现均匀的缓存利用。

 一、备选分配器

在大型系统上仅slab的数据结构就须要不少GB内存。对嵌入式系统来讲,slab分配器代码量和复杂性都过高,所以诞生了slob分配器和slub分配器。

slob分配器进行了特别优化,以便减小代码量。它围绕一个简单的内存块链表展开,在分配内存时,使用了一样简单的最早适配算法(速度非最高效,不适用大型系统);

slub分配器经过将页帧打包为组,并经过struct page中未使用的字段来管理这些组,试图最小化所需的内存开销。

全部分配器的前端接口都是相同的。每一个分配器都实现了一组特定的函数,用于内存分配和缓存。

  • kmalloc、__kmalloc和kmalloc_node是通常的(特定于结点)内存分配函数;
  • kmem_cache_alloc、kmem_cache_alloc_node提供(特定于结点)特定类型的内核缓存。

13阐释了物理页帧、伙伴系统、通用分配器与通常内核代码接口关联。

13 伙伴系统、通用分配器与通常内核代码接口关联示意图

二、内核中的内存管理

内核中通常的内存分配和释放函数与C标准库中等价函数的名称相似,用法也几乎相同。

  • kmalloc(size, flags)分配长度为size字节的一个内存区,并返回指向该内存区起始处的一个void指针,若是没有足够内存,则结果为NULL指针;
  • kfree(*ptr)释放*ptr指向的内存区。

与用户空间程序设计相比,内核还包括percpu_alloc和percpu_free函数,用于为各个系统CPU分配和释放所需内存区。

全部活动缓存的列表保存在/proc/slabinfo中(终端输入cat /proc/slabinfo便可查看),包含用于标识各个缓存的字符串名称,缓存中活动对象的数量,缓存中对象的总数(已用和未用),所管理对象的长度(按字节计算),一个slab中对象的数量,每一个slab中页的数量,活动slab的数量,在内核决定向缓存分配更多内存时,所分配对象的数量。

三、slab分配的原理

slab分配器由一个紧密地交织的数据和内存结构的网络组。如图14所示,slab缓存由保存管理性数据的缓存对象和保存被管理对象的各个slab。

14 slab分配器各个部分

每一个缓存只负责一种对象类型,或提供通常性的缓冲区。各个缓存中slab的数目各有不一样,这与已经使用的页的数目、对象长度和被管理对象的数目有关。

系统中全部的缓存都保存在一个双链表中。这使得内核有机会依次遍历全部的缓存。

1)缓存的精细结构

15 slab缓存的精细结构

15描述了缓存各组成部分,除了管理性数据,缓存结构包括两个特别重要的成员:

  • 指向一个数组的指针,其中保存了各个CPU最后释放的对象;
  • 每一个内存结点都对应3个表头,用于组织slab的链表。第1个链表包含彻底用尽的slab,第2个是部分空闲的slab,第3个是空闲的slab。

缓存结构指向一个数组,其中包含了与系统CPU数目相同的数组项。每一个元素都是一个指针,指向一个进一步的结构称之为数组缓存(array cache),其中包含了对应于特定系统CPU的管理数据(就整体来看,不是用于缓存)。管理性数据以后的内存区包含了一个指针数组,各个数组项指向slab中未使用的对象。

为最好地利用CPU高速缓存,在分配和释放对象时,采用后进先出原理(LIFO,last in first out)。内核假定刚释放的对象仍然处于CPU高速缓存中,会尽快再次分配它。仅当per-CPU缓存为空时,才会用slab中的空闲对象从新填充它们。这样,对象分配的体系就造成了一个三级的层次结构(分配成本和操做对CPU高速缓存和TLB的负面影响逐级升高):

  • 仍然处于CPU高速缓存中的per-CPU对象;
  • 现存slab中未使用的对象;
  • 刚使用伙伴系统分配的新slab中未使用的对象。

2)slab精细结构

用于每一个对象的长度进行了舍入,以知足某些对齐方式的要求,对于对齐方案,有两种:

slab建立时使用标志SLAB_HWCACHE_ALIGN,slab用户能够要求对象按硬件缓存行对齐;

若是不要求按硬件缓存行对齐,那么内核保证对象按BYTES_PER_WORD对齐,该值是表示void指针所需字节的数目。

32位处理器上,void指针须要4个字节。所以,对有6个字节的对象,则须要8 = 2×4个字节,多余的字节称为填充字节。填充字节能够加速对slab中对象的访问,若是使用对齐的地址,那么在几乎全部的体系结构上,内存的访问都会更快。

slab的起始处是管理结构,保存了全部的管理数据(和用于链接缓存链表的链表元素)。其后面是一个数组,每一个(整数)数组项对应于slab中的一个对象(只有在对象没有分配时,相应的数组项才有意义)。此时,它指定了下一个空闲对象的索引。因为最低编号的空闲对象的编号还保存在slab起始处的管理结构中,内核无需使用链表或其余复杂的关联机制,便可找到当前可用的全部对象。数组的最后一项老是一个结束标记,值为BUFCTL_END。管理数组与slab对象的关系如图16所示。

16 slab中空闲对象的管理

管理数据能够放置在slab自身,也能够放置到使用kmalloc分配的不一样内存区中。内核的选择取决于slab的长度和已用对象的数量。

最后,内核经过对象自身(page结构的一个链表元素lru.next和lru.prev)识别slab(以及对象驻留的缓存)。根据对象的物理内存地址,找到相关的页,从而在全局mem_map数组中找到对应的page实例。

四、实现

 slab系统带有大量调试选项,代码中遍及着预处理语句:

  • 危险区(Red Zoning):在每一个对象的开始和结束处增长一个额外的内存区,其中填充已知的字节模式。若是模式被修改,程序员在分析内核内存时注意到,可能某些代码访问了不属于它们的内存区;
  • 对象毒化(Object Poisoning):在创建和释放slab时,将对象用预约义的模式填充。若是在对象分配时注意到该模式已经改变,此处已经发生了未受权访问等。

1)数据结构

每一个缓存由kmem_cache结构的一个实例表示。结构内容以下:

 1 struct kmem_cache {
 2 /* 1) per-CPU数据,在每次分配/释放期间都会访问 */
 3     struct array_cache *array[NR_CPUS];    //指向数组的指针,每一个数组项都对应于系统中的一个CPU。每一个数组项都包含了另外一个指针,指向下文讨论的array_cache结构的实例
 4 /* 2) 可调整的缓存参数。由cache_chain_mutex保护 */
 5     unsigned int batchcount;    //per-CPU列表为空时,从缓存的slab中获取对象的数目;还表示在缓存增加时分配的对象数目
 6     unsigned int limit;    //指定了per-CPU列表中保存的对象的最大数目
 7     unsigned int shared;
 8     unsigned int buffer_size;        //指定了缓存中管理的对象的长度
 9     u32 reciprocal_buffer_size;
10 /* 3) 后端每次分配和释放内存时都会访问 */
11     unsigned int flags; /* 常数标志 */
12     unsigned int num; /* 每一个slab中对象的数量 */
13 /* 4) 缓存的增加/缩减 */
14     unsigned int gfporder;    //指定了slab包含的页数目以2为底的对数
15 /* 强制的GFP标志,例如GFP_DMA */
16     gfp_t gfpflags;        
17     size_t colour;     //颜色的最大数目
18     unsigned int colour_off;     //着色偏移 
19     struct kmem_cache *slabp_cache;    //slab头部的管理数据存储在slab外部时,指向分配所需内存的通常性缓存; slab头部在slab上时,为NULL指针
20     unsigned int slab_size;
21     unsigned int dflags;     // 标志集合,描述slab的“动态性质”
22     void (*ctor)(struct kmem_cache *, void *);    //指向在对象建立时调用的构造函数
23 /* 5) 缓存建立/删除 */
24     const char *name;    //是一个字符串,包含该缓存的名称
25     struct list_head next;    //用于将kmem_cache的全部实例保存在全局链表cache_chain上
26 /* 6) 统计量 */
27 ...
28     struct kmem_list3 *nodelists[MAX_NUMNODES];
29 };

2)初始化

为初始化slab数据结构,内核须要若干小内存块(最适合由kmalloc分配),可是只有slab系统启用以后,才能使用kmalloc,于是内核借助了一些技巧。

kmem_cache_init函数用于初始化slab分配器。它在内核初始化阶段(start_kernel)、伙伴系统启用以后调用。第一步:kmem_cache_init建立系统中的第一个slab缓存,以便为kmem_cache的实例提供内存,内核使用的主要是在编译时建立的静态数据;第二步:kmem_cache_init接下来初始化通常性的缓存,用做kmalloc内存的来源(针对所需的各个缓存长度,分别调用kmem_cache_create);第三步:在kmem_cache_init的最后一步,把到如今为止一直使用的数据结构的全部静态实例化的成员,用kmalloc动态分配的版本替换。

3)建立缓存

建立新的slab缓存必须调用kmem_cache_create,这是一个冗长的过程,其代码示意图如图17所示。

17 kmem_cache_create的代码流程图

  • 首先,进行参数检查,以确保没有指定无效值,而后才执行第一个重要步骤,计算对齐所需填充字节数;
  • 接着在数据对齐值计算完毕后,分配struct kmem_cache一个实例(一个独立的slab缓存,名为cache_cache);
  • 而后肯定是否将slab头存储在slab之上,若是对象长度大于页帧的1/8,则将头部管理数据存储在slab以外,不然存储在slab上,随后,增长对象的长度size,直至对应到上文计算的对齐值;
  • 至此,对象长度定义完成,如下定义slab长度(选择适当的页数做为slab长度)。
  • 首先,内核经过calculate_slab_order进行迭代,找到理想的slab长度(基于给定对象长度,cache_estimate针对特定的页数,来计算对象数目、浪费的空间、着色所需的空间);
  • 接着计算颜色(即slab上的浪费空间除以颜色偏移量的商);
  • 而后经过enable_cpucache产生per-CPU缓存;
  • 最后将初始化过的kmem_cache实例添加到全局链表,表头为cache_chain。

4)分配对象

kmem_cache_alloc用于从特定的缓存获取对象,它须要用于获取对象的缓存,以及精确描述分配特征的标志变量两个参数,结果多是指向分配内存区的指针,也可能分配失败返回NULL。

18 kmem_cache_alloc的代码流程图

  • 首先,kmem_cache_alloc基于参数相同的内部函数__cache_alloc,后者能够直接调用(采用这种结构,目的是尽快合并kmalloc和kmem_cache_alloc的实现)。__cache_allloc只是一个前端函数,只执行了全部必要的锁定操做。实际工做委托给____cache_alloc进行;
  • 而后选择被缓存对象,若是在per-CPU缓存中有对象,则从缓存中获取对象后返回;若是没有对象在per-CPU缓存中,须要调用cache_alloc_refill从新填充缓存,内核先按必定的顺序扫描slab,若是找到空闲对象则返回,若是没有找到空闲对象,那么必须使用cache_grow扩大缓存(见下)。

5)缓存的增加

19描述了cache_grow代码流程图。

19 cache_grow的代码流程图

  • 首先计算颜色和偏移量,若是达到了颜色的最大数目,则内核从新开始从0计数,这自动致使零偏移;
  • 接着使用kmem_getpages辅助函数从伙伴系统逐页分配所需的内存空间;
  • 而后调用相关的alloc_slabmgmt函数分配所需空间;
  • 接下来,调用slab_map_pages建立slab的各页与slab或缓存之间的关联;
  • 随后cache_init_objs调用各个对象的构造器函数(假若有的话),初始化新slab中的对象;
  • 最后将彻底初始化的slab添加到缓存的slabs_free链表中。

6)释放对象

当一个分配的对象再也不须要时,使用kmem_cache_free将其返回给slab分配器。图20为kmem_cache_free代码流程图。

20 kmem_cache_free的代码流程图

当即调用__cache_free,根据per-CPU缓存的状态不一样,执行如下两种操做:

  • 若是per-CPU缓存中的对象数目低于容许的限制,则在其中存储一个指向缓存中对象的指针;
  • 不然,必须将一些对象(准确的数目由array_cache->batchcount给出)从缓存移回slab,从编号最低的数组元素开始:缓存的实现依据先进先出原理,这些对象在数组中已经很长时间,所以不太可能仍然驻留在CPU高速缓存中。此后,将slab从新插入到缓存的链表中,若是删除后,slab中全部对象都未使用,则置于slabs_free链表,若是同时包含使用和未使用对象,则插入slabs_partial链表。

7)销毁缓存

若是要销毁只包含未使用对象的一个缓存,则必须调用kmem_cache_destroy函数。该函数主要在删除模块时调用,此时须要将分配的内存都释放。主要步骤以下:

  • 依次扫描slabs_free链表上的slab。首先对每一个slab上的每一个对象调用析构器函数,而后将slab的内存空间返回给伙伴系统;
  • 释放用于per-CPU缓存的内存空间;
  • cache_cache链表移除相关数据。

五、通用缓存

若是不涉及对象缓存,而是传统意义上的分配/释放内存,则必须调用kmalloc和kfree函数。kmalloc和kfree实现为slab分配器的前端,其语义尽量地模仿C标准库malloc和free。

7、处理器高速缓存和TLB控制

内核提供了一些命令直接做用于处理器的高速缓存和TLB,用于维护缓存内容的一致性,确保不出现不正确和过期的缓存项。

不一样体系结构上,高速缓存和TLB的硬件实现不一样,所以内核须要创建TLB和高速缓存的视图,在其中考虑到各类不一样的硬件实现方法,兼顾各个体系结构的特定性质。

TLB的语义抽象是将虚拟地址转换为物理地址的一种机制;

内核将高速缓存视为经过虚拟地址快速访问数据的一种机制,该机制无需访问物理内存。数

据和指令高速缓存并不老是明确区分。

内核中各个特定于CPU的部分都必须提供下列函数(即便只是空操做),以便控制TLB和高速缓存:

  • flush_tlb_all和flush_cache_all刷出整个TLB/高速缓存;
  • flush_tlb_mm和flush_cache_mm刷出全部属于地址空间mm的TLB/高速缓存项;
  • flush_tlb_range和flush_cache_range刷出地址范围vma->vm_mm中虚拟地址start和end之间的全部TLB/高速缓存项;
  • flush_tlb_page和flush_cache_page刷出虚拟地址在[page, page + PAGE_SIZE]范围内全部的TLB/高速缓存项;
  • update_mmu_cache在处理页失效以后调用。它在处理器的内存管理单元MMU中加入信息,使得虚拟地址address由页表项pte描述。

内核对数据和指令高速缓存不做区分。若是须要区分,特定于处理器的代码可根据vm_area_struct->flags的VM_EXEC标志位是否设置,来肯定高速缓存包含的是指令仍是数据。

flush_cache_...和flush_tlb_...函数常常成对出现。

好比在使用fork复制进程地址空间时,操做的顺序是:刷出高速缓存、操做内存、刷出TLB,缘由有两个:

  • 若是顺序反过来,那么在TLB刷出以后、正确信息提供以前,多处理器系统中的另外一个CPU可能从进程的页表取得错误的信息。
  • 在刷出高速缓存时,某些体系结构须要依赖TLB中的“虚拟->物理”转换规则。flush_tlb_mm必须在flush_cache_mm以后执行,以确保这点。
相关文章
相关标签/搜索