文章说明:
Linux内核版本:5.0
架构:ARM64
参考资料及图片来源:《奔跑吧Linux内核》
Linux 5.0内核源码注释仓库地址:
Linux内核中触发页面回收的机制大致有3个:
注意:
页面回收机制的主要调用路径如下图所示:
下面将根据源码围绕这张图中的关键部分进行讲解。
kswapd是Linux内核中一个非常重要的内核线程,它负责在内存不足的情况下回收页面。kswapd内核线程初始化时会为系统中每个NUMA内存节点创建一个名为“kswapd%d”的内核线程。
触发周期性回收内存机制的逻辑如下所示:
balance_pgdat()
函数是回收页面的主函数,其主体函数是一个很长的while循环,简化后的代码框架如下:
static int balance_pgdat(pg_data_t *pgdat, int order, int classzone_idx)
{
...
restart:
// 使用 sc.priority 表示页面扫描粒度或者优先级
sc.priority = DEF_PRIORITY;
do {
...
// 检查这个内存节点中是否有合格的 zone,其水位高于高水位并且能分配出 2 的 sc.priority 次方个连续的物理页面
balanced = pgdat_balanced(pgdat, sc.order, classzone_idx);
if (!balanced && nr_boost_reclaim) {
nr_boost_reclaim = 0;
goto restart;
}
// 若符合条件,则跳转到 out 标签处
if (!nr_boost_reclaim && balanced)
goto out;
...
// 对匿名页面的活跃 LRU 链表进行老化
age_active_anon(pgdat, &sc);
...
// 回收页面的核心函数
if (kswapd_shrink_node(pgdat, &sc))
raise_priority = false;
...
if (raise_priority || !nr_reclaimed)
// 不断加大扫描粒度
sc.priority--;
} while (sc.priority >= 1);
if (!sc.nr_reclaimed)
pgdat->kswapd_failures++;
out:
if (boosted) {
...
// 若设置了 boosted,则唤醒 kcompactd 内核线程
wakeup_kcompactd(pgdat, pageblock_order, classzone_idx);
}
...
// 返回已经回收的页面数量
return sc.order;
}
shrink_node()
函数用于扫描和回收内存节点中所有可回收的页面,还会做一些数据的统计和反馈工作:
// pgdat 表示内存节点
// sc 表示扫描的控制参数
static bool shrink_node(pg_data_t *pgdat, struct scan_control *sc)
{
...
do {
...
// 遍历 memory cgroup,调用 shrink_node_memcg() 回收页面
do {
...
// 基于内存节点的页面回收函数,它会被 kswapd 内核线程和直接页面回收机制调用
shrink_node_memcg(pgdat, memcg, sc, &lru_pages);
node_lru_pages += lru_pages;
if (sc->may_shrinkslab) {
// shrink_slab() 调用内存管理系统中的 shrinker 接口,用于回收 slab 对象
shrink_slab(sc->gfp_mask, pgdat->node_id,
memcg, sc->priority);
}
// vmpressure() 函数通过计算 scanned/reclaimed 比例来判断内存压力
vmpressure(sc->gfp_mask, memcg, false,
sc->nr_scanned - scanned,
sc->nr_reclaimed - reclaimed);
...
} while ((memcg = mem_cgroup_iter(root, memcg, &reclaim)));
...
// 判断当前进程是否是 kswapd 内核线程
if (current_is_kswapd()) {
// 若当前系统回写的页面数量等于这一轮页面扫描的数量,说明这些系统有大量回写页
// 面,因此应该设置 PGDAT_WRITEBACK,表示发现有大量页面正在等待回写到磁盘
if (sc->nr.writeback && sc->nr.writeback == sc->nr.taken)
set_bit(PGDAT_WRITEBACK, &pgdat->flags);
// 若当前系统的脏页数量等于正在块设备 I/O 上进行回写数据的页面数量,说明系统有大量
// 页面堵塞在块设备的 I/O 操作上,因此应该设置 PGDAT_CONGESTED,表示内存节点中发
// 现有大量脏页拥堵在一个 BDI 设备中
if (sc->nr.dirty && sc->nr.dirty == sc->nr.congested)
set_bit(PGDAT_CONGESTED, &pgdat->flags);
// 若当前系统还没有开始回写的脏页数量等于这一轮扫描的文件映射的页面数量,说明系统有
// 大量脏页面,因此应该设置 PGDAT_DIRTY,表示发现有大量的脏文件页面
if (sc->nr.unqueued_dirty == sc->nr.file_taken)
set_bit(PGDAT_DIRTY, &pgdat->flags);
// 统计数据有 immediate 个页面,说明在处理正在回写的页面时发现已经有大量的页面在等待回写,
// 因此需要调用 congestion_wait() 函数让页面等待 100ms
if (sc->nr.immediate)
congestion_wait(BLK_RW_ASYNC, HZ/10);
}
...
// 当前页面回收者是直接页面回收者的情况下:
// current_may_throttle() 判断当前回写设备是否拥堵,若拥堵则睡眠一段时间来缓解拥堵情况。
// 若成功回收了 sc->nr_reclaimed 个页面,返回 true
if (!sc->hibernation_mode && !current_is_kswapd() &&
current_may_throttle() && pgdat_memcg_congested(pgdat, root))
wait_iff_congested(BLK_RW_ASYNC, HZ/10);
// 通过这一轮中回收页面的数量和扫描页面的数量来判断是否需要继续扫描
} while (should_continue_reclaim(pgdat, sc->nr_reclaimed - nr_reclaimed,
sc->nr_scanned - nr_scanned, sc));
...
return reclaimable;
}
shrink_active_list()函数用于扫描活跃LRU链表,包括匿名页面或者文件映射页面,把最近一直没有人访问的页面添加到不活跃LRU链表中。
// nr_to_scan:待扫描页面的数量
// lruvec:LRU 链表集合
// sc:页面扫描控制参数
// lru:待扫描的 LRU 链表类型
static void shrink_active_list(unsigned long nr_to_scan,
struct lruvec *lruvec,
struct scan_control *sc,
enum lru_list lru)
{
...
// 定义 3 个临时链表
LIST_HEAD(l_hold); /* The pages which were snipped off */
LIST_HEAD(l_active);
LIST_HEAD(l_inactive);
...
// is_file_lru() 判断链表是否为文件映射的 LRU 链表
int file = is_file_lru(lru);
// 从 lruvec 中返回内存节点描述符 pgdat
struct pglist_data *pgdat = lruvec_pgdat(lruvec);
...
// 在操作链表时,有一个保护 LRU 的自旋锁 pgdat->lru_lock
spin_lock_irq(&pgdat->lru_lock);
// isolate_lru_pages() 批量地把 LRU 链表的部分页面迁移到临时链表(l_hold链表)中,
// 这样可以缩短加锁的时间
nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold,
&nr_scanned, sc, isolate_mode, lru);
// 增加内存节点中的 NR_ISOLATED_ANON 计数
__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, nr_taken);
// 增加 recent_scanned[] 计数
reclaim_stat->recent_scanned[file] += nr_taken;
...
// 页面迁移到临时链表 l_hold 后,释放 pgdat->lru_lock 自旋锁
spin_unlock_irq(&pgdat->lru_lock);
// while 循环扫描临时链表 l_hold 中的页面,有些页面会添加到 l_active 中,
// 有些会添加到 l_inactive 中
while (!list_empty(&l_hold)) {
cond_resched();
// lru_to_page() 从链表中取一个页面
page = lru_to_page(&l_hold);
list_del(&page->lru);
// 如果页面是不可回收的,就把它放回不可回收的 LRU 链表中
if (unlikely(!page_evictable(page))) {
putback_lru_page(page);
continue;
}
...
// page_referenced() 函数返回该页面最近访问、引用 PTE 的个数
// 若返回 0,表示最近没有访问、引用
if (page_referenced(page, 0, sc->target_mem_cgroup,
&vm_flags)) {
...
}
// 如果页面没有被引用,清除页面的 PG_Active 标志位并且将页面加入 l_inactive 链表中
ClearPageActive(page); /* we are de-activating */
SetPageWorkingset(page);
list_add(&page->lru, &l_inactive);
}
// 这段加锁期间,把 l_inactive 和 l_active 链表中的页面迁移到相应的 LRU 链表中
spin_lock_irq(&pgdat->lru_lock);
// 把最近引用的页面数量保存到 recent_rotated 中,以便下一次扫描时在
// get_scan_count() 中重新计算匿名页面和文件映射页面 LRU 链表的扫描比值
reclaim_stat->recent_rotated[file] += nr_rotated;
nr_activate = move_active_pages_to_lru(lruvec, &l_active, &l_hold, lru);
nr_deactivate = move_active_pages_to_lru(lruvec, &l_inactive, &l_hold, lru - LRU_ACTIVE);
__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, -nr_taken);
spin_unlock_irq(&pgdat->lru_lock);
mem_cgroup_uncharge_list(&l_hold);
// l_hold 链表中是剩下的页面,可以释放
free_unref_page_list(&l_hold);
trace_mm_vmscan_lru_shrink_active(pgdat->node_id, nr_taken, nr_activate,
nr_deactivate, nr_rotated, sc->priority, file);
}
shrink_inactive_list()函数扫描不活跃LRU链表以尝试回收页面,并且返回已经回收的页面的数量。该函数的逻辑过于复杂,因此这里用图来理解,如下图所示(感兴趣的道友可以根据该流程图去阅读该函数的源代码):