Linux内核机制总结内存管理之反碎片技术
重要:本系列文章内容摘自<Linux内核深度解析>基于ARM64架构的Linux4.x内核一书,作者余华兵。系列文章主要用于记录Linux内核的大部分机制及参数的总结说明
1 反碎片技术
内存碎片分为内部碎片和外部碎片,内部碎片指内存页里面的碎片,外部碎片指空闲的内存页分散,很难找到一组物理地址连续的空闲内存页,无法满足超过一页的内存分配请求。
对于内核来说,外部碎片是一个问题,内核有时候需要分配超过一页的物理内存,因为内核使用线性映射区域的虚拟地址,所以必须分配连续的物理页。
如果进程使用巨型页,外部碎片是一个问题,因为巨型页需要连续的物理页。
为了解决外部碎片问题,内核引入了以下反碎片技术: (1)2.6.23版本引入了虚拟可移动区域。
(2)2.6.23版本引入了成块回收(lumpy reclaim,有的书中翻译为集中回收),3.5版本废除,被内存碎片整理技术取代。
成块回收不是一个完整的解决方案,它只是缓解了碎片问题。成块回收,就是尝试成块回收目标页相邻的页面,以形成一块满足需求的高阶连续页块。这种方法有其局限性,就是成块回收时没有考虑被连带回收的页面可能是“热页”,即被高强度使用的页,这对系统性能是损伤。
(3)2.6.24版本引入了根据可移动性分组的技术,把物理页分为不可移动页、可移动页和可回收页3种类型,在之前文章已经介绍了这种反碎片技术。
(4)2.6.35版本引入了内存碎片整理技术。
虚拟可移动区域和根据可移动性分组是预防外部碎片的技术,成块回收和内存碎片整理是在出现外部碎片以后消除外部碎片的技术。
【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)


1.1 虚拟可移动区域
可移动区域(ZONE_MOVABLE)是一个伪内存区域,基本思想很简单:把物理内存分为两个区域,一个区域用于分配不可移动的页,另一个区域用于分配可移动的页,防止不可移动页向可移动区域引入碎片。
1.使用方法 可移动区域必须由管理员配置,配置方法如下: (1)使用内核引导参数“kernelcore=nn[KMGTPE]”(K表示单位是KB,M表示单位是MB)指定不可移动区域的大小;也可以使用“kernelcore=mirror”指定使用镜像的内存作为不可移动区域,使用其他内存作为可移动区域。
内存镜像是内存冗余技术的一种,是为了提高服务器的可靠性,防止内存故障导致服务器的数据永久丢失或者系统宕机。内存镜像的工作原理与硬盘的热备份类似,内存镜像是将内存数据做两个拷贝,分别放在主内存和镜像内存中。系统工作时会向两个内存中同时写入数据,因此使得内存数据有两套完整的备份。
(2)使用内核引导参数“movablecore=nn[KMG]”指定可移动区域的大小。
(3)如果同时指定参数kernelcore和movablecore,那么不可移动区域的大小取参数kernelcore和(物理内存容量 − 参数movablecore)的最大值。
默认认为巨型页是不可移动的,不会从可移动区域分配巨型页,可以通过文件“/proc/ sys/vm/hugepages_treat_as_movable”配置允许从可移动区域分配巨型页。
在NUMA系统上,如果打开配置宏CONFIG_MOVABLE_NODE(允许一个内存节点只有可移动的内存),并且指定内核引导参数“movable_node”,那么忽略内核引导参数“kernelcore”和“movablecore”,所有可以热插拔的物理内存都作为可移动区域。
2.技术原理 可移动区域(ZONE_MOVABLE)没有包含任何物理内存,所以我们说它是伪内存区域,或者说是虚拟的内存区域。可移动区域借用最高内存区域的内存,在32位系统上最高的内存区域通常是高端内存区域(ZONE_HIGHMEM),在64位系统上最高的内存区域通常是普通区域(ZONE_NORMAL)。
(1)解析内核引导参数。 (2)确定可移动区域的范围。 函数find_zone_movable_pfns_for_nodes确定可移动区域的范围。
1)确定可移动区域从哪个内存区域借用物理页:调用函数find_usable_zone_for_movable以查找包含物理页的最高内存区域,全局变量movable_zone保存借用区域的索引。
2)确定每个内存节点中可移动区域的起始物理页号,使用全局数组zone_movable_pfn[MAX_NUMNODES]保存,分3种情况:
使用可以热插拔的物理内存作为可移动区域。
使用镜像内存作为不可移动区域,使用其他内存作为可移动区域。
如果管理员配置了不可移动区域或可移动区域的大小,处理如下:如果同时指定参数kernelcore和movablecore,那么不可移动区域的大小取参数kernelcore和(物理内存容量 − 参数movablecore)的最大值;把不可移动区域的内存按比例分布到所有内存节点上。
函数calculate_node_totalpages负责计算一个内存节点中所有内存区域的起始物理页号和物理页总数,针对每个内存区域,调用函数zone_spanned_pages_in_node来计算内存区域的起始物理页号和结束物理页号。
1)从全局数组arch_zone_lowest_possible_pfn和arch_zone_highest_possible_pfn中分别得到内存区域的起始物理页号和结束物理页号。
2)调用函数adjust_zone_range_for_zone_movable,根据数组zone_movable_pfn修正借用区域的结束物理页号,以及得到可移动区域的起始物理页号。
(3)从可移动区域分配物理页。
申请物理页的时候,如果同时指定了分配标志__GFP_HIGHMEM和__GFP_MOVABLE,页分配器的核心函数__alloc_pages_nodemask(-> prepare_alloc_pages -> gfp_zone)计算出首选的内存区域是可移动区域,首先尝试从可移动区域分配物理页。如果可移动区域分配失败,从备用的内存区域借用物理页。
分配标志__GFP_MOVABLE有两个用处。 1)和__GFP_HIGHMEM组合表示从可移动区域分配物理页。 2)在根据可移动性分组技术中表示申请迁移类型是可移动类型的物理页。
为用户空间分配物理页时,通常使用分配标志组合GFP_HIGHUSER_MOVABLE,这个组合包含标志__GFP_HIGHMEM和__GFP_MOVABLE。
例如,进程访问匿名页的时候,如果没有映射到物理页,生成页错误异常。页错误异常处理程序在函数do_anonymous_page中调用函数alloc_zeroed_user_highpage_movable以分配物理页,函数alloc_zeroed_user_highpage_movable使用分配标志组合GFP_HIGHUSER_MOVABLE分配物理页。
1.2 内存碎片整理
内存碎片整理(memory compaction,直译为“内存紧缩”,意译为“内存碎片整理”)的基本思想是:从内存区域的底部扫描已分配的可移动页,从内存区域的顶部扫描空闲页,把底部的可移动页移到顶部的空闲页,在底部形成连续的空闲页。
1.使用方法 编译内核时,如果需要内存碎片整理功能,必须开启配置文件“mm/Kconfig”定义的配置宏CONFIG_COMPACTION,默认开启。
内存碎片整理技术提供了以下配置文件: (1)文件“/proc/sys/vm/compact_memory”:向这个文件写入任何整数值(数值没有意义),触发内存碎片整理。 (2)文件“/proc/sys/vm/compact_unevictable_allowed”:用来设置是否允许内存碎片整理移动不可回收的页(进程使用系统调用mlock把页锁定在内存中),如果设置为1,表示允许,默认值是1。 (3)文件“/proc/sys/vm/extfrag_threshold”:用来设置外部碎片的阈值,取值范围是 0~1000,默认值是500。这个参数影响内核在申请连续页失败的时候选择直接回收页还是选择内存碎片整理。内核计算出内存区域的碎片指数,碎片指数趋向0表示分配失败是因为内存不足,碎片指数趋向1000表示分配失败是因为内存碎片。如果碎片指数小于或等于外部碎片的阈值,选择直接回收页;如果碎片指数大于阈值,那么选择内存碎片整理。
2.技术原理 我们假设有一个很小的内存区域,包含16个页,如下所示:

白色表示页是空闲的,这个内存区域已经碎片化。最大的连续空闲页是两页,从这个区域分配四页将会失败,甚至分配两页也会失败,因为连续的两个空闲页的起始地址没有对齐到两页的整数倍。
首先,内存碎片整理算法从内存区域的底部向顶部扫描,把可以移动的已分配页组成一条链表,我们把这个扫描称为迁移扫描器,如下所示:

然后,内存碎片整理算法从内存区域的顶部向底部扫描,把空闲的页组成一条链表,我们把这个扫描称为空闲扫描器,如下所示:

最后,迁移扫描器和空闲扫描器在内存区域的中间相遇,把可以移动的已分配页移到顶部的空闲页,形成连续的8个空闲页,可以满足申请连续8页的需求,如下所示:

在真实的系统中,内存区域大得多,内存碎片整理以内存区域为单位执行,在内存区域内部以分组页块为单位执行。 内存碎片整理的算法如下: (1)首先从内存区域的底部向顶部以页块为单位扫描,在页块内部从起始页向结束页扫描,把这个页块里面的可移动页组成一条链表。 (2)然后从内存区域的顶部向底部以页块为单位扫描,在页块内部也是从起始页向结束页扫描,把空闲页组成一条链表。 (3)最后把底部的可移动页的数据复制到顶部的空闲页,修改进程的页表,把虚拟页映射到新的物理页。
内存碎片整理有3种优先级,从高到低依次如下所示: (1)COMPACT_PRIO_SYNC_FULL:完全同步模式,允许阻塞,允许把脏的文件页回写到存储设备上,并且等待回写完成。 (2)COMPACT_PRIO_SYNC_LIGHT:轻量级同步模式,允许大多数操作阻塞,但是不允许把脏的文件页回写到存储设备上(因为可能需要等待很长的时间)。 (3)COMPACT_PRIO_ASYNC:异步模式,不允许阻塞。
完全同步模式的成本最高,轻量级同步模式的成本其次,异步模式的成本最低。
执行内存碎片整理的时机如下: (1)页分配器使用最低水线分配页失败以后,如果调用者允许直接回收页(即设置了分配标志__GFP_DIRECT_RECLAIM)和写存储设备(即设置了分配标志__GFP_IO),并且是昂贵的分配(申请的阶数大于 3)或者申请不可移动类型的连续页,那么在尝试直接回收页之前,先尝试执行异步模式的内存碎片整理。 (2)页分配器直接回收页以后分配连续页仍然失败,如果调用者允许写存储设备,尝试执行轻量级同步模式的内存碎片整理。 (3)每个内存节点有一个页回收线程和一个内存碎片整理线程,当页回收线程准备睡眠一小段时间的时候,唤醒内存碎片整理线程,内存碎片整理线程执行轻量级同步模式的内存碎片整理。 内存碎片整理线程的名称是“kcompactd<node_id>”,内存节点的pglist_data 实例的成员“kcompactd”指向内存碎片整理线程的进程描述符。 (4)当管理员向文件“/proc/sys/vm/compact_memory”写入任何整数值的时候,在所有内存节点的所有内存区域上执行完全同步的内存碎片整理。
判断一个内存区域是否适合执行内存碎片整理的标准如下: (1)如果管理员通过写文件“/proc/sys/vm/compact_memory”触发内存碎片整理,那么这个内存区域强制执行内存碎片整理。 (2)如果内存区域同时满足以下3个条件,适合执行内存碎片整理。 1)如果(空闲页数 − 申请页数)低于水线,或者虽然大于或等于水线但是没有一个足够大的空闲页块,那么这个内存区域适合执行内存碎片整理。 2)如果(空闲页数 − 两倍的申请页数)大于或等于水线,说明有足够多的空闲页作为迁移的目的地,那么这个内存区域适合执行内存碎片整理。 3)对于昂贵的分配(阶数大于3),计算碎片指数(fragmentation index)。如果碎片指数在范围[0,外部碎片的阈值]以内,说明分配失败是内存不足导致的,不是外部碎片导致的,那么这个内存区域不适合执行内存碎片整理。
如果不存在空闲页块,那么碎片指数 = 0。 如果至少存在一个足够大的空闲页块,那么碎片指数 = −1000。 其他情况,碎片指数 = 1000 −(1000 + 1000 * 空闲页数 / 申请页数)/ 空闲页块的总数 碎片指数趋向0表示分配失败是因为内存不足,趋向1000表示分配失败是因为外部碎片。外部碎片的阈值是内存不足和外部碎片的分界线:如果碎片指数小于或等于阈值,分配失败是因为内存不足,应该直接回收页;如果碎片指数大于阈值,分配失败是因为外部碎片,应该执行内存碎片整理。
内存碎片整理的结束条件如下: (1)如果迁移扫描器和空闲扫描器相遇,那么内存碎片整理结束。 (2)如果迁移扫描器和空闲扫描器没有相遇,但是申请或备用的迁移类型至少有一个足够大的空闲页块,那么内存碎片整理结束。
如果管理员通过写文件“/proc/sys/vm/compact_memory”触发内存碎片整理,结束的唯一条件是迁移扫描器和空闲扫描器相遇。
内存碎片整理推迟
内存碎片整理成功的标准是:(空闲页数 − 申请页数)大于或等于水线,并且申请或备用的迁移类型至少有一个足够大的空闲页块。
执行完全同步模式或轻量级同步模式的内存碎片整理,当迁移扫描器和空闲扫描器相遇的时候,没有达到成功标准,以后试图执行轻量级同步或异步模式的内存碎片整理,如果申请阶数大于或等于内存碎片整理失败时的申请阶数,需要推迟若干次。
内核在内存区域中增加了3个成员用来记录内存碎片整理推迟的信息:
(1)成员compact_considered记录推迟的次数。 (2)成员compact_defer_shift是推迟的最大次数以2为底的对数,当推迟的次数达到(1 << compact_defer_shift)时,不能推迟。
每次内存碎片整理执行失败,把成员compact_defer_shift加1,不允许超过COMPACT_MAX_DEFER_SHIFT(值为6),即把推迟的最大次数翻倍,但是不能超过64。
页分配器在执行内存碎片整理以后,如果分配页成功,那么把成员compact_defer_shift设置为0。
(3)成员compact_order_failed记录内存碎片整理失败时的申请阶数。 内存碎片整理执行成功的时候,如果申请阶数order大于或等于成员compact_order_failed,那么把成员compact_order_failed设置为(order+1)。
内存碎片整理执行失败的时候,如果申请阶数order小于成员compact_order_failed,那么把成员compact_order_failed设置为order。
3.代码分析 如下所示,函数__alloc_pages_nodemask是页分配器的核心函数,函数__alloc_pages_slowpath是页分配器的慢速路径,执行内存碎片整理的代码如下:

