文章目录
当linux kernel内存不足时,都需要回收一些最近很少使用的页面保证linux内核能持续有内存使用。内存回收方法主要有如下方式
- page回写:例如一个很少使用的dirty文件page,该page的内容已经和其映射的文件内容不一致,则现将文件page中的内容回写到磁盘文件中,再将该业释放为空闲内存,供内核分配使用
- page交换:例如一个很少使用的匿名文件page,该page的内容可以先交换到swap分区,然后将该page释放为空闲page,供内核分配使用
- page丢弃: 例如很少使用一个非dirty文件page,因为其page中的内容和磁盘中文件内容一致,可以直接将该page释放为空闲page,供内核分配使用
linux kernel触发内存回收的时机主要有以下几个时机:(1)内存分配时,内存不足触发内存回;(2)kswapd内核线程触发内存回收;(3)用户触发内存回收
内存紧张回收
linux kernel内存紧张时,不同的内存分配方式会触发不同的内存回收方法。下面以伙伴系统的内存分配函数alloc_pages函数为例,来分析在该内存分配的过程中那些时机会触发内存回收
快速内存回收
如下函数调用关系所示,快速内存回收触发在get_page_from_freelist()函数中,在遍历zonelist的过程中,每个zone在分配内存前都会进行一次内存检查,当内存分配后zone的空闲内存小于该zone的水线阈值和保留页框数量,这个时候该zone所在节点就会进行快速内存回收。注意上面提到的水线阈值是由当前场景中的alloc_flags决定。通常情况如下:
- 快速内存分配调用get_page_from_freelist时水线阈值取low
- 慢速内存分配过程中调用get_page_from_freelist时水线阈值取min值
/*
* alloc_pages()---> alloc_pages_nodes()---> __alloc_pages()---> alloc_pages_node_mask()
* ---> get_page_from_freelist()--->node_reclaim()---> __node_reclaim
*/
// /mm/page_alloc.c
static struct page *get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
const struct alloc_context *ac)
{
for_each_zone_zonelist_nodemask(zone, z, zonelist, ac->high_zoneidx,
ac->nodemask) {
//水线阀值
mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
//判断该zone内存是否紧张
if (!zone_watermark_fast(zone, order, mark,
ac_classzone_idx(ac), alloc_flags)) {
int ret;
...
if (node_reclaim_mode == 0 ||
!zone_allows_reclaim(ac->preferred_zoneref->zone, zone))
continue;
//回收
ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
switch (ret) {
...
}
}
}
node_reclaim该函数主要是对当前节点内存情况做一些判断,看是否能进行内存回收,它主要是通过调用函数__node_reclaim来执行本次内存回收。
在介绍__node_reclaim前需要先了解与页面回收相关的一个结构体struct scan_control
struct scan_control结构体
struct scan_control {
/*需要回收的页面数量 */
unsigned long nr_to_reclaim;
/*
*申请分配的掩码,用户申请页面时可以通过设置标志来限制调用底层文件系统或不允许读写存储设备,最终传递给LRU处理 */
gfp_t gfp_mask;
/* 申请分配内存块的阶,最终期望内存回收后,系统能分配该阶的连续内存块 */
int order;
/* 内存节点掩码,空指针则访问所有的节点.*/
nodemask_t *nodemask;
/*
* The memory cgroup that hit its limit and as a result is the
* primary target of this reclaim invocation.
*/
// 目标memcg,如果是针对整个zone进行的,则此为NULL
struct mem_cgroup *target_mem_cgroup;
/*
*扫描LRU链表的优先级,用于计算每次扫描页面的数量(total_size >> priority,初始值12),值越小,扫描的
*页面数越大,逐级增加扫描粒度
* (1)代表一次扫描(total_size >> priority)个页框
* (2)优先级越低,一次扫描的页框数量就越多
* (3)默认优先级为12
*/
int priority;
/* The highest zone to isolate pages for reclaim from */
enum zone_type reclaim_idx;
/*是否允许把修改过文件页写回存储设备,与分配标志的__GFP_IO和__GFP_FS有关*/
unsigned int may_writepage:1;
/* 是否取消页面的映射并进行回收处理,将所有映射了此页的页表项清空 */
unsigned int may_unmap:1;
/* 是否将匿名页交换到swap分区,并进行回收处理,将所有映射了此页的页表项清空 */
unsigned int may_swap:1;
/* Can cgroups be reclaimed below their normal consumption range? */
unsigned int may_thrash:1;
unsigned int hibernation_mode:1;
/* 扫描结束后会标记,用于内存回收判断是否需要进行内存压缩 */ */
unsigned int compaction_ready:1;
/* 统计扫描过的非活动页面总数 */
unsigned long nr_scanned;
/* 统计回收了的页面总数*/
unsigned long nr_reclaimed;
};
__node__reclaim函数介绍
/*
*node_reclaim()---> __node_reclaim
* Try to free up some pages from this node through reclaim.
*/
static int __node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask, unsigned int order)
{
/* Minimum pages needed in order to stay on node */
const unsigned long nr_pages = 1 << order;
struct task_struct *p = current;
struct reclaim_state reclaim_state;
//快速内存回收条件
struct scan_control sc = {
//需要回收的页数,最少回收32个页
.nr_to_reclaim = max(nr_pages, SWAP_CLUSTER_MAX),
//申请内存过程中进行页回收,此处代表内存分配使用的分配标志
.gfp_mask = memalloc_noio_flags(gfp_mask),
//内存分配内存块的阶order
.order = order,
//页回收优先级,越低,扫描的node的页数越多,此处为4
.priority = NODE_RECLAIM_PRIORITY,
//能否进行回写操作,/proc/sys/vm/zone_reclaim_mode
.may_writepage = !!(node_reclaim_mode & RECLAIM_WRITE),
//能否进行unmap操作,清除映射了页的所有进程中对应的pte
.may_unmap = !!(node_reclaim_mode & RECLAIM_UNMAP),
//是否可以页交换
.may_swap = 1,
//指定页回收的最大zone id
.reclaim_idx = gfp_zone(gfp_mask),
};
//调用cond_resched主动让出cpu,防止其在内核态执行时间过长导致可能发生的soft lockup或者造成较大的调度延迟
cond_resched();
/*
* We need to be able to allocate from the reserves for RECLAIM_UNMAP
* and we also need to be able to write out pages for RECLAIM_WRITE
* and RECLAIM_UNMAP.
*/
p->flags |= PF_MEMALLOC | PF_SWAPWRITE;
lockdep_set_current_reclaim_state(sc.gfp_mask);
reclaim_state.reclaimed_slab = 0;
p->reclaim_state = &reclaim_state;
/*
* pgdat->min_unmapped_pages 是“/proc/sys/vm/min_unmapped_ratio”乘上总的页数。
* 页缓存中潜在可回收页数如果大于pgdat->min_unmapped_pages才做页回收
*/
if (node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages) {
/*
* Free memory by calling shrink zone with increasing
* priorities until we have enough memory freed.
*/
do {
//通过sc控制该节点的内存回收
shrink_node(pgdat, &sc);
} while (sc.nr_reclaimed < nr_pages && --sc.priority >= 0);//回收的页面不满足就降低优先级,扫描更多的页
}
p->reclaim_state = NULL;
current->flags &= ~(PF_MEMALLOC | PF_SWAPWRITE);
lockdep_clear_current_reclaim_state();
return sc.nr_reclaimed >= nr_pages;
}
快速内存回收不能进行unmap,writeback操作,且回收时会循环调用shrink_node函数对对应节点进行内存回收,循环次数为sc.priority,每循环一次若回收的页面数满足分配需要的页面数,则退出循环;若不满足则继续进行循环,且每进行一次循环,sc.priority就会降低,因此下一次循环中shrink_node函数扫描的页面数就会更多…(循环结束条件:回收的页面数大于等于需要分配的页面数且循环次数不大于sc.priority)
因为慢速内存分配和oom中都可能会调用到get_page_from_freelist()函数,所以快速内存回收不仅仅发生在快速内存分配中,在慢速内存分配过程中也会发生。
对于shrink_node内存回收核心函数后面会统一讲解,其功能就是依靠sc的控制来对指定节点进行内存回收
快速内存回收注意事项和小结
快速内存回收ps:
即使zone_reclaim_mode允许回写操作,在快速内存回收过程中也不能对脏文件页进行回写操作。
快速内存回收小结:
- 触发条件:内存分配过程中指定zone分配内存后剩余空闲的页框数量 < 此zone阀值 + 此zone保留的页框数量(阀值可根据实际情况调节:min,low,high中的一个)
- 结束条件:下列条件满足一项即可退出内存回收
- 回收的页框数大于等于本次分配任务的页框数
- sc->priority降为0
- 回收对象:
- 指定zone对应节点中的干净文件页
- 可能会回收匿名页
- slab
直接内存回收
直接内存回收在慢速分配过程中被触发,只有一种情况下会使用,在伙伴系统慢速内存分配中无法从zonelist的所有zone中以min阀值分配页框,并且进行异步内存压缩后,还是无法分配到页框的时候。
/*
* alloc_pages()---> alloc_pages_nodes()---> __alloc_pages()---> alloc_pages_node_mask()
* ---> __alloc_pages_slowpath()--->__alloc_pages_direct_reclaim()
* --->__perform_reclaim()--->try_to_free_pages()--->do_try_to_free_pages()
* --->shrink_zones()--->shrink_node()
*
*/
直接内存回收触发点:
1. 先调用函数get_page_from_freelist进行快速内存分配,分配失败
2. 然后进行慢速内存分配
2.1 将水线降低(从low---》min),接着若设置了__GFP_KSWPAD_RECLAIM调用函数wake_all_kswapds函数唤醒异步
回收线程kswap对内存进行异步回收,然后调用函数get_page_from_freelist再次进行快速内存分配,内存分配失
败
2.2 调用函数__alloc_pages_direct_compact函数对内存进行压缩规整后,再进行内存分配,内存分配失败
2.3 若设置了__GFP_KSWPAD_RECLAIM再次调用wake_all_kswapds函数唤醒异步回收线程kswap对内存进行异步回收,
然后重新调整nodemask和zonelist后使用get_page_from_freelist函数对内存进行分配,内存分配失败。
2.4 进行一系列的判断,若满足相关条件,则调用__alloc_pages_direct_reclaim函数:首先进行直接内存回收,
然后通过get_page_from_freelist函数尝试分配内存
上面执行流程的详细分析可移步文章"arm64 Linux内核内存伙伴系统4---alloc_pages(内存块分配)"进行了解
// /mm/page_alloc.c
static inline struct page *__alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order,
unsigned int alloc_flags, const struct alloc_context *ac,
unsigned long *did_some_progress)
{
.....
//进行直接内存回收操作
*did_some_progress = __perform_reclaim(gfp_mask, order, ac);
.....
//页面回收后尝试内存回收
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
.....
return page;
}
__perform_reclaim函数
- 若设置cpuset_memory_pressure_enabled,先更新当前任务的cpuset频率表fmeter
- 为防止递归调用页面的回收流程,将当前任务标志上PF_MEMALLOC
- 通过函数try_to_free_pages进行内存回收处理
- 恢复当前任务的标志到回收前的状态
static int
__perform_reclaim(gfp_t gfp_mask, unsigned int order,
const struct alloc_context *ac)
{
struct reclaim_state reclaim_state;
int progress;
//调用cond_resched主动让出cpu,防止其在内核态执行时间过长导致可能发生的soft lockup或者造成较大的调度延迟
cond_resched();
/* We now go into synchronous reclaim */
//如果设置了cpuset_memory_pressure_enabled,则先更新当前任务的cpuset频率表fmeter
cpuset_memory_pressure_bump();
//将当前任务的标志置上`PF_MEMALLOC`,防止递归调用页面回收进程
current->flags |= PF_MEMALLOC;
lockdep_set_current_reclaim_state(gfp_mask);
reclaim_state.reclaimed_slab = 0;
current->reclaim_state = &reclaim_state;
//进行回收处理
progress = try_to_free_pages(ac->zonelist, order, gfp_mask,
ac->nodemask);
//恢复当前任务标志
current->reclaim_state = NULL;
lockdep_clear_current_reclaim_state();
current->flags &= ~PF_MEMALLOC;
cond_resched();
return progress;
}
try_to_free_pages函数
- 初始化scan_control sc结构体,对内存回收流程进行控制
- 通过函数throttle_direct_reclaim解析来对用户态任务的直接回收请求进行限制
- 若步奏2中throttle_direct_reclaim函数:
- 返回False—>通过do_try_to_free_pages函数对内存进行直接回收处理
- 返回True —>则跳过直接内存回收流程,进行后续内存分配尝试
unsigned long try_to_free_pages(struct zonelist *zonelist, int order,
gfp_t gfp_mask, nodemask_t *nodemask)
{
unsigned long nr_reclaimed;
//初始化scan_control sc结构体
struct scan_control sc = {
//计划回收32个页框
.nr_to_reclaim = SWAP_CLUSTER_MAX,
.gfp_mask = memalloc_noio_flags(gfp_mask),
.reclaim_idx = gfp_zone(gfp_mask),
//本次内存回收的order
.order = order,
//允许进行内存回收的node掩码
.nodemask = nodemask,
//优先级默认为12
.priority = DEF_PRIORITY,
/*与/proc/sys/vm/laptop_mode有关laptop_mode为0,则允许进行回写操作,即使允许回写,
*直接内存回收也不能对脏文件页进行回写,不过允许回写时,可以对非文件页进行回写
*/
.may_writepage = !laptop_mode,
//允许进行unmap操作
.may_unmap = 1,
//允许进行非文件页的操作(匿名页)
.may_swap = 1,
};
/*
*throttle_direct_reclaim会对任务(进程)是否进行直接内存回收请求进行限制:
*1.return False:
* 触发条件:
* (a).当前进程是内核进程或当前进程收到了kill信号
* (b).当前进程对应的最优节点不平衡,然后kswapd进程被唤醒用于平衡最优节点,于此同时当前进程被
* 加入到最优节点的pfmemalloc_wait等待队列等待节点达到平衡状态。当kswapd将节点调节到平
* 衡状态后,唤醒被加入到队列中的进程,进程唤醒后继续执行,并再次检查进程是否收到kill信
* 号,若未收到kill信号返回False
* 产生的结果:内存分配任务继续执行直接内存回收操作
*2.return True :
* 触发条件:当前进程对应的最优节点不平衡,同上kswapd被唤醒,进程被加入到等待对列。在进程被唤醒
* 后,若检查到进程收到了kill信号,则返回True
* 产生的结果:内存分配函数跳过直接内存内存回收操作
*/
if (throttle_direct_reclaim(sc.gfp_mask, zonelist, nodemask))
return 1;
trace_mm_vmscan_direct_reclaim_begin(order,
sc.may_writepage,
sc.gfp_mask,
sc.reclaim_idx);
//调用do_try_to_free_pages函数进行回收处理
nr_reclaimed = do_try_to_free_pages(zonelist, &sc);
trace_mm_vmscan_direct_reclaim_end(nr_reclaimed);
return nr_reclaimed;
}
throttle_direct_reclaim函数
函数throttle_direct_reclaim返回值直接决定直接内存回收的后续操作是否会继续进行:
-
返回False:
- 触发条件:
- 当前进程是内核进程或当前进程收到了kill信号
- 内存分配任务对应的最优节点平衡
- 当前进程对应的最优节点不平衡,然后kswapd进程被唤醒用于平衡最优节点,于此同时当前进程被加入到最优节点的pfmemalloc_wait等待队列等待节点达到平衡状态。当kswapd将节点调节到平衡状态后会唤醒被加入到队列中的进程。进程唤醒后继续执行,并再次检查进程是否收到kill信号,若未收到kill信号函数返回False
- 产生结果:函数退出后进程后续会继续向下执行直接内存回收相关的操作
- 触发条件:
-
返回True:
-
触发条件:当前进程对应的最优节点不平衡,同上kswapd被唤醒,进程被加入到等待对列。在进程被唤醒后,若检查到进程收到了kill信号,则返回True
-
产生结果: 函数退出后进程后续不进行直接内存回收,try_to_free_pages函数直接退出并返回1。
-
static bool throttle_direct_reclaim(gfp_t gfp_mask, struct zonelist *zonelist,
nodemask_t *nodemask)
{
struct zoneref *z;
struct zone *zone;
pg_data_t *pgdat = NULL;
/*
*不应对内核线程的直接内存回收进行限制,因为有些内核线程可能间接负责清理必要页面以此来保证内存回
*收操作正常进行。例如kjournald可能在提交事物时进入直接内存回收流程,如果此时限制kjournald的直
*接内存回收它会导致其他相关进程阻塞在log_wait_commit()上
*/
if (current->flags & PF_KTHREAD)
goto out;
/*
*如果该任务有致命信号被挂起(进程受到了kill信号),那么该任务的直接内存回收操作流程是不因该
*被限制的。它因该快速返回以便退出并释放其内存
*/
if (fatal_signal_pending(current))
goto out;
//遍历zonelist,在获取到第一个有效的pgdat时就会跳出循环
for_each_zone_zonelist_nodemask(zone, z, zonelist,
gfp_zone(gfp_mask), nodemask) {
//只遍历当前节点中不高于ZONE_NORMAL的zone区域
if (zone_idx(zone) > ZONE_NORMAL)
continue;
/* Throttle based on the first usable node */
//获取zone对应的节点
pgdat = zone->zone_pgdat;
/*
*通过函数allow_direct_reclaim判断该节点是否平衡:
*(1)如果平衡则返回真
*(2)如果不平衡:若该节点kswapd进程未被唤醒,则唤醒该进程。且唤醒的kswapd
* 进程只会对ZONE_NORMAL以下的zone进行内存回收
*/
if (allow_direct_reclaim(pgdat))
goto out;
break;
}
/* If no zone was usable by the allocation flags then do not throttle */
//若无可用节点,则进行内存回收
if (!pgdat)
goto out;
/* Account for the throttling */
count_vm_event(PGSCAN_DIRECT_THROTTLE);
/*
*如果内存分配标志禁止文件系统操作,则当前任务(进程)会被设置位TASK_INTERRUPTIBLE状态(中断进程是可以被信号
*和wake_up()唤醒),接着将该进程加入到node的pgdat->pfmemalloc_wait等待队列中,并且会设置1s的超时时间该任务
*被唤醒的情况如下所示:
*1.allow_direct_reclaim(pgdat)为真时被唤醒,而1s没超时,返回剩余timeout(jiffies)
*2.睡眠超过1s时会唤醒,而allow_direct_reclaim(pgdat)此时为真,返回1
*3.睡眠超过1s时会唤醒,而allow_direct_reclaim(pgdat)此时为假,返回0
*4.接收到信号被唤醒,返回-ERESTARTSYS
*/
if (!(gfp_mask & __GFP_FS)) {
wait_event_interruptible_timeout(pgdat->pfmemalloc_wait,
allow_direct_reclaim(pgdat), HZ);
goto check_pending;
}
/* Throttle until kswapd wakes the process */
/*
*执行到此处表示内存分配标志没有禁止文件系统操作,则将要当前的任务设置为TASK_KILLABLE状(表示该进程睡眠后可以
*被等到的资源唤醒,不能被常规信号唤醒,但是可以被致命信号唤醒),并将当前任务加入到入到对应node的
*pgdat->pfmemalloc_wait队列。任务被唤醒的情况如下所示:
*1.allow_direct_reclaim(pgdat)为真时
*2.接收到致命信号时
*/
wait_event_killable(zone->zone_pgdat->pfmemalloc_wait,
allow_direct_reclaim(pgdat));
check_pending:
/*
*如果当前任务被加入到pgdat->pfmemalloc_wait队列后被唤醒,就会执行到此处,任务唤醒后会再次检查当
*前任务是否接受到了kill信号(收到kill信号,直接返回True任务跳过直接内存回收)
*/
if (fatal_signal_pending(current))
return true;
out:
return false;
}
ps:在直接内存回收过程中若整个node的空闲页数量不满足要求时,会造成当前需要分配的进程被加入到一个等待对列中,这些进程最终会由kswapd唤醒它。上面描述的等待队列的head就是node节点中的pfmemalloc_wait成员。若果当前的任务加入到了pgdat->pfmemalloc_wait这个等待队列中,则该内存分配进程会等待kswapd线程的唤醒(节点平衡时)。
allow_direct_reclaim函数
该函数就是遍历指定节点的低内存区域(Normal以下的zone区域,包括Normal),判断该节点是否平衡:
- 若平衡返回True,允许直接内存回收操作。三种情况返回true
- 节点kswapd回收失败次数超过MAX_RECLAIM_RETRIES
- free pages >= pfmemalloc_reserve(其中pfmemalloc_reserve是累加对应node中所有低内存zone的min水位值结果,而free page是累加所有低内存zone的空闲页的结果)
- pfmemalloc_reserve==0
- 若不平衡返回False,唤醒kswapd进程来平衡对应节点,限制执行直接内存回收操作
static bool allow_direct_reclaim(pg_data_t *pgdat)
{
struct zone *zone;
unsigned long pfmemalloc_reserve = 0;
unsigned long free_pages = 0;
int i;
bool wmark_ok;
//kswapd回收次数超过MAX_RECLAIM_RETRIES,允许直接内存回收
if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES)
return true;
/*
*(1)累加低内存zone区域的min值,用pfmemalloc_reserve保存
*(2)累加低内存zone区域的空闲页框数,用free_pages保存
*PS:低zone区域小于等于NORMAL_ZONE
*/
for (i = 0; i <= ZONE_NORMAL; i++) {
zone = &pgdat->node_zones[i];
if (!managed_zone(zone))
continue;
if (!zone_reclaimable_pages(zone))
continue;
pfmemalloc_reserve += min_wmark_pages(zone);
free_pages += zone_page_state(zone, NR_FREE_PAGES);
}
/* If there are no reserves (unexpected config) then do not throttle */
//若pfmemalloc_reserve为0,函数返回true,允许直接内存回收
if (!pfmemalloc_reserve)
return true;
//用于判断free pages是否超过pfmemalloc_reserve的一半
wmark_ok = free_pages > pfmemalloc_reserve / 2;
/*
*(1)当free_pages<pfmemalloc_reserve/2,若kswapd线程未被唤醒,先唤醒kswapd线程,
* 然后返回False不允许进行直接内存回收操作.
*(2)当free_pages>=pfmemalloc_reserve/2,直接返回true,允许执行直接内存回收操作
*/
if (!wmark_ok && waitqueue_active(&pgdat->kswapd_wait)) {
pgdat->kswapd_classzone_idx = min(pgdat->kswapd_classzone_idx,
(enum zone_type)ZONE_NORMAL);
wake_up_interruptible(&pgdat->kswapd_wait);
}
return wmark_ok;
}
注意如果内存分配进程被加入到了对应节点的pgdat->pfmemalloc_wait等待队列,该进程会进入等待状态;当对应节点的kswapd进程进行内存回收后,会再次通过allow_direct_reclaim函数来判断对应节点是否平衡。如果平衡则唤醒该进程,若果不平衡,kswapd还会继续对节点进行内存回收直到其达到平衡状态,若极端情况下kswapd实在无法让节点达到平衡状态度,则在kswapd睡眠前会将加入到pgdat->pfmemalloc_wait队列的进程唤醒
do_try_to_free_pages函数
do_try_to_free_pages函数是直接内存回收主要的入口函数,该函数通过循环调用shrink_zones
来回收页面,函数返回值表示回收的页面数。
- 函数返回0:函数扫描了尽可能多的lru inactive list后都未回收到足够的页面用于内存分配,函数返回0,后续内存分配任务将进入OOM流程。
- 函数返回1:未回收到页框,但是此时空闲页框数能够进行内存压缩操作,函数退出后进行内心压缩流程
- 函数返回N(N>1):返回直接内存回收的页面数,函数退出后进行内存分配尝试操作
/*
* This is the main entry point to direct page reclaim.
*
* If a full scan of the inactive list fails to free enough memory then we
* are "out of memory" and something needs to be killed.
*
* If the caller is !__GFP_FS then the probability of a failure is reasonably
* high - the zone may be full of dirty or under-writeback pages, which this
* caller can't do much about. We kick the writeback threads and take explicit
* naps in the hope that some of these pages can be written. But if the
* allocating task holds filesystem locks which prevent writeout this might not
* work, and the allocation attempt will fail.
*
* returns: 0, if no pages reclaimed
* else, the number of pages reclaimed
*/
static unsigned long do_try_to_free_pages(struct zonelist *zonelist,
struct scan_control *sc)
{
int initial_priority = sc->priority;
unsigned long total_scanned = 0;
unsigned long writeback_threshold;
retry:
//通过delayacct_freepages_start/delayacct_freepages_end量化页面回收的时间开销
delayacct_freepages_start();
if (global_reclaim(sc))
__count_zid_vm_events(ALLOCSTALL, sc->reclaim_idx, 1);
/*
*循环调用shrink_zones来回收页面.
*循环退出触发点:
*(1)回收的页面足够(sc->nr_reclaimed >= sc->nr_to_reclaim)
*(2)可以进行内存压缩(sc->compaction_ready==true)
*(3)循环的次数sc->priority次,随着优先降低,一次扫描的页框数量就越多(total_size >> priority)
*/
do {
//随着回收优先级的调整,利用vmpressure_prio来更新memory pressure的值
vmpressure_prio(sc->gfp_mask, sc->target_mem_cgroup,
sc->priority);
sc->nr_scanned = 0;
shrink_zones(zonelist, sc);
total_scanned += sc->nr_scanned;
/*
*回收的页面足够,直接内存回收默认为32个页框(.nr_to_reclaim = SWAP_CLUSTER_MAX),
*退出循环,然后函数返回回收的页面数(sc->nr_reclaimed)
*/
if (sc->nr_reclaimed >= sc->nr_to_reclaim)
break;
/*
*可以进行内存压缩,退出循环
*/
if (sc->compaction_ready)
break;
/*
*当优先级降到10的时候,即使原来不允许回写操作,这个时候开始允许回写操作,因为此时若不允许回写
*很难再回收到页框。
*PS:即使允许回写,直接内存回收也不能对脏文件页进行回收。
*/
if (sc->priority < DEF_PRIORITY - 2)
sc->may_writepage = 1;
/*
* Try to write back as many pages as we just scanned. This
* tends to cause slow streaming writers to write data to the
* disk smoothly, at the dirtying rate, which is nice. But
* that's undesirable in laptop mode, where we *want* lumpy
* writeout. So in laptop mode, write out the whole world.
*/
/*
*直接内存回收计划回收的页块数是SWAP_CLUSTER_MAX(等于32)个,如果扫描的页框数超过
*(sc->nr_to_reclaim + sc->nr_to_reclaim / 2,则会根据laptop_mode的值来唤醒flush内核线程:
*(1)laptop_mode==0表示允许回写操作:通过flush内核线程尝试尽可能多地回写所有的可以回写的页框
*(2)laptop_mode!=0表示不允许回写操作:通过flush内核线程尝试尽可能多地回写刚刚扫描过的页框
*/
writeback_threshold = sc->nr_to_reclaim + sc->nr_to_reclaim / 2;
if (total_scanned > writeback_threshold) {
wakeup_flusher_threads(laptop_mode ? 0 : total_scanned,
WB_REASON_TRY_TO_FREE_PAGES);
sc->may_writepage = 1;
}
} while (--sc->priority >= 0);//若当前优先级回收的页面不满足就降低优先级,扫描更多的页,循环执行回收操作
delayacct_freepages_end();
//成功回收到页框,返回收的页框数
if (sc->nr_reclaimed)
return sc->nr_reclaimed;
/* Aborted reclaim to try compaction? don't OOM, then */
//若未回收到页框,但此时空闲页框数足够进行内存压缩,函数返回1,后续不会立即进行oom流程
if (sc->compaction_ready)
return 1;
/* Untapped cgroup reserves? Don't OOM, retry. */
/*
*若sc的may_thrash为0,在页框回收失败且空闲页框不能够支持内存压缩,此时将直接内存回收
*优先级调整到initial_priority后,再次执行一些直接内存回收流程,执行前将sc->may_thrash设置位1
*避免下次继续retry,这样可以可以再给系统一次回收机会,避免立即进入OOM流程
*/
if (!sc->may_thrash) {
sc->priority = initial_priority;
sc->may_thrash = 1;
goto retry;
}
//返回0,会进入oom流程
return 0;
}
shrink_zones
shrink_zones按预定的优先级遍历zonelist中满足内存分配进程要求的zone区域,每次循环中如果判定该zone需要进行直接内存回收就会调用shrink_node函数来尽力回收该zone所属节点中的所有能够回收的页框。注意zonlist中的多个zone可能属于同一个node,则该节点只调用shrink_node回收一次,后续循环若遇到回收过的node,直接continue跳过,所以对于uma系统循环只执行一次。
// /mm/vmscan.c
static void shrink_zones(struct zonelist *zonelist, struct scan_control *sc)
{
......
/*
* If the number of buffer_heads in the machine exceeds the maximum
* allowed level, force direct reclaim to scan the highmem zone as
* highmem pages could be pinning lowmem pages storing buffer_heads
*/
orig_mask = sc->gfp_mask;
if (buffer_heads_over_limit) {
sc->gfp_mask |= __GFP_HIGHMEM;
sc->reclaim_idx = gfp_zone(sc->gfp_mask);
}
/*
*按预定的优先级遍历zonelist中满足内存分配进程要求的zone区域,每次循环中如果判定该zone需要进行内存回收
*就会调用shrink_node函数来尽力回收该zone所属节点中的所有能够回收的页框。注意zonlist中的多个zone可能
*属于同一个node,则该节点只调用shrink_node回收一次,后续循环若遇到回收过的node直接continue跳过,所
*以对于uma系统循环只执行一次。
*/
for_each_zone_zonelist_nodemask(zone, z, zonelist,
sc->reclaim_idx, sc->nodemask) {
......
/*
*如果该zone空闲内存页框数满足进行内存压缩的条件,跳过该次循环的直接内存回收操作,并将
*sc->compaction_ready赋值true
*/
if (IS_ENABLED(CONFIG_COMPACTION) &&
sc->order > PAGE_ALLOC_COSTLY_ORDER &&
compaction_ready(zone, sc)) {
sc->compaction_ready = true;
continue;
}
/*
*每个节点只回收一次,如果zonelist中的zone不是按照默认的方式进行排序,
*那么会导致一个节点被回收多次,这时较低的zone区域期望被保留
*/
if (zone->zone_pgdat == last_pgdat)
continue;
......
shrink_node(zone->zone_pgdat, sc);
}
/*
* Restore to original mask to avoid the impact on the caller if we
* promoted it to __GFP_HIGHMEM.
*/
sc->gfp_mask = orig_mask;
}
对于shrink_node内存回收核心函数后面会统一讲解,其功能就是依靠sc的控制来对指定节点进行内存回收
直接内存回收注意事项和小结
直接内存回收ps:
- 因为直接内存回收的默认优先级sc->priority等于12,因此该回收会最多遍历12遍zonelist中包含的所有node,每遍历一次后sc->priority–。当优先级降到10以下时,即使原来的sc->may_writepage等于1不允许回写,这个时候开始允许回写。因为这个时候若不允许回写将很难回收到页框
- 直接内存回收计划回收页框数sc->nr_to_reclaim 为32个,若回收过程中扫描的页框数超过sc->nr_to_reclaim + sc->nr_to_reclaim / 2,此时会根据laptop_mod的值来唤醒flush内核线程
- 直接内存回收始终不会对脏文件页进行回写,当sc->may_writepage==1,只会对非文件页进行回写操作
- 直接内存回收
- 对文件页的操作—> unmmp
- 对非文件页的操作—>加入swap cache,unmap,回写
- 直接内存回收会先回收在memcg中并且超过memcg的soft_limit_in_bytes的进程的内存
- 直接内存会调用shrink_slab()对slab进行回收
- 直接内存回收期望回收32个页框,而快速内存回收期望回收的页框数为1<<order
直接内存回收小结:
- 触发条件:内存分配过程中zonelist中所有zone的内存水线都低于min阀值
- 结束条件:下列条件满足一项即可退出直接内存回收
- throttle_direct_reclaim函数返回True
- 经过一次shrink_zones()空闲页框数足够进行内存压缩
- 回收页框数达到32(sc->nr_to_reclaim)个
- sc->priority降到0,进行了12次循环回收操作
- 回收对象:
- 节点中干净的文件页,匿名页和slab
- 超过所在memcg的soft_limit_in_bytes的进程的内存