kswapd进程工作原理(二)——回收内存上半部

注:本文分析基于linux-4.18.0-193.14.2.el8_2内核版本,即CentOS 8.2

在上篇文章——kswapd进程工作原理(一)中,我们分析了kswapd进程的初始化以及触发场景,那kswap到底是怎么回收内存,回收哪些内存呢,我们来看下kswap进程实体。

1 kswapd

主要流程:

  • 标识自己为kswap进程,并进行一些初始化
  • 尝试睡眠,有可能kswap是被意外唤醒
  • 如果kswap确实需要工作,调用balance_pgdat开始回收内存
static int kswapd(void *p)
{
	unsigned int alloc_order, reclaim_order;
	unsigned int classzone_idx = MAX_NR_ZONES - 1;
	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);

	if (!cpumask_empty(cpumask))
		set_cpus_allowed_ptr(tsk, cpumask);
	current->reclaim_state = &reclaim_state;

    //标识自己是kswap进程,并允许回写脏页到swap分区
	tsk->flags |= PF_MEMALLOC | PF_SWAPWRITE | PF_KSWAPD;
	set_freezable();

	pgdat->kswapd_order = 0;
    //初始时都设置为扫描所有内存域
	pgdat->kswapd_classzone_idx = MAX_NR_ZONES;
	for ( ; ; ) {
		bool ret;

		alloc_order = reclaim_order = pgdat->kswapd_order;
		classzone_idx = kswapd_classzone_idx(pgdat, classzone_idx);

kswapd_try_sleep:
        //kswap进程尝试睡眠
		kswapd_try_to_sleep(pgdat, alloc_order, reclaim_order,
					classzone_idx);

        //走到这,说明kswap需要工作了,获取需要回收的page数量以及需要回收的zone区
		alloc_order = reclaim_order = pgdat->kswapd_order;
		classzone_idx = kswapd_classzone_idx(pgdat, classzone_idx);
		pgdat->kswapd_order = 0;
		pgdat->kswapd_classzone_idx = MAX_NR_ZONES;

		ret = try_to_freeze();
		if (kthread_should_stop())
			break;
		if (ret)
			continue;

		trace_mm_vmscan_kswapd_wake(pgdat->node_id, classzone_idx,
						alloc_order);
        //开始真正回收页面
		reclaim_order = balance_pgdat(pgdat, alloc_order, classzone_idx);
		if (reclaim_order < alloc_order)
			goto kswapd_try_sleep;
	}

	tsk->flags &= ~(PF_MEMALLOC | PF_SWAPWRITE | PF_KSWAPD);
	current->reclaim_state = NULL;

	return 0;
}

2 kswapd_try_to_sleep

主要流程:

  • 通过prepare_kswapd_sleep判断kswap是否可以睡眠
  • 可以睡眠则先尝试睡眠0.1s
  • 如果中途没有被唤醒,说明kswap可以睡眠,让出CPU,schedule出去
  • 如果中途被唤醒则返回上层函数,执行内存回收
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;
    //将当前进程,即kswap进程加入到kswapd_wait等待队列,并设置为TASK_INTERRUPTIBLE状态
	prepare_to_wait(&pgdat->kswapd_wait, &wait, TASK_INTERRUPTIBLE);

    //kswap进程睡眠前一些准备,比如唤醒等待内存回收的进程
	if (prepare_kswapd_sleep(pgdat, reclaim_order, classzone_idx)) {
        //重置内存整理时的记录
		reset_isolation_suitable(pgdat);

        //由于我们目前系统有空闲内存,因此可以尝试内存整理进程,居安思危
		wakeup_kcompactd(pgdat, alloc_order, classzone_idx);
        //kswap尝试睡眠0.1s,等待唤醒或超时
		remaining = schedule_timeout(HZ/10);

        //如果睡眠中途被唤醒,重置kswap相关变量,用于这次的内存回收
		if (remaining) {
			pgdat->kswapd_classzone_idx = kswapd_classzone_idx(pgdat, classzone_idx);
			pgdat->kswapd_order = max(pgdat->kswapd_order, reclaim_order);
		}
        //将kswap从kswapd_wait链表中摘除,并设置为TASK_RUNNING
		finish_wait(&pgdat->kswapd_wait, &wait);
		prepare_to_wait(&pgdat->kswapd_wait, &wait, TASK_INTERRUPTIBLE);
	}

    //如果kswap进程刚才睡眠超时后才返回,也就是remaining=0,意味着kswap可以真正的睡眠
    //也就是可以睡久一点,同时再次确认下kswap进程可以睡眠
	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);
        //让出CPU,让内核调度别的进程运行
		if (!kthread_should_stop())
			schedule();

		set_pgdat_percpu_threshold(pgdat, calculate_pressure_threshold);
	} else {
        //更新统计值
		if (remaining)
			count_vm_event(KSWAPD_LOW_WMARK_HIT_QUICKLY);
		else
			count_vm_event(KSWAPD_HIGH_WMARK_HIT_QUICKLY);
	}
    //kswap进程无法睡眠,将kswap从kswapd_wait链表中摘除,并设置为TASK_RUNNING
	finish_wait(&pgdat->kswapd_wait, &wait);
}

3 prepare_kswapd_sleep

主要流程:

  • 唤醒所有等待内存回收的进程
  • 如果此前kswap回收失败达到16次了,没必要再唤醒kswap
  • 如果此时刚好有zone能满足high_wmark水位,那也没必要唤醒kswap了
static bool prepare_kswapd_sleep(pg_data_t *pgdat, int order, int classzone_idx)
{
    //kswap睡眠前,把所有之前由于等待直接回收的进程唤醒
    //kswap可以睡眠说明系统中有可用内存,或者是在没法回收内存,这时需要唤醒
    //这些等待内存的进程去获取内存或者让进程自行进行内存直接回收
	if (waitqueue_active(&pgdat->pfmemalloc_wait))
		wake_up_all(&pgdat->pfmemalloc_wait);

	//kswap回收失败次数达到16次,不再尝试回收,留给进程自行处理
	if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES)
		return true;
    //如果至少有一个zone区域满足在high_wmark水位,kswap进程也无需再工作
	if (pgdat_balanced(pgdat, order, classzone_idx)) {
		clear_pgdat_congested(pgdat);
		return true;
	}

	return false;
}

4 balance_pgdat

在kswapd回收内存过程中有一个扫描控制结构体,用于控制这个回收过程。既然是回收内存,就需要明确要回收多少内存,在哪里回收,以及回收时的操作权限等,我们看下这个控制结构struct scan_control主要的一些变量,

struct scan_control {
	unsigned long nr_to_reclaim; //shrink_list()需要回收的页面数量
	gfp_t gfp_mask;
	int order; //进程内存分配指定的order
	nodemask_t	*nodemask; //指定可以在那个node回收内存
	struct mem_cgroup *target_mem_cgroup; //是否针对某个cgroup扫描回收内存
	int priority; //控制每次扫描数量,默认是总页数的1/4096
	enum zone_type reclaim_idx; //进行页面回收的最大zone id
	unsigned int may_writepage:1; //是否可以回写
	unsigned int may_unmap:1; //是否可以执行unmap
	unsigned int may_swap:1; //是否可以将页面交换
    ...
	unsigned int compaction_ready:1; //是否可以进行内存压缩,即碎片整理
	unsigned long nr_scanned; //已扫描的非活动页面数量
	unsigned long nr_reclaimed; //shrink_zones()中已回收页面数量
	...
};

有了这个控制结构,我们对此次的内存回收便有了大概的预期。
主要流程:

  • 定义kswap进程的内存回收控制结构,允许umap和swap操作
  • 判断buffer_head缓存是否太多,太多就从最高内存域开始回收
  • 判断是否有zone能满足此次内存分配,有则此次kswap回收可以停止
  • 如果非活动匿名页太少,对匿名active链表做老化处理
  • 调用kswapd_shrink_node开始回收内存
  • 回收完毕判断是否需要唤醒等待内存的进程
  • 此次kswap没有回收到页面,失败次数加1,达到16次就放弃
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;
	unsigned long pflags;
	struct zone *zone;
	struct scan_control sc = { //页扫描控制结构
		.gfp_mask = GFP_KERNEL,
		.order = order, //kswap需要回收到该阶的连续内存
		.priority = DEF_PRIORITY, //控制每次扫描数量,默认是总页数的1/4096
		.may_writepage = !laptop_mode,
		.may_unmap = 1, //允许解除页到进程的映射
		.may_swap = 1, //允许内存回收时将页面放入swap分区
	};
	...
    //循环回收内存,直到达到要求
	do {
		unsigned long nr_reclaimed = sc.nr_reclaimed;
		bool raise_priority = true;
		bool ret;

		sc.reclaim_idx = classzone_idx;
        //如果buffer_head缓存太多就从最高内存域开始回收
        //buffer_head最多占10%的ZONE_NORMAL
		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;
			}
		}
        //如果有一个zone区域能满足当前内存分区需求,无需回收
        //只有当所有zone都无法满足此order的需求,才会真正去回收内存
        //这也是kswap停止工作的条件
		if (pgdat_balanced(pgdat, sc.order, classzone_idx))
			goto out;

        //如果非活动匿名页太少,对匿名active链表做老化处理,
        //让页面有机会在回收之前被引用,如果系统没有swap分区无需操作
		age_active_anon(pgdat, &sc);

        //如果优先级较高,就允许回写操作,以便能回收更多内存
		if (sc.priority < DEF_PRIORITY - 2)
			sc.may_writepage = 1;

		/* Call soft limit reclaim before calling shrink_node. */
		sc.nr_scanned = 0;
		nr_soft_scanned = 0;
        //cgroup层面先进行一次轻度内存回收
		nr_soft_reclaimed = mem_cgroup_soft_limit_reclaim(pgdat, sc.order,
						sc.gfp_mask, &nr_soft_scanned);
		sc.nr_reclaimed += nr_soft_reclaimed;

        //针对该节点回收内存页面
		if (kswapd_shrink_node(pgdat, &sc))
			raise_priority = false;

        //kswap处理完回收,判断是否需要让之前等待内存释放而睡眠的进程醒来
        //有可能是回收到了需要的内存,也有可能是kswap无能为力,
		if (waitqueue_active(&pgdat->pfmemalloc_wait) &&
				allow_direct_reclaim(pgdat))
			wake_up_all(&pgdat->pfmemalloc_wait);

		/* Check if kswapd should be suspending */
		__fs_reclaim_release();
		ret = try_to_freeze();
		__fs_reclaim_acquire();
		if (ret || kthread_should_stop())
			break;

		//如果此次循环没有回收到页面,提高优先级,扫描更多页面
		nr_reclaimed = sc.nr_reclaimed - nr_reclaimed;
		if (raise_priority || !nr_reclaimed)
			sc.priority--;
	} while (sc.priority >= 1);//如果直到扫描系统所有页面仍无法回收到内存,结束工作

	if (!sc.nr_reclaimed)
        //如果此次kswap没有回收到页面,失败次数加1,达到16次就放弃
		pgdat->kswapd_failures++;
	...
	return sc.order;
}

5 kswapd_shrink_node

主要流程:

  • 计算high_wmark水位值,回收页面数量要大于该值
  • 对node进行内存回收,包括LRU链表和slab缓存
  • 如果已经回收到两倍order大小的内存,设置检测内存阈值检测odrer为0阶,避免过度回收
static bool kswapd_shrink_node(pg_data_t *pgdat,
			       struct scan_control *sc)
{
	struct zone *zone;
	int z;

	sc->nr_to_reclaim = 0;
    //统计每个zone区的high_wmark水位值,用于后面回收时使用
    //kswap回收工作至少要让空闲内存高于high_wmark水位值
	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);
	}

    //对node进行内存回收,包括LRU链表和slab缓存
	shrink_node(pgdat, sc);

    //如果已经回收到两倍order大小的内存,则用0阶去检测是否满足high_water水位
    //避免过度回收
	if (sc->order && sc->nr_reclaimed >= compact_gap(sc->order))
		sc->order = 0;
    //如果扫描页面数量大于等于需要回收的页面数量,返回真,这样就不需要提高优先级
	return sc->nr_scanned >= sc->nr_to_reclaim;
}

6 shrink_node

主要流程:

  • 回收LRU链表上的内存页面
  • 调用系统注册的所有shrinker,回收slab缓存
static bool shrink_node(pg_data_t *pgdat, struct scan_control *sc)
{
	struct reclaim_state *reclaim_state = current->reclaim_state;
	unsigned long nr_reclaimed, nr_scanned;
	bool reclaimable = false;
    //循环回收内存
	do {
		...
		memcg = mem_cgroup_iter(root, NULL, &reclaim);
        //遍历该cgroup,回收内存
		do {
			...
			reclaimed = sc->nr_reclaimed;
			scanned = sc->nr_scanned;
            //针对该cgroup回收内存,主要是LRU链表
			shrink_node_memcg(pgdat, memcg, sc, &lru_pages);
			node_lru_pages += lru_pages;
            //调用系统注册的所有shrinker,回收slab缓存
			shrink_slab(sc->gfp_mask, pgdat->node_id,
				    memcg, sc->priority);
            ...
            //直接回收和kswap路径需要扫描node上所有内存,但其他情况下,只要
            //回收到足够的空闲内存就可以停止扫描
			if (!global_reclaim(sc) && sc->nr_reclaimed >= sc->nr_to_reclaim) {
				mem_cgroup_iter_break(root, memcg);
				break;
			}
		} while ((memcg = mem_cgroup_iter(root, memcg, &reclaim)));

		if (reclaim_state) {
			sc->nr_reclaimed += reclaim_state->reclaimed_slab;
			reclaim_state->reclaimed_slab = 0;
		}
        ...
		//本次有回收到内存,置位reclaimable标志
		if (sc->nr_reclaimed - nr_reclaimed)
			reclaimable = true;
		...
    //循环条件是此时空闲内存还无法满足进程需要也无法满足内存整理需求
    //且非活动LRU链表页面数量大于两倍需回收页面
	} while (should_continue_reclaim(pgdat, sc->nr_reclaimed - nr_reclaimed,
					 sc));

	if (reclaimable)
		pgdat->kswapd_failures = 0;

	return reclaimable;
}

7 shrink_node_memcg

主要流程:

  • 根据每个LRU链表的数量和系统swappiness参数确定此次内存回收中各个LRU需要扫描的页面数量
  • 根据上步确定的扫描数量,开始对活动LRU链表和非活动LRU链表回收内存

关于LRU链表相关分析可以参看——LRU链表及LRU缓存

static void shrink_node_memcg(struct pglist_data *pgdat, struct mem_cgroup *memcg,
			      struct scan_control *sc, unsigned long *lru_pages)
{
	...
    //确定各个LRU链表需要扫描的页面数量
	get_scan_count(lruvec, memcg, sc, nr, lru_pages);
	...
	while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] ||
					nr[LRU_INACTIVE_FILE]) {
		unsigned long nr_anon, nr_file, percentage;
		unsigned long nr_scanned;
		//遍历每个LRU链表回收内存
		for_each_evictable_lru(lru) {
			if (nr[lru]) {
				//扫描数量不高于SWAP_CLUSTER_MAX,32页
				nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX);
				nr[lru] -= nr_to_scan;
				//根据之前确定的扫描数量,开始回收内存
				nr_reclaimed += shrink_list(lru, nr_to_scan,
							    lruvec, sc);
			}
		}

		cond_resched();
		//如果没有回收到目标数量页面,或者此时是直接回收的情况,
		//不调整扫描数量,返回继续回收
		if (nr_reclaimed < nr_to_reclaim || scan_adjusted)
			continue;
		...
	}
	blk_finish_plug(&plug);
	sc->nr_reclaimed += nr_reclaimed;

	//如果非活动匿名LRU链表太短,尝试收缩活动LRU链表
	if (inactive_list_is_low(lruvec, false, sc, true))
		shrink_active_list(SWAP_CLUSTER_MAX, lruvec,
				   sc, LRU_ACTIVE_ANON);
}
  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
kswapd是Linux系统的一个内核进程,主要负责管理系统内存的交换和预取机制,确保系统能够高效地利用内存。而Docker则是一种虚拟化技术,可以在单个操作系统上运行多个相互隔离的应用程序。 如果kswapd进程一直占用CPU,可能意味着系统内存出现了问题。以下是一些可能的解决方案: 1. 增加系统内存:当系统内存不足时,kswapd进程会经常运行以提高内存利用率。这时可以通过增加系统内存来缓解问题。 2. 优化内存占用:如果系统中存在进程或应用程序占用过多内存,可以通过优化它们的内存占用降低kswapd进程的负载。可以考虑关闭一些不必要的应用或进程,或者通过修改应用程序或脚本来减少它们的内存占用。 3. 调整系统参数:可以通过修改系统内存管理的一些参数来调整kswapd进程的行为。例如,可以修改/proc/sys/vm/swappiness参数来调整内存页面管理的行为。 4. 检查Docker容器:如果问题与Docker相关,可以检查Docker容器是否占用过多的系统资源,例如内存和CPU。可以使用Docker stats命令查看Docker容器使用的资源情况,然后根据情况调整容器配置。 总之,如果kswapd进程一直占用CPU,需要先了解系统的内存使用情况,并采取相应的措施来调整系统配置。如果问题与Docker有关,也需要检查Docker容器的资源使用情况,并进行必要的优化。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值