
[linux内存管理] 第22篇 buddy内存管理之慢速分配
iliuqi0. 前言
在上一篇文章中我们分析了__alloc_pages
中的get_page_from_freelist
,也就是快速分配部分。这个函数会根据分配掩码和分配order进行快速分配,若快速分配过程并不能分配到合适的内存时,则会进入慢速分配的过程。
本文紧接前文继续分析__alloc_pages
函数,继续剖析buddy内存分配的另一个过程:慢速分配
1. __alloc_pages_slowpath
当快速路径分配内存失败时,内核会调用这个函数来尝试各种方法(如回收内存、整理内存碎片、甚至启动 OOM 杀手)来成功分配内存
1 | /* |
下面针对一些重要的细节单独分析:
1.1 can_direct_reclaim
这个参数是用来表示是否允许调用直接内存回收的,那些隐含了 __GFP_DIRECT_RECLAIM 标志的分配掩码都会使用直接页面回收机制,如常用的 GFP_KERNEL、GFP_KERNEL_ACCOUNT、GFP_NOWAIT、GFP_NOIO、GFP_NOFS、GFP_USER、GFP_HIGHUSER_MOVABLE 等
1.2 costly_order
costly_order 高成本的申请,表示会形成一定的内存分配压力。 PAGE_ALLOC_CONSTLY_ORDER 定义为3,如当分配请求 order 为4,即要分配 64KB 大小的物理内存,会给页面分配器带来一定的内存压力
1.3 检查滥用__gfp_ATOMIC
1 | if (WARN_ON_ONCE((gfp_mask & (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)) == |
__GFP_ATIOMIC 表示调用页面分配器的进程不能直接回收页面或休眠等待,调用者通常在中断上下文中,使用与 GFP_KERNEL 相反的,GFP_KERNEL 是内核分配的常用标志之一,它 可能会被阻塞,即分配过程中可能会睡眠,在中断下文和不能睡眠的内核路径里使用该类型标志需要特别警惕,因为这个会引起死锁或者其他系统异常
另外,__GFP_ATOMIC 是优先级比较高的分配行为,它允许访问部分的系统预留内存
1.4 尝试直接内存规整
1 | if (can_direct_reclaim && |
若以最低警戒水位无法分配到内存,可以在满足以下的条件时,考虑尝试先进行 直接内存规整 来解决页面分配失败的问题
- can_direct_reclaim 为true,即允许调用直接页面回收机制;
- 高成本申请,这时系统有可能有足够的空闲内存,但是没有满足分配需要的 order,调用内存规整机制可能解决这个问题。或者order 大于0,但申请非 MOVABLE 页面;
- 不能访问系统预留内存。 gfp_pfmemalloc_allowed() 函数表示是否允许访问系统预留的内存,若返回 ALLOC_NO_WATERMARKS,表示不用考虑水位;若返回 0,表示不允许访问系统预留内存;
上面三个条件是并且关系,且内存规整的priority 是COMPACT_PRIO_ASYNC,即异步模式。
1.5 __alloc_pages_direct_reclaim
直接内存回收的核心处理函数,后面单独出一篇分析
TODO
1.6 __alloc_pages_direct_compact
直接内存规整的核心处理函数,后面单独出一篇分析
TODO
1.7 warn_alloc
如果经过一些列的尝试之后还是无法分配到需要的page 时,会调用 warn_alloc() 来宣告此次内存分配失败,输出内核打印信息:
1 | fail: |
输出信息:
- 基本信息;
- 此次分配内存的进程名
- 字符串 page allocation failure: //// 这个也是稳定性经常遇到的问题的关键字
- order
- gfp_mask
- 等等
- 上下文调用堆栈
- 系统内存信息
1 |
|
例如这里,free pages 有39540kB,min 水位为 39544kB,已经处于内存严重不足的情况,在经过直接内存回收并不能回收到需要分配的页面时,就会进入到这里
2. 慢速分配的流程图
3. 页框分配器总结
伙伴系统分配可用页框给申请者时,首先会根据zonelist对每个可用的zone进行快速分配,成功则返回第一个页框的页描述符,如果所有zone的快速分配都不成功,则会zonelist中的zone进行慢速分配,慢速分配中会进行内存回收、内存压缩和唤醒kswapd线程也同时进行内存的回收工作,之后再尝试继续分配
在快速分配中,如果条件允许会以low阀值遍历两次zonelist中的zone,整个快速分配的流程是:从zonelist中取出一个zone,检查此zone标志判断是否可通过此zone分配内存,如果 zone的空闲内存 - 需要申请的内存 < 阀值 ,伙伴系统则会将zone的一些快速内存回收,然后再次判断阀值和空闲内存与申请内存大小直接的关系,如果 zone的空闲内存 - 需要申请的内存 > 阀值,则调用buffered_rmqueue()函数从此zone中的分配内存,否则,选取下一个zone继续执行这段操作。当zonelist中的所有zone都遍历完成后,还是没有分配到内存,如果条件允许会再次遍历一遍。由于在慢速过程中也会调用此函数进行快速内存分配,所以阀值是由调用者传进来,因为不同情况使用的阀值是不同的,比如第一次快速分配过程中,会使用zone的low阀值进行分配,而进入慢速分配过程中,会使用min阀值进行分配。
在伙伴系统中有一个每CPU高速缓存,里面保存着以migratetype分类的单页框的双向链表,当申请内存者只需要一个页框时,内核会从每CPU高速缓存中相应类型的单页框链表中获取一个页框交给申请者,这样的好处是,但释放单个页框时会放入每CPU高速缓存链表,如果这时有需要申请单个页框,就把这个刚刚释放的页框交付出去,因为这个页框可能还存在于cache中,处理时就可直接处理cache而不用把这个页框再放入cache中,提高了cache的命中率,这样的页框就称为“热”页。每CPU高速缓存维护的这些所有类型的单页框双向链表时,把刚释放的页框从链表头插入,申请“热”页时就从链表头拿出页框,申请“冷”页时则从链表位拿出。
最后整理一下,如果一次分配,从开始到最后都没有成功,所走的路径是:
- 遍历zonelist,从zonelist中获取一个zone
- 检查zone如果分配后,空闲页框是否会低于allow_low
- 对此zone回收一些文件映射页和slab使用的页
- 再次检查zone如果分配后,空闲页框是否会低于allow_low
- 尝试从此zone分配页框(1个页优先从每CPU高速缓存分配,连续页框优先从需要的类型(migratetype)分配,如果不行再从其他migratetype分配)
- free_order小于11的情况, free_order++, 再次尝试第5步.如果free_order大于等于11, 则走第7步
- 跳到第1步,遍历zonelist结束则到下一步
- 再重新遍历zonelist一次,如果重新遍历过则到下一步
- 进入慢速分配
- 唤醒所有kswapd内核线程
- 再次尝试一次1~7步骤进行分配
- 如果有ALLOC_NO_WATERMARKS,则尝试分配预留的内存
- 进行异步内存压缩,然后尝试分配内存
- 尝试调用__alloc_pages__direct_reclaim()进行内存回收,然后尝试分配内存
- 使用oom杀掉oom_score较大的进程,每个进程都有一个oom_score(在/proc/PID/oom_score)
- 尝试轻同步内存压缩,然后尝试分配内存
- 压缩后再次尝试1~7步骤进行分配