概览
为了支持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;
...
}
...
}