内存管理(二)Zone和Watermark

概览

为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点

  • 首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为pg_data_t的实例. 系统中每个节点被链接到一个以NULL结尾的pgdat_list链表中,而其中的每个节点利用pg_data_tnode_next字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构.
  • 接着各个节点又被划分为内存管理区域, 一个管理区域通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射.

Node,Zone和Page
在这里插入图片描述

zone

* kernel/msm-5.4/include/linux/mmzone.h
enum zone_type {
#ifdef CONFIG_ZONE_DMA
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	ZONE_DMA32,
#endif
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
	ZONE_DEVICE,
#endif
	__MAX_NR_ZONES

};
管理内存域描述
ZONE_DMA在物理内存的低端,主要是ISA设备只能用低端的地址做DMA操作。标记了适合DMA的内存域. 该区域的长度依赖于处理器类型. 这是由于古老的ISA设备强加的边界. 但是为了兼容性, 现代的计算机也可能受此影响
ZONE_DMA32标记了使用32位地址字可寻址, 适合DMA的内存域. 显然, 只有在53位系统中ZONE_DMA32才和ZONE_DMA有区别, 在32位系统中, 本区域是空的, 即长度为0MB, 在Alpha和AMD64系统上, 该内存的长度可能是从0到4GB
ZONE_NORMAL标记了可直接映射到内存段的普通内存域. 这是在所有体系结构上保证会存在的唯一内存区域, 但无法保证该地址范围对应了实际的物理地址. 例如, 如果AMD64系统只有两2G内存, 那么所有的内存都属于ZONE_DMA32范围, 而ZONE_NORMAL则为空
ZONE_HIGHMEM保留给系统使用,是系统中预留的可用内存空间。现在64位系统已经不再使用
ZONE_MOVABLE内核定义了一个伪内存域ZONE_MOVABLE, 在防止物理内存碎片的机制memory migration中需要使用该内存域. 供防止物理内存碎片的极致使用
ZONE_DEVICE为支持热插拔设备而分配的Non Volatile Memory非易失性内存
MAX_NR_ZONES充当结束标记, 在内核中想要迭代系统中所有内存域, 会用到该常亮

在64位系统中, 并不需要高端内存, 因为AM64的linux采用4级页表,支持的最大物理内存为64TB, 对于虚拟地址空间的划分,将0x0000,0000,0000,0000 – 0x0000,7fff,ffff,f000这128T地址用于用户空间;而0xffff,8000,0000,0000以上的128T为系统空间地址, 这远大于当前我们系统中的内存空间, 因此所有的物理地址都可以直接映射到内核中, 不需要高端内存的特殊映射

* kernel/msm-5.4/include/linux/mmzone.h
struct zone {
// 该管理区的三个水平线值,min,low,high
	unsigned long _watermark[NR_WMARK];
	...
	//这个zone区域保留的内存,当系统内存出现不足的时候,
	// 系统就会使用这些保留的内存来做一些操作,比如使用保留的内存进程用来可以释放更多的内存
	long lowmem_reserve[MAX_NR_ZONES];
...
 // 指向这个zone所在的pglist_data对象
	struct pglist_data	*zone_pgdat;
	// 这个数组用于实现每个CPU的热/冷页帧列表。
	//内核使用这些列表来保存可用于满足实现的“新鲜”页。
	//但冷热页帧对应的高速缓存状态不同:
	// 有些页帧很可能在高速缓存中,因此可以快速访问,故称之为热的;
	// 未缓存的页帧与此相对,称之为冷的
	struct per_cpu_pageset __percpu *pageset;
...
 // 通过buddy管理的所有可用的页,计算公式是:present_pages - reserved_pages
	unsigned long       managed_pages;
  //个zone中所有的页,包含空洞,计算公式是:   zone_end_pfn - zone_start_pfn 
	unsigned long		spanned_pages;
	//这个zone中可用的所有物理页,不包含空洞,,计算公式是:spanned_pages-hole_pages
	unsigned long		present_pages;
	//指向管理区的传统名字, "DMA", "NROMAL"或"HIGHMEM" */
	const char		*name;
	...
	/*页面使用状态的信息,以每个bit标识对应的page是否可以分配
       是用于伙伴系统的,每个数组元素指向对应阶也表的数组开头
       以下是供页帧回收扫描器(page reclaim scanner)访问的字段
       scanner会跟据页帧的活动情况对内存域中使用的页进行编目
       如果页帧被频繁访问,则是活动的,相反则是不活动的,
       主要用于维护空闲的页,其中数组的下标对应页的order数。最大order目前是11*/
	struct free_area	free_area[MAX_ORDER];
	// 描述了内存域的当前状态
    unsigned long		flags;
    // 维护了大量有关该内存域的统计信息
    // enum zone_stat_item枚举变量标识
   atomic_long_t		vm_stat[NR_VM_ZONE_STAT_ITEMS];
...
};

  • spanned_pages: 代表的是这个zone中所有的页,包含空洞,计算公式是: zone_end_pfn - zone_start_pfn
  • present_pages: 代表的是这个zone中可用的所有物理页,计算公式是:spanned_pages-hole_pages
  • managed_pages: 代表的是通过buddy管理的所有可用的页,计算公式是:present_pages - reserved_pages
  • 三者的关系是: spanned_pages > present_pages > managed_pages
    超过高水位的页数计算方法是:managed_pages减去watermark[HIGH]
free_area

用于维护空闲的页,其中数组的下标对应页的order数。free_area共有MAX_ORDER个元素,其中第order个元素记录了2 ^ order的空闲块,这些空闲块在free_list中以双向链表的形式组织起来,对于同等大小的空闲块,其类型不同,将组织在不同的free_list中,nr_free记录了该free_area中总共的空闲内存块的数量。MAX_ORDER的默认值为11,这意味着最大内存块的大小为2^10=1024个页框。对于同等大小的内存块,每个内存块的起始页框用于链表的节点进行相连,这些节点对应的着struct page中的lru域

struct free_area {
 // 用于将各个order的free page链接在一起
    struct list_head    free_list[MIGRATE_TYPES];
    // 代表这个order中还有多个空闲page
    unsigned long       nr_free;
};

enum migratetype {
	MIGRATE_UNMOVABLE,
	MIGRATE_MOVABLE,
	MIGRATE_RECLAIMABLE,
#ifdef CONFIG_CMA
	MIGRATE_CMA,
#endif
	MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
	MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_MEMORY_ISOLATION
	MIGRATE_ISOLATE,	/* can't allocate from here */
#endif
	MIGRATE_TYPES
};

free_area结构中存有MIGRATE_TYPES个free_list,这些数组是根据页框的移动性来划分的

  • MIGRATE_UNMOVABLE: 不可移动的页,这类页在内存当中有固定的位置,不能移动。内核的核心分配的内存大多属于这种类型
  • MIGRATE_MOVABLE:可以移动的页,用户空间应用程序所用到的页属于该类别。它们通过页表来映射,如果他们复制到新的位置,页表项也会相应的更新,应用程序不会注意到任何改变。当出现内存碎片的时候,就可以移动此页,腾出更多连续的空间
  • MIGRATE_RECLAIMABLE:可以回收的页,这类页不能直接移动,但可以删除,其内容页可以从其他地方重新生成,例如,映射自文件的数据属于这种类型,针对这种页,内核有专门的页面回收处理
  • MIGRATE_CMA:用于专门CMA申请的页,便于进行连续物理内存申请的一块区域
  • MIGRATE_PCPTYPES:per_cpu_pageset,即用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目
  • MIGRATE_HIGHATOMIC:高阶原子分配
  • MIGRATE_ISOLATE:隔离,不能从此分配页

可以通过我当前的设备,查看page的信息cat /proc/pagetypeinfo

Page block order: 10
Pages per block:  1024
Free pages count per migrate type at order       0      1      2      3      4      5      6      7      8      9     10 
Node    0, zone   Normal, type    Unmovable     97     18      4      1      1      1      1      1     12      1      0 
Node    0, zone   Normal, type      Movable    959    905    428    283    105     32     11      4      2      1      0 
Node    0, zone   Normal, type  Reclaimable      6     23    437    180     28     12      2      1      0      0      0 
Node    0, zone   Normal, type          CMA      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone   Normal, type   HighAtomic      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone   Normal, type      Isolate      0      0      0      0      0      0      0      0      0      0      0 

Number of blocks type     Unmovable      Movable  Reclaimable          CMA   HighAtomic      Isolate 
Node 0, zone   Normal          385          849           30           97            0            0 

可以很清晰的看到各个order中不同类型,不同zone,page的剩余情况。当然也可以从cat /proc/buddyinfo看各个page的剩余情况

Node 0, zone   Normal   1082    980    877    467    135     45     14      6     14      2      0 

随着时间的推移,order值最大的page就会慢慢的分解开,变为更小的order的page。这时候当申请一个连续的大page都没有的时候,就会做碎片整理操作

内存碎片测试就用通过分析Unmovable/(Unmovable+Movable+Reclaimable + CMA)占比来计算的

也可以通过cat /proc/zoneinfo去查看zone的详细信息的

pageset

内核经常请求和释放单个页框. 为了提升性能, 每个内存管理区都定义了一个每CPU(Per-CPU)的页面高速缓存. 所有”每CPU高速缓存”包含一些预先分配的页框, 他们被定义满足本地CPU发出的单一内存请求.

struct zone的pageset成员用于实现冷热分配器(hot-n-cold allocator)

内核说页面是热的, 意味着页面已经加载到CPU的高速缓存, 与在内存中的页相比, 其数据访问速度更快. 相反, 冷页则不再高速缓存中. 在多处理器系统上每个CPU都有一个或者多个高速缓存. 各个CPU的管理必须是独立的.

struct per_cpu_pageset {
	struct per_cpu_pages pcp;
...
};

struct per_cpu_pages {
 /*列表中的页数  */
	int count;		
	/* 页数上限水印, 在需要的情况清空列表
	 如果count的值超过了high, 则表明列表中的页太多了
	  */
	int high;	
	/*添加/删除多页块的时候, 块的大小 */	
	int batch;		
	/* 页的链表 , 保存了当前CPU的冷页或热页*/
	struct list_head lists[MIGRATE_PCPTYPES];
};

在内核中只有一个子系统会积极的尝试为任何对象维护per-cpu上的list链表, 这个子系统就是slab分配器.

struct per_cpu_pages维护了链表中目前已有的一系列页面, 高极值和低极值决定了何时填充该集合或者释放一批页面, 变量决定了一个块中应该分配多少个页面, 并最后决定在页面前的实际链表中分配多少各页面

pglist_data

zone是通过struct pglist_data管理的,pglist_date结构每个node是对应一个的,在numa机器上每个node对应一个pglist_data结构体,在Uma机器上只有一个pglist_data结构来描述整个内存

typedef struct pglist_data {
//描述此node下存在几个zone
    struct zone node_zones[MAX_NR_ZONES];
    // 备用zone的list
    // 当首选的zone去分配失败后,就会去备用zone去查找可用的page
    struct zonelist node_zonelists[MAX_ZONELISTS];
    int nr_zones;
    ...
 	//保留的总共的page
    unsigned long       totalreserve_pages;
    ...
    // 所有申请的page都会加到lru链表中,用于回收page时使用的
    // LRU链表中会根据不同的LRU类型分为不同的列表
    // 常见的有匿名活动页,匿名低活动页,活动的文件页,低活动的文件页等
    struct lruvec       lruvec;
 ...
} pg_data_t;

在这里插入图片描述
其中node_zonelist中有两种类型,分别为ZONELIST_FALLBACK和ZONELIST_NOFALLBACK

enum {
	ZONELIST_FALLBACK,	/* zonelist with fallback */
#ifdef CONFIG_NUMA
	ZONELIST_NOFALLBACK,	/* zonelist without fallback (__GFP_THISNODE) */
#endif
	MAX_ZONELISTS
};

通过pglist_data结构就可以完全的描述一个内存的layout了。

  • 通过pglist_data知道存在几个zone,每个zone中又存在freelist来表示各个order空闲的page,以及各个page是属于什么迁移类型
  • 当申请page的时候根据zone中的水位去申请,当内存不足的时候,就会开启内核swapd来回收内存
  • 每次申请的page都会挂到lru链表中,当出现内存不足的时候,就会根据lru算法找出那些page最近很少使用,然后释放

watermark

什么情况下触发direct reclaim,什么情况下又会触发kswapd,是由内存的watermark决定的

Linux中物理内存的每个zone都有自己独立的min, low和high三个档位的watermark值,在代码中以struct zone中的_watermark[NR_WMARK]来表示。

  • WMARK_MIN: 最低水位,代表内存显然已经不够用了。这里要分两种情况来讨论,一种是默认的操作,此时分配器将同步等待内存回收完成,再进行内存分配,也就是direct reclaim。还有一种特殊情况,如果内存分配的请求是带了PF_MEMALLOC(kswapd)标志位的,并且现在空余内存的大小可以满足本次内存分配的需求,那么也将是先分配,再回收.
    分配页面的动作和kswapd线程同步运行.WMARK_MIN所表示的page的数量值,是在内存初始化的过程中调用free_area_init_core中计算的。这个数值是根据zone中的page的数量除以一个>1的系数来确定的。通常是这样初始化的ZoneSizeInPages/12
  • WMARK_LOW:低水位,代表内存已经开始吃紧,则kswapd线程将被唤醒,并开始释放回收页面
  • WMARK_HIGH:高水位,代表内存还是足够的。 不需要回收, kswapd线程将重新休眠,通常这个数值是page_min的3倍
enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK
};
#define min_wmark_pages(z) (z->watermark[WMARK_MIN])
#define low_wmark_pages(z) (z->watermark[WMARK_LOW])
#define high_wmark_pages(z) (z->watermark[WMARK_HIGH])
#define wmark_pages(z, i) (z->_watermark[i] + z->watermark_boost)

在进行内存分配的时候,如果分配器(比如buddy allocator)发现当前空余内存的值低于"low"但高于"min",说明现在内存面临一定的压力,那么在此次内存分配完成后,kswapd将被唤醒,以执行内存回收操作。在这种情况下,内存分配虽然会触发内存回收,但不存在被内存回收所阻塞的问题,两者的执行关系是异步的。

这里所说的"空余内存"其实是一个zone总的空余内存减去其lowmem_reserve的值。对于kswapd来说,要回收多少内存才算完成任务呢?只要把空余内存的大小恢复到"high"对应的watermark值就可以了,当然,这取决于当前空余内存和"high"值之间的差距,差距越大,需要回收的内存也就越多。"low"可以被认为是一个警戒水位线,而"high"则是一个安全的水位线。

在这里插入图片描述

  • 我们分配页第一次尝试是从LOW水位开始分配的,当所剩余的空闲页小于LOW水位的时候,则会唤醒Kswapd内核线程进行内存回收
  • 如果回收内存效果很显著,当空闲页大于HIGH水位的时候,则会停止Kswapd内核线程回收
  • 如果回收内存效果不明显,当空闲内存直接小于MIN水位的时候,则会进行直接的内存回收(Direct-reclaim),这样空闲内存就会逐渐增大
  • 当回收效果依然不明显的时候,则会启动OOM杀死进程
Watermark的取值

在知道HIGH,MIN,LOW水位是如何获得的,先要知道min_free_kbytes值的含义

  • min_free_kbyes代表的是系统保留空闲内存的最低限
  • watermark[WMARK_MIN]的值是通过min_free_kbytes计算出来的

总的"min"值约等于所有zones可用内存的总和乘以16再开平方的大小,可通过"/proc/sys/vm/min_free_kbytes"查看和修改。假设可用内存的大小是4GiB,那么其对应的"min"值就是8MiB √410241024*16

/*
 * Initialise min_free_kbytes.
 *
 *	min_free_kbytes = 4 * sqrt(lowmem_kbytes), for better accuracy:
 *	min_free_kbytes = sqrt(lowmem_kbytes * 16)
 * 16MB:	512k
 * 32MB:	724k
 * 64MB:	1024k
 * 128MB:	1448k
 * 256MB:	2048k
 * 512MB:	2896k
 * 1024MB:	4096k
 * 2048MB:	5792k
 * 4096MB:	8192k
 * 8192MB:	11584k
 * 16384MB:	16384k
 */
int __meminit init_per_zone_wmark_min(void)
{
	...
	//lowmem_kbytes代表的意思是lowmem中超过高水位的页的总和
	// 这里的单位是kbytes, 这就是lowmem中超过high水位的页乘以4这就是lowmem_kbytes
	lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);
	new_min_free_kbytes = int_sqrt(lowmem_kbytes * 16);
...
		min_free_kbytes = new_min_free_kbytes;
		if (min_free_kbytes < 128)
			min_free_kbytes = 128;
		if (min_free_kbytes > 65536)
			min_free_kbytes = 65536;
	...
}

一个zone的"low"和"high"的值都是根据它的"min"值算出来的,"low"比"min"的值大1/4左右,"high"比"min"的值大1/2左右,三者的比例关系大致是4:5:6。

/**
 *   nr_free_zone_pages = managed_pages - high_pages
 * Return: number of pages beyond high watermark.
 */
static unsigned long nr_free_zone_pages(int offset)
{
	...
	struct zonelist *zonelist = node_zonelist(numa_node_id(), GFP_KERNEL);
	for_each_zone_zonelist(zone, z, zonelist, offset) {
		unsigned long size = zone_managed_pages(zone);
		unsigned long high = high_wmark_pages(zone);
		if (size > high)
			sum += size - high;
	}
	return sum;
}

对每个zone做计算,将每个zone中超过high水位的值放到sum中。超过高水位的页数计算方法是:managed_pages减去watermark[HIGH], 这样就可以获取到系统中各个zone超过高水位页的总和

在这里插入图片描述
现在的手机只有一个NORAML_ZONE,根据上面的公式managed_pages - watermark[WMARK_HIGH]的值就是这个NORMAL_ZONE中空闲的页面,然后经过int_sqrt可以计算出系统中可用空闲页面的最低限

使用"cat /proc/zoneinfo"可以查看这三个值的大小

cat /proc/zoneinfo
Node 0, zone   Normal
 pages free     82075
        min      2702
        low      10899
        high     11574
        spanned  2094976
        present  1959807
        managed  1911161
        protection: (0, 0)

cat /proc/sys/vm/min_free_kbytes
10811

把"/proc/zoneinfo"中所有zones的"min"值加起来乘以4(如果page size是4KiB的话),基本等于"/proc/sys/vm/min_free_kbytes"的值。

lowmem_kbytes = (1911161 - 11574 )*4= 1899587 *4 = 7598348
min_free_kbytes = int_sqrt(7598348 * 16) = 11026 ~= 10811

Watermark的调节

为了尽量避免出现direct reclaim,我们需要空余内存的大小一直保持在"min"值之上。在网络收发的时候,数据量可能突然增大,需要临时申请大量的内存,这种场景被称为"burst allocation"。此时kswapd回收内存的速度可能赶不上内存分配的速度,造成direct reclaim被触发,影响系统性能。

比如当前空闲内存是在LOW水位以下MIN以上,这时候后台会启动Kswaped内核线程在进程内存回收,假设这时候突然有一个很大的进程需要很大的内存请求,这样一来Kswaped回收速度赶不上分配速度,内存一下掉到了MIN水位,这样直接就进行了直接回收,直接回收很影响系统的性能的。这样看来linux原生的代码涉及MIN-LOW之间的间隙太小,很容易导致进入直接回收的情况的。所以在android的版本上增加了一个变量:extra_free_kbytes,这个"extra"是额外加在"low"与"min"之间的,它在保持"min"值不变的情况下,让"low"值有所增大,可以通过/proc/sys/vm/extra_free_kbytes调节
在这里插入图片描述

static void __setup_per_zone_wmarks(void)
{
    //将最小保留的内存转化为以page为单位,最小预留的空闲页
   unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10);
	unsigned long pages_low = extra_free_kbytes >> (PAGE_SHIFT - 10);
	...
	for_each_zone(zone) {
	...
		min = (u64)pages_min * zone->managed_pages;
		// 取商,得到的值就是min的值
	 	tmp = (u64)pages_min * zone_managed_pages(zone);
		do_div(tmp, lowmem_pages);
		 //low水位的值是通过extra_free_kbytes计算的
		low = (u64)pages_low * zone_managed_pages(zone);
		do_div(low, vm_total_pages);
	        ...
		//设置MIN水位的值
		zone->_watermark[WMARK_MIN] = tmp;
		tmp = max_t(u64, tmp >> 2,
				    mult_frac(zone_managed_pages(zone),
					      watermark_scale_factor, 10000));
		zone->watermark_boost = 0;
		  //LOW水位的值,2min + low
		zone->_watermark[WMARK_LOW]  = min_wmark_pages(zone) +
					low + tmp;
	    // 3min + low
		zone->_watermark[WMARK_HIGH] = min_wmark_pages(zone) +
					low + tmp * 2;
	...
	}
	...
}
  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值