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

1、关于kswap进程

kswap用于在内存不足时进行内存回收,每个NUMA内存节点会有一个kswapd进程,

[root@localhost ~]# numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 32689 MB
node 0 free: 593 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 32768 MB
node 1 free: 417 MB
node distances:
node   0   1 
  0:  10  21 
  1:  21  10 
[root@localhost ~]#  ps aux | grep kswap | grep -v grep
root        340  0.0  0.0      0     0 ?        S    Apr24   7:14 [kswapd0]
root        341  0.0  0.0      0     0 ?        S    Apr24   6:00 [kswapd1]

我们把node结构体中与kswap的成员变量择出来,便于了解其工作原理,

typedef struct pglist_data {
    ...
	wait_queue_head_t kswapd_wait; //kswapd进程的等待队列
	wait_queue_head_t pfmemalloc_wait; //直接内存回收过程中的进程等待队列
	struct task_struct *kswapd;	//指向该结点的kswapd进程的task_struct
	int kswapd_order; //kswap回收页面大小
    enum zone_type kswapd_classzone_idx; //kswap扫描的内存域范围
	...
} pg_data_t;

2、kswap的初始化

系统初始化期间,调用kswapd_init,在每个内存节点上创建kswap进程,设置kswap进程的回调函数—kswapd,也就是实际执行的主体函数。

static int __init kswapd_init(void)
{
	int nid, ret;

	swap_setup();
    //遍历系统上所有内存节点,创建kswapd进程
	for_each_node_state(nid, N_MEMORY)
 		kswapd_run(nid);
	ret = cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN,
					"mm/vmscan:online", kswapd_cpu_online,
					NULL);
	WARN_ON(ret < 0);
	return 0;
}

int kswapd_run(int nid)
{   
    //获取当前节点对象
	pg_data_t *pgdat = NODE_DATA(nid);
	int ret = 0;

	if (pgdat->kswapd)
		return 0;
    //创建内核进程kswapd
	pgdat->kswapd = kthread_run(kswapd, pgdat, "kswapd%d", nid);
	...
	return ret;
}

3、kswap的触发

kswap进程虽然是系统启动时就会创建,但是大多数时候它处于睡眠状态,只有在进程由于内存不足导致分配内存失败时会被唤醒,从而回收内存,供进程使用。

__alloc_pages_slowpath
    wake_all_kswapds //路径1,进入慢分配路径会先唤醒kswap进程
        wakeup_kswapd
            wake_up_interruptible(&pgdat->kswapd_wait)
    __alloc_pages_direct_reclaim //路径1失败后,进行直接内存回收
        __perform_reclaim
            try_to_free_pages
                throttle_direct_reclaim
                    allow_direct_reclaim
                        wake_up_interruptible(&pgdat->kswapd_wait)
  • kswap唤醒路径1

在这种情况下要判断kswap是否需要环境,比如当前node此前kswap回收失败次数大于16次,始终无法回收到满足用户需求大小的内存,没必要再唤醒kswap,只能进入直接回收内存流程了;或者此时刚好有内存释放,导致当前node至少有一个zone能满足用户需求,那也不需要唤醒kswap。这两种情况都是返回上级函数,用户会再次尝试分配内存来区分是实在无法分配还是可以继续分配。

static void wake_all_kswapds(unsigned int order, gfp_t gfp_mask,
			     const struct alloc_context *ac)
{
	struct zoneref *z;
	struct zone *zone;
	pg_data_t *last_pgdat = NULL;
	enum zone_type high_zoneidx = ac->high_zoneidx;
    //遍历每个zone,唤醒每个zone对应的kswap进程回收内存
    //有点奇怪,为啥不直接遍历node就好,还得从zone返回查找node
	for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, high_zoneidx,
					ac->nodemask) {
		if (last_pgdat != zone->zone_pgdat)
            //唤醒kswap进程
			wakeup_kswapd(zone, gfp_mask, order, high_zoneidx);
		last_pgdat = zone->zone_pgdat;
	}
}

void wakeup_kswapd(struct zone *zone, gfp_t gfp_flags, int order,
		   enum zone_type classzone_idx)
{
	pg_data_t *pgdat;

	if (!managed_zone(zone))
		return;

	if (!cpuset_zone_allowed(zone, gfp_flags))
		return;
    //获取该zone对应的node节点对象
	pgdat = zone->zone_pgdat;

	if (pgdat->kswapd_classzone_idx == MAX_NR_ZONES)
		pgdat->kswapd_classzone_idx = classzone_idx;
	else
		pgdat->kswapd_classzone_idx = max(pgdat->kswapd_classzone_idx,
						  classzone_idx);
    //kswap回收的内存不能小于进程分配的值,不然回收就没有意义了
	pgdat->kswapd_order = max(pgdat->kswapd_order, order);
    //如果该node的kswap进程已启动就返回
	if (!waitqueue_active(&pgdat->kswapd_wait))
		return;
		
    //如果此时kswap回收失败次数大于16次
    //或者有至少有一个zone在high_wmark条件下有满足此order的页面
    //则不必唤醒kswap进程,留给需要使用内存的进程自行处理
	if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES ||
	    pgdat_balanced(pgdat, order, classzone_idx)) {
		
        //此时内存碎片太多,如果内存分配没有启动直接回收内存,
        //尝试启动内存整理进程,将碎片整理为连续内存,回收为高阶页面
		if (!(gfp_flags & __GFP_DIRECT_RECLAIM))
			wakeup_kcompactd(pgdat, order, classzone_idx);
		return;
	}

	trace_mm_vmscan_wakeup_kswapd(pgdat->node_id, classzone_idx, order,
				      gfp_flags);
    //唤醒在kswapd_wait队列上的kswap进程
	wake_up_interruptible(&pgdat->kswapd_wait);
}
  • kswap唤醒路径2——直接内存回收(阻塞)

当路径1唤醒kswap返回后,会尝试分配内存,如果还是失败,会再一次切换zone,如果切换后还是无法分配出内存,就只能进行直接内存回收了

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
						struct alloc_context *ac)
{
	...
    //如果允许唤醒kswap,先赶紧唤醒回收点内存
	if (gfp_mask & __GFP_KSWAPD_RECLAIM)
		wake_all_kswapds(order, gfp_mask, ac);

	//尝试是否能分配出来,就看kswap有没有那么快了,
	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
	if (page)
		goto got_pg;
    ...
retry:
	/* Ensure kswapd doesn't accidentally go to sleep as long as we loop */
    //再次确认kswap没有意外进入睡眠
	if (gfp_mask & __GFP_KSWAPD_RECLAIM)
		wake_all_kswapds(order, gfp_mask, ac);

	reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
	if (reserve_flags)
		alloc_flags = reserve_flags;

	//尝试换个zone
	if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
		ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
					ac->high_zoneidx, ac->nodemask);
	}

	//在新的zone上分配内存
	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
	if (page)
		goto got_pg;
    
	/* Try direct reclaim and then allocating */
    //到现在还是没法满足用户需求,直接内存回收吧
	page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
							&did_some_progress);
	if (page)
		goto got_pg;

	/* Try direct compaction and then allocating */
    //内存碎片整理
	page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
					compact_priority, &compact_result);
	if (page)
		goto got_pg;
    ...
}

直接回收内存阻塞在于throttle_direct_reclaim,它会一直唤醒kswap回收内存,直到空闲内存满足要求才返回

static bool throttle_direct_reclaim(gfp_t gfp_mask, struct zonelist *zonelist,
					nodemask_t *nodemask)
{
	...
	//唤醒当前zonelist上对应的kswap进程
	for_each_zone_zonelist_nodemask(zone, z, zonelist,
					gfp_zone(gfp_mask), nodemask) {
		if (zone_idx(zone) > ZONE_NORMAL)
			continue;

		//唤醒第一个可用的node对应的kswap进程,如果内存足够了,直接退出
        //就不需要进程后面的阻塞性内存回收
		pgdat = zone->zone_pgdat;
		if (allow_direct_reclaim(pgdat))
			goto out;
		break;
	}
    ...
	//如果当前内存分配调用者无法进入文件系统,阻塞1s,让kswap回收内存,然后唤醒进程
	if (!(gfp_mask & __GFP_FS)) {
		wait_event_interruptible_timeout(pgdat->pfmemalloc_wait,
			allow_direct_reclaim(pgdat), HZ);

		goto check_pending;
	}

	//如果内存分配调用者可以进入文件系统,那在这里阻塞等待,直到kswap回收到足够内存,
    //再唤醒进程,否则就通过allow_direct_reclaim不停的唤醒kswap回收内存
	wait_event_killable(zone->zone_pgdat->pfmemalloc_wait,
		allow_direct_reclaim(pgdat));
    ...
}

关于kswapd进程实际回收过程,我们下篇继续分析。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐