注:本文分析基于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进程实际回收过程,我们下篇继续分析。