文章目录
1.kswapd线程初始化
kswapd是linux内核中一个用于在内存不足的情况下进行内存回收线程。该线程的初始化函数为kswapd_init。由下代码可知,内核会给每个节点分配一个kswapd%d的进程(d代表节点编号)。
// /mm/vmscan.c
static int __init kswapd_init(void)
{
int nid;
/*
*根据物理内存的大小设定一个全局变量page_cluster,该参数与磁盘设备驱动相关。在读磁盘前需要经过寻到,
*寻到操作很费时,为了省时每次会多读几个页面(预读)。每次预读的页面数就是该函数根据内存的大小计算出
*来(一般为2,3或4)。
*/
swap_setup();
/*
*通过kswapd_run函数给每个节点创建一个专属的kswapd内核线程,uma系统只有一个kswapd
*/
for_each_node_state(nid, N_MEMORY)
kswapd_run(nid);
/*
*这是和cpu热插拔选项相关的一个宏.主要是定义了一个结构体notifier_block变量,
*然后调用register_cpu_notifier()将给定的参数(一般是个函数))注册.相当于一
*个CPU插拔动作的回调函数.例如kswapd线程初始化前,就注册了cpu_swap_callback函数.linux内核中,
*每个节点都有一个kswapd线程.该函数的意思是当某个节点的cpu被拔下来以后,要将该CPU在内核中的
*私有数组(pagevec)里面的pages加入LRU队列.
*/
hotcpu_notifier(cpu_callback, 0);
return 0;
}
//系统启动自动运行
module_init(kswapd_init)
/*
* This kswapd start function will be called by init and node-hot-add.
* On node-hot-add, kswapd will moved to proper cpus if cpus are hot-added.
*/
int kswapd_run(int nid)
{
pg_data_t *pgdat = NODE_DATA(nid);
int ret = 0;
if (pgdat->kswapd)
return 0;
//用kthread_run创建kswapd内核线程,立刻运行
pgdat->kswapd = kthread_run(kswapd, pgdat, "kswapd%d", nid);
if (IS_ERR(pgdat->kswapd)) {
/* failure at boot is fatal */
BUG_ON(system_state == SYSTEM_BOOTING);
pr_err("Failed to start kswapd on node %d\n", nid);
ret = PTR_ERR(pgdat->kswapd);
pgdat->kswapd = NULL;
}
return ret;
}
linux内核会给每个节点分配一个kswapd内核线程。每个节点的数据结构struct pg_data_t中用4个成员变量来维护kswapd线程。
typedef struct pglist_data {
...
wait_queue_head_t kswapd_wait;
...
struct task_struct *kswapd; /* Protected by
mem_hotplug_begin/end() */
int kswapd_order;
enum zone_type kswapd_classzone_idx;
...
} pg_data_t;
2.kswapd线程的唤醒
我们知道内核会为每个节点创建一个kswapd线程后,每个节点的kswapd线程创建会进入睡眠状态。那么什么时候kswapd线程会被唤醒呢?(为了简化,下面假设linux 系统是uma结构,即只有一个内存节点)
kswapd线程主要会在下面的场景下被唤醒:
1.被动唤醒场景:
- 内存分配过程中快速内存分配失败,进入慢速内存分配流程,此时会将水印由low降至min,若系统设置了__GFP_KSWAPD_RECLAIM,内存分配进程则会通过调用wake_all_kswapds函数唤醒kswapd线程对当前节点进行异步内存回收。(简而言之就是zone的low水位分配失败时,会唤醒kswapd内存核线程对当前节点进行内存回收)
- 内存分配过程已经触发了慢速内存分配中的直接内存回收流程,此时内存已经严重不足。若直接内存回收过程中发现回收的最优节点处于不平衡状态,则当前任务会通过调用wake_all_kswapds函数唤醒kswapd内核线程对当前节点进行异步回收
2.主动唤醒场景:kswapd在被内存分配进程唤醒并完成异步内存回收操作后,会对当前节点的内存环境做一个平衡度检查 (用prepare_kswapd_sleep函数).若此时节点平衡,则kswapd线程会执行schedule_timeout(HZ/10)让出cpu控制权并短暂休眠一段时间(100ms),100ms后kswapd线程会主动被唤醒,唤醒后kswapd线程会继续对当前节点做一个平衡度检查:
- 若当前节点平衡,则kswapd线程会执行schedule()让出cpu控制权并进入一个长期的睡眠状态,后续只能通过wake_all_kswapds函数被被动唤醒。
- 若当前节点不平衡则kswapd线程会在睡眠100ms后主动被唤醒,kwapd线程唤醒后会多次调用balance_pgdat函数回收当前节点的内存,直到内存节点达到平衡状态(注意若在这睡眠的100ms过程中kswapd被被动唤醒,则kswapd相关执行会跳转到被动唤醒场景流程中)。当节点平衡后,kswpad会再次调用schedule_timeout(HZ/10)进入短期睡眠状…后续循环执行上述步骤,直到当前节点达到平衡状态,最终kswapd线程会调用schedule()让出cpu控制权并进入下一个长期的随眠状态。
上面两类触发场景都是殊途同归,最终都是通过唤醒kswapd给对应的节点进行内存回收,让对应节点达到平衡状态(zone_wartermark_ok)。
内存回收已经有快速内存回收和直接内存回收,为什么系统还周期性地调用kswapd线程去平衡不满足要求的节点?那是因为有些任务的内存分配不允许被阻塞,还有少量任务的内存分配不允许激活I/O访问,如果这些任务在内存分配过程中发现内存不足才进行回收,这相当于亡羊补牢。所以linux内核在利用系统空闲的时候进行内存回收是很有必要的机制。
3.kswapd线程源码分析
内核线程kswapd在系统初始化时通过module_init(kswapd_init)被创建,并一直运行。一般情况kswapd线程处于sleep状态并被放置在对应节点的swapd_wait队列中等待被唤醒。在系统内存环境比较紧张的情况下kswapd内核线程会被唤醒,并尽力将不平衡的节点调整至平衡状态。先上流程图
3.1 kswapd函数
static int kswapd(void *p)
{
//变量初始化
unsigned int alloc_order, reclaim_order, classzone_idx;
pg_data_t *pgdat = (pg_data_t*)p;
struct task_struct *tsk = current;
struct reclaim_state reclaim_state = {
.reclaimed_slab = 0,
};
const struct cpumask *cpumask = cpumask_of_node(pgdat->node_id);
lockdep_set_current_reclaim_state(GFP_KERNEL);
if (!cpumask_empty(cpumask))
set_cpus_allowed_ptr(tsk, cpumask);
current->reclaim_state = &reclaim_state;
/*
*给任务添加标志:(1)内存分配线程,(2)允许回写到swap,(3)是一个kswapd线程
*/
tsk->flags |= PF_MEMALLOC | PF_SWAPWRITE | PF_KSWAPD;
set_freezable();
pgdat->kswapd_order = alloc_order = reclaim_order = 0;
pgdat->kswapd_classzone_idx = classzone_idx = 0;
for ( ; ; ) {
bool ret;
kswapd_try_sleep:
/*
*kswapd_try_to_sleep函数判断kswapd是否进入睡眠状态和让出cpu控制权,同时函数内部也是kswapd被唤
*醒后的线程的入口点
*/
kswapd_try_to_sleep(pgdat, alloc_order, reclaim_order,
classzone_idx);
/*
*kswapd被唤醒后更新局部变量,alloc_order和classzone_idx两个局部变量是kswapd线程流程控制中的重
*要判断依据
*/
alloc_order = reclaim_order = pgdat->kswapd_order;
classzone_idx = pgdat->kswapd_classzone_idx;
pgdat->kswapd_order = 0;
pgdat->kswapd_classzone_idx = 0;
ret = try_to_freeze();
//线程被杀死
if (kthread_should_stop())
break;
/*
* We can speed up thawing tasks if we don't call balance_pgdat
* after returning from the refrigerator
*/
if (ret)
continue;
/*
* Reclaim begins at the requested order but if a high-order
* reclaim fails then kswapd falls back to reclaiming for
* order-0. If that happens, kswapd will consider sleeping
* for the order it finished reclaiming at (reclaim_order)
* but kcompactd is woken to compact for the original
* request (alloc_order).
*/
trace_mm_vmscan_kswapd_wake(pgdat->node_id, classzone_idx,
alloc_order);
/*调用balance_pgdat对节点内存进行回收,返回回收内存块的order,reclaim_order表明该次回收过程获取
*到的最大连续内存块的阶数
*/
reclaim_order = balance_pgdat(pgdat, alloc_order, classzone_idx);
/*此处指是针对被动唤醒kwapd线程的情况:
*最大阶(alloc_order)内存块回收失败,就会跳转到kswapd_try_sleep处用0阶的reclaim_order进行判
*断,这样kswapd线程直接sleep让直接内存回收去回收内存,kswapd在sleep前会唤醒compact内核线程去规
*整内存,期望其能规整出alloc_order阶大小的连续内存块
*/
if (reclaim_order < alloc_order)
goto kswapd_try_sleep;
/*记录本次kswapd线程执行balance_pgdat函数后相关局部变量的数据,为下一次kswapd回收提供比较数据*/
alloc_order = reclaim_order = pgdat->kswapd_order;
classzone_idx = pgdat->kswapd_classzone_idx;
}
tsk->flags &= ~(PF_MEMALLOC | PF_SWAPWRITE | PF_KSWAPD);
current->reclaim_state = NULL;
lockdep_clear_current_reclaim_state();
return 0;
}
alloc_order,reclaim_order和classzone_idx局部变量解析
alloc_order,reclaim_order和classzone_idx这3个值的来源:
- 若是内存分配任务唤醒的kswapd线程:wake_all_kswapds函数会将内存分配任务中需要分配内存块的阶order和第一个最合适分配内存的zone序号分别赋值给当前节点的pgdat->kswapd_order和pgdat->kswapd_classzone_idx两个成员变量(注意在进行赋值时取新值和原始值中的较大者),然后通过这两个成员变量将值分别传递给kswapd线程中alloc_order和classzone_idx这两个局部变量。reclaim_order是上一次执行kswapd线程中balance_pgdat函数回收到的最大连续内存块的阶。
- 若是周期性主动唤醒的kswapd线程:alloc_order和reclaim_order往往是0,classzone_idx=pgdat->nr_zones-1(linux 4.9.115版本的内核此处classzone_idx=0,不知道具体原因,若为0,感觉系统kswapd线程并不存在主动唤醒周期回收内存的情况)
kswapd中alloc_order和classzone_idx这两个局部变量用于控制着kswapd线程执行流程。
-
reclaim_order和classzone_idx用于判断当前节点是能否进入sleep状态,若当前节点中0到classzone_idx中每个zone区域在分配了reclaim_order阶内存后空闲内存值都高于high水线阈值则当前节点的kswapd线程可进入sleep状态(空闲内存要减去该zone的为classzone_idx保留的内存),若0到classzone_idx中有一个zone区域在分配了reclaim_order阶内存后空闲内存值低于了high水线阈值则当前节点的kswapd线程不能进入sleep状态
//返回true节点kswapd线程进入sleep状态,否则节点kswapd继续running static bool prepare_kswapd_sleep(pg_data_t *pgdat, int order, int classzone_idx) { int i; //唤醒节点pfmemalloc_wait队列中所有等待的任务,该操作与直接内存回收有关联 if (waitqueue_active(&pgdat->pfmemalloc_wait)) wake_up_all(&pgdat->pfmemalloc_wait); /* *Hopeless node, leave it to direct reclaim *该节点的kswapd在进行的MAX_RECLAIM_RETRIES次内存回收操作中每次回收的内存页数都为0,kswapd线程已经 *无法从节点中回收到内存页主动让kswapd线程睡去,寄希望于直接内存回收 */ if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES) return true; for (i = 0; i <= classzone_idx; i++) { struct zone *zone = pgdat->node_zones + i; if (!managed_zone(zone)) continue; /*zone_balance(zone, order, classzone_idx): *该zone的空闲内存值减去该zone为classzone_idx对应zone保留内存值后获得的内存值大小若大于zone *的high水线值zone_balance函数返回True */ if (!zone_balanced(zone, order, classzone_idx)) return false; } return true; }
/* *该zone的空闲内存值减去该zone为classzone_idx对应zone保留内存值后获得的内存值大小若大于zone的high水线值 *zone_balance函数返回True: * free_pages - < zone->lowmem_reserve[classzone_idx] = zone->watermark[high] */ static bool zone_balanced(struct zone *zone, int order, int classzone_idx) { unsigned long mark = high_wmark_pages(zone); if (!zone_watermark_ok_safe(zone, order, mark, classzone_idx)) return false; /* * If any eligible zone is balanced then the node is not considered * to be congested or dirty */ clear_bit(PGDAT_CONGESTED, &zone->zone_pgdat->flags); clear_bit(PGDAT_DIRTY, &zone->zone_pgdat->flags); clear_bit(PGDAT_WRITEBACK, &zone->zone_pgdat->flags); return true; }
-
用于控制balance_pgdat函数对节点的回收操作,见balance_pgdat函数分析
kswapd_try_to_sleep函数
kswapd_try_to_sleep用于判断kswapd线程是否sleep,该函数是内核线程kswapd睡眠时让出cpu控制权的地方,同时也是睡眠kswapd被唤醒时进入的地方。
static void kswapd_try_to_sleep(pg_data_t *pgdat, int alloc_order, int reclaim_order,
unsigned int classzone_idx)
{
long remaining = 0;
DEFINE_WAIT(wait);
if (freezing(current) || kthread_should_stop())
return;
//定义等待队列,并设置kswapd线程的状态
prepare_to_wait(&pgdat->kswapd_wait, &wait, TASK_INTERRUPTIBLE);
/* Try to sleep for a short interval */
/*
*prepare_kswapd_sleep判断kswapd能否进入随眠状态,此处睡眠的条件是pgdat中0到classzone_idx的zone
*区域都是balance:返回TRUE--->睡眠
*/
if (prepare_kswapd_sleep(pgdat, reclaim_order, classzone_idx)) {//睡眠
/*
* Compaction records what page blocks it recently failed to
* isolate pages from and skips them in the future scanning.
* When kswapd is going to sleep, it is reasonable to assume
* that pages and compaction may succeed so reset the cache.
*节点的kswapd线程睡眠前清除该节点中所有页块的PB_migrate_skip标记,下次节点的内存规整时,就会对扫描更多
*的页块
*/
reset_isolation_suitable(pgdat);
/*
*在kswapd线程睡眠的过程中唤醒kcompactd线程对节点内存做规整,alloc_order是内分配任务需要的页块阶数,表明
*想让kcompactd线程规则出一块alloc_order阶大的内存块
*/
wakeup_kcompactd(pgdat, alloc_order, classzone_idx);
/*
*1. 睡眠100ms(调用schedule进入睡眠状态,让出cpu)
*2. HZ节拍率,每1s触发的cpu时间中断数
*3. linux内存在时钟中断每次发生后都后检测每个定时器是否到期
*4.该定时器可被中断,若remaining为0则kswapd是周期性主动被唤醒的,不为0说明kswapd在100ms
* 睡眠过程中提前被其它任务打断,一般是被wake_up唤醒
*/
remaining = schedule_timeout(HZ/10);
/*
*若remainig不为0,说明kswapd在100ms内提前被其它任务唤醒,属于被动唤醒情况,这个时候需要更新
*节点中pgdat->kswapd_classzone_idx和pgdat->kswapd_order成员变量的值。更新值是取
*唤醒请求传递过来的值和最近一次执行kswapd保留的值中的较大值。
*/
if (remaining) {
pgdat->kswapd_classzone_idx = max(pgdat->kswapd_classzone_idx, classzone_idx);
pgdat->kswapd_order = max(pgdat->kswapd_order, reclaim_order);
}
//设置线程状态位running
finish_wait(&pgdat->kswapd_wait, &wait);
prepare_to_wait(&pgdat->kswapd_wait, &wait, TASK_INTERRUPTIBLE);
}
/*
*定时器时间中断数耗尽,kswapd线程被主动唤醒,此时通过prepare_kswapd_sleep函数
*检查当前节点中0到classzone_idx的zone区域是否都是zone_balanced的。
*(1)是:进入if语句,kswapd通过调用schedule()继续随眠并让出cpu控制权
*(2)否:进入else语句块,执行kswapd线程后续内存回收操作
*/
if (!remaining &&
prepare_kswapd_sleep(pgdat, reclaim_order, classzone_idx)) {
trace_mm_vmscan_kswapd_sleep(pgdat->node_id);
set_pgdat_percpu_threshold(pgdat, calculate_normal_threshold);
if (!kthread_should_stop())
//主动让出CPU,调度出去,等待wake_up唤醒
schedule();
set_pgdat_percpu_threshold(pgdat, calculate_pressure_threshold);
} else {//需要唤醒kswapd
if (remaining)
count_vm_event(KSWAPD_LOW_WMARK_HIT_QUICKLY);
else
count_vm_event(KSWAPD_HIGH_WMARK_HIT_QUICKLY);
}
//设置线程状态为running
finish_wait(&pgdat->kswapd_wait, &wait);
}
balance_pgdat函数
balance_pgdat函数是kswapd线程实际进行内存回收操作函数。本节将围绕如下场景对balance_pgdat函数进行流程分析:linux内核伙伴系统需要分配order阶的连续内存块,内存分配任务(alloc_pages_node)执行快速内存分配失败,进入慢速内存分配,慢速内存分配前期内存分配任务唤醒kswapd内核线程,kswapd线程调用balance_pgdat函数对指定节点进行异步内存回收。
//mm/vmscan.c
static int balance_pgdat(pg_data_t *pgdat, int order, int classzone_idx)
{
int i;
unsigned long nr_soft_reclaimed;
unsigned long nr_soft_scanned;
struct zone *zone;
//页面回收控制参数
struct scan_control sc = {
//分配掩码
.gfp_mask = GFP_KERNEL,
//此处指期望回收到order阶的内存块(来源内存分配任务中的连续内存块的order)
.order = order,
/* 扫描LRU链表的优先级,用于计算每次扫描页面的数量(total_size >> priority,初始值12),值越小,扫描的
*页面数越大,逐级增加扫描粒度
* (1)代表一次扫描(total_size >> priority)个页框
* (2)优先级越低,一次扫描的页框数量就越多
* (3)默认优先级为12
*/
.priority = DEF_PRIORITY,
/*
*与/proc/sys/vm/laptop_mode有关laptop_mode为0,则允许进行回写操作,即使允许回写,也不能对脏文件页进行
*回写,不过允许回写时,可以对非文件页进行回写
*/
.may_writepage = !laptop_mode,
.may_unmap = 1,
.may_swap = 1,
};
count_vm_event(PAGEOUTRUN);
do {
unsigned long nr_reclaimed = sc.nr_reclaimed;
bool raise_priority = true;
sc.reclaim_idx = classzone_idx;
/*
* If the number of buffer_heads exceeds the maximum allowed
* then consider reclaiming from all zones. This has a dual
* purpose -- on 64-bit systems it is expected that
* buffer_heads are stripped during active rotation. On 32-bit
* systems, highmem pages can pin lowmem memory and shrinking
* buffers can relieve lowmem pressure. Reclaim may still not
* go ahead if all eligible zones for the original allocation
* request are balanced to avoid excessive reclaim from kswapd.
*/
//如果buffer_heads超过了允许的最大值,则将考虑回收该节点所有在线的zone
if (buffer_heads_over_limit) {
for (i = MAX_NR_ZONES - 1; i >= 0; i--) {
zone = pgdat->node_zones + i;
if (!managed_zone(zone))
continue;
sc.reclaim_idx = i;
break;
}
}
/*
* Only reclaim if there are no eligible zones. Check from
* high to low zone as allocations prefer higher zones.
* Scanning from low to high zone would allow congestion to be
* cleared during a very small window when a small low
* zone was balanced even under extreme pressure when the
* overall node may be congested. Note that sc.reclaim_idx
* is not used as buffer_heads_over_limit may have adjusted
* it.
*/
/*遍历节点中classzone_idx到0的每个zone区域,检查每个zone是否zone_balance,
*若遍历过程中出现一个zone是均衡的,则不对该节点进行内存回收操作直接return
*/
for (i = classzone_idx; i >= 0; i--) {
zone = pgdat->node_zones + i;
if (!managed_zone(zone))
continue;
if (zone_balanced(zone, sc.order, classzone_idx))
goto out;
}
/*
* Do some background aging of the anon list, to give
* pages a chance to be referenced before reclaiming. All
* pages are rotated regardless of classzone as this is
* about consistent aging.
*若当前节点不活跃匿名页lru链表中页面数量过低(通过inactive_list_is_low函数判断),则通过
*shrink_active_list函数将该节点活跃匿名页lru链表上的页迁移到不活跃匿名页lru链表上去。
*/
age_active_anon(pgdat, &sc);
/*
* If we're getting trouble reclaiming, start doing writepage
* even in laptop mode.
*/
/*
*如果扫描3次实际回收的页面仍然都小于需要回收的页面或者扫描的页面数量大于等于可回收页面的6倍,则允许回收过
*程中页面能够回写(即使是在laptop模式下),其目的可能是扫描中的页中有很多脏页面,需要回写
*/
if (sc.priority < DEF_PRIORITY - 2 || !pgdat_reclaimable(pgdat))
sc.may_writepage = 1;
/* Call soft limit reclaim before calling shrink_node. */
sc.nr_scanned = 0;
nr_soft_scanned = 0;
nr_soft_reclaimed = mem_cgroup_soft_limit_reclaim(pgdat, sc.order,
sc.gfp_mask, &nr_soft_scanned);
sc.nr_reclaimed += nr_soft_reclaimed;
/*
* There should be no need to raise the scanning priority if
* enough pages are already being scanned that that high
* watermark would be met at 100% efficiency.
*/
/*
*扫描节点的lru链表,进行内存回收(sc用于控制节点内存回收)
*(1)扫描的页面数不小于期望回收页面数,此次不更新扫描优先级(因为不需要继续加大扫描量)
*(2)进行内存回收时扫描该节点的整个node,这样做是为了避免zone的老化速度不同
*/
if (kswapd_shrink_node(pgdat, &sc))
raise_priority = false;
/*
* If the low watermark is met there is no need for processes
* to be throttled on pfmemalloc_wait as they should not be
* able to safely make forward progress. Wake them
*/
//若允许直接内存回收,唤醒节点pfmemalloc_wait队列中sleep的任务进行直接内存回收处理
if (waitqueue_active(&pgdat->pfmemalloc_wait) &&
allow_direct_reclaim(pgdat))
wake_up_all(&pgdat->pfmemalloc_wait);
/* Check if kswapd should be suspending */
if (try_to_freeze() || kthread_should_stop())
break;
/*
* Raise priority if scanning rate is too low or there was no
* progress in reclaiming pages
*/
nr_reclaimed = sc.nr_reclaimed - nr_reclaimed;
if (raise_priority || !nr_reclaimed)
sc.priority--;
} while (sc.priority >= 1);
//此次回收页面数为0,pgdat->kswapd_failures计算+1
if (!sc.nr_reclaimed)
pgdat->kswapd_failures++;
out:
/*
* Return the order kswapd stopped reclaiming at as
* prepare_kswapd_sleep() takes it into account. If another caller
* entered the allocator slow path while kswapd was awake, order will
* remain at the higher level.
*/
return sc.order;
}
kswapd_shrink_node函数
kswapd_shrink_node通过shrink_node函数对节点中低于sc->reclaim_idx的非平衡zone区域进行回收操作
static bool kswapd_shrink_node(pg_data_t *pgdat,
struct scan_control *sc)
{
struct zone *zone;
int z;
/* Reclaim a number of pages proportional to the number of zones */
//初始化此次期望回收多少页面
sc->nr_to_reclaim = 0;
for (z = 0; z <= sc->reclaim_idx; z++) {
zone = pgdat->node_zones + z;
if (!managed_zone(zone))
continue;
sc->nr_to_reclaim += max(high_wmark_pages(zone), SWAP_CLUSTER_MAX);
}
/*
* Historically care was taken to put equal pressure on all zones but
* now pressure is applied based on node LRU order.
*/
//对节点中低于sc->reclaim_idx的非平衡zone区域进行回收操作
shrink_node(pgdat, sc);
/*
* Fragmentation may mean that the system cannot be rebalanced for
* high-order allocations. If twice the allocation size has been
* reclaimed then recheck watermarks only at order-0 to prevent
* excessive reclaim. Assume that a process requested a high-order
* can direct reclaim/compact.
*/
if (sc->order && sc->nr_reclaimed >= compact_gap(sc->order))
sc->order = 0;
//此次对当前内存节点的回收操作扫描的页面总数若大于等于期望回收的页面数返回True,反之则返回False
return sc->nr_scanned >= sc->nr_to_reclaim;
}
函数shrink_node后面会详细分析
通过上面的分析可知:
-
kswapd线程是循环检查pgdat节点中的各个zone是否均衡,若所有zone都均衡则kswapd线程sleep,若发现了有一个zone不zone_balance就会调用balance_pgdat函数去调节pgdat节点,直到所有的zone节点都zone_balance了后,kswapd进入sleep状态
-
对于balance_pgdat函数其内部也是一个大循环,每次循环都会对pgdat的所有zone区域进行扫描回收,直到该节点中有一个zone达到zone_balance或者循环次数超过sc.priority时为止。
3.kswapd执行流程总结
kswapd函数和linux os具有相同的生命周期。平时kswap线程会在函数kswapd_try_to_sleep中sleep,让出CPU控制权。若系统出现内存紧张的情况kswapd会被唤醒。通过仔细分析kswapd_try_to_sleep函数可发现,kswap唤醒条件有其它任务被动唤醒和周期性主动唤醒这两种:
-
kswapd被其它任务被动唤醒:
- 这里说的其它任务主要是内存分配任务,下面介绍一种被动唤醒kswapd线程的场景:若内存分配进程在快速内存分配流程中失败,进入慢速度内存分配流程。在慢速内存分配时若__GFP_KSWAPD_RECLAIM被设置,则内存分配进程会先通过wake_all_kswapds函数尝试将当前节点的kswapd线程唤醒,需要注意的是只有在当前内存节点0到classzone_idx中的所有zone区域在分配了order阶内存后,空闲内存值都低于high水线阈值(空闲内存值要减去该zone为classzone_idx保留的内存)时,wake_all_kswapds函数才会唤醒该节点的kswapd内核线程,通过异步方式对当前节点的内存进行回收。
- wake_all_kswapds函数会将内存分配任务中需要分配内存块的阶order和当前节点中第一个最合适分配内存的zone序号分别赋值给当前内存节点的pgdat->kswapd_order和pgdat->kswapd_classzone_idx两个成员变量(取新旧值中的较大者),目的是将它们的值分别传递给kswapd内核线程中的两个局部变量alloc_order和classzone_idx,在kswapd内核线程中利用这两个局部变量来控制balance_pgdat函数去努力平衡当前节点,让节点能够提供内存分配所需的内存块,同时这两个参数也是kswapd线程是否sleep的重要判断依据。
-
在伙伴系统慢速内存分配流程中有两处场景会调用wake_all_kswapds函数去尝试唤醒节点的kswapd线程。而直接内存回收中流程中则有一次场景会通过wake_up_interruptible(&pgdat->kswapd_wait)方式去尝试唤醒节点的kwapd线程(通过allow_direct_reclaim函数判断出当前节点不平衡,及节点的free_pages<pfmemalloc_reserve/2时尝试唤醒该节点kswapd函数)。上面a介绍的唤醒时机只是慢速内存分配中第一次尝试唤醒内存节点kswapd线程的时机点.
static void wake_all_kswapds(unsigned int order, const struct alloc_context *ac)
{
struct zoneref *z;
struct zone *zone;
pg_data_t *last_pgdat = NULL;
/*
*此处对于uma体系结构只会执行一次循环,对于NUMA若ac->zonelist中的zone都位于一个节点也只会执行一次循
*环,目的是唤醒zonelist中所有节点对应的kswapd线程
*/
for_each_zone_zonelist_nodemask(zone, z, ac->zonelist,
ac->high_zoneidx, ac->nodemask) {
if (last_pgdat != zone->zone_pgdat)
wakeup_kswapd(zone, order, ac->high_zoneidx);
last_pgdat = zone->zone_pgdat;
}
}
/*
*唤醒zone对应节点的kswapd线程:
* (1)分配内存的阶order和通过分配标志计算出当前节点中最适合分配内存的zone区域序号classzone_idx赋值给节
* 点的两个成员变量kswapd_order和kswapd_classzone_idx,传递给kswapd线程的两个控制局部变量
* alloc_order和classzone_idx
*(2) 若节点中0到classzone_idx所有zone区域的空闲内存在减去oder阶大小的内存后都不高于high水位值则唤醒
* 节点的kswapd内核线程
*/
void wakeup_kswapd(struct zone *zone, int order, enum zone_type classzone_idx)
{
pg_data_t *pgdat;
int z;
if (!managed_zone(zone))
return;
if (!cpuset_zone_allowed(zone, GFP_KERNEL | __GFP_HARDWALL))
return;
pgdat = zone->zone_pgdat;
/*
*classzone_idx是根据分配掩码计算出第一个最合适分配内存zone的序号,order是内存分配任需要内存块
*的阶。这两个参数赋值给节点的两个成员变量主要是像将这其传递到将要唤醒的kswapd线程中去。用于控制
*和协调kswapd线程的内存回收
*/
pgdat->kswapd_classzone_idx = max(pgdat->kswapd_classzone_idx, classzone_idx);
pgdat->kswapd_order = max(pgdat->kswapd_order, order);
if (!waitqueue_active(&pgdat->kswapd_wait))
return;
/* Hopeless node, leave it to direct reclaim */
//kswapd多次内存回收失败,直接放弃让kswapd睡眠,内存分配任务进行直接内存回收
if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES)
return;
/* Only wake kswapd if all zones are unbalanced */
/*只有在当前节点中0到classzone_idx中所有zone区域空闲内存不高于high水线的情况下才唤醒节点的kswapd线
*程
*/
for (z = 0; z <= classzone_idx; z++) {
zone = pgdat->node_zones + z;
if (!managed_zone(zone))
continue;
//zone空闲内存减去分配的order阶内存若不低于high水线值,函数返回True
if (zone_balanced(zone, order, classzone_idx))
return;
}
trace_mm_vmscan_wakeup_kswapd(pgdat->node_id, zone_idx(zone), order);
wake_up_interruptible(&pgdat->kswapd_wait);
}
- kswapd线程周期唤醒:kswapd线程被加入到pgdat->kswapd_wait等待队列睡眠时,会给该线程设置一个休眠时间HZ/10(100ms),线程睡眠100ms后会被唤醒,线程会通过函数prepare_kswapd_sleep去检查当前节点中0到classzone_idx的zone区域内存是否都zone_balanced,若所有zone区域都zone_balanced则线程通过调用schedule()函数让出cpu控制权进入长期的睡眠状态等待被其它任务wake_up,若出现了一个不zone_balanced的zone区域则线程被主动唤醒.kswapd线程唤醒后会调用balance_pgdat函数对当前节点进行内存回收。回收满足要求后kwapd线程会继续给kswapd线程设置一个休眠时间HZ/10(100ms),线程睡眠100ms后会被唤醒…kswapd会循环进行上面的操作,周期性对系统内存节点进行检查回收,以此来保证平常状态下系统剩余内存尽可能够用。最终kswapd线程会调用schedule()让出cpu控制权并进入一个长期的随眠状态。
从上面的流程分析中我们可以看到kswapd线程的周期唤醒是在线程被动唤醒执行完异步内存回收操作后的一小段时间内发生的。究其原因:kswapd被动唤醒是在系统环境内存不足的情况,内存分配任务获取不到内存而触发,表明这段时间系统内存环境紧张,因此在这时间段进行内存周期性回收是有必要的。