内存管理(2): 物理内存管理

2       物理内存管理

2.1             概述

整个内存管理系统可以分为2部分来看待: 第一部分是对物理内存的管理, 第二部分是对虚拟内存的管理.

物理内存管理的对象是板载的物理内存(DDRAM, 它把物理内存按页划分, 并把这些页放到一个池子里面. 物理内存管理的目的就是提供API给到内存消费者, 消费者在需要内存的时候, 调用API向物理内存管理系统申请, 物理内存管理系统就从池子里面取出物理页帧给到消费者.

 

虚拟内存管理的对象是虚拟地址空间, 它的主要作用是分配虚拟页面, 并维护页表, 把虚拟页面与磁盘或者物理页帧关联起来. 细节我们在下一章详述.

 

本章主要关注物理内存管理系统, 我们可以从下述的几个角度来深入了解该系统:

         池子的内部细节, 也就是在这个池子内部, 如何描述物理页帧, 如何管理物理页帧. 我们把这一部分称之为“内存组织”

         如何向池子里面注水, 也就是如何把一个板载的DRAM划分为页帧, 并把这些页帧添加到池子中. 我们把这一部分称之为“初始化内存管理”

         如何向消费者提供获取内存的接口, 也就是如何分配物理内存. 这一部分称之为“物理内存分配”

2.2             池子--内存组织

2.2.1         概念介绍

我们先从最简单的情况开始介绍, 假设有一个单核CPU, 板载了一块DDRAM. DDRAM的大小为2GB, 地址是连续的.

 

这种情况下物理内存如何组织?

         我们已知物理内存会被划分为一个个page, 如果page_size4KB, 则总共就有512KBpages.

         这些pages又被划归到不同的区域(zone, 典型的区域类型有3: DMA内存域(ZONE_DMA, 低端内存域(ZONE_NORMAL, 高端内存域(ZONE_HIGHMEM.

         不同的区域隶属于某一个节点(node.

因此, 上述情形的内存组织形式就是:

Node

|-- ZONE_DMA

|-- Pages

|-- ZONE_NORMAL

|-- Pages

|-- ZONE_HIGHMEM

|-- Pages

Page的概念以及为何要引入Page这个概念我们在第一章已经介绍了, 主要目的是为了方便管理内存和地址转换.

Zone这个概念的引入原因也不难理解, 第一章我们提到过高端内存的概念, 内核的虚拟地址空间被划分为逻辑地址和虚拟地址, 逻辑地址一一映射到物理内存的低端区域ZONE_NORMAL, 虚拟地址动态映射到物理内存的高端区域ZONE_HIGHMEM. 在某些CPU体系架构中, DMA的寻址能力有限, 只能访问0-16M的地址空间, 因而物理地址在低端区域又划分出来了一个DMA区域(ZONE_DMA.

那为什么要引入Node这个概念呢?

 

想象这样一种情况, 假设单核CPU板载了2DDRAM, 每块的大小是128MB, 2DDRAM的地址不连续, 其中一个从0x20000000开始, 另一个从0x40000000开始.

这种情况下, 我们就必须用2node来描述, 每个Node下又有自己的ZonesPages.

Node1

|-- ZONE_DMA

|-- Pages

|-- ZONE_NORMAL

|-- Pages

|-- ZONE_HIGHMEM

|-- Pages

Node2

|-- ZONE_DMA

|-- Pages

|-- ZONE_NORMAL

|-- Pages

|-- ZONE_HIGHMEM

|-- Pages

 

除了上述物理地址空间存在大的洞的情况下需要用到多个node, 还有一种情况也需要用到多个node.

有一类计算机称之为NUMA计算机(非一致内存访问,non-uniform memory access. NUMA总是多处理器计算机. 系统的各个CPU都有本地内存,可支持特别快速的访问。各个处理器之间通过总线连接起来,以支持对其他CPU的本地内存的访问,当然比访问本地内存慢些。

这种情况下也需要用多个node来描述内存, 在为进程分配内存时,内核总是试图在当前运行的CPU相关联的NUMA结点上进行, 这样可以提高性能.

 

在当前节点分配内存虽然是最理想的情况,但这并不总是可行的,例如,该结点的内存可能已经用尽。对此类情况,每个结点都提供了一个备用列表(借助于struct zonelist )。该列表包含了其他结点的各内存域,可用于代替当前结点分配内存。列表项的位置越靠后,就越不适合分配

 

至此, 我们引入了如下几个概念:

Node

Zone & Zone_type

Page

Zonelist

接下来, 我们会从代码层面详细介绍这几个数据结构

2.2.2         主要数据结构

Nodepg_data_t

pg_data_t 是用于表示结点的基本元素, 定义如下.

 

头文件: include/linux/mmzone.h

typedef struct pglist_data {

    。。。。

}pg_data_t;

Comment

struct zone node_zones[MAX_NR_ZONES]

代表该节点下所有的内存域.

struct zone结构体代表一个内存域, 下文会详细介绍该数据结构

MAX_NR_ZONES是宏定义, 代表着一个节点下最多可能有多少个内存域

struct zonelist node_zonelists[MAX_ZONELISTS]

指定了备用结点内存域的列表,以便在当前结点没有可用空间时,在备用结点分配内存

int nr_zones

代表该节点下内存域的个数

struct page *node_mem_map

node_mem_map 是指向page 实例数组的指针,用于描述结点的所有物理内存页。它包含了结点中所有内存域的页

struct bootmem_data *bdata

在系统启动期间,内存管理子系统初始化之前,内核也需要使用内存(另外,还必须保留部分内存用于初始化内存管理子系统)。为解决这个问题,内核使用了后文讲解的自举内存分配器(boot memory allocatorbdata 指向自举内存分配器数据结构的实例

unsigned long node_start_pfn

node_start_pfn 是该结点第一个页帧的逻辑编号。系统中所有结点的页帧是依次编号的, 每个页帧的号码都是全局唯一的(不只是结点内唯一)

node_start_pfn UMA系统中总是0因为其中只有一个结点,因此其第一个页帧编号总是0

unsigned long node_present_pages

node_present_pages指定了结点中页帧的数目

unsigned long node_spanned_pages

node_spanned_pages 则给出了该结点以页帧为单位计算的长度。

二者的值不一定相同,因为结点中可能有一些空洞,并不对应真正

的页帧

int node_id

全局结点ID。系统中的NUMA结点都从0开始编号

struct pglist_data *pgdat_next

连接到下一个内存结点,系统中所有结点都通过单链表连接起来,其末尾通过空指针标记

wait_queue_head_t kswapd_wait

是交换守护进程(swap daemon)的等待队列,在将页帧换出结点时会用到(本文暂不详细讨论页面交换机制)

struct task_struct *kswapd

指向负责该结点的交换守护进程的task_struct

int kswapd_max_order

用于页交换子系统的实现,用来定义需要释放的区域的长度(我们当前不感兴趣)

node_states结点状态管理

如果系统中结点多于一个,内核会维护一个位图,用以提供各个结点的状态信息。状态是用位掩码指定的,可使用下列值:

 

头文件include/linux/nodemask.h

enum node_states

Comment

N_POSSIBLE

结点在某个时候可能变为联机

N_ONLINE

结点是联机的

N_NORMAL_MEMORY

结点有普通内存域

#ifdef CONFIG_HIGHMEM

   N_HIGH_MEMORY,

#else

  N_HIGH_MEMORY = N_NORMAL_MEMORY,

#endif

结点有普通或高端内存域

N_CPU

结点有一个或多个CPU

NR_NODE_STATES

 

状态N_POSSIBLE N_ONLINE N_CPU 用于CPU和内存的热插拔,在本书中不考虑这些特性。对内存管理有必要的标志是N_HIGH_MEMORY N_NORMAL_MEMORY 。如果结点有普通或高端内存则使N_HIGH_MEMORY ,仅当结点没有高端内存才设置N_NORMAL_MEMORY.

 

两个辅助函数用来设置或清除位域或特定结点中的一个比特位:

头文件include/linux/nodemask.h

void node_set_state(int node, enum node_states state)

void node_clear_state(int node, enum node_states state)

 

此外,for_each_node_state(__node, __state) 用来迭代处于特定状态的所有结点,for_each_online_node(node) 则迭代所有活动结点。

 

如果内核编译为只支持单个结点(即使用平坦内存模型),则没有结点位图,上述操作该位图的函数则变为空操作

Zonestruct zone

内核使用zone 结构来描述内存域。其定义如下:

 

头文件: include/linux/mmzone.h

struct zone

Comment

unsigned longwatermark[NR_WMARK]

页换出时使用的“水印”。这是一个数组,有3个元素:

watermark[WMARK_MIN]watermark[WMARK_LOW]watermark[WMARK_HIGH]

 

如果内存不足,内核可以将页写到硬盘。这3个成员会影响交换守护进程的行为:

         如果空闲页多于watermark[WMARK_HIGH],则内存域的状态是理想的

         如果空闲页的数目低于watermark[WMARK_LOW],则内核开始将页换出到硬盘

         如果空闲页的数目低于watermark[WMARK_MIN],那么页回收工作的压力就比较大,因为内存域中急需空闲页。《深入Linux内核架构18会讨论内核用于缓解此情形的各种方法

 

本小节后面会有一篇短文专门介绍这3个水印的初值是如何得来的

unsigned long lowmem_reserve[MAX_NR_ZONES]

lowmem_reserve 数组分别为各种内存域指定了若干页,用于一些无论如何都不能失败的关键性内存分配。各个内存域的份额根据重要性确定

struct pglist_data  *zone_pgdat

内存域和父结点之间的关联由zone_pgdat 建立,zone_pgdat 指向对应的pglist_data 实例

#ifndef CONFIG_SPARSEMEM

unsigned long*pageblock_flags;

#endif /* CONFIG_SPARSEMEM */

/*

* Flags for a pageblock_nr_pages block. See pageblock-flags.h.

* In SPARSEMEM, this map is stored in struct mem_section

*/

关于该参数的意义, 详见2.4.3《辅助函数与变量》

struct per_cpu_pageset __percpu *pageset

pageset 是一个数组,用于实现每个CPU的热/冷页帧列表。内核使用这些列表来保存可用于满足实现的“新鲜”页。但冷热页帧对应的高速缓存状态不同:有些页帧也很可能仍然在高速缓存中,因此可以快速访问,故称之为热的;未缓存的页帧与此相对,故称之为冷的。

本文暂不打算详细讨论热/冷页机制, 关于struct per_cpu_pageset数据结构细节, 有兴趣可以阅读《深入Linux内核架构3.2.2 数据结构--冷热页

unsigned long  zone_start_pfn

/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */

zone_start_pfn 是内存域第一个页帧的索引

unsigned long  spanned_pages

spanned_pages is the total pages spanned by the zone, includingholes, which is calculated as:

spanned_pages = zone_end_pfn - zone_start_pfn;

unsigned long  present_pages

present_pages is physical pages existing within the zone, whichis calculated as:

present_pages = spanned_pages - absent_pages(pages in holes);

unsigned  long managed_pages

managed_pages is present pages managed by the buddy system, whichis calculated as (reserved_pages includes pages allocated by thebootmem allocator):

managed_pages = present_pages - reserved_pages;

const  char *name;

name 是一个字符串,保存该内存域的惯用名称。目前有3个选项可用:Normal DMA HighMem

seqlock_t      span_seqlock

详情查看源代码中的注释

* Locking rules:

……..(这里介绍了锁使用的细节)

wait_queue_head_t   *wait_table

3个变量实现了一个等待队列,可供等待某一页变为可用的进程使用。该机制的细节本文不讨论,直观的概念是很好理解的:进程排成一个队列,等待某些条件。在条件变为真时,内核会通知进程恢复工作

unsigned long wait_table_hash_nr_entries

unsigned long wait_table_bits

ZONE_PADDING(_pad1_)

该结构比较特殊的方面是它由ZONE_PADDING 分隔为几个部分。这是因为对zone 结构的访问非常频繁。在多处理器系统上,通常会有不同的CPU试图同时访问结构成员。因此使用锁机制防止它们彼此干扰,避免错误和不一致。由于内核对该结构的访问非常频繁,因此会经常性地获取该结构的两个自旋锁zone->lock zone->lru_lock

如果数据保存在CPU高速缓存中,那么会处理得更快速。高速缓存分为行,每一行负责不同的内存区。内核使用ZONE_PADDING 宏生成“填充”字段添加到结构中,以确保每个自旋锁都处于自身的缓存行中。还使用了编译器关键字__cacheline_maxaligned_in_smp ,用以实现最优的高速缓存对齐方式。

该结构的第一部分和最后一部分也通过填充字段彼此分隔开来。两者都不包含锁,主要目的是将数据保持在一个缓存行中,便于快速访问,从而无需从内存加载数据(与CPU高速缓存相比,内存比较慢)。由于填充造成结构长度的增加是可以忽略的,特别是在内核内存中zone 结构的实例相对很少。

struct free_area        free_area[MAX_ORDER]

free_area 是同名数据结构的数组,用于实现伙伴系统。每个数组元素都表示某种固定长度的一些连续内存区。对于包含在每个区域中的空闲内存页的管理,free_area 是一个起点。

此处使用的数据结构自身就很值得讨论一番,后文会专门介绍伙伴系统的实现细节到时会仔细研究此结构

unsigned long           flags

flags 描述内存域的当前状态。允许使用下列标志(标志的意义参见源码注释)

<mmzone.h>

enum zone_flags {

        ZONE_RECLAIM_LOCKED,

        ZONE_OOM_LOCKED,

        ZONE_CONGESTED,

        ZONE_DIRTY,

        ZONE_WRITEBACK,

        ZONE_FAIR_DEPLETED,

};

也有可能这些标志均未设置。这是内存域的正常状态。

 

内核提供了3个辅助函数用于测试和设置内存域的标志:

<mmzone.h>

void zone_set_flag(struct zone *zone, zone_flags_t flag)

int zone_test_and_set_flag(struct zone *zone, zone_flags_t flag)

void zone_clear_flag(struct zone *zone, zone_flags_t flag)

spinlock_t              lock

详情查看源代码中的注释

* Locking rules:

……..(这里介绍了锁使用的细节)

ZONE_PADDING(_pad2_)

同上ZONE_PADDING(_pad1_)

/* Write-intensive fields used by page reclaim */

spinlock_t        lru_lock

这些标志都跟页面回收和页面交换机制相关, 本文不讨论

详情见《深入Linux内核架构18章》

 

struct lruvec      lruvec

atomic_long_t     inactive_age

unsigned long percpu_drift_mark

compact_xxx

……

ZONE_PADDING(_pad3_)

同上ZONE_PADDING(_pad1_)

atomic_long_t           vm_stat[NR_VM_ZONE_STAT_ITEMS]

vm_stat 维护了大量有关该内存域的统计信息。由于其中维护的大部分信息目前没有多大意义,对该结构的详细讨论请参见《深入Linux内核架构17.7.1。现在,只要知道内核中很多地方都会更新其中的信息即可。辅助函数zone_page_state 用来读取vm_stat 中的信息:

<vmstat.h>

static inline unsigned long zone_page_state(struct zone *zone, enum zone_stat_item item)

例如,可以将item 参数设置为NR_FREE_PAGES 则可以获得内存域中空闲页的数目; 设置为NR_xxx又可以查看其它的信息

     

Zone typezone_type

根据第一章介绍的背景知识, 内存域可划分为多种不同的类型, 定义如下:

 

头文件:include/linux/mmzone.h

enum zone_type

Comment

#ifdef CONFIG_ZONE_DMA

ZONE_DMA,

#endif

ZONE_DMA is used when there are devices that are not ableto do DMA to all of addressable memory (ZONE_NORMAL). Then wecarve out the portion of memory that is needed for these devices.

The range is arch specific.

 

Some examples:

Architecture         Limit

parisc, ia64, sparc    <4G

s390               <2G

arm                Various

i386, x86_64 and multiple other arches

                    <16M

#ifdef CONFIG_ZONE_DMA32

ZONE_DMA32,

#endif

x86_64 needs two ZONE_DMAs because it supports devices that areonly able to do DMA to the lower 16M but also 32 bit devices thatcan only do DMA areas below 4G.

ZONE_NORMAL,

Normal addressable memory is in ZONE_NORMAL. DMA operations can beperformed on pages in ZONE_NORMAL if the DMA devices supporttransfers to all addressable memory.

#ifdef CONFIG_HIGHMEM

ZONE_HIGHMEM,

#endif

ZONE_HIGHMEM 标记了超出内核可直接映射的物理内存

A memory area that is only addressable by the kernel throughmapping portions into its own address space. This is for exampleused by i386 to allow the kernel to address the memory beyond900MB. The kernel will set up special mappings (pagetable entries on i386) for each page that the kernel needs toaccess.

ZONE_MOVABLE,

内核定义了一个伪内存域ZONE_MOVABLE 在防止物理内存碎片的机制中需要使用该内存域。

后文会更仔细地讲解该机制。

__MAX_NR_ZONES

充当结束标记,在内核想要迭代系统中的所有内存域时,会用到该常量。

注意, 根据编译时的配置,可能无需考虑某些内存域。例如在64位系统中,并不需要高端内存域。如果支持了只能访问4 GiB以下内存的32位外设,才需要DMA32 内存域。

Page

页帧代表系统内存的最小单位,对内存中的每个页都会创建struct page 的一个实例。内核程序员需要注意保持该结构尽可能小,因为即使在中等程度的内存配置下,系统的内存同样会分解为大量的页。例如,IA-32系统的标准页长度为4 KiB,在主内存大小为384 MiB时,大约共有100 000页。就当今的标准而言,这个容量算不上很大,但页的数目已经非常可观。

 

这也是为什么内核尽力保持struct page 尽可能小的原因。在典型系统中,由于页的数目巨大,因此对page 结构的小改动,也可能导致保存所有page 实例所需的物理内存暴涨。

 

页的广泛使用,增加了保持结构长度的难度:内存管理的许多部分都使用页,用于各种不同的用途。内核的一个部分可能完全依赖于struct page 提供的特定信息,而该信息对内核的另一部分可能完全无用,该部分依赖于struct page 提供的其他信息,而这部分信息对内核的其他部分也可能是完全无用的,等等。

C语言的联合很适合于该问题,尽管它未能增加struct page 的清晰程度。考虑一个例子:一个物理内存页能够通过多个地方的不同页表映射到虚拟地址空间,内核想要跟踪有多少地方映射了该页。为此,struct page 中有一个计数器用于计算映射的数目。如果一页用于slub分配器(将整页细分为更小部分的一种方法,请参见下文关于slub的介绍,那么可以确保只有内核会使用该页,而不会有其他地方使用,因此映射计数信息就是多余的。因此内核可以重新解释该字段,用来表示该页被细分为多少个小的内存对象使用。在数据结构定义中,这种双重解释如下所示:

struct page {

...

union {

atomic_t _mapcount; /* 内存管理子系统中映射的页表项计数,

                       * 用于表示页是否已经映射,还用于限制逆向映射搜索。

                       */

unsigned int inuse; /* 用于SLUB分配器:对象的数目*/

};

...

}

 

page 的定义

 

头文件: include/linux/mm_types.h

该结构的格式是体系结构无关的,不依赖于使用的CPU类型,每个页帧都由该结构描述。

struct page

Comment

/* First double word block */

 

unsigned long flags;

flags 存储了体系结构无关的标志,用于描述页的属性.

 

页的不同属性通过一系列页标志描述,存储为struct page flags 成员中的各个比特位。这些标志独立于使用的体系结构,因而无法提供特定于CPU或计算机的信息(该信息保存在页表中,见下文可知)

 

各个标志是由include/linux/page-flags.h中的宏定义的,此外还生成了一些宏,用于标志的设置、删除、查询。这样做时,内核遵守了一种通用的命名方案(利用C语言的连接符##来自动构建这些宏, 细节请阅读代码, 例如<TESTPAGEFLAG>用于构建查询宏).

 

这些宏的实现是原子的。尽管其中一些由若干语句组成,但使用了特殊的处理器命令,确保其行为如同单一的语句。即这些语句是无法中断的,否则会导致竞态条件。

 

有哪些页标志可用?page-flags.h中的enum pageflags

列出了所有的标志.

 (这里暂不对每个标志的含义作细节描述, 后面有需要在来补充, 有兴趣可以阅读《深入Linux内核架构》第122, 3.2.2)

union {

struct address_space *mapping;

void *s_mem;

}

mapping很重要, 也比较复杂, 根据其最低位的不同:

         如果最低位为0,则指向inode address_space (代表某个文件的地址空间, 简单理解就是如何从这个文件读取文件数据), 或者为null.

         如果最低位为1, 则指向anon_vma对象, 代表它是一个匿名物理页

mapping的具体作用现在还理解不了, 等后面理解了在来完善吧.

 

s_mem : slab first object (现在不是很理解)

/* Second double word */

 

struct {

union {

pgoff_t index;

void *freelist;

};

index: 暂时不太理解

 

freelist : 如果此页用于slab分配器, 那么freelist指向slab缓存的头部管理数据

union {

#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && \

defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE)

/* Used for cmpxchg_double in slub */

unsigned long counters;

#else

unsigned counters;

#endif

细节暂不太理解

struct {

union {

atomic_t _mapcount;

 

struct { /* SLUB */

unsigned inuse:16;

unsigned objects:15;

unsigned frozen:1;

};

int units;/* SLOB */

};

atomic_t _count;

};

细节暂不太理解

unsigned int active;/* SLAB */

};

细节暂不太理解

};

这个超长的struct结构体终于结束了

Third double word block

 

union {

……

}

又是一个超长的union, 反正现在也理解不了, 暂时就不把细节贴出来了, 后面理解了在来细化

/* Remainder is not double word aligned */

 

union {

unsigned long private;

#if USE_SPLIT_PTE_PTLOCKS

#if ALLOC_SPLIT_PTLOCKS

spinlock_t *ptl;

#else

spinlock_t ptl;

#endif

#endif

struct kmem_cache *slab_cache;

};

private: 是一个指向“私有”数据的指针,虚拟内存管理会忽略该数据。根据页的用途,可以用不同的方式使用该指针。大多数情况下它用于将页与数据缓冲区关联起来,本文不细述

 

ptl : 不理解

 

slab_cache: 用于SLUB分配器:指向slab的指针

slab_cache与前述的freelist inuse 成员用于slub分配器。我们不需要关注这些成员的具体布局,如果内核编译没有启用slub分配器支持,则不会使用这些成员

#ifdef CONFIG_MEMCG

struct mem_cgroup *mem_cgroup;

#endif

不理解

#if defined(WANT_PAGE_VIRTUAL)

void *virtual;

#endif

virtual 用于高端内存区域中的页,换言之,即无法直接映射到内核内存中的页。virtual 用于存储该页的虚拟地址。如果没有映射则为NULL

 

按照预处理器语句#if defined(WANT_PAGE_VIRTUAL) ,只有定义了对应的宏,virtual 才能成为struct page 的一部分。当前只有几个体系结构是这样,即摩托罗拉m68kFRVExtensa

所有其他体系结构都采用了一种不同的方案来寻址虚拟内存页。其核心是用来查找所有高端内存页帧的散列表。2.4《内核映射》会更详细地研究该技术。处理散列表需要一些数学运算,在前述的计算机上比较慢,因此只能选择这种直接的方法。

#ifdef CONFIG_KMEMCHECK

void *shadow;

#endif

kmemcheck wants to track the status of each byte in a page; this is a pointer to such a status block. NULL if not tracked.

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS

int _last_cpupid;

#endif

不理解

Zonelist

pg_data_t结构体中, 有这样一个元素:

typedefstruct pglist_data {

    ...

    struct zonelist node_zonelists[MAX_ZONELISTS];

    ...

} pg_data_t;

node_zonelists指定了备用结点内存域的列表,以便在当前结点没有可用空间时,在备用结点分配内存

 

zonelist这个结构体时如何定义的呢?

头文件在: include/linux/mmzone.h

 

我们直接贴出代码

MAX_ZONELISTS的定义:

/*

* The NUMA zonelists are doubled because we need zonelists that restrict the

* allocations to a single node for __GFP_THISNODE.

*

* [0]  : Zonelist with fallback

* [1]  : No fallback (__GFP_THISNODE)

*/

#define MAX_ZONELISTS 2

#else

#define MAX_ZONELISTS 1

#endif

 

zonelist的定义

/* Maximum number of zones on a zonelist */

#define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES)

 

#ifdef CONFIG_NUMA

 

/*

* This struct contains information about a zone in a zonelist. It is stored

* here to avoid dereferences into large structures and lookups of tables

*/

struct zoneref {

    struct zone *zone;  /* Pointer to actual zone */

    int zone_idx;       /* zone_idx(zoneref->zone) */

};

 

/*

* One allocation request operates on a zonelist. A zonelist

* is a list of zones, the first one is the 'goal' of the

* allocation, the other zones are fallback zones, in decreasing

* priority.

*

* To speed the reading of the zonelist, the zonerefs contain the zone index

* of the entry being read. Helper functions to access information given

* a struct zoneref are

*

* zonelist_zone()  - Return the struct zone * for an entry in _zonerefs

* zonelist_zone_idx()  - Return the index of the zone for an entry

* zonelist_node_idx()  - Return the index of the node for an entry

*/

struct zonelist {

    struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST +1];

};

一个zonelist结构体代表一个备用内存域列表, 由于该备用列表必须包括所有结点的所有内存域,因此由MAX_NUMNODES * MAX_NZ_ZONES 项组成,外加一个用于标记列表结束的空指针。

 

备用列表的创建必须慎重考虑, 必须让最“廉价”的节点位于列表的最前面, 随而次之, “廉价”的意思就是访问起来开销最小. 因为在当前结点没有可用空间时, 系统会从备用列表的第一个元素开始选择, “廉价”的在最前面, 就能保证系统的最佳性能.

 

备用列表的创建委托给build_zonelists函数, 该函数在初始化内存管理系统时会自动被调用.

由于备用列表与驱动开发的关系不太大, 它更像是一个操作系统策略层面的东西, 因此本文暂不详述.

内存域水印的计算(min_free_kbytes

在计算各种水印之前,内核首先确定需要为关键性分配保留的内存空间的最小值。该值随可用内存的大小而非线性增长,并保存在全局变量min_free_kbytes 中。下图概述了这种非线性比例关系,其中主图的横轴采用了对数坐标,插图的横轴采用的是普通坐标,插图放大了总内存容量在04 GiB之间的变化曲线。下表给出了一些典型值,主要适用于配备了适量内存的桌面系统,用来给读者提供一点感性认识。一个不变的约束是,不能少于128 KiB,也不能多于64 MiB。但要注意,只有内存数量确实比较大的时候,才能达到上界。

用户层可通过文件/proc/sys/vm/min_free_kbytes 来读取和修改该设置。

 

水印计算-代码流程(init_per_zone_wmark_min)

struct zone结构体的各个字段的填充由init_per_zone_wmark_minmm/page_alloc.c)处理,该函数由内核在启动期间调用,无需显式调用.

 

init_per_zone_wmark_min会进一步调用setup_per_zone_wmarkssetup_per_zone_lowmem_reserve.

         setup_per_zone_wmarks主要负责填充zone->watemark字段

         setup_per_zone_lowmem_reserve主要负责填充zone->lowmem_reserve. 内核迭代系统的所有结点,对每个结点的各个内存域分别计算预留内存最小值,具体的算法是将内存域中页帧的总数除以sysctl_lowmem_reserve_ratio[zone] 。除数的默认设置对低端内存域是256,对高端内存域是32

2.3             初始化内存管理

初始化内存管理系统的关键pg_data_t 数据结构的初始化(及其下级的结构).

2.3.1         背景知识

NODE_DATA

所有平台上都实现了特定于体系结构的NODE_DATA 宏,用于通过结点编号,来查询与一个NUMA结点相关的pgdata_t实例。

 

NODE_DATA 宏的定义如下:

头文件:  include/linux/mmzone.h

#ifndef CONFIG_NEED_MULTIPLE_NODES

 

externstruct pglist_data contig_page_data;

#define NODE_DATA(nid)      (&contig_page_data)

#define NODE_MEM_MAP(nid)   mem_map

 

#else /* CONFIG_NEED_MULTIPLE_NODES */

 

#include <asm/mmzone.h>

 

#endif /* !CONFIG_NEED_MULTIPLE_NODES */

 

NODE_DATA的实现分为单节点和多节点两种情况. 如果定义了CONFIG_NEED_MULTIPLE_NODES则代表系统中有多个节点, 否则系统中就只有一个节点.

 

如果系统有多个节点, NODE_DATA的实现依赖于具体的体系架构, <asm/mmzone.h>中定义. 由于大多数系统都只有一个节点, 因此我们暂不关注多节点的细节.

 

如果系统只有一个节点, NODE_DATA的定义为#define NODE_DATA(nid)      (&contig_page_data),此时nid一般是0. 它直接获取contig_page_data的地址.

 

contig_page_datamm/bootmem.c中定义的一个pg_data_t 实例, 管理所有的系统内存.

#ifndef CONFIG_NEED_MULTIPLE_NODES

struct pglist_data __refdata contig_page_data ={

    .bdata =&bootmem_node_data[0]

};

EXPORT_SYMBOL(contig_page_data);

#endif

内核代码在内存中的布局

在讨论各个具体的内存初始化操作之前,我们需要弄清楚,在启动装载程序将内核复制到内存,而初始化例程的汇编程序部分也已经执行完毕后,此时内存中的具体布局。我专注于默认情况,内核被装载到物理内存中的一个固定位置,该位置在编译时确定。

 

如果启用了故障转储机制,那么也可以配置内核二进制代码在物理内存中的初始位置。此外,一些嵌入式系统也需要这种能力。配置选项PHYSICAL_START 用于确定内核在内存中的位置,会受到配置选项PHYSICAL_ALIGN 设置的物理对齐方式的影响。

 

此外,内核可以连编为可重定位二进制程序,在这种情况下完全忽略编译时给定的物理起始地址。启动装载程序可以判断将内核放置到何处。这种情况我们遇到的比较多,详细的阐述将放在《Linux内核启动过程》一文中介绍。

 

下图给出物理内存最低几兆字节的布局,以及内核映像的各个部分在其中的驻留情况。

该图给出了物理内存的前几兆字节,具体的长度依赖于内核二进制文件的长度。前4 KiB是第一个页帧,一般会忽略,因为通常保留给BIOS使用。接下来的640 KiB原则上是可用的,但也不用于内核加载。其原因是,该区域之后紧邻的区域由系统保留,用于映射各种ROM(通常是系统BIOS和显卡ROM)。不可能向映射ROM的区域写入数据。但内核总是会装载到一个连续的内存区中,如果要从4 KiB处作为起始位置来装载内核映像,则要求内核必须小于640 KiB

为解决这些问题,内核一般使用0x100000 作为起始地址。这对应于内存中第二兆字节的开始处。从此处开始,有足够的连续内存区,可容纳整个内核。(在ARM体系架构中, 内核一般使用0x8000<32KB>作为起始地址, 0 32KB预留, 其中16KB-32KB用于存放初始的内核页表swapper_pg_dir

 

内核占据的内存分为几个段,其边界保存在变量中。

         _text _etext 是代码段的起始和结束地址,包含了编译后的内核代码。

         数据段位于_etext _edata 之间,保存了大部分内核变量。

         初始化数据在内核启动过程结束后不再需要(例如,包含初始化为0的所有静态全局变量的BSS段)保存在最后一段,从_edata _end 。在内核初始化完成后,其中的大部分数据都可以从内存删除,给应用程序留出更多空间。这一段内存区划分为更小的子区间,以控制哪些可以删除,哪些不能删除,但这对于我们现在的讨论没多大意义。

 

虽然用来划定段边界的变量定义在内核源代码(arch/$(SRCARCH)/kernel/setup.c )中,但此时尚未赋值。这是因为不太可能。编译器在编译时间怎么能知道内核最终有多大?只有在目标文件链接完成后,才能知道确切的数值,接下来则打包为二进制文件。该操作是由arch/$(SRCARCH)/kernel/vmlinux.ld.S 控制的(对ARM来说,该文件是arch/arm/kernel/vmlinux.ld.S ,其中也划定了内核的内存布局。

准确的数值依内核配置而异,因为每种配置的代码段和数据段长度都不相同,这取决于启用和禁用了内核的哪些部分。只有起始地址(_text )总是相同的。

 

每次编译内核时,都生成一个文件System.map 并保存在源代码目录下。除了所有其他(全局)变量、内核定义的函数和例程的地址,该文件还包括如下给出的常数的值。

wolfgang@meitner> cat System.map

...

c0100000 A _text

...

c0381ecd A _etext

...

c04704e0 A _edata

...

c04c3f44 A _end

...

上述所有地址值都偏移了0xC0000000 ,这是在用户和内核地址空间之间采用标准的3 : 1划分时,内核段的起始地址。该地址是虚拟地址,因为物理内存映射到虚拟地址空间的时候,采用了从该地址开始的线性映射方式。减去0xC0000000 ,则可获得对应的物理地址。

 

/proc/iomem 也提供了有关物理内存划分出的各个段的一些信息。

wolfgang@meitner> cat /proc/iomem

00000000-0009e7ff : System RAM

0009e800-0009ffff : reserved

000a0000-000bffff : Video RAM area

000c0000-000c7fff : Video ROM

000f0000-000fffff : System ROM

00100000-17ceffff : System RAM

00100000-00381ecc : Kernel code

00381ecd-004704df : Kernel data

...

内核映像从第一兆字节之后开始0x00100000 代码段的长度大约为2.5 MiB数据段大约0.9 MiB

 

AMD64系统上也可以获得类似的信息。这里内核在第一个页帧之后2 MiB开始,物理内存映射到虚拟地址空间中从0xffffffff80000000 开始。System.map 中相关的项如下所示:

wolfgang@meitner> cat System.map

ffffffff80200000 A _text

...

ffffffff8041fc6f A _etext

...

ffffffff8056c060 A _edata

...

ffffffff8077548c A _end

在运行时,也可以从/proc/iomem 获得内核的相关信息:

root@meitner # cat/proc/iomem

...

00100000-cff7ffff : System RAM

00200000-0041fc6e : Kernel code

0041fc6f-0056c05f : Kernel data

006b6000-0077548b : Kernel bss

...

memblock

2.3节开篇我们就提到过, 内存管理子系统初始化的关键就是pg_data_t 数据结构的初始化.

2.6.19之前的内核版本必须根据不同的体系结构来自行建立所需的数据结构. 随着内核的进化, 在最新版本的内核中, 大部分的初始化工作都被独立出来了, 与体系架构无关了.

体系架构相关的代码只需要把所有活动内存区添加到一个池子里面, 内核的通用代码则据此生成主数据结构.

 

这个池子就是memblock, 池子的基本元素是一个个活动内存区, 称之为memory block.

 

一个memory block就代表一块物理内存, 这个“块”即可以是物理上的概念, 也可以是逻辑上的概念.所谓物理上的概念就比如我们板载了2块物理内存, 起始地址不一样, 大小都为1GB, 就这就是2个内存块; 所谓逻辑上的概念就是我们逻辑上把一块物理内存划分为多个block, 每个block都有起始地址都长度.

不管是物理上的还是逻辑上的, 对我们来说没有区别. 本质上来讲, 只要给定起始物理地址和长度, 就给定了一个memory block.

需要注意的是, 一个memory block内部不能有空洞.

 

主要的数据结构

 

struct memblock_region : 我们用该结构体来抽象一个memory block.

头文件: include/linux/memblock.h

struct memblock_region {

    phys_addr_t base;

    phys_addr_t size;

    unsignedlong flags;

#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP

    int nid;

#endif

};

 

struct memblock_type : 多个memory block合在一起, 就叫做memblock_type.

头文件: include/linux/memblock.h

struct memblock_type {

    unsignedlong cnt;  /* number of regions */

    unsignedlong max;  /* size of the allocated array */

    phys_addr_t total_size;/* size of all regions */

    struct memblock_region *regions;

};

 

struct memblock : 系统中可能有多个memblock_type, 合在一起, 就叫做memblock. 内核代码规定了3种类型的memblock_type, 分别是: memoryreservedphysmem

头文件: include/linux/memblock.h

struct memblock {

    bool bottom_up;  /* is bottom up direction? */

    phys_addr_t current_limit;

    struct memblock_type memory;

    struct memblock_type reserved;

#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP

    struct memblock_type physmem;

#endif

};

         memory这种memblock_type是用于添加内存块的, 每当有一个新的内存块, 就会添加到这里面

         reserved这种memblock_type是用于内存分配的, 在内存管理子系统初始化完毕之前, 如果内核需要申请内存, 就会通过这种方式进行. 我们将在《自举分配器》一节中详细讨论这种情形.

 

memblockmm/memblock.c: 它是一个静态定义的struct memblock的实例. 这个实例就相当于把池子的架子搭建好了, 但是池子是空的. 内核在启动过程中, setup_arch函数里面会检测系统可用的物理内存, 并往池子里面添加memory block.

 

主要的API

头文件: include/linux/memblock.h

实现文件: mm/memblock.c

 

int __init_memblock memblock_add_range(struct memblock_type *type,

                phys_addr_t base, phys_addr_t size,

                int nid,unsignedlong flags)

         往对应的memblock_type里面添加一个block.

         它是下面添加类APIs的基础, 下列APIs最终都会调用它完成实际的工作.

         nid是指该block所属结点的NUMA IDUMA系统设置为0.

         注意, blockblock之间可以overlap; 另外在注册两个毗邻的内存区时,API将它们合并为一个.

 

int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)

         memory这个memblock_type里面添加一个block

 

int __init_memblock memblock_remove(phys_addr_t base, phys_addr_t size)

         memory这个memblock_type里面移除一个block

 

int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size)

         reserved这个memblock_type里面添加一个block

 

phys_addr_t __init memblock_alloc(phys_addr_t size, phys_addr_t align)

         分配一块内存, 大小为size, 并把这块分配的内存添加到reserved这个memblock_type里面

 

int __init_memblock memblock_free(phys_addr_t base, phys_addr_t size)

         释放内存, reserved这个memblock_type里面移除一个block

2.3.2         初始化的宏观过程

pg_data_t 数据结构的初始化是从全局启动例程start_kernel 中开始的,该例程在加载内核并激活各个子系统之后执行。由于内存管理是内核一个非常重要的部分,因此在特定于体系结构的代码中检测内存并确定系统中内存的分配情况后,会立即执行内存管理的初始化。此时,已经存在一个pgdata_t实例,用于保存诸如结点中内存数量以及内存在各个内存域之间分配情况的信息,初始化过程主要就是对该实例的相应字段赋值。

 

大体的流程如下:

start_kernel init/main.c

       setup_arch:

setup_arch 是一个特定于体系结构的设置函数,它的几个主要任务是

          检测系统可用的物理内存。

          确定低端/高端内存的分界值

          负责初始化自举分配器。

          初始化pg_data_t 数据结构

       setup_per_cpu_areas

在SMP系统上, setup_per_cpu_areas 初始化源代码中 (使用 per_cpu 宏) 定义的静态 per-cpu变量, 这种变量对系统中的每个CPU都有一个独立的副本。 此类变量保存在内核二进制映像的一个独立的段中。 setup_per_cpu_areas 的目的是为系统的各个CPU分别创建一份这些数据的副本。

 

在非SMP系统上该函数是一个空操作。

本文暂不打算讨论此函数的细节

       build_all_zonelists

建立当前节点的备用内存域列表, 以便在当前节点没有剩余内存时, 从备用列表分配内存

       mm_init

  • mem_init

mem_init是另一个特定于体系结构的函数,用于停用自举分配器并迁移到实际的内存管理函数,稍后讨论

  • kmem_cache_init

kmem_cache_init用于初始化slab分配器, buddy分配器分配的最小单位是页,slab工作在buddy之上,用于分配小于一页的内存空间。

我们将在2.5节讨论此函数。

       setup_per_cpu_pageset

用于初始化zone结构体的pageset成员, 该成员与冷热页机制有关, 本文暂不讨论

       rest_init

如果你去看内核代码, 会发现rest_init是start_kernel调用的最后一个函数. rest_init最终会执行用户空间的init进程, 至此整个内核就初始化就结束了.

本文不打算讨论内核的启动过程, 而是关注rest_init调用的一个函数:

rest_init -> kernel_init -> free_initmem.

内核会在执行init进程的前一刻, 调用了free_initmem函数, 释放初始化数据.

2.3.3         setup_arch

在内核已经载入内存、而初始化的汇编程序部分已经执行完毕后,内核必须执行哪些特定于体系架构的步骤,以便完成内存管理系统的初始化?

 

宏观上的流程如下:

  • setup_arch
  • 检测系统可用的物理内存setup_machine_fdt/setup_machine_tags , and parse_early_param
  • 确定低端/高端内存的分界值(sanity_check_meminfo
  • 初始化自举分配器(arm_memblock_init
  • paging_init
    • bootmem_init
      • zone_sizes_init
        • free_area_init_node(初始化pg_data_t 数据结构

paging_init中有一项重要功能是建立内核逻辑地址空间的映射页表, 这一部分内容会在第三章讨论内核虚拟地址空间时详细介绍, 本章暂时只关注free_area_init_node函数.

检测系统可用的物理内存

检测系统可用的物理内存有两种途径:

第一种是通过查找dtb里面的memory这个node, 确定系统可用的物理内存. (在没有使用dtb的机器上, 物理内存是在uboot里面检测的, 然后uboot会通过atags机制传递给内核).

第二种是用户可以在bootargs里面通过mem=size@start的方式指定系统可用的物理内存.

如果以上两种方式都存在, 内核会用第二种方式的值覆盖第一种方式, 也就是说优先使用第二种方式.

 

第一种方式的代码调用流程大致是这样:

  • setup_arch
    • setup_machine_fdt
      • early_init_dt_scan_nodes
        • early_init_dt_scan_memory
          • 解析memory node (memory node的语法规则参考《device tree》一文), 获取memory的起始物理地址和长度
            • early_init_dt_add_memory_arch
              • memblock_add

 

第二种方式的代码调用流程大致是这样:

  • setup_arch
    • parse_early_param
      • 通过parse_cmdline机制(参见《内核代码基础元素》一文), 最终调用early_mem(arch/arm/kernel/setup.c)函数, 该函数解析uboot传递过来的bootargs中的mem=size@start参数, 获取memory的起始物理地址和长度
        • arm_add_memory
          • 做一些对齐性质的检查
          • memblock_add

 

以上两种方式最终都会调用memblock_add API, 我们在2.3.1节的背景知识中介绍过memblock及其相关APIs.

确定低端/高端内存的分界值

还记得什么是低端/高端内存吗? 我们在1.1.2《内核虚拟地址空间》小节中介绍了它们. 简单来说, 我们把物理内存划分为2部分: 一部分用于内核逻辑映射(一一映射), 称之为低端内存; 另一部分用于内核通过页表动态映射, 称之为高端内存.

 

那么低端/高端的分界值如何确定呢?

 

我们先来看看一张图, 这张图代表着内核虚拟地址空间的划分方式, 已经在1.1.2《内核虚拟地址空间》小节介绍过了, 这里再重复贴一次:

PAGE_OFFSET high_memory : 代表内核可以一一映射的虚拟地址空间.

high_memory VMALLOC_START : 是一段8M的分割区.

VMALLOC_START VMLLOC_END: 代表内核可以动态映射的虚拟地址空间.

VMALLOC_END PKMAP_BASE : 是一段分割区, 一般是2page.

ARM体系架构中, 持久映射区在是放在PAGE_OFFSET之前的, VMALLOC区域后紧跟的是固定映射区, 3章在讨论其中的细节.

 

从上图来看, high_memory就是低端/高端内存的分界值, 那么high_memory到底是多少呢? 是谁来确定high_memory的值的呢?

 

答案是sanity_check_meminfo函数, 它的调用流程是start_kernel -> setup_arch -> sanity_check_meminfo.

 

下面我们来看看sanity_check_meminfo的代码. 为了更加清晰, 我们分段介绍这个函数.

实现文件: arch/arm/mm/mmu.c

 

void __init sanity_check_meminfo(void)

{

    phys_addr_t memblock_limit =0;

    int highmem =0;

    phys_addr_t vmalloc_limit = __pa(vmalloc_min -1)+1;

    struct memblock_region *reg;

    bool should_use_highmem = false;

         定义一些初始变量. 特别要注意vmalloc_limit这个值, 后文的主要逻辑都跟这个值有关. 它依赖于vmalloc_min, 我们先介绍一下vmalloc_min, 理解它有助于理解后面的逻辑.

 

vmalloc_min

//arch/arm/mm/mmu.c

staticvoid* __initdata vmalloc_min =

    (void*)(VMALLOC_END -(240<<20)- VMALLOC_OFFSET);

 

//arch/arm/include/asm/pgtable.h   

/*

* Just any arbitrary offset to the start of the vmalloc VM area: the

* current 8MB value just means that there will be a 8MB "hole" after the

* physical memory until the kernel virtual memory starts.  That means that

* any out-of-bounds memory accesses will hopefully be caught.

* The vmalloc() routines leaves a hole of 4kB between each vmalloced

* area for the same reason. ;)

*/

#define VMALLOC_OFFSET      (8*1024*1024)

#define VMALLOC_START       (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))

#define VMALLOC_END     0xff800000UL

         结合内核虚拟地址空间划分那张图来看, VMALLOC_END被定义为固定值. VMALLOC_END 4G还剩下8M的空间, 8M是给固定映射区使用的.

         VMALLOC_START的值依赖于high_memory,  high_memory是低端/高端内存的划分值, 下文会介绍它的值时如何确定的. 从这里可以看出, 内核的vmalloc区域的大小跟high_memory有关.

         vmalloc_min的值是从VMALLOC_END开始, 减去240M的空间, 然后在减去VMALLOC_OFFSET(8M的分割段). 从这里可以看出, 内核默认给vmalloc区域预留的虚拟地址空间大小是240M.

vmalloc_min这个值有什么意义呢? 想象一下, 如果我们从0xc0000000处开始一一映射物理内存, 如果物理内存比较大, 那一一映射的上界就会超过vmalloc_min, 此时就会挤占vmalloc区域的虚拟地址空间. 因此, 如果(0xc0000000 + 物理内存的容量) > vmalloc_min, 那么high_memory就等于vmalloc_min; 如果(0xc0000000 + 物理内存的容量) < vmalloc_min, 那么high_memory就等于(0xc0000000 + 物理内存的容量).

按照默认预留240Mvmalloc区域来计算, 一一映射区域的最大值是(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET) = 768M. 也就是说如果物理内存超过768M, 那么内核能一一映射的区域最大只有768M, 多余的部分必须开启CONFIG_HIGHMEM才能使用, 如果系统在这种情况下没有开启HIGHMEM的支持, 则多余的内存内核将无法使用, 这种情况下内容会通过打印信息提醒用户使能CONFIG_HIGHMEM.

 

vmalloc的默认地址空间大小虽然是240M, 不过我们可以通过bootargs来修改, 参数规则是vmalloc=size. size的单位是字节.

负责解析的函数定义在arch/arm/mm/mmu.c.

/*

* vmalloc=size forces the vmalloc area to be exactly 'size'

* bytes. This can be used to increase (or decrease) the vmalloc

* area - the default is 240m.

*/

staticint __init early_vmalloc(char*arg)

{

    unsignedlong vmalloc_reserve = memparse(arg,NULL);

 

    if(vmalloc_reserve < SZ_16M){

        vmalloc_reserve = SZ_16M;

        pr_warn("vmalloc area too small, limiting to %luMB\n",

            vmalloc_reserve >>20);

    }

 

    if(vmalloc_reserve > VMALLOC_END -(PAGE_OFFSET + SZ_32M)){

        vmalloc_reserve = VMALLOC_END -(PAGE_OFFSET + SZ_32M);

        pr_warn("vmalloc area is too big, limiting to %luMB\n",

            vmalloc_reserve >>20);

    }

 

    vmalloc_min =(void*)(VMALLOC_END - vmalloc_reserve);

    return0;

}

early_param("vmalloc", early_vmalloc);

 

好了, 接着来看sanity_check_meminfo

    for_each_memblock(memory, reg){

        phys_addr_t block_start = reg->base;

        phys_addr_t block_end = reg->base + reg->size;

        phys_addr_t size_limit = reg->size;

 

        if(reg->base >= vmalloc_limit)

            highmem =1;

        else

            size_limit = vmalloc_limit - reg->base;

 

 

        if(!IS_ENABLED(CONFIG_HIGHMEM)|| cache_is_vipt_aliasing()){

 

            if(highmem){

                pr_notice("Ignoring RAM at %pa-%pa (!CONFIG_HIGHMEM)\n",

                      &block_start,&block_end);

                memblock_remove(reg->base, reg->size);

                should_use_highmem = true;

                continue;

            }

 

            if(reg->size > size_limit){

                phys_addr_t overlap_size = reg->size - size_limit;

 

                pr_notice("Truncating RAM at %pa-%pa to -%pa",

                      &block_start,&block_end,&vmalloc_limit);

                memblock_remove(vmalloc_limit, overlap_size);

                block_end = vmalloc_limit;

                should_use_highmem = true;

            }

        }

 

        if(!highmem){

            if(block_end > arm_lowmem_limit){

                if(reg->size > size_limit)

                    arm_lowmem_limit = vmalloc_limit;

                else

                    arm_lowmem_limit = block_end;

            }

 

            /*

             * Find the first non-pmd-aligned page, and point

             * memblock_limit at it. This relies on rounding the

             * limit down to be pmd-aligned, which happens at the

             * end of this function.

             *

             * With this algorithm, the start or end of almost any

             * bank can be non-pmd-aligned. The only exception is

             * that the start of the bank 0 must be section-

             * aligned, since otherwise memory would need to be

             * allocated when mapping the start of bank 0, which

             * occurs before any free memory is mapped.

             */

            if(!memblock_limit){

                if(!IS_ALIGNED(block_start, PMD_SIZE))

                    memblock_limit = block_start;

                elseif(!IS_ALIGNED(block_end, PMD_SIZE))

                    memblock_limit = arm_lowmem_limit;

            }

 

        }

    }

 

    if(should_use_highmem)

        pr_notice("Consider using a HIGHMEM enabled kernel.\n");

 

    high_memory = __va(arm_lowmem_limit -1)+1;

         这段代码是核心逻辑, 最主要的目的就是最后一句话, 确定出high_memory的值到底是多少. 一旦确定之后, 内核低端/高端内存的划分就确定了.

         high_memory的具体值依赖于arm_lowmem_limit, 后者是通过前面的代码计算出来的, 计算原则在<vmalloc_min>中已经介绍过了, 这里不分析细节了, 自行阅读代码.

 

继续来看sanity_check_meminfo

    /*

     * Round the memblock limit down to a pmd size.  This

     * helps to ensure that we will allocate memory from the

     * last full pmd, which should be mapped.

     */

    if(memblock_limit)

        memblock_limit = round_down(memblock_limit, PMD_SIZE);

    if(!memblock_limit)

        memblock_limit = arm_lowmem_limit;

 

    memblock_set_current_limit(memblock_limit);

}

         最主要的目的是调用memblock_set_current_limit. 它的作用是什么呢? high_memory是内核虚拟地址空间中低端/高端地址的分界值, 是一个虚拟地址; 而此处的memblock_limit则是物理地址空间低端/高端地址的分界值, 是一个物理地址, memblock模块会存储该值.

 

代码运行到这里之后, memblock模块里面就已经存有了系统所有可用的物理内存物理内存低端/高端地址的分界值. 有了这两个信息, 内核通用代码就可以初始化pg_data_t结构体中代表所有内存页帧的struct page *node_mem_map代表各个内存域的struct zone node_zones[MAX_NR_ZONES].

初始化自举分配器

在启动过程期间,尽管内存管理尚未初始化,但内核仍然需要分配内存以创建各种数据结构。自举分配器用于在启动阶段早期分配内存。

 

显然,对该分配器的需求集中于简单性方面,而不是性能和通用性。当内存管理子系统初始化完毕之后,自举分配器就不再使用了,转而使用由内存管理子系统提供的API接口,因为这些API更加高效。

 

自举分配器的实现由两种方式, 第一种方式称之为bootmem, 《深入Linux内核架构3.4.3节》详细的介绍过它. 这里不细述了.

 

如果内核在编译时使能了CONFIG_NO_BOOTMEM, 则不会使用bootmem, 而会借助《2.3.1 背景知识》里面介绍的memblock.

 

memblock不需要初始化, 内核代码静态定义了一个同名的struct memblock的实例.

 

memblock里面有两个memblock_type, 一个是memory, 另一个是reserved.

在《检测系统可用的物理内存》阶段, 会往memory里面添加所有可用的物理内存块.

如果内核在早期初始化过程中想分配内存, 则会从memblock里面获取一个内存块, 并把这个内存块添加到reserved里面, 表示该内存块已经被分配, 释放之前不能被它人使用.

 

除了已经被分配的内存块需要添加到reserved里面, 还有一些内存空间是不能用于分配的, 例如内核代码本身所占据的内存空间, 内核初始页表swapper_pg_dir所占据的空间, 等等.

 

内核初始页表swapper_pg_dir的主要目的是在真正的内核页表创建之前, 把内核启动过程中的代码(虚拟地址)映射到物理内存. ARM体系架构中, swapper_pg_dir本身位于16KB-32KB的位置, 32KB0x8000)开始存放内核代码. swapper_pg_dir只映射了1M的地址空间, 这已经足够了, 内核的初始化代码不会超过1M.

 

对于那些不能用于分配的内存, arm_memblock_init函数用于预留这些内存块, 把它们添加到reserved里面.

 

arm_memblock_init

它的实现文件是arch/arm/mm/init.c, 调用流程是setup_arch->arm_memblock_init. 该函数主要预留了如下内存块:

arm_memblock_init

         memblock_reserve(__pa(_stext), _end - _stext): 预留内核本身所占据的物理内存(《2.3.1 背景知识内核代码在内存中的布局》小节介绍了_text/_stect , _end的含义)

         memblock_reserve(phys_initrd_start, phys_initrd_size): 预留Ramdisk(initrd)占据的内存空间, initrd是由bootloader加载到内存中的, 这时bootloader会把起始地址和结束地址传递给内核, 内核中的全局initrd_startinitrd_end分别指向initrd的起始地址和结束地址.

         arm_mm_memblock_reserve(void) : 预留swapper_pg_dir所占据的内存空间

         if (mdesc->reserve)  mdesc->reserve(): 如果machine_desc中定义了reserve, 则预留这一部分空间

         early_init_fdt_reserve_self(): 预留dtb本身所占据的内存空间

         early_init_fdt_scan_reserved_mem(): 如果在dtb里面定义了需要预留的空间, 则预留这一部分空间

         dma_contiguous_reserve(arm_dma_limit): /* reserve memory for DMA contiguous allocations */

         memblock_dump_all : 如果使能了memblock_debug=1, 则会打印memblockmemoryreserved里面的内容, 从这里我们可以看出可用的物理内存总量和预留的物理内存状况.

默认情况下memblock_debug=0, 我们可以在bootargs里面添加memblock=debug来使memblock_debug=1, 这样在内核启动过程中, memblock_dump_all会打印如下内容:

beaglebone black板上的打印信息截图

 

内存分配API

内核启动过程中如果想通过memblock来分配内存, 可以用如下APIs:

 

头文件: include/linux/memblock.h

phys_addr_t __init memblock_alloc(phys_addr_t size, phys_addr_t align)

......

 

int memblock_free(phys_addr_t base, phys_addr_t size);

 

API实现细节自行阅读代码, 这里就不多说了.

初始化pg_data_t 数据结构

首先回想一下pg_data_t这个数据结构的组成, 我们只考虑简单的情况, 即系统中只有一个节点. 那么系统中就只有一个pg_data_t实例, 这个实例下面有两个重要的数据结构是我们当前需要关注的:

pg_data_t

         struct page *node_mem_map

         struct zone node_zones[MAX_NR_ZONES]

 

node_mem_map是指向page 实例数组的指针,用于描述结点的所有物理内存页。它包含了结点中所有内存域的页。在《检测系统可用的物理内存》阶段,我们已经把系统可用的物理内存都添加到了memblock->memory里面了,因此此时内核的通用代码就可以从memblock里面获取可用的物理内存,然后为每个页帧创建struct page数据结构,然后初始化node_mem_map这个指针。

 

node_zones代表该节点下所有的内存域,内存域一般分为ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM3种。在ARM的体系架构中,对DMA区域一般没有特殊要求,NORMAL区域的内存也可用于DMA操作,很多ARM体系结构上没有使能CONFIG_ZONE_DMA,因此这里最关键的就是要确定NORMAL/HIGHMEM分界值。所幸在《确定低端/高端内存的分界值》时,我们已经确定分界值(memblock_limit)并将它存储在了memblock里面,系统代码此时只需从memblock获取即可.

 

下面我们来看看具体的代码吧:

setup_arch->paging_init->bootmem_init

 

bootmem_init的实现文件是arch/arm/mm/init.c

         find_limits(&min, &max_low, &max_high);

         zone_sizes_init(min, max_low, max_high);

 

find_limits的主要目的是确定3个值, min=PFN_UP(memblock_start_of_DRAM()); max_high=PFN_DOWN(memblock_end_of_DRAM()); max_low就是NORMAL/HIGHMEM的分界值, max_low=PFN_DOWN(memblock_get_current_limit()).

 

获取到这3个值之后, zone_sizes_init就开始用这3个值来初始化pg_data_t的相关数据结构.

//arch/arm/mm/init.c

staticvoid __init zone_sizes_init(unsignedlong min,unsignedlong max_low,

    unsignedlong max_high)

{

    unsignedlong zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES];

    struct memblock_region *reg;

 

    /*

     * initialise the zones.

     */

    memset(zone_size,0,sizeof(zone_size));

 

    /*

     * The memory size has already been determined.  If we need

     * to do anything fancy with the allocation of this memory

     * to the zones, now is the time to do it.

     */

    zone_size[0]= max_low - min;

#ifdef CONFIG_HIGHMEM

    zone_size[ZONE_HIGHMEM]= max_high - max_low;

#endif

 

    /*

     * Calculate the size of the holes.

     *  holes = node_size - sum(bank_sizes)

     */

    memcpy(zhole_size, zone_size,sizeof(zhole_size));

    for_each_memblock(memory, reg){

        unsignedlong start = memblock_region_memory_base_pfn(reg);

        unsignedlong end = memblock_region_memory_end_pfn(reg);

 

        if(start < max_low){

            unsignedlong low_end = min(end, max_low);

            zhole_size[0]-= low_end - start;

        }

#ifdef CONFIG_HIGHMEM

        if(end > max_low){

            unsignedlong high_start = max(start, max_low);

            zhole_size[ZONE_HIGHMEM]-= end - high_start;

        }

#endif

    }

 

#ifdef CONFIG_ZONE_DMA

    /*

     * Adjust the sizes according to any special requirements for

     * this machine type.

     */

    if(arm_dma_zone_size)

        arm_adjust_dma_zone(zone_size, zhole_size,

            arm_dma_zone_size >> PAGE_SHIFT);

#endif

 

    free_area_init_node(0, zone_size, min, zhole_size);

}

前面所有的动作都是在确定各个内存域的页帧数, 最后一句调用free_area_init_node

 

free_area_init_node

//mm/page_alloc.c

void __paginginit free_area_init_node(int nid,unsignedlong*zones_size,

        unsignedlong node_start_pfn,unsignedlong*zholes_size)

{

    pg_data_t *pgdat = NODE_DATA(nid);

    unsignedlong start_pfn =0;

    unsignedlong end_pfn =0;

 

    /* pg_data_t should be reset to zero when it's allocated */

    WARN_ON(pgdat->nr_zones || pgdat->classzone_idx);

 

    reset_deferred_meminit(pgdat);

    pgdat->node_id = nid;

    pgdat->node_start_pfn = node_start_pfn;

#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP

    get_pfn_range_for_nid(nid,&start_pfn,&end_pfn);

    pr_info("Initmem setup node %d [mem %#018Lx-%#018Lx]\n", nid,

        (u64)start_pfn << PAGE_SHIFT,

        end_pfn ?((u64)end_pfn << PAGE_SHIFT)-1:0);

#endif

    calculate_node_totalpages(pgdat, start_pfn, end_pfn,

                  zones_size, zholes_size);

 

    alloc_node_mem_map(pgdat);

#ifdef CONFIG_FLAT_NODE_MEM_MAP

    printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n",

        nid,(unsignedlong)pgdat,

        (unsignedlong)pgdat->node_mem_map);

#endif

 

    free_area_init_core(pgdat);

}

         初始化pgdat->node_id, pgdat->node_start_pfn

         calculate_node_totalpages

         初始化pgdat->node_spanned_pages, pgdat->node_present_pages

         alloc_node_mem_map

         为每个页帧分配struct page数据结构, 然后初始化pgdat->node_mem_map

         free_area_init_core : 遍历节点的所有内存域并初始化每一个内存域

 

free_area_init_core

//mm/page_alloc.c

staticvoid __paginginit free_area_init_core(struct pglist_data *pgdat)

{

    enum zone_type j;

    int nid = pgdat->node_id;

    unsignedlong zone_start_pfn = pgdat->node_start_pfn;

    int ret;

 

    ......

    for(j =0; j < MAX_NR_ZONES; j++){

        struct zone *zone = pgdat->node_zones + j;

        unsignedlong size, realsize, freesize, memmap_pages;

 

        size = zone->spanned_pages;

        realsize = freesize = zone->present_pages;

         zone->spanned_pages代表内存域的所有pages, 包含空洞, 详见针对该数据结构的解释

         zone->present_pages代表除去空洞后的所有pages

 

        if(!is_highmem_idx(j))

            nr_kernel_pages += freesize;

        /* Charge for highmem memmap if there are enough kernel pages */

        elseif(nr_kernel_pages > memmap_pages *2)

            nr_kernel_pages -= memmap_pages;

        nr_all_pages += freesize;

         内核使用两个全局变量跟踪系统中的页数。nr_kernel_pages 统计所有一致映射的页,而nr_all_pages 还包括高端内存页在内

 

free_area_init_core 剩余部分的任务是初始化zone 结构中的各个表头,并将各个结构成员初始化为0我们比较感兴趣的是调用的几个辅助函数。

         zone_pcp_init初始化该内存域的冷热缓存. 关于冷热缓存, 没怎么看懂, 这里就不细述了, 有兴趣可以阅读《深入Linux内核架构3.4.2-- 冷热缓存的初始化

         init_currently_empty_zone初始化free_area 列表,并将属于该内存域的所有page 实例都设置为初始默认值。初始化是由zone_init_free_lists来完成

//mm/page_alloc.c

staticvoid __meminit zone_init_free_lists(struct zone *zone)

{

    unsignedint order, t;

    for_each_migratetype_order(order, t){

        INIT_LIST_HEAD(&zone->free_area[order].free_list[t]);

        zone->free_area[order].nr_free =0;

    }

}

空闲页的数目(nr_free )当前仍然规定为0,这显然没有反映真实情况。因为此时所有的物理内存页都是memblock在管理的,直至memblcok分配器停用,普通的伙伴分配器生效,才会设置正确的数值。

         memmap_init , 该函数最终会调用memmap_init_zone初始化内存域的页, 将所有页面的属性都设置为MIGRATE_MOVABLE类型. MIGRATE_MOVABLE我们前文没有提到过, 它主要目的是避免碎片. 由于碎片是在内存分配的过程中产生的, 所以我们会在2.4介绍伙伴系统时在详细讨论memmap_init_zone函数以及MIGRATE_MOVABLE类型.

2.3.4         build_all_zonelists

我们在2.2.2节介绍zonelist数据结构的时候, 提到过build_all_zonelists的作用. 该函数的目的就是建立当前节点的备用内存域列表, 以便在当前节点没有剩余内存时, 从备用列表分配内存.

 

由于该函数的实现与CPU体系架构关系不大(事实上UMANUMA架构的实现略有不同), 是操作系统策略层面的东西, 因此这里暂不细述, 只给出大致的代码流程, 以后有需要在分析.

 

实现文件: mm/page_alloc.c

build_all_zonelists

         build_all_zonelists_init

         __build_all_zonelists

       #ifdef CONFIG_NUMA

build_zonelists

#else

build_zonelists

#endif

2.3.5         mem_init

在系统初始化进行到伙伴系统分配器能够承担内存管理的责任后,必须停用自举分配器,毕竟不能同时用两个分配器管理内存。mem_init的目的是停用自举分配器

 

核心动作由free_unused_memmapfree_all_bootmem函数完成, 这两个函数的处理细节没有仔细研究, 它们最终会调用__free_pagesmm/page_alloc.c, __free_pages最终会使得zone->free_area[order].nr_free++

 

在此之后, 只有伙伴系统可用于内存分配。

2.3.6         free_initmem

许多内核代码块和数据表只在系统初始化阶段需要。例如,对于链接到内核中的驱动程序而言,则不必要在内核内存中保持其数据结构的初始化例程。在结构建立之后,这些例程就不再需要了。类似地,驱动程序用于检测其设备的硬件数据库,在相关的设备已经识别之后,就不再需要了。

 

内核提供了两个属性(__init __initcall )用于标记初始化函数和数据。这些必须置于函数或数据的声明之前。例如,网卡HyperHopper2000(假想的)的探测例程在系统已经初始化之后将不再使用。

int __init hyper_hopper_probe(struct net_device *dev)

__init 属性插入到函数声明中返回类型和函数名之间。

 

数据段也可以标记为初始化数据。例如,假想的网卡驱动程序需要一些只在系统初始化阶段使用的字符串,此后这些字符串可以丢弃。

static char search_msg[] __initdata = "%s: Desperately looking for HyperHopper at address %x...";

static char stilllooking_msg[] __initdata = "still searching...";

static char found_msg[] __initdata = "found.\n";

static char notfound_msg[] __initdata = "not found (reason = %d)\n";

static char couldnot_msg[] __initdata = "%s: HyperHopper not found\n";

 

__init __initdata 不能使用普通的C语言实现,因此内核必须借助于特殊的GNU C编译器语句。

 

初始化函数实现的背后,其一般性的思想在于,将数据保持在内核映像的一个特定部分,在启动结束时可以完全从内存删除。下列宏的定义即怀着这个目的:

<include/linux/init.h>

#define __init __attribute__ ((__section__ (".init.text"))) __cold

#define __initdata __attribute__ ((__section__ (".init.data")))

__attribute__ 是一个特殊的GNU C关键字,属性即通过该关键字使用。__section__ 属性用于通知编译器将随后的数据或函数分别写入二进制文件的.init.data .init.text 段。前缀__cold 还通知编译器,通向该函数的代码路径可能性较低,即该函数不会经常调用,对初始化函数通常是这样。

 

readelf 工具可用于显示内核映像的各个段。

 

为从内存中释放初始化数据,内核不必知道数据的性质,即哪些数据和函数保存在内存中和它们的用途都是完全不相干的。唯一相关的信息是这些数据和函数在内存中开始和结束的地址。由于该信息在编译时无法得到,它是内核在链接时插入的。vmlinux.ld.S提供了该信息,它定义了一对变量__init_begin __init_end ,其含义很明显。

 

free_initmem 负责释放用于初始化的内存区,并将相关的页返回给伙伴系统。在启动过程刚好结束时会调用该函数,紧接其后init 作为系统中第一个进程启动。启动日志包含了一条信息,指出释放了多少内存。

[    7.800069] Freeing unused kernel memory: 608K (c0b94000 - c0c2c000)

与当今通常配备的主内存大小比较,释放的大约600 KiB内存数量不算大,但也具有比较重要的作用。特别是在手持或嵌入式系统上,清除初始化数据是很重要的,这种设备的性质决定了它们只能用少量内存凑合着运行。

2.4             物理内存分配buddy

2.4.1         原理介绍

一个优秀的存储分配系统需要考虑两方面的因素, 速度和效率.

所谓速度是指分配速度要快, 所谓效率是指要尽量避免碎片化. 两者是相对的, 需要仔细寻求一个平衡点.这里我们不打算讨论伙伴系统是如何找到这个平衡点的, 只用知道伙伴系统基于一种相对简单然而令人吃惊的强大算法,已经伴随我们几乎40年。它结合了优秀内存分配器的两个关键特征:速度和效率。

 

前面已经介绍过, 物理内存被划分为一个个页帧, 我们可以简单的把这些页用单向链表连接起来, 需要分配内存时, 从链表头开始搜寻满足要求的空闲块. 假设需要分配4个连续的页面, 我们就从链表头开始搜寻4个连续的空闲页面, 找到之后就把它们标记为已经分配, 然后把页面地址返回给申请者.

 

这样做的一个问题是分配速度会随着物理内存的增加而加大. 物理内存越大, 页面数就越多, 链表就越长, 搜寻的时间就会越长.

 

要解决链表过长的问题, 我们可以结合链表和数组的优点, 如下图:

纵向是数组, 横向是链表.

数组项0代表其后都是1个空闲页(20=1).

数组项1代表其后都是2个连续的空闲页(21=2).

数组项2代表其后都是4个连续的空闲页(22=4).

数组项MAX_ORDER代表其后都是N个连续的空闲页面(2MAX_ORDER=N).

上述数组项称之为, 比如0, 1.

 

这样, 当我们需要申请4个连续的物理页面时, 就可以直接在2数组后面的链表中查找, 不需要从头搜寻链表了.

上述就是伙伴系统的基本思路, 伙伴系统只能分配2order个页面, 也就是说能分配1, 2, 4, 8个连续的物理页面, 但是不能分配3个连续的物理页面.

 

再来思考一个问题, 在内核初始化过程中, 我们已经把物理内存划分为一个个页帧了, 初始阶段这些页帧全是连续的, 那么我们应该给0阶数组多少个页帧, 1阶多少个页帧块(多个连续页帧就称作页帧块), ?

 

内核的真实做法是初始时把所有的物理页面当做一个页块, 挂载在对应阶的数组上. 然后在分配内存时把这个大的页块拆分为伙伴.

假设系统总共有16个物理页, 下图示范了伙伴系统的工作情况:

如果系统现在需要8个页帧,则将16个页帧组成的块拆分为两个伙伴。其中一块用于满足应用程序的请求,而剩余的8个页帧则放置到对应8页大小内存块的列表中。

 

如果下一个请求只需要2个连续页帧,则由8页组成的块会分裂成2个伙伴,每个包含4个页帧。其中一块放置回伙伴列表中,而另一个再次分裂成2个伙伴,每个包含2页。其中一个回到伙伴系统,另一个则传递给应用程序。

 

在应用程序释放内存时,内核可以直接检查地址,来判断是否能够创建一组伙伴,并合并为一个更大的内存块放回到伙伴列表中,这刚好是内存块分裂的逆过程。这提高了较大内存块可用的可能性。

为什么直接检查地址就能判断是否能够创建一组伙伴? 比如我们把8-168个连续的物理页面拆分成了2个页块: 则第一个页块的起始地址是0x8(二进制1000); 第二个页块的起始地址是0xC(二进制1100); 一个块与它的伙伴在地址上只有1bit不一样. 因此我们可以通过检查地址, 当发现一对伙伴时, 把它俩合并为一个更大的块. 合并的目的是为了避免碎片化, 否则当系统长时间运行之后, 我们就不可能申请到一个大的空闲块了.

 

不过伙伴系统也不可能完全消除碎片化, 在内核版本2.6.24开发期间, 增加了一些有效措施来防止内存碎片, 我们将在后文介绍它的细节.

2.4.2         相关数据结构

free_area

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

//include/linux/mmzone.h

struct zone {

    ...

        /*

        * 不同长度的空闲区域

        */

        struct free_area free_area[MAX_ORDER];

    ...

};

 

free_area 是一个辅助数据结构,我们此前尚未详细介绍过它。其定义如下:

//include/linux/mmzone.h

struct free_area {

    struct list_head    free_list[MIGRATE_TYPES];

    unsignedlong       nr_free;

};

nr_free 指定了当前内存区中空闲的数目(对0阶内存区逐页计算,对1阶内存区计算页对的数目,对2阶内存区计算4页集合的数目,依次类推). free_list 是用于连接空闲页的链表。按《原理介绍》中的论述,页链表包含大小相同的连续内存区块。尽管定义提供了多个页链表,我暂时忽略该事实,在下文《避免碎片化》中讨论其原因。

 

MAX_ORDER代表系统的最大阶, 它的定义如下:

//include/linux/mmzone.h

/* Free memory management - zoned buddy allocator.  */

#ifndef CONFIG_FORCE_MAX_ZONEORDER

#define MAX_ORDER 11

#else

#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER

#endif

#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1))

该常数通常设置为11,这意味着一次分配可以请求的页数最大是211 =2048。但如果特定于体系结构的代码设置了FORCE_MAX_ZONEORDER 配置选项,该值也可以手工改变。

free_area[MAX_ORDER]就是我们在《原理介绍》中讨论的那个数组.

 

基于伙伴系统的内存管理专注于某个结点的某个内存域,例如,DMA或高端内存域。但所有内存域和结点的伙伴系统都通过备用分配列表连接起来。如下图:

在首选的内存域或节点无法满足内存分配请求时,首先尝试同一结点的另一个内存域,接下来再尝试另一个结点,直至满足请求。

/proc/buddyinfo

最后要注意,有关伙伴系统当前状态的信息可以在/proc/buddyinfo 中获得:

上述输出给出了各个内存域中每个分配阶中空闲项的数目,从左至右,阶依次升高。上面给出的信息取自4 GiB物理内存的AMD64系统。

 

我们再来看看BeagleBone Black这块板子的情况, 它使用的是TI 3358 CPU, 512M板载内存.

系统中只有1个节点, 节点下面只有1个内存域. 没有DMA内存域, 因为ARM体系架构对DMA的访问没有特殊要求; 页没有HIGHMEM, 因为板载内存太小了.

2.4.3         避免碎片化可移动性分组

按照《原理介绍》一节的思路, 每个阶的内存块, 只需要挂载到一个链表头下即可, 为啥struct free_area里面定义链表头居然是个数组?

 

在内核版本2.6.23之前, 的确只有一个链表头. 但在内核2.6.24开发期间, 内核开发者对伙伴系统的争论持续了相当长时间. 这是因为伙伴系统是内核最值得尊敬的一部分, 对它的改动不会被大家轻易接受. 争论的最终结果就是每个阶的内存块分类挂载到多个链表头下, 这样做的目的是防止碎片化.

 

下面我们来看看为什么这样能防止碎片化.

 

背景知识: 依据可移动性组织页

伙伴系统的基本原理前文已经讨论过了,其方案在最近几年间确实工作得非常好。但在Linux

内存管理方面,有一个长期存在的问题:在系统启动并长期运行后,物理内存会产生很多碎片。该情形如下图所示。

假定内存由60页组成,这显然不是超级计算机,但用于示例却足够了。左侧的地址空间中散布着空闲页。尽管大约25%的物理内存仍然未分配,但最大的连续空闲区只有一页。这对用户空间应用程序没有问题:其内存是通过页表映射的,无论空闲页在物理内存中的分布如何,应用程序看到的内存似乎总是连续的。右图给出的情形中,空闲页和使用页的数目与左图相同,但所有空闲页都位于一个连续区中。

 

但对内核来说,碎片是一个问题。由于(大多数)物理内存一致映射到地址空间的内核部分,那么在左图的场景中,无法映射比一页更大的内存区。尽管许多时候内核都分配的是比较小的内存,但也有时候需要分配多于一页的内存。显而易见,在分配较大内存的情况下,右图中所有已分配页和空闲页都处于连续内存区的情形,是更为可取的。

 

很有趣的一点是,在大部分内存仍然未分配时,也可能发生碎片问题。考虑下图的情形。只分配了4页,但可分配的最大连续区只有8页,因为伙伴系统所能工作的分配范围只能是2的幂次。

3-25

 

很长时间以来,物理内存的碎片确实是Linux的弱点之一。尽管已经提出了许多方法,但没有哪个方法能够既满足Linux需要处理的各种类型工作负荷提出的苛刻需求,同时又对其他事务影响不大。在内核2.6.24开发期间,防止碎片的方法最终加入内核。在我讨论具体策略之前,有一点需要澄清。文件系统也有碎片,该领域的碎片问题主要通过碎片合并工具解决。它们分析文件系统,重新排序已分配存储块,从而建立较大的连续存储区。理论上,该方法对物理内存也是可能的,但由于许多物理内存页不能移动到任意位置,阻碍了该方法的实施。因此,内核的方法是反碎片anti-fragmentation,即试图从最初开始尽可能防止碎片。

 

反碎片的工作原理如何?为理解该方法,我们必须知道内核将已分配页划分为下面3种不同类型。

         不可移动页:在内存中有固定位置,不能移动到其他地方。核心内核分配的大多数内存属于该类别。

         可回收页:不能直接移动,但可以删除,其内容可以从某些源重新生成。例如,映射自文件的数据属于该类别。

kswapd 守护进程会根据可回收页访问的频繁程度,周期性释放此类内存。这是一个复杂的过程,本身就需要详细论述。目前,了解到内核会在可回收页占据了太多内存时进行回收,就足够了。

另外,在内存短缺(即分配失败)时也可以发起页面回收。有关内核发起页面回收的时机,更具体的信息请参考《深入Linux内核架构18章》

         可移动页可以随意地移动。属于用户空间应用程序的页属于该类别。它们是通过页表映射的。如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事。

 

页的可移动性,依赖该页属于3种类别的哪一种。内核使用的反碎片技术,即基于将具有相同可移动性的页分组的思想。为什么这种方法有助于减少碎片?回想图3-25中,由于页无法移动,导致在原本几乎全空的内存区中无法进行连续分配。根据页的可移动性,将其分配到不同的列表中,即可防止这种情形。例如,不可移动的页不能位于可移动内存区的中间,否则就无法从该内存区分配较大的连续内存块。

 

假如图3-25中大多数空闲页都属于可回收的类别,而分配的页则是不可移动的。如果这些页聚集到两个不同的列表中,如图3-26所示。在不可移动页中仍然难以找到较大的连续空闲空间,但对可回收的页,就容易多了。

但要注意,从最初开始,内存并未划分为可移动性不同的区。这些是在运行时形成的。内核的另一种方法确实将内存分区,分别用于可移动页和不可移动页的分配,我会下文讨论其工作原理。但这种划分对这里描述的方法是不必要的。

相关数据结构MIGRATE_XXX

尽管内核使用的反碎片技术卓有成效,它对伙伴分配器的代码和数据结构几乎没有影响。内核定义了一些宏来表示不同的迁移类型:

//include/linux/mmzone.h

enum{

    MIGRATE_UNMOVABLE,

    MIGRATE_MOVABLE,

    MIGRATE_RECLAIMABLE,

    MIGRATE_PCPTYPES,   /* the number of types on the pcp lists */

    MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,

#ifdef CONFIG_CMA

    /*

     * MIGRATE_CMA migration type is designed to mimic the way

     * ZONE_MOVABLE works.  Only movable pages can be allocated

     * from MIGRATE_CMA pageblocks and page allocator never

     * implicitly change migration type of MIGRATE_CMA pageblock.

     *

     * The way to use it is to change migratetype of a range of

     * pageblocks to MIGRATE_CMA which can be done by

     * __free_pageblock_cma() function.  What is important though

     * is that a range of pageblocks must be aligned to

     * MAX_ORDER_NR_PAGES should biggest page be bigger then

     * a single pageblock.

     */

    MIGRATE_CMA,

#endif

#ifdef CONFIG_MEMORY_ISOLATION

    MIGRATE_ISOLATE,    /* can't allocate from here */

#endif

    MIGRATE_TYPES

};

类型MIGRATE_UNMOVABLE MIGRATE_RECLAIMABLE MIGRATE_MOVABLE 已经介绍过。

MIGRATE_ISOLATE 是一个特殊的虚拟区域,用于跨越NUMA结点移动物理内存页。在大型系统上,它有益于将物理内存页移动到接近于使用该页最频繁的CPU

MIGRATE_TYPES 只是表示迁移类型的数目,也不代表具体的区域。

 

对伙伴系统数据结构的主要调整,是将空闲列表分解为MIGRATE_TYPE 个列表:

//include/linux/mmzone.h

struct free_area {

    struct list_head    free_list[MIGRATE_TYPES];

    unsigned long       nr_free;

};

nr_free 统计了所有列表上空闲页的数目,而每种迁移类型都对应于一个空闲列表。宏for_each_migratetype_order(order, type) 可用于迭代指定迁移类型的所有分配阶。

 

如果内核无法满足针对某一给定迁移类型的分配请求,会怎么样?此前已经出现过一个类似的问题,即特定的NUMA内存域无法满足分配请求时。内核在这种情况下的做法是类似的,提供了一个备用列表,规定了在指定列表中无法满足分配请求时,接下来应使用哪一种迁移类型:

mm/page_alloc.c

/*

* 该数组描述了指定迁移类型的空闲列表耗尽时,其他空闲列表在备用列表中的次序。

*/

该数据结构大体上是自明的:例如在内核想要分配不可移动页时,如果对应链表为空,则后退到可回收页链表,接下来到可移动页链表。

使能可移动性分组page_group_by_mobility_disabled

尽管页可移动性分组特性总是编译到内核中,但只有在系统中有足够内存可以分配到多个迁移类型对应的链表时,才是有意义的。如果各迁移类型的链表中没有一块较大的连续内存,那么页面迁移不会提供任何好处,因此在可用内存太少时内核会关闭该特性。

 

include/linux/mmzone.h中申明了一个全局变量: extern int page_group_by_mobility_disabled;

page_group_by_mobility_disabled = 1 : 代表禁止使用可移动性分组特性

page_group_by_mobility_disabled = 0 : 代表可以使用可移动性分组特性

 

该全局变量是在build_all_zonelists函数里面初始化的:

//mm/page_alloc.c

void __ref build_all_zonelists(pg_data_t *pgdat,struct zone *zone)

{

    ......

 

    vm_total_pages = nr_free_pagecache_pages();

    /*

     * Disable grouping by mobility if the number of pages in the

     * system is too low to allow the mechanism to work. It would be

     * more accurate, but expensive to check per-zone. This check is

     * made on memory-hotadd so a system can start with mobility

     * disabled and enable it later

     */

    if(vm_total_pages <(pageblock_nr_pages * MIGRATE_TYPES))

        page_group_by_mobility_disabled =1;

    else

        page_group_by_mobility_disabled =0;

 

    ......

}

判断的条件是if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES)): vm_total_pages代表系统中可用的内存页总数; MIGRATE_TYPES 是一个常量, pageblock_nr_pages是一个宏, 其定义如下:

 

// include/linux/pageblock-flags.h

 

#ifdef CONFIG_HUGETLB_PAGE

 

#ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE

 

/* Huge page sizes are variable */

externunsignedint pageblock_order;

 

#else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */

 

/* Huge pages are a constant size */

#define pageblock_order     HUGETLB_PAGE_ORDER

 

#endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */

 

#else /* CONFIG_HUGETLB_PAGE */

 

/* If huge pages are not used, group by MAX_ORDER_NR_PAGES */

#define pageblock_order     (MAX_ORDER-1)

 

#endif /* CONFIG_HUGETLB_PAGE */

 

#define pageblock_nr_pages  (1UL << pageblock_order)

pageblock_nr_pages依赖于pageblock_order, 而后者取决于系统是否使能了CONFIG_HUGETLB_PAGE:

         如果没有使能CONFIG_HUGETLB_PAGE, pageblock_order =  (MAX_ORDER-1). ARM体系架构大多数是这种情况.

         如果使能了CONFIG_HUGETLB_PAGE, 例如IA-32体系结构上,巨型页长度是4MiB,因此每个巨型页由1 024个普通页组成,而HUGETLB_PAGE_ORDER 则定义为10

相比之下,IA-64体系结构允许设置可变的普通和巨型页长度,因此HUGETLB_PAGE_ORDER 的值取决于内核配置。

辅助函数与变量

如果可移动性分组的特性可用的话, 当我们调用伙伴系统的API申请一块内存时, 内核如何知道到底应该从哪个分组里面获取内存? 答案是申请者指定. 申请者在申请内存时, 需要指明从哪个node的哪个内存域的哪个迁移类型中申请内存.

gfpflags_to_migratetype

内核提供了两个标志,分别用于表示分配的内存是可移动的__GFP_MOVABLE或可回收的__GFP_RECLAIMABLE。如果这些标志都没有设置,则分配的内存假定为不可移动的。下列辅助函数可用于转换分配标志及对应的迁移类型:

//include/linux/gfp.h

static inline intgfpflags_to_migratetype(const gfp_t gfp_flags)

{

    VM_WARN_ON((gfp_flags & GFP_MOVABLE_MASK)== GFP_MOVABLE_MASK);

    BUILD_BUG_ON((1UL<< GFP_MOVABLE_SHIFT)!= ___GFP_MOVABLE);

    BUILD_BUG_ON((___GFP_MOVABLE >> GFP_MOVABLE_SHIFT)!= MIGRATE_MOVABLE);

 

    if(unlikely(page_group_by_mobility_disabled))

        return MIGRATE_UNMOVABLE;

 

    /* Group based on mobility */

    return(gfp_flags & GFP_MOVABLE_MASK)>> GFP_MOVABLE_SHIFT;

}

如果停用了页面迁移特性,则所有的页都是不可移动的。否则,该函数的返回值可以直接用作free_area.free_list 的数组索引。

pageblock_flags

假设在初始阶段, 所有的页面的可移动性属性都是MOVABLE(事实上也是如此, 下文紧接着我们介绍初始化过程中何时把页面属性都标记为可移动的), 当我们申请了一块不可移动的内存时, 我们必须记录这块内存的可移动性属性, 这样当这块内存被释放时, 我们才能把它挂载到正确的迁移链表free_list[MIGRATE_TYPES])上.

 

那内存块的可移动性属性应该记录在哪里? 保存在struct page结构体里面吗? 这样是不合适的. 可移动性属性是针对内存块而言的, 不是针对page. 一个内存块可能包含多个page, 如果属性保存在page结构体里面, 那该内存块的每个page都需要标记为相应的属性, 这样会导致操作繁琐, 而且会增加page结构体的size.

 

所以内核把这个字段保存在了struct zone结构体中, 由于该字段当前只有与页可移动性相关的代码使用,我在此前没有详细介绍该字段:

//include/linux/mmzone.h

struct zone {

    ...

    unsignedlong*pageblock_flags;

    ...

}

每一个内存块的迁移属性只需要占用几个bit, 因此一个unsignedlong型的数据可以表示多个内存块, unsignedlong*相当于定义了多个unsignedlong, 因而可以表示很多很多个内存块

在内存管理系统初始化期间, 内核会为*pageblock_flags指针分配足够的内存空间, 以确保它能存储系统中所有内存块的迁移属性. 负责分配存储空间的函数是setup_usemapmm/page_alloc.c.

 

当前, 一个内存块的迁移属性需要4bit来标示:

//include/linux/pageblock-flags.h

 

/* Bit indices that affect a whole block of pages */

enum pageblock_bits {

    PB_migrate,

    PB_migrate_end = PB_migrate +3-1,

            /* 3 bits required for migrate types */

    PB_migrate_skip,/* If set the block is skipped by compaction */

 

    /*

     * Assume the bits will always align on a word. If this assumption

     * changes then get/set pageblock needs updating.

     */

    NR_PAGEBLOCK_BITS

};

set/get_pageblock_migratetype

set_pageblock_migratetype 负责设置以page 为首的一个内存的迁移类型:

//mm/page_alloc.c

 

void set_pageblock_migratetype(struct page *page,intmigratetype)

{

    if(unlikely(page_group_by_mobility_disabled &&

             migratetype < MIGRATE_PCPTYPES))

        migratetype = MIGRATE_UNMOVABLE;

 

    set_pageblock_flags_group(page,(unsignedlong)migratetype,

                    PB_migrate, PB_migrate_end);

}

migratetype 参数可以通过上文介绍的gfpflags_to_migratetype辅助函数构建。

 

在释放内存时,页必须返回到正确的迁移链表。这之所以可行,是因为能够从get_pageblock_

migratetype 获得内存块的迁移类型。

//include/linux/mmzone.h

 

#define get_pageblock_migratetype(page)                 \

    get_pfnblock_flags_mask(page, page_to_pfn(page),        \

            PB_migrate_end, MIGRATETYPE_MASK)

初始默认值(memmap_init_zone

在内存子系统初始化期间,memmap_init_zone setup_arch->paging_init->bootmem_init -> free_area_init_node -> free_area_init_core -> memmap_init -> memmap_init_zone)负责处理内存域的page 实例。该函数完成了一些不怎么有趣的标准初始化工作,但其中有一件是实质性的,即所有的页最初都标记为可移动的!

//mm/page_alloc.c

 

void __meminit memmap_init_zone(unsignedlong size,int nid,unsignedlong zone,

        unsignedlong start_pfn,enum memmap_context context)

{

......

        if(!(pfn &(pageblock_nr_pages -1))){

            struct page *page = pfn_to_page(pfn);

 

            __init_single_page(page, pfn, zone, nid);

            set_pageblock_migratetype(page,MIGRATE_MOVABLE);

        }else{

            __init_single_pfn(pfn, zone, nid);

        }

......

}

 

按《相关数据结构(MIGRATE_XXX)》中的讨论,在分配内存时,如果要求的迁移类型没有可用内存,则向更大的迁移类型申请。由于所有页最初都是可移动的,那么在内核分配不可移动的内存区时,则必须“盗取”。

 

实际上,在启动期间分配可移动内存区的情况较少,那么分配器有很高的几率从可移动区获取内存,并将其从可移动列表转换到不可移动列表。此时并不会向可移动内存中引入碎片。

 

总而言之,这种做法避免了启动期间内核分配的内存(经常在系统的整个运行时间都不释放)散布到物理内存各处,从而使其他类型的内存分配免受碎片的干扰,这也是页可移动性分组框架的最重要的目标之一。

/proc/pagetypeinfo

/proc/pagetypeinfo反映了Free pages count per migrate type at order, 它相当于对/proc/buddyinfo的细化.

 

看看BBB板子上的输出结果:

2.4.4         避免碎片化虚拟可移动内存域

背景知识

依据可移动性组织页是防止物理内存碎片的一种可能方法,内核还提供了另一种阻止该问题的手段:虚拟内存域ZONE_MOVABLE。该机制在内核2.6.23开发期间已经并入内核,比可移动性分组框架加入内核早一个版本。

 

与可移动性分组相反,ZONE_MOVABLE特性不会默认编译进内核,只有使能了CONFIG_HAVE_MEMBLOCK_NODE_MAP开关,该特性才可用

 

基本思想很简单:可用的物理内存划分为两个内存域,一个用于可移动分配,一个用于不可移动分配。这会自动防止不可移动页向可移动内存域引入碎片。

 

这马上引出了另一个问题:内核如何在两个竞争的内存域之间分配可用的内存?这显然对内核要求太高,因此系统管理员必须作出决定。毕竟,人可以更好地预测计算机需要处理的场景,以及各种类型内存分配的预期分布。

相关数据结构

required_kernelcore & required_movablecore

内核代码定义了两个early_param, 可通过bootargs来修改它们, 代码如下:

//mm/page_alloc.c

 

#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP

......

staticunsignedlong __initdata required_kernelcore;

staticunsignedlong __initdata required_movablecore;

......

#endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */

 

 

#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP

... ...

/*

* kernelcore=size sets the amount of memory for use for allocations that

* cannot be reclaimed or migrated.

*/

staticint __init cmdline_parse_kernelcore(char*p)

{

    return cmdline_parse_core(p,&required_kernelcore);

}

 

/*

* movablecore=size sets the amount of memory for use for allocations that

* can be reclaimed or migrated.

*/

staticint __init cmdline_parse_movablecore(char*p)

{

    return cmdline_parse_core(p,&required_movablecore);

}

 

early_param("kernelcore", cmdline_parse_kernelcore);

early_param("movablecore", cmdline_parse_movablecore);

#endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */

         kernelcore 参数用来指定用于不可移动分配的内存数量,即用于既不能回收也不能迁移的内存数量。剩余的内存用于可移动分配。在分析该参数之后,结果保存在全局变量required_kernelcore中。

         还可以使用参数movablecore 控制用于可移动内存分配的内存数量。结果保存在全局变量required_movablecore中。required_kernelcore 的大小将会据此计算。

         如果有些聪明人同时指定两个参数,内核会按前述方法计算出required_kernelcore 的值,然后取计算值和指定值的较大者。

ZONE_MOVABLE

取决于体系结构和内核配置,ZONE_MOVABLE 内存域可能位于高端或普通内存域:

//include/linux/mmzone.h

 

enum zone_type {

#ifdef CONFIG_ZONE_DMA

    ZONE_DMA,

#endif

#ifdef CONFIG_ZONE_DMA32

    ZONE_DMA32,

#endif

    /*

     * Normal addressable memory is in ZONE_NORMAL. DMA operations can be

     * performed on pages in ZONE_NORMAL if the DMA devices support

     * transfers to all addressable memory.

     */

    ZONE_NORMAL,

#ifdef CONFIG_HIGHMEM

    ZONE_HIGHMEM,

#endif

    ZONE_MOVABLE,

#ifdef CONFIG_ZONE_DEVICE

    ZONE_DEVICE,

#endif

    __MAX_NR_ZONES

 

};

与系统中所有其他的内存域相反,ZONE_MOVABLE 并不关联到任何硬件上有意义的内存范围。实际上,该内存域中的内存取自高端内存域或普通内存域,因此我们在下文中称ZONE_MOVABLE 是一个虚拟内存域。

movable_zone & zone_movable_pfn

另外还有两个全局变量简单介绍下:

//mm/page_alloc.c

 

#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP

......

 

staticunsignedlong __meminitdata zone_movable_pfn[MAX_NUMNODES];

 

/* movable_zone is the "real" zone pages in ZONE_MOVABLE are taken from */

int movable_zone;

EXPORT_SYMBOL(movable_zone);

#endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */

         movable_zone代表虚拟内存域ZONE_MOVABLE的内存到底来至哪个真实的物理内存域

         对每个结点来说,zone_movable_pfn[node_id] 表示ZONE_MOVABLE movable_zone 内存域中所取得内存的起始地址。

实现

到现在为止描述的数据结构如何应用?类似于页面迁移方法,分配标志在此扮演了关键角色。具体的实现将在2.4.5节更详细地讨论。目前只要知道所有可移动分配都必须指定__GFP_HIGHMEM __GFP_MOVABLE 即可。

 

由于内核依据分配标志确定进行内存分配的内存域,在设置了上述的标志时,可以选择ZONE_MOVABLE 内存域。这是将ZONE_MOVABLE 集成到伙伴系统中所需的唯一改变!其余的可以通过适用于所有内存域的通用例程处理,我们将在下文讨论。

2.4.5         分配器API

就伙伴系统的API而言,NUMAUMA体系结构是没有差别的,二者的调用语法都是相同的。所有函数的一个共同点是:只能分配2的整数幂个页。因此,接口中不像C标准库的malloc 函数或自举分配器那样指定了所需内存大小作为参数。相反,必须指定的是分配阶,伙伴系统将在内存中分配2order页。内核中细粒度的分配只能借助于slab分配器(或者slubslob分配器),后者基于伙伴系统(更多细节在2.5节给出)

分配页API

头文件: include/linux/gfp.h

Note: gfpget free page的缩写.

分配页API

Comment

alloc_pages(gfp_mask,order)

分配2order页并返回一个struct page 的实例,表示分配的内存块的起始页

alloc_page(gfp_mask)

是前者在order = 0 情况下的简化形式,只分配一页

get_zeroed_page(gfp_mask)

分配一页并返回一个page 实例,页对应的内存填充0 (所有其他函数,分配之后页的内容是未定义的)

__get_free_pages(gfp_mask, order)

__get_free_page(gfp_mask)

工作方式与上述函数相同,但返回分配内存块的虚拟地址,而不是page 实例

__get_dma_pages(gfp_mask, order)

用来获得适用于DMA的页.

API的实现很简单, 借助了上一个API:  __get_free_pages((gfp_mask) | GFP_DMA, (order))

在空闲内存无法满足请求以至于分配失败的情况下,所有上述函数都返回空指针(alloc_pagesalloc_page )或者0get_zeroed_page __get_free_pages __get_free_page 。因此内核在各次分配之后都必须检查返回的结果。这种惯例与设计得很好的用户层应用程序没什么不同,但在内核中忽略检查会导致严重得多的故障。

 

内核除了伙伴系统提供的API之外,还提供了其他内存管理函数。它们以伙伴系统为基础,但并不属于伙伴分配器自身。这些函数包括

         vmalloc vmalloc_32 ,使用页表将不连续的内存映射到内核地址空间中,使之看上去是连续的。其实现将在第三章介绍虚拟内存管理时详细讨论。

         还有一组kmalloc 类型的函数,用于分配小于一整页的内存区。其实现将在slab分配器中讨论。

释放页API

头文件: include/linux/gfp.h

释放页API

Comment

__free_pages(struct page *, order)

用于将一个或2order页返回给内存管理子系统。内存区的起始地址由指向该内存区的第一个page 实例的指针表示。

__free_page(struct page *)

是前者在order = 0 情况下的简化形式,只释放一页

free_pages(addr, order)

free_page(addr)

语义类似于前两个函数,但在表示需要释放的内存区时,使用了虚拟内存地址而不是page 实例。

分配掩码GFP_XXX

前述所有函数中强制使用的gfp_mask参数,到底是什么语义?

 

  1. 选择内存域

前文的讨论中我们知道,Linux将内存划分为内存域。内核提供了所谓的内存域修饰符,来指定从哪个内存域分配所需的页。

//include/linux/gfp.h

 

/* Plain integer GFP bitmasks. Do not use this directly. */

#define ___GFP_DMA      0x01u

#define ___GFP_HIGHMEM      0x02u

#define ___GFP_DMA32        0x04u

#define ___GFP_MOVABLE      0x08u

#define ___GFP_RECLAIMABLE  0x10u

#define ___GFP_HIGH     0x20u

......

 

/*

* Physical address zone modifiers (see linux/mmzone.h - low four bits)

*

* Do not put any conditional on these. If necessary modify the definitions

* without the underscores and use them consistently. The definitions here may

* be used in bit comparisons.

*/

#define __GFP_DMA   ((__force gfp_t)___GFP_DMA)

#define __GFP_HIGHMEM   ((__force gfp_t)___GFP_HIGHMEM)

#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32)

#define __GFP_MOVABLE   ((__force gfp_t)___GFP_MOVABLE)  /* Page is movable */

#define __GFP_MOVABLE   ((__force gfp_t)___GFP_MOVABLE)  /* ZONE_MOVABLE allowed */

#define GFP_ZONEMASK    (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)

......

__GFP_MOVABLE 不表示物理内存域,通知内核应该在特殊的虚拟内存域ZONE_MOVABLE

进行相应的分配.

很有趣的一点是,没有__GFP_NORMAL 常数,不过内存分配的主要负担却落到ZONE_NORMAL 内存域!

 

内核提供了一个函数,用于计算与给定分配标志兼容的最高内存域。内存分配可以从该内存域或更低的内存域进行。

//include/linux/gfp.h

 

static inline enum zone_type gfp_zone(gfp_t flags)

{

    enum zone_type z;

    int bit =(__force int)(flags & GFP_ZONEMASK);

 

    z =(GFP_ZONE_TABLE >>(bit * ZONES_SHIFT))&

                     ((1<< ZONES_SHIFT)-1);

    VM_BUG_ON((GFP_ZONE_BAD >> bit)&1);

    return z;

}

这个函数的实现还依赖于GFP_ZONE_TABLE, ZONES_SHIFT, GFP_ZONE_BAD这几个宏, 它们都是在gfp.h中定义的. 函数的实现逻辑就不具体分析了, 看上去有点烧脑袋.

 

不过这个函数的运行结果倒是清晰明了, 给大家分享一下:

参数flags__GFP_DMA, __GFP_DMA32, __GFP_MOVABLE and __GFP_HIGHMEM的组合, 4个值可以组合出0x0 0xf16种情形, 每种情形的运行结果如下:

*       bit       result

*       =================

*       0x0    => NORMAL

*       0x1    => DMA or NORMAL

*       0x2    => HIGHMEM or NORMAL

*       0x3    => BAD (DMA+HIGHMEM)

*       0x4    => DMA32 or DMA or NORMAL

*       0x5    => BAD (DMA+DMA32)

*       0x6    => BAD (HIGHMEM+DMA32)

*       0x7    => BAD (HIGHMEM+DMA32+DMA)

*       0x8    => NORMAL (MOVABLE+0)

*       0x9    => DMA or NORMAL (MOVABLE+DMA)

*       0xa    => MOVABLE (Movable is valid only if HIGHMEM is set too)

*       0xb    => BAD (MOVABLE+HIGHMEM+DMA)

*       0xc    => DMA32 (MOVABLE+DMA32)

*       0xd    => BAD (MOVABLE+DMA32+DMA)

*       0xe    => BAD (MOVABLE+DMA32+HIGHMEM)

*       0xf    => BAD (MOVABLE+DMA32+HIGHMEM+DMA)

*

值得注意的是, 单独设置__GFP_MOVABLE时并不会从MOVABLE域分配内存, 除非同时设置__GFP_MOVABLE | __GFP_HIGHMEM才会尝试从MOVABLE域分配内存.

 

  1. 其它掩码

除了内存域修饰符之外,掩码中还可以设置一些标志。与内存域修饰符相反,这些额外的标志并不限制从哪个物理内存段分配内存,但确实可以改变分配器的行为。

//include/linux/gfp.h

 

......

#define ___GFP_HIGH0x20u

#define ___GFP_IO       0x40u

#define ___GFP_FS       0x80u

#define ___GFP_COLD     0x100u

#define ___GFP_NOWARN       0x200u

#define ___GFP_REPEAT       0x400u

#define ___GFP_NOFAIL       0x800u

#define ___GFP_NORETRY      0x1000u

#define ___GFP_MEMALLOC     0x2000u

#define ___GFP_COMP     0x4000u

#define ___GFP_ZERO     0x8000u

#define ___GFP_NOMEMALLOC   0x10000u

#define ___GFP_HARDWALL     0x20000u

#define ___GFP_THISNODE     0x40000u

#define ___GFP_ATOMIC       0x80000u

#define ___GFP_NOACCOUNT    0x100000u

#define ___GFP_NOTRACK      0x200000u

#define ___GFP_DIRECT_RECLAIM   0x400000u

#define ___GFP_OTHER_NODE   0x800000u

#define ___GFP_WRITE        0x1000000u

#define ___GFP_KSWAPD_RECLAIM   0x2000000u

 

#define __GFP_IO    ((__force gfp_t)___GFP_IO)

#define __GFP_...   ((__force gfp_t)___GFP_...) //不一一列举了

以上给出的常数,其中一些很少使用,因此我不会讨论。其中最重要的一些常数语义如下所示。

         如果请求非常重要,则设置__GFP_HIGH,即内核急切地需要内存时。在分配内存失败可能给内核带来严重后果时(比如威胁到系统稳定性或系统崩溃),总是会使用该标志。注意它与HIGHMEM没有任何关系

         __GFP_IO说明在查找空闲内存期间内核可以进行I/O操作。实际上,这意味着如果内核在内存分配期间换出页,那么仅当设置该标志时,才能将选择的页写入硬盘。

         __GFP_FS允许内核执行VFS操作。在与VFS层有联系的内核子系统中必须禁用,因为这可能引起循环递归调用

         如果需要分配不在CPU高速缓存中的“冷”页时,则设置__GFP_COLD

         __GFP_NOWARN在分配失败时禁止内核故障警告。在极少数场合该标志有用

         __GFP_REPEAT在分配失败后自动重试,但在尝试若干次之后会停止

         __GFP_NOFAIL在分配失败后一直重试,直至成功

         __GFP_ZERO在分配成功时,将返回填充字节0的页

         __GFP_HARDWALL只在NUMA系统上有意义。它限制只在分配到当前进程的各个CPU所关联的结点分配内存。如果进程允许在所有CPU上运行(默认情况),该标志是无意义的。只有进程可以运行的CPU受限时,该标志才有效果

         __GFP_THISNODE也只在NUMA系统上有意义。如果设置该比特位,则内存分配失败的情况下不允许使用其他结点作为备用,需要保证在当前结点或者明确指定的结点上成功分配内存

         __GFP_ATOMIC用于原子分配,在任何情况下都不能中断,也不能sleep. 中断处理函数中分配内存经常使用此标志. 该标志经常与__GFP_HIGH一起使用, 代表可能使用紧急分配链表中的内存

         还有一些没介绍的标志, 有兴趣的读者可以阅读gfp.h, 源码中的注释意思也很清楚

 

由于这些标志几乎总是组合使用,内核作了一些分组,包含了用于各种标准情形的适当的标志。如果有可能的话,在向内存管理子系统申请内存的时候应该尽量使用下列分组之一。在内核源代码中,双下划线通常用于内部数据和定义。而这些预定义的分组名没有双下划线前缀。

//include/linux/gfp.h

 

#define GFP_ATOMIC  (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)

#define GFP_KERNEL  (__GFP_RECLAIM | __GFP_IO | __GFP_FS)

#define GFP_NOWAIT  (__GFP_KSWAPD_RECLAIM)

#define GFP_NOIO    (__GFP_RECLAIM)

#define GFP_NOFS    (__GFP_RECLAIM | __GFP_IO)

#define GFP_TEMPORARY   (__GFP_RECLAIM | __GFP_IO | __GFP_FS | \

             __GFP_RECLAIMABLE)

#define GFP_USER    (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)

#define GFP_DMA     __GFP_DMA

#define GFP_DMA32   __GFP_DMA32

#define GFP_HIGHUSER    (GFP_USER | __GFP_HIGHMEM)

#define GFP_HIGHUSER_MOVABLE    (GFP_HIGHUSER | __GFP_MOVABLE)

#define GFP_TRANSHUGE   ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \

             __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN) & \

             ~__GFP_KSWAPD_RECLAIM)

         GFP_ATOMIC 用于原子分配,在任何情况下都不能中断

         GFP_KERNEL GFP_USER 分别是内核和用户申请内存的默认设置。二者的失败不会立即威胁系统稳定性。GFP_KERNEL 绝对是内核源代码中最常使用的标志。

         GFP_HIGHUSER GFP_USER 的一个扩展,也用于用户空间。它允许分配无法直接映射的高端内存。使用高端内存页是没有坏处的,因为用户过程的地址空间总是通过非线性页表组织的。GFP_HIGHUSER_MOVABLE 用途类似于GFP_HIGHUSER ,但分配将从虚拟内存域ZONE_MOVABLE进行。

         GFP_DMA 用于分配适用于DMA的内存,当前是__GFP_DMA 的同义词。GFP_DMA32 也是__GFP_GMA32 的同义词。

2.4.6         页分配的实现细节__alloc_pages

如果你去翻查源代码, 会发现分配页的各个API, 最终都会调用到__alloc_pages函数, __alloc_pages会直接调用__alloc_pages_nodemask, 内核源代码将__alloc_pages_nodemask称之为“伙伴系统的心脏”, 因为它处理的是实质性的内存分配.

 

__alloc_pages_nodemask的实现是在mm/page_alloc.c里面, 它的申明如下:

struct page *

__alloc_pages_nodemask(gfp_t gfp_mask,unsignedint order,

            struct zonelist *zonelist, nodemask_t *nodemask)

         gfp_mask是分配掩码, 《分配掩码GFP_XXX》已经解释过具体含义

         order是要分配的阶

         zonelist是内存域列表, 2.2.2Zonelist》介绍过它的数据结构. 这个内存域列表包含所有可用于分配内存的内存域(包括本节点的内存域和备用节点的内存域). 按照优先级从高到低的顺序依次排列. 内核会优先尝试从列表的第一个zone分配内存.

举个例子来说明列表的顺序:

假设分配掩码设置了__GFP_HIGHMEM, 代表从HIGHMEM域分配内存, 则列表的顺序是[本节点->HIGHMEM; 本节点->NORMAL; 本节点->DMA; 备用节点->XXX]

假设分配掩码设置了__GFP_DMA, 代表从DMA域分配内存, 则列表的顺序是[本节点->DMA; 备用节点->XXX]

2.3.4build_all_zonelists》负责初始化内存域列表.

         nodemask 暂时不明白具体作用

该函数的实现细节非常复杂, 特别是在系统内存不足的时候, 该函数会通过各种手段反复尝试(例如唤醒页面回收/交换进程, 把不常用的页换出到磁盘; 或者启用OMM killer, 杀死不常用进程; 等等). 这些细节暂时不打算在这里讨论了, 有兴趣的读者可以阅读《深入Linux内核架构3.5.5分配页》并研究内核代码.

 

这里我们只关注最简单的情形, 就是系统内存充足, 代码很顺利的就获得了内存. 通过这个简单的情形, 我们宏观感受一下页分配的大致流程.

精简情形

__alloc_pages_nodemask -> get_page_from_freelist.

get_page_from_freelist

// mm/page_alloc.c

 

staticstruct page *

get_page_from_freelist(gfp_t gfp_mask,unsignedint order,int alloc_flags,

                        conststruct alloc_context *ac)

{

    ......

    for_each_zone_zonelist_nodemask(zone, z, zonelist, ac->high_zoneidx, ac->nodemask){

        ......

        mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];

        if(!zone_watermark_ok(zone, order, mark,

                       ac->classzone_idx, alloc_flags)){

            int ret;

 

            /* Checked here to keep the fast path fast */

            BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);

            if(alloc_flags & ALLOC_NO_WATERMARKS)

                goto try_this_zone;

 

            if(zone_reclaim_mode ==0||

                !zone_allows_reclaim(ac->preferred_zone, zone))

                continue;

 

            ret = zone_reclaim(zone, gfp_mask, order);

            switch(ret){

            case ZONE_RECLAIM_NOSCAN:

                /* did not scan */

                continue;

            case ZONE_RECLAIM_FULL:

                /* scanned but unreclaimable */

                continue;

            default:

                /* did we reclaim enough */

                if(zone_watermark_ok(zone, order, mark,

                        ac->classzone_idx, alloc_flags))

                    goto try_this_zone;

 

                continue;

            }

        }

 

try_this_zone:

        page =buffered_rmqueue(ac->preferred_zone, zone, order,

                gfp_mask, alloc_flags, ac->migratetype);

 

            ......

 

            return page;

        }

    }

    ......

}

for循环针对每一个内存域(zone:

         调用zone_watermark_ok, 判断该内存域是否有足够的空闲内存. 此时考虑了内存域水印. 详情自行阅读该函数.

         如果有足够的空闲内存, 则调用buffered_rmqueue分配内存, 分配成功会得到一个page结构体.

buffered_rmqueue

buffered_rmqueue又是一个巨复杂的函数, 同样这里我们只是精简的看一下:

// mm/page_alloc.c

 

static inline

struct page *buffered_rmqueue(struct zone *preferred_zone,

            struct zone *zone,unsignedint order,

            gfp_t gfp_flags,int alloc_flags,int migratetype)

{

    ......

 

    if(likely(order ==0)){

        struct per_cpu_pages *pcp;

        struct list_head *list;

 

        local_lock_irqsave(pa_lock, flags);

        pcp =&this_cpu_ptr(zone->pageset)->pcp;

        list =&pcp->lists[migratetype];

        if(list_empty(list)){

            pcp->count += rmqueue_bulk(zone,0,

                    pcp->batch, list,

                    migratetype, cold);

            if(unlikely(list_empty(list)))

                goto failed;

        }

 

        if(cold)

            page = list_entry(list->prev,struct page, lru);

        else

            page = list_entry(list->next,struct page, lru);

 

        list_del(&page->lru);

        pcp->count--;

    }else{

        ......

 

        if(!page)

            page = __rmqueue(zone, order, migratetype, gfp_flags);

 

        ......

}

一个if / else把这个函数分为2部分:

         如果oder != 0, 代表分配是多个连续页, 直接就调用__rmqueue向伙伴系统申请页面

         如果order = 0, 代表分配的是单页. 这个地方很有趣, 它不会直接向伙伴系统申请一页, 而是尝试从pcp->lists这个池子里面获取一页. 如果这个池子为空, 则会调用rmqueue_bulk先填充这个池子, 然后在从这个池子分配. rmqueue_bulk最终也会调用__rmqueue向伙伴系统申请页面, 用得到的页面填充池子.

 

pcpper-CPU的意思, 与冷/热页机制有关. 在只请求一页时, 内核试图借助于per-CPU缓存加速请求的处理, 本文不深入讨论pcp机制

 

在第4章《vmalloc&buddyinfo》中我们做过一个实验, 当用vmalloc分配8个页面时, buddyinfo的输出没有任何变化.原因就是vmalloc申请页面都是一页一页申请的, 此时是从pcp->lists里面得到的内存, pcp->lists在之前就已经被填充了, 所以此时并未从伙伴系统分配内存, 所以buddyinfo的输出没有任何变化.

__rmqueue

__rmqueue函数充当进入伙伴系统核心的看门人:

// mm/page_alloc.c

 

staticstruct page *__rmqueue(struct zone *zone,unsignedint order,

                int migratetype, gfp_t gfp_flags)

{

    struct page *page;

 

    page = __rmqueue_smallest(zone, order, migratetype);

    if(unlikely(!page)){

        if(migratetype == MIGRATE_MOVABLE)

            page = __rmqueue_cma_fallback(zone, order);

 

        if(!page)

            page = __rmqueue_fallback(zone, order, migratetype);

    }

 

    trace_mm_page_alloc_zone_locked(page, order, migratetype);

    return page;

}

根据传递进来的分配阶、用于获取页的内存域、迁移类型,__rmqueue_smalles 扫描页的列表,直至找到适当的连续内存块。在这样做的时候,可以按《2.4.1 原理介绍》里面描述的方法拆分更高阶的内存块。

如果指定的迁移列表不能满足分配请求,则调用__rmqueue_fallback 尝试其他的迁移列表,作为应急措施。

__rmqueue_smallest

__rmqueue_smallest 的实现不是很长。本质上,它由一个循环组成,按递增顺序遍历内存域的各个特定迁移类型的空闲页列表,直至找到合适的一项。

// mm/page_alloc.c

 

static inline

struct page *__rmqueue_smallest(struct zone *zone,unsignedint order,

                        int migratetype)

{

    unsignedint current_order;

    struct free_area *area;

    struct page *page;

 

    /* Find a page of the appropriate size in the preferred list */

    for(current_order = order; current_order < MAX_ORDER;++current_order){

        area =&(zone->free_area[current_order]);

        if(list_empty(&area->free_list[migratetype]))

            continue;

 

        page = list_entry(area->free_list[migratetype].next,

                            struct page, lru);

        list_del(&page->lru);

        rmv_page_order(page);

        area->nr_free--;

        expand(zone, page, order, current_order, area, migratetype);

        set_pcppage_migratetype(page, migratetype);

        return page;

    }

 

    returnNULL;

}

搜索从指定分配阶对应的项开始。小的内存区无用,因为分配的页必须是连续的。

选定分配阶后, 在选择分配阶的某个迁移类型的列表.

 

检查是否有合适内存块的操作非常简单。如果列表中有一个元素,那么它就是可用的,因为其中包含了所需数目的连续页。否则,内核将选择下一个更高分配阶,并进行类似的搜索。

 

在用list_del 从链表移除一个内存块之后,要注意,必须将struct free_area nr_free 成员减1(此时buddyinfo里面的输出就会有变化了)rmv_page_order 是一个辅助函数,从页标志删除PG_buddy 位,表示该页不再包含于伙伴系统中,并将struct page private 成员设置为0

 

如果需要分配的内存块长度小于所选择的连续页范围,即如果因为没有更小的适当内存块可用,而从较高的分配阶分配了一块内存,那么该内存块必须按照伙伴系统的原理分裂成小的块。这是通过expand 函数完成的。

expand

基本上将说就是实现2.4.1 原理介绍》里面描述的拆分方法. 这里就不细说了, 有兴趣可以阅读《深入Linux内核架构3.5.5节关于expand函数的分析》

2.4.7         页释放的实现细节__free_pages

所有释放页API最终都会调用__free_pages函数.

// mm/page_alloc.c

 

void __free_pages(struct page *page,unsignedint order)

{

    if(put_page_testzero(page)){

        if(order ==0)

            free_hot_cold_page(page, false);

        else

            __free_pages_ok(page, order);

    }

}

如果是单页, 则不会返回给伙伴系统, 而是返回给冷/热页池子里面(pcp-lists.

如果是多页, 则调用__free_pages_ok.

 

__free_pages_ok经过一些迂回的操作, 最终会调用__free_one_page. 与其名称不同,该函数不仅处理单页的释放,也处理复合页释放。

__free_one_page是内存释放功能的基础。相关的内存区被添加到伙伴系统中适当的free_area 列表。在释放伙伴对时,该函数将其合并为一个连续内存区,放置到高一阶的free_area 列表中。如果还能合并一个进一步的伙伴对,那么也进行合并,转移到更高阶的列表。该过程会一直重复下去,直至所有可能的伙伴对都已经合并. 该过程的细节有兴趣可以阅读《深入Linux内核架构3.5.6 释放页》。

2.5             slab分配器

2.5.1         背景介绍

每个C程序员都熟悉malloc ,及其在C标准库中的相关函数。大多数程序分配若干字节内存时,经常会调用这些函数。

 

内核也必须经常分配内存,但无法借助于标准库的函数。上面描述的伙伴系统支持按页分配内存,但这个单位太大了。如果需要为一个10个字符的字符串分配空间,分配一个4 KiB或更多空间的完整页面,不仅浪费而且完全不可接受。显然的解决方案是将页拆分为更小的单位,可以容纳大量的小对象。

 

为此必须引入新的管理机制,这会给内核带来更大的开销。为最小化这个额外负担对系统性能的影响,该管理层的实现应该尽可能紧凑,以便不要对处理器的高速缓存和TLB带来显著影响。同时,内核还必须保证内存利用的速度和效率。不仅Linux,而且类似的UNIX和所有其他的操作系统,都需要面对这个问题。经过一定的时间,已经提出了一些或好或坏的解决方案,在一般的操作系统文献中都有讲解,例如[Tan07

 

此类提议之一,所谓slab分配,证明对许多种类工作负荷都非常高效。它是由Sun公司的一个雇员Jeff Bonwick,在Solaris 2.4中设计并实现的。由于他公开了其方法[Bon94],因此也可以为Linux实现一个版本。

 

提供小内存块不是slab分配器的唯一任务。由于结构上的特点,它也用作一个缓存,主要针对经常分配并释放的对象。通过建立slab缓存,内核能够储备一些对象,供后续使用,即使在初始化状态,也是如此。举例来说,为管理与进程关联的文件系统数据,内核必须经常生成struct fs_struct 的新实例。此类型实例占据的内存块同样需要经常回收(在进程结束时)。换句话说,内核趋向于非常有规律地分配并释放大小为sizeof{fs_struct} 的内存块。slab分配器将释放的内存块保存在一个内部列表中,并不马上返回给伙伴系统。在请求为该类对象分配一个新实例时,会使用最近释放的内存块。这有两个优点。首先,由于内核不必使用伙伴系统算法,处理时间会变短。其次,由于该内存块仍然是“新”的,因此其仍然驻留在CPU高速缓存的概率较高。

 

slab分配器还有两个更进一步的好处。

         调用伙伴系统的操作对系统的数据和指令高速缓存有相当的影响。内核越浪费这些资源,这些资源对用户空间进程就越不可用。更轻量级的slab分配器在可能的情况下减少了对伙伴系统的调用,有助于防止不受欢迎的缓存“污染”。

         由于CPU的高速缓存也是按页大小缓存物理页帧的,如果数据存储在伙伴系统直接提供的页中,习惯上我们每次都会从得到的内存的首地址开始操作,结果就是我们会从页的首地址开始反复读写某一段内存。这对CPU高速缓存的利用有负面影响,由于这种地址分布,使得某些缓存行过度使用,而其他则几乎没有被用到。多处理器系统可能会加剧这种不利情况,因为不同的内存地址可能在不同的总线上传输,上述情况会导致某些总线拥塞,而其他总线则几乎没有使用。

 

通过slab着色(slab coloringslab分配器能够控制对象在物理页帧中的起始位置,以实现均匀的缓存利用。经常使用的内核对象保存在CPU高速缓存中,这是我们想要的效果。前文的注释提到,slab分配器的角度进行衡量,伙伴系统的高速缓存和TLB占用较大,这是一个负面效应。因为这会导致不重要的数据驻留在CPU高速缓存中,而重要的数据则被置换到内存,显然应该防止这种情况出现。

着色这个术语是隐喻性的。它与颜色无关,只是表示slab中的对象的起始地址需要移动的特定偏移量,以便使对象放置到不同的缓存行。

2.5.2         slob & slub简介

尽管slab分配器对许多可能的工作负荷都工作良好,但也有一些情形,它无法提供最优性能。如微小的嵌入式系统,配备有大量物理内存的大规模并行系统。在第二种情况下,slab分配器所需的大量元数据可能成为一个问题:开发者称,在大型系统上仅slab的数据结构就需要很多吉字节内存。对嵌入式系统来说,slab分配器代码量和复杂性都太高。

 

为处理此类情形,在内核版本2.6开发期间,增加了slab分配器的两个替代品。

         slob分配器进行了特别优化,以便减少代码量。它围绕一个简单的内存块链表展开slobsimple linked list of block的缩写)。在分配内存时,使用了同样简单的最先适配算法。

slob分配器只有大约600行代码,总的代码量很小。事实上,从速度来说,它不是最高效的分配器,也肯定不是为大型系统设计的。

         slub分配器通过将页帧打包为组,并通过struct page 中未使用的字段来管理这些组,试图最小化所需的内存开销。读者此前已经看到,这样做不会简化该结构的定义,但在大型计算机上slubslab提供了更好的性能,说明了这样做是正确的。

 

由于slab分配是大多数内核配置的默认选项,我不会详细讨论备选的分配器。但有很重要的一点需要强调,内核的其余部分无需关注底层选择使用了哪个分配器。所有分配器提供的API都是相同的。实际上,不管是slabslob或者slub,它们都引用了头文件slab.h并实现了slab.h中定义的API。内核编译时使能了哪个分配器,调用者使用的就是哪个分配器。

 

除了标准API之外,内核封装了一些更方便的函数。举例来说,kcalloc 为数组分配内存,而kzalloc 分配一个填充字节0的内存区。

2.5.3         slab分配的原理

概念介绍

对象: slab系统的主要用途是用于分配小块内存, 这个小块内存称之为一个对象(obj, 本节后文都用obj代指一个对象).

 

对象缓存: 所有对象的集合就称作对象缓存(kmem_cache, 本节后文都用kmem_cache代指一个对象缓存), 你可以把它理解为一个池子, 每次向slab系统申请一个存储空间时, 它就从这个池子里面获取一个小块内存返回给申请者.

不过在申请存储空间之前, 申请者必须先创建一个对象缓存, 创建对象缓存时, 申请者只需要提供两个参数: namesize.

 

name指的是对象缓存的名称, size就是小块内存的大小. 比如我们有一个结构体struct this_obj: 若我们想向slab系统申请这个结构体的储存空间, 那么我们就得先创建针对struct this_obj的对象缓存, 创建时size参数一般为sizeof(struct this_obj).至于对象缓存这个池子里面初始有多个小块内存, 不用你关心, slab系统会自动计算. 而且当池子里面的小块内存不足时, slab系统还会自动扩容; 当池子里面的空闲小块内存过多时, slab系统也会自动收缩.

 

池子里面的小块内存来源于伙伴系统, 在创建池子的时候, slab系统会根据情况, 从伙伴系统申请一页或多页, 然后得到的页面划分为一个个小块内存管理. 池子扩容的时候, 也会向伙伴系统申请页面; 池子收缩和销毁池子的时候, 会把内存归还给伙伴系统.

 

从上述描述, 你发现对象缓存这个池子的特点没? 对象缓存是针对某个具体对象的, 一个特定的对象缓存, 它里面的小块内存的大小是固定的. 如上例所述, 当我们创建了针对struct this_obj的对象缓存后, 这个缓存就只能用来给struct this_obj分配内存.

 

不过不同类型的对象数以千计, 他们的大小都各不一样(想想内核代码中有多少个struct结构体), 怎么办? 答案很简单, 针对每个对象创建自己的对象缓存即可. 因此slab系统中可能会在很多个对象缓存, 这些对象缓存都被挂载在一个链表上, 链表头是slab系统中定义的一个全局变量.

 

对象缓存是个池子, 不过它是个大池子, 这个大池子内部又做了进一步的细化, 分为per-CPU缓存和slab缓存, 这是两个小池子.

 

per-CPU缓存: per-CPU缓存是针对CPU, 大池子里面有多少个per-CPU缓存取决于系统有多少个CPU. per-CPU缓存里面存放的是那些刚刚被CPU访问过的obj小块内存. 每次向这个大池子里面申请obj, slab系统会优先从per-CPU缓存里面获取, 因为它们可能刚刚被CPU访问过, 还在CPU的硬件cache里面, 重新分配这块内存给CPU, CPU就可以直接通过cache访问, 加快访问速度. 这也是slab系统的优点之一.

 

slab缓存: 在创建或者扩容大池子的时候, slab系统会先从伙伴系统获页帧, 获取页帧是按次进行的(例如创建时会获取一次, 每次扩容都会获取一次), 一次获取几页(可能是一页, 也可能是多页)是slab系统自动决定的. 每次获取的所有页帧由一个slab缓存管理, 所以大池子里面有几个slab缓存, 取决于大池子向伙伴系统申请了几次内存. (注意, 由于slab缓存和slab系统名称上都有slab, 为了区分, 本节后文slab缓存代指这里描述的小池子, slab系统代指整个slab分配器).

当每次向伙伴系统申请到页帧之后, 就会创建一个slab缓存, 这个slab缓存会把得到的页帧划分为一个个小的obj管理起来. 一个slab缓存的细节如下所示:

         头部管理数据可以划分为2个数组, 每个数组的元素个数取决于slab缓存将页面划分为多少个obj, 具体数值也是slab系统自动计算决定的.

 

第一个数组的目的是通过它, 我们能很快得到obj的地址, 以便分配obj. 数组的每个元素存储的是什么? 最好理解的就是obj的起始地址. 但实际上不是, 实际上存储的是每个obj的索引, 通过索引内核代码能计算出obj的起始地址, 计算细节后文会介绍.

为什么要用索引? 因为如果对象的个数不多, 例如对象个数少于256, 那么用一个unsigned char代表索引就行; 就是对象个数多一些, unsigned short型也足够了. 如果存储的是对象的起始地址的话, 每个地址都得占用4字节(sizeof(void *)).

 

第二个数组的目的是为了标示每个obj的状态, 状态分两种: OBJECT_ACTIVE , OBJECT_FREE. 用于表示对象到底是空闲状态还是已经被分配给使用者了.

 

这两个数组是紧挨在一起的, 也就是地址上连续的.

 

头部管理数据的地址既可以位于slab缓存管理的页帧上(上图就是这种情形), 也可以单独分配一块内存来存储头部管理数据(后文会说明细节).

 

头部管理数据就代表这个slab缓存, 找到头部管理数据也就找到了对应的slab缓存. 头部管理数据的地址存储在struct page -> freelist里面, 该字段在之前介绍page数据结构的时候并没有提到它.

 

         颜色空间其实就是一个偏移量, 偏移量的目的是为了让CPU硬件Cache得到充分利用, 而不是CPU每次都访问Cache的固定行. 2.5.1 背景介绍》一节描述了这样做的原因.

 

注意, 不同的slab缓存, 这里的偏移量不一样, 目的也是为了保证Cache得到充分利用. 如果偏移量都一样, CPU还是会访问某些固定行, 只是行数变了(例如原来每次都会访问1-5, 现在每次都会访问10-15行).

 

         每个obj的大小都是经过对齐操作的, 因此每个小块内存的大小可能大于实际的对象的大小. 例如sizeof(struct this_obj)只有10字节, 但是每次申请内存时, 得到的小块内存大小都是12字节. 填充字节可以加速对slab中对象的访问。如果使用对齐的地址,那么在几乎所有的体系结构上,内存访问都会更快。这弥补了使用填充字节必然导致需要更多内存的不利情况。

 

头部管理数据的存储空间可以单独分配, 示例图如下:

单独分配时, 头部管理数据的地址也是存储在struct page -> freelist里面.

 

最后, 内核需要一种方法, 通过对象自身即可识别slab缓存(以及slab缓存所在的对象缓存).

这是可行的, 通过对象的物理地址就可以定位到所在物理页帧的起始地址, 通过页帧地址就能找到对应的page(因为描述页帧的所有page实例都存储在一个线性数组pg_data_t->node_mem_map里面, 页帧地址可以作为这个数组的索引, 找到对应的page实例). 找到page实例之后, page->freelist指向slab缓存, page->slab_cache指向对象缓存, page的这两个元素是在创建对象缓存和slab缓存时初始化的.

 

总述

接下来, 我们用一张图示画出slab系统内部的构造:

上图灰色箭头反映了物理内存的流向, 从伙伴系统分配给slab分配器的每个物理内存页都设置标志PG_SLAB.

对象分配的体系就形成了一个三级的层次结构,分配成本和操作对CPU高速缓存和TLB的负面影响逐级升高。

         仍然处于CPU高速缓存中的per-CPU对象

         现存slab中未使用的对象

         刚使用伙伴系统分配的新slab中未使用的对象

主要的数据结构

我们在解释下面这些数据结构的时候, 是边阅读代码边理解边写的, 如果有不清楚的地方, 可以在slab.c中搜索对应的元素, 看看它是怎么用的, 以帮助理解.

另外, 《概念介绍》中的描述对理解下面的内容也很重要, 如果不理解, 建议反复阅读《概念介绍》.

kmem_cache

kmem_cache用于描述一个对象缓存, 也就是描述那个大池子

 

头文件: include/linux/slab_def.h

struct kmem_cache

Comment

struct array_cache __percpu *cpu_cache

每一个array_cache代表一个per-CPU的小池子, __percpu的意思是系统有几个cpu, 就定义几个指针, 每个指针指向对应的小池子

/* 1) Cache tunables. Protected by slab_mutex */

unsigned int batchcount

指定了在per-CPU缓存为空的情况下, 每次slab缓存中获取对象的数目

unsigned int limit

指定了per-CPU缓存空闲对象的最大数目, 如果超出该值, 内核会将batchcount 个对象返回到slab缓存.

unsigned int shared

暂不明

unsigned int size

对象缓存中每个对象(小块内存)的实际大小, 它是经过对齐操作的.

假设object_size = sizeof(struct obj), 那么这里的size就等于ALIGN(object_size, kmem_cache->align). kmem_cache->align下面会介绍, 一般来说, 对齐之后, size都会大于object_size.

struct reciprocal_value reciprocal_buffer_size

假设某个对象取至一个slab缓存, 如果知道了该对象的地址, 如何确定该对象在slab缓存中的索引?

 

由于对象在slab缓存中是依次排列的, 每个对象的占据kmem_cache->size大小的字节, 计算偏索引最容易的方法是, 用对象所在的地址,减去slab缓存中第一个对象的起始地址,然后将获得的对象偏移量,除以size,即可得到索引值

 

例如某个对象的地址是115, slab缓存中第一个对象的起始地址是100, size大小为5, 则该对象的索引值就是(115 100) / 5 = 3.

 

但是在某些古老的计算机上, 除法比较慢, 而乘法确快多了, 因此内核使用所谓的Newton-Raphson方法, 这只需要乘法和移位. 尽管对我们来说, 数学细节没什么趣味(可以在任何标准教科书中找到), 但需要知道, 内核可以不计算C = A/B , 而是采用C = reciprocal_divide(A, reciprocal_value(B))的方式, 后者涉及的两个函数都是库程序.

由于特定的对象缓存中, 每个对象的size都是固定的, 因此内核sizerecpirocal 值存储在recpirocal_buffer_size 中,该值可以在后续的除法计算中使用.

/* 2) touched by every alloc & free from the backend (buddy) */

unsigned int flags

用于slab系统的SLAB_Flags,  定义如下:

头文件: include/linux/slab.h

#define SLAB_DEBUG_FREE0x00000100UL/* DEBUG: Perform (expensive) checks on free */

#define SLAB_RED_ZONE0x00000400UL/* DEBUG: Red zone objs in a cache */

#define SLAB_POISON0x00000800UL/* DEBUG: Poison objects */

#define SLAB_HWCACHE_ALIGN0x00002000UL/* Align objs on cache lines */

#define SLAB_CACHE_DMA0x00004000UL/* Use GFP_DMA memory */

#define SLAB_STORE_USER0x00010000UL/* DEBUG: Store the last owner for bug hunting */

#define SLAB_PANIC0x00040000UL/* Panic if kmem_cache_create() fails */

#define SLAB_DESTROY_BY_RCU0x00080000UL/* Defer freeing slabs to RCU */

#define SLAB_MEM_SPREAD0x00100000UL/* Spread some memory over cpuset */

#define SLAB_TRACE0x00200000UL/* Trace allocations and frees */

 

另外还有一个FLAG定义在slab.c:

#define CFLGS_OFF_SLAB(0x80000000UL)

这个标志的意思是slab缓存的头部管理数据到底是存储在slab缓存外部还是内部(概念介绍中有讨论它们的区别).

unsigned int num

当新建一个slab缓存时, num决定了将该缓存划分多少个obj.

它也决定了slab缓存的头部管理数据的数组的元素个数.

/* 3) cache_grow/shrink */

unsigned int gfporder

当新建一个slab缓存时, 从伙伴系统申请2gfporder个页面, 当缩减对象缓存时, 一次向伙伴系统返回2gfporder个页面

gfp_t allocflags

伙伴系统相关的分配掩码, 详见2.4.5《分配掩码GFP_XXX

size_t colour

每次新建一个slab缓存时, 都会在slab缓存的一个对象前面放置一段颜色空间, 放置颜色空间的目的见《2.5.3 概念介绍》.

 

颜色空间的大小= (kmem_cache_node->colour_next此处的colour_off).

colour_next表示下次新建一个slab缓存时颜色值, 这个颜色值从0开始每次加1, (colour_next == 此处的colour), colour_next又从0开始循环.

 

这样, 每个slab缓存前面的颜色空间长度都不一样, 可以保证CPU硬件cache的每个缓存行被均衡利用

unsigned int colour_off

struct kmem_cache *freelist_cache

slab缓存的头部管理数据可以单独分配存储空间, 如果单独分配的话, 存储空间从freelist_cache所指向的对象缓存中获取

unsigned int freelist_size

slab缓存头部管理数据的大小

void (*ctor)(void *obj)

构造函数, 在创建并初始化一个slab缓存的时候, 针对缓存的每个obj, 都调用一次构造函数

/* 4) cache creation/removal */

const char *name

对象缓存的name, 在整个slab系统中唯一. slab系统挂接了多个对象缓存, name来区分不同的对象缓存

struct list_head list

用于把本对象缓存挂接到slab系统的全局链表下

int refcount

每当新创建一个对象缓存时, refcount = 1, 如果下次重新创建一个同名的对象缓存, 则不会进行实际性的创建动作, 只会把refcount + 1.

 

当销毁一个对象缓存时, refcount--, 只有refcount == 0时才执行实际的销毁动作.

int object_size

对象缓存中每个对象的实际大小, 指的是对齐操作前的大小, 例如object_size = sizeof(struct this_obj)

int align

对齐值, 用于把object_size对齐到size. 计算公式size=ALIGN(object_size, align).

 

align的值具体是多少分为以下两种情况:

         如果在创建对象缓存时使用标志SLAB_HWCACHE_ALIGN, 那么会按照cache_line_size 的返回值进行对齐, 该函数返回特定于处理器的L1缓存大小.

如果对象小于缓存行长度的一半, 那么将多个(例如n)对象放入一个缓存行. 也就是说对齐到cache_line_size/n

         如果不要求按硬件缓存行对齐, 那么内核保证对象按BYTES_PER_WORD 对齐, 该值是表示void指针所需字节的数目. 32位系统上是4字节.

/* 5) statistics */

#ifdef CONFIG_DEBUG_SLAB

    ......

#endif

 

struct kmem_cache_node *node[MAX_NUMNODES]

每一个kmem_cache_node对应一个中等池子, 里面包含多个小池子(slab缓存).

系统有多少个内存节点, 就对应多少个中等池子

   
array_cache

array_cache用于描述一个per-CPU缓存, 每个cpu对应一个array_cache, 为了简化起见, 下文我们假设系统中只有一个CPU.

 

头文件: mm/slab.c

struct  array_cache

Comment

unsigned int avail

per-CPU缓存里面有多少个空闲对象.

每分配一个对象, avail++

每补充一个对象, avail--

unsigned int limit

它的值等于kmem_cache->limit, 它的意义与kmem_cachelimit的意义一样.

per-CPU缓存中空闲对象的个数超过limit, 就会返回batchcount个对象给slab缓存

unsigned int batchcount

它的值等于kmem_cache->batchcount, 它的意义与kmem_cachebatchcount的意义一样.

unsigned int touched

per-CPU缓存移除一个对象时, touched 设置为1. per-CPU缓存收缩时, 则将touched 设置为0.

这使得内核能够确认在缓存上一次收缩之后是否被访问过, 也是缓存重要性的一个标志

void *entry[]

0长度数组(0长度数组是GNU C的一个特性, 其意义参见《编程基础》一文), 每个数组元素指向一个空闲obj的起始地址.

当我们想从per-CPU获取一个对象时, 获取的方式是void *obj = entry[--avail] ; 而当我们往per-CPU缓存释放一个对象时, 方式是entry[avail++] = obj.

 

注意到了吗, 在分配和释放对象时, 采用后进先出原(LIFOlast in first out). 内核假定刚释放的对象仍然处于CPU高速缓存中, 会尽快响应下一个分配请求时)再次分配它, 目的也是利用cache的特性加速访问.

kmem_cache_node

kmem_cache_node用于描述一个slab缓存的集合, 一个kmem_cache_node下面可能会有多个slab缓存.

每个内存节点(node)对应一个kmem_cache_node, 为了简化起见, 下文我们假设系统中只有一个node.

 

头文件: mm/slab.h

struct  kmem_cache_node

Comment

spinlock_t list_lock

原子锁

struct list_head slabs_partial

部分空闲链表头, 用于挂接所有的部分空闲slab缓存.

如果一个slab缓存有部分对象被使用了, 另外一部分是空闲的, 就是部分空闲slab缓存

struct list_head slabs_full

用于挂接所有的满的slab缓存.

如果一个slab缓存的所有对象都被使用了, 就称作满的slab缓存

struct list_head slabs_free

用于挂接所有的空闲的slab缓存.

如果一个slab缓存所有的对象未被使用, 就称作空闲slab缓存

unsigned long free_objects

free_objects 表示slabs_partial slabs_free 的所有slab中空闲对象的总数

unsigned int free_limit

指定了所有slab上容许未使用对象的最大数目, 也就是说如果free_objects > free_limit, 那么slab系统就要收缩, 把一部分内存返还给伙伴系统

unsigned int colour_next

内核建立的下一个slab颜色, 详情见struct kmem_cache中关于colourcolour_off的描述

struct array_cache *shared

可在节点内共享的per-CPU缓存.

struct alien_cache **alien

暂时不理解

unsigned long next_reap

定义了内核在两次尝试收缩缓存之间,必须经过的时间间隔。其想法是防止由于频繁的缓存收缩和增长操作而降低系统性能,这种操作可能在某些系统负荷下发生。该技术只在NUMA系统上使用,我们不会进一步关注。

int free_touched

表示slab缓存是否是活动的。在从slab缓存获取一个对象时,内核将该变量的值设置为1。在缓存收缩时,该值重置为0

 

该变量将应用到整个缓存,因而不同于per-CPU变量touched

2.5.4         APIs

普通内核代码只需要包含slab.h ,即可使用内存分配的所有标准内核函数。下图显示了类slab分配器与伙伴系统的大致关系。

 

头文件: include/linux/slab.h

Slab/Slob/Slub  API

Comment

kmem_cache_create(const char *name, size_t size, size_t align,unsigned long flags, void (*ctor)(void *))

* kmem_cache_create - Create a cache.

* @name: A string which is used in /proc/slabinfo to identify this cache.

* @size: The size of objects to be created in this cache.

* @align: The required alignment for the objects.

* @flags: SLAB flags

* @ctor: A constructor for the objects.

 

关于SLAB flags, 在介绍kmem_cache数据结构时介绍过了

#define KMEM_CACHE(__struct, __flags) kmem_cache_create(#__struct,\

sizeof(struct __struct), __alignof__(struct __struct),\

(__flags), NULL)

Please use this macro to create slab caches. Simply specify thename of the structure and maybe some flags that are listed above.

 

The alignment of the struct determines object alignment. If youf.e. add ____cacheline_aligned_in_smp to the struct declarationthen the objects will be properly aligned in SMP configurations.

 

这是内核定义的一个宏, 用于创建一个slab caches

kmem_cache_alloc(struct kmem_cache *, gfp_t flags)

分配其中包含的一个对象

kmem_cache_free(struct kmem_cache *, void *)

释放其中包含的一个对象

kmem_cache_zalloc(struct kmem_cache *k, gfp_t flags)

分配并用0填充

kmem_cache_destroy(struct kmem_cache *s)

销毁创建的对象缓存

 

 

Note:上述API用于建立一个对象缓存, 然后从对象缓存中分配/释放对象. 下述API不需要你显示创建对象缓存, 它们会使用slab系统创建的通用对象缓存.

kmalloc(size_t size, gfp_t flags)

分配长度为size 字节的一个内存区,并返回指向该内存区起始处的一

void 指针。如果没有足够内存(在内核中这种情形不大可能,但却始终要考虑到),则结果为NULL 指针。

flags 参数使用2.4.5节讨论的GFP_ XXX常数,来指定分配内存的具体内存域,例如GFP_DMA 指定分配适合于DMA的内存区。

 

kmalloc 在内核源代码中的使用数以千计,但模式都是相同的。用kmalloc 分配的内存区,首先通过类型转换变为正确的类型,然后赋值到指针变量。

info = (struct cd_info *) kmalloc (sizeof (struct cd_info), GFP_KERNEL);

kfree(const void *)

释放*ptr 指向的内存区

kzalloc(size_t size, gfp_t flags)

分配一个填充字节0的内存区

 

它的实现借用了kmalloc, 非常简单明了:

return kmalloc(size, flags | __GFP_ZERO)

kzfree(const void *)

释放kzalloc分配的内存

kcalloc(size_t n, size_t size, gfp_t flags)

为数组分配内存, 总共n个元素, 每个元素的内存大小为size

注意该API会用0填充所分配的内存区

kmalloc_node(size_t size, gfp_t flags, int node)

分配特定于某个节点的内存区

未完待添加

 

   

/proc/slabinfo

所有对象缓存的列表保存在/proc/slabinfo 中(为节省空间,下文的输出省去了不重要的部分)

第一列是每个对象缓存的name, 后面几列是各个对象缓存的细节信息.

 

API一节我们提到, 当用kmalloc分配内存时, 使用的是slab系统自动创建的对象缓存, 这些对象缓存就是kmalloc-8192, kmalloc-4096, ... ,kmalloc-64. slab系统会根据kmalloc(size_t size, gfp_t flags)size参数决定使用哪个对象缓存.

2.5.5         关键代码分析

slab系统初始化(kmem_cache_init)

初看起来,slab系统的初始化不是特别麻烦,因为伙伴系统已经完全启用,内核没有受到其他特别的限制。尽管如此,由于slab分配器的结构所致,这里有一个鸡与蛋的问题。

 

为初始化slab数据结构,内核需要若干远小于一整页的内存块,这些最适合由kmalloc 分配。这里是关键所在:此时slab系统还未使用,不能使用kmalloc .

 

kmem_cache_init 函数用于初始化slab分配器。它在内核初始化阶段(start_kernel -> mm_init -> kmem_cache_init)、伙伴系统启用之后调用。kmem_cache_init 采用了一个多步骤过程,逐步激活slab分配器,以解决上述鸡与蛋的问题。

         首先, 在创建任何对象缓存时, 我们都需要分配struct kmem_cache结构体. 因此, slab系统定义了一个静态的对象缓存, 该对象缓存的obj就是struct kmem_cache. 该对象缓存的定义如下:

//mm/slab.c

 

staticstruct kmem_cache kmem_cache_boot ={

    .batchcount =1,

    .limit = BOOT_CPUCACHE_ENTRIES,

    .shared =1,

    .size =sizeof(struct kmem_cache),

    .name ="kmem_cache",

};

kmem_cache_init首先就会调用create_boot_cache初始化该对象缓存name, size, object_size, align等参数.

然后create_boot_cache会进一步调用__kmem_cache_create(注意此时slab_state = NONE), 初始化该对象缓存的per-CPU缓存和slab缓存.

 

上述动作完成后, 我们就可以向该对象缓存申请分配obj.

         随后, kmem_cache_init就会创建多个kmalloc_caches对象缓存. 创建完毕之后会初始化这些对象缓存. 这些对象缓存是用于kmalloc分配的.

上述步骤完成之后, 我们就可以用kmalloc分配存储空间了.

 

kmem_cache_init的实现在mm/slab.c, 我们只是简单了描述了下它的大体思想, 有很多技巧和细节这里没有阐述, 有兴趣的可以自行阅读源码.

创建对象缓存(kmem_cache_create)

创建新的slab缓存必须调用kmem_cache_create 。该函数需要很多参数。

//mm/slab_common.c

 

struct kmem_cache *

kmem_cache_create(constchar*name,size_t size,size_t align,

          unsignedlong flags,void(*ctor)(void*))

         name : 此对象缓存的名称, 随后会出现在/proc/slabinfo

         size : 被管理对象以字节计的长度

         align : 在对齐数据时使用的偏移量align ,几乎所有的情形下都是0

         flags: SLAB_Flags, 2.5.3 kmem_cache》中有细述

         ctor: 构造函数

 

kmem_cache_create 的代码相对比较简单, 下所示:

 

kmem_cache_create

         __kmem_cache_alias : 检查如果slab系统已经存在同名的对象缓存, 则将该对象缓存的refcount++, 然后将此对象缓存返回给调用者

         create_cache : 否则调用create_cache创建一个新的对象缓存

 

create_cache首先给该对象缓存分配存储空间, 然后初始它的name, size等参数, 然后调用__kmem_cache_create, 之后会将该对象缓存的refcount赋值为1, 然后将它添加到用于挂接所有对象缓存的全局链表头下.

 

__kmem_cache_create的实现是一个复杂而冗长的过程, 码流程图如下:

__kmem_cache_create(mm/slab.c)

  • 计算对齐值
  • calculate_slab_order : 计算出对象缓存的numgfporder这两个重要数据
  • calculate_freelist_size : 确定slab缓存部管理数据的大小, freelist_size的值
  • 决定头部管理数据的存储位置:  ON_SLAB 还是OFF_SLAB
  • 计算出偏移量(colour_off)和最大颜色值(colour
  • 如果头部管理数据是OFF_SLAB, 则需要创建一个对象缓存用于给头部管理数据分配存储空间, 然后将freelist_cache指向该对象缓存
  • setup_cpu_cache : 初始化该对象缓存的per-CPU缓存和slab缓存注意此时还只是给相关数据结构分配了存储空间, 并未向伙伴系统申请内存, 也就是说缓存里面没有任何obj可用

 

代码细节这里就不贴了, 结合2.5.3节介绍的知识和数据结构中对各个元素的描述, 阅读理解代码不难.

分配对象(kmem_cache_alloc)

kmem_cache_alloc 用于从特定的缓存获取对象。类似于所有的malloc 函数,其结果可能是指向分配内存区的指针,也可能分配失败,返回NULL 指针。该函数需要两个参数:用于获取对象的缓存,以及精确描述分配特征的标志变量。

//mm/slab.c

 

void*kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)

注意到了吗? 我们在创建对象缓存的时候并未指定GFP_XXX标志, 而是在这里才指定. 这说明此时才需要跟伙伴系统打交道.

 

kmem_cache_alloc 基于参数相同的内部函数slab_alloc, 采用这种结构, 目的是合并kmalloc kmem_cache_alloc 的通用实现部分.

slab_alloc会调用__do_cache_alloc, 后者进一步调用____cache_alloc.

 

____cache_alloc是干实事的, 实现也很复杂, 如下:

如果per-CPU缓存中有空闲对象,则从中获取。但如果其中的所有对象都已经分配,则必须重新填充缓存。在最坏的情况下,可能需要新建一个slab。下文我们就分情况阐述.

per-CPU缓存中有空闲对象

如果在per-CPU缓存中有对象,那么____cache_alloc 检查相对容易,如下列代码片段所示:

//mm/slab.c : ____cache_alloc

 

    ac = cpu_cache_get(cachep);

    if(likely(ac->avail)){

        ac->touched =1;

        objp = ac_get_obj(cachep, ac, flags, false);

        ......

    }

 

ac_get_obj用于获取一个obj, 返回其起始地址:

//mm/slab.c

 

static inline void*ac_get_obj(struct kmem_cache *cachep,

            struct array_cache *ac, gfp_t flags,bool force_refill)

{

    void*objp;

 

    if(unlikely(sk_memalloc_socks()))

        objp = __ac_get_obj(cachep, ac, flags, force_refill);

    else

        objp =ac->entry[--ac->avail];

 

    return objp;

}

红色的那句, entry最末尾得到一个对象地址, 并使per-CPU缓存的空闲对象个数减一. 从末尾分配是为了实现后进先出原则(LIFOlast in first out), 以提高访问速度.

per-CPU缓存中有空闲对象(cache_alloc_refill)

per-CPU缓存中没有对象时,工作负荷会加重。该情形下所需的重新填充操作由cache_alloc_refill 实现,在per-CPU缓存无法直接满足分配请求时,则调用该函数.

 

内核现在必须slab缓存中找到array_cache->batchcount 个未使用对象重新填充per-CPU缓存。

 

首先检查slab缓存的shared指针释放为空, 如果不为空, 则调用transfer_objects函数, 尝试从共享的per-CPU缓存中转移一些obj到当前per-CPU缓存.

 

如果transfer_objects失败, 扫描所有部分空闲slab的链表(slabs_partial ),然后通过ac_put_obj依次将对象放至per-CPU缓存,直至相应的slab中没有空闲对象为止。

 

如果仍未找到所需数目的对象,内核会遍历slabs_free链表中所有未使用的slab。在从slab获取对象时,内核还必须将slab放置到正确的slab链表中(slabs_full slabs_partial ,取决于slab已经完全用尽还是仍然包含一些空闲对象)

 

如果扫描了所有的slab仍然没有找到空闲对象,那么必须使用cache_grow 扩大缓存。这是一个代价较高的操作,将在下一节讲述。

 

cache_alloc_refill的代码细节就不贴了, 自行阅读吧.

缓存扩容(cache_grow)

图给出了cache_grow 的代码流程图

kmem_cache_alloc 的参数也会传递给cache_grow 还可以明确指定一个结点,用于从中分配新的内存页。

 

首先计算颜色和偏移量:

//mm/slab.c : cache_grow

 

    spin_lock(&n->list_lock);

 

    /* Get colour for the slab, and cal the next value. */

    offset = n->colour_next;

    n->colour_next++;

    if(n->colour_next >= cachep->colour)

        n->colour_next =0;

    spin_unlock(&n->list_lock);

 

    offset *= cachep->colour_off;

如果达到了颜色的最大数目,则内核重新开始从0计数,这自动导致了零偏移。

 

所需的内存空间是使用kmem_getpages 辅助函数从伙伴系统逐页分配的。该函数唯一的目的就是用适当的参数调用2.4.5节讨论的alloc_pages_node 函数。各个页都设置了PG_slab 标志位,表示该页属于slab分配器。在一个slab用于满足短期或可回收分配时,则将标志__GFP_RECLAIMABLE 传递到伙伴系统。回想2.4.5节的内容,我们知道重要的是从适当的迁移列表分配页。

 

slab头部管理数据的分配没什么趣味。如果slab头存储在slab之外,则调用相关的alloc_slabmgmt函数分配所需空间。否则,相应的空间已经在slab中分配。在两种情况下,都会设置如下两个值:

page->active =0;

page->s_mem = addr + colour_off;

page->active变量存储着slab缓存中有多少个obj已经被分配, 此时显然是0

page->s_mem存储着slab缓存的第一个obj的起始地址

 

接下来,内核调用slab_map_pages创建页帧对象缓存和slab缓存之间的关联: 首先初始化page-> slab_cache, 使之指向当前对象缓存; 然后初始化page->freelist, 使之指向头部管理数据的地址, 也就是指向slab缓存.

 

cache_init_objs 调用各个对象的构造器函数(假如有的话),初始化新slab缓存中的对象,不过现实情况中很少有人会定义构造函数。另外cache_init_objs还会初始化头部管理数据的那2个数组。

 

现在slab已经完全初始化,可以添加到缓存的slabs_free 链表。新产生的对象的数目也加到缓存中空闲对象的数目上(kmem_cache_node->free_objects

释放对象(kmem_cache_free)

如果一个分配的对象已经不再需要,那么必须使用kmem_cache_free 返回给slab分配器。下图给出了该函数的代码流程图。

 

kmem_cache_free 立即调用了__cache_free 参数直接传递过去。其原因也是防止kfree 中实现重复代码。

类似于分配,根据per-CPU缓存的状态不同,有两种可选的操作流程。

         如果per-CPU缓存中的空闲对象数目低于允许的限制,则调用ac_put_obj在其中存储一个指向缓存中对象的指针。

 

//mm/slab.c

 

static inline void ac_put_obj(struct kmem_cache *cachep,struct array_cache *ac,

                                void*objp)

{

    if(unlikely(sk_memalloc_socks()))

        objp = __ac_put_obj(cachep, ac, objp);

 

    ac->entry[ac->avail++]= objp;

}

         否则,必须将一些对象(准确的数目由array_cache->batchcount 给出)从缓存移回slab,从编号最低的数组元素开始:缓存的实现依据先进先出原理,这些对象在数组中已经很长时间,因此不太可能仍然驻留在CPU高速缓存中。

具体的实现委托给cache_flusharray 。该函数又调用了free_block ,将对象从缓存移动到原来的slab,并将剩余的对象向数组起始处移动。例如,如果缓存中有30个对象的空间,而batchcount 15,则位置014的对象将移回slab。剩余编号1529的对象则在缓存中向上移动,现在占据位置014

 

例外情况:slab缓存中空闲对象的数目超过预定义的限制cachep->free_limit 。在这种情况下,使用slab_destroy 将整个slab返回给伙伴系统。

 

如果slab同时包含使用和未使用对象,则插入到slabs_partial 链表。

销毁对象缓存(kmem_cache_destroy)

如果要销毁只包含未使用对象的一个缓存,则必须调用kmem_cache_destroy 函数。该函数主要在删除模块时调用,此时需要将分配的内存都释放。当然这不是强制性的。如果模块需要获取持久内存,在卸载后一直保存到下一次装载时(当然,需要假定系统在此期间没有重启),它可以保留一个缓存,以便重用其中的数据。

 

由于该函数的实现没什么新东西,下面我们只是概述一下删除缓存的主要步骤。

         kmem_cache-> refcount--; 如果refcount == 0, 则继续下面的动作, 否则结束退出, 不销毁当前对象缓存.

         调用shutdown_cache进行实际性的销毁动作

         __kmem_cache_shutdown

       释放per-CPU缓存

       针对每个内存节点, 释放节点下所有的slab缓存

通用缓存

如果不涉及对象缓存,而是传统意义上的分配/释放内存,则必须调用kmalloc kfree 函数。这两个函数,相当于用户空间中C标准库malloc free 函数的内核等价物。

 

kmalloc kfree 实现为slab分配器的前端,其语义尽可能地模仿malloc/free 。因此我们只简单讨论一下其实现。

kmalloc 的实现

kmalloc实现的基础是一个数组, 数组的每个元素都代表一个对象缓存:

//mm/slab_common.c

 

struct kmem_cache *kmalloc_caches[KMALLOC_SHIFT_HIGH +1]

该数组是在kmem_cache_init阶段初始化的.

 

kmalloc的申明如下:

static __always_inline void*kmalloc(size_t size, gfp_t flags)

每次我们调用kmalloc分配一段内存时, slab系统都会根据size从上述数组中选择一个合适的对象缓存, 然后从该对象缓存中分配一个对象.

 

选择对象缓存的函数如下:

kmalloc -> __kmalloc -> __do_kmalloc -> kmalloc_slab.

//mm/slab_common.c

 

struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)

{

    int index;

 

    if(unlikely(size > KMALLOC_MAX_SIZE)){

        WARN_ON_ONCE(!(flags & __GFP_NOWARN));

        returnNULL;

    }

 

    if(size <=192){

        if(!size)

            return ZERO_SIZE_PTR;

 

        index = size_index[size_index_elem(size)];

    }else

        index = fls(size -1);

 

#ifdef CONFIG_ZONE_DMA

    if(unlikely((flags & GFP_DMA)))

        return kmalloc_dma_caches[index];

 

#endif

    return kmalloc_caches[index];

}

代码就不解释了, 当选择到合适的对象缓存后, 会针对该对象缓存调用slab_alloc函数, 前文《分配对象(kmem_cache_alloc)》一节介绍过该函数, 这里就不多说了.

kfree 的实现

kfree同样易于实现:

//mm/slab.c

 

void kfree(constvoid*objp)

{

    struct kmem_cache *c;

    unsignedlong flags;

 

    trace_kfree(_RET_IP_, objp);

 

    if(unlikely(ZERO_OR_NULL_PTR(objp)))

        return;

    local_irq_save(flags);

    kfree_debugcheck(objp);

    c = virt_to_cache(objp);

    debug_check_no_locks_freed(objp, c->object_size);

 

    debug_check_no_obj_freed(objp, c->object_size);

    __cache_free(c,(void*)objp, _RET_IP_);

    local_irq_restore(flags);

}

通过对象地址得到对象缓存, 然后针对该对象缓存调用__cache_free函数, 前文《释放对象(kmem_cache_free)》一节介绍过该函数, 不多说了.

posted @ 2020-12-13 17:58  johnliuxin  阅读(1090)  评论(0)    收藏  举报