Linux内存管理:(七)页面回收机制

文章说明:

1. 触发页面回收

Linux内核中触发页面回收的机制大致有3个:

  • 直接页面回收机制。在内核态里调用页面分配接口函数alloc_pages()分配物理页面时,由于系统内存短缺,不能满足分配请求,因此内核会直接自陷到页面回收机制,尝试回收内存来解决当前的燃眉之急,这称为直接页面回收。
  • 周期性回收内存机制。这是kswapd内核线程的工作职责。当内核路径调用alloc_pages() 分配物理页面时,由于系统内存短缺,没法在低水位情况下分配出内存,因此会唤醒kswapd内核线程来异步回收内存。
  • slab收割机(slab shrinker)机制。这是用来回收slab对象的。当内存短缺时,直接页 面回收和周期性回收内存两种机制都会调用slab收割机机制来回收slab对象。slab机制分配的内存主要用于slab对象和kmalloc接口,也可用于内核空间的内存分配。

注意:

  • 直接回收内存的进程主体是调用者本身。
  • 直接回收内存是同步回收,这会阻塞调用者进程的执行。
  • kswapd本身是内核线程,它和调用者的关系是异步的。如test进程尝试调用alloc_pages()来分配内存,当发现在低水位情况下无法分配出内存时,它唤醒kswapd内核线程。这时,kswapd 内核线程就开始执行页面回收工作了。test进程会继续尝试其他办法来分配内存,如调用直接回收内存机制。

页面回收机制的主要调用路径如下图所示:

下面将根据源码围绕这张图中的关键部分进行讲解。

2. kswapd内核线程

kswapd是Linux内核中一个非常重要的内核线程,它负责在内存不足的情况下回收页面。kswapd内核线程初始化时会为系统中每个NUMA内存节点创建一个名为"kswapd%d"的内核线程。

触发周期性回收内存机制的逻辑如下所示:

balance_pgdat()函数是回收页面的主函数,其主体函数是一个很长的while循环,简化后的代码框架如下:

c 复制代码
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;
}

3. shrink_node()函数

shrink_node()函数用于扫描和回收内存节点中所有可回收的页面,还会做一些数据的统计和反馈工作:

c 复制代码
// 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;
}

4. shrink_active_list()函数

shrink_active_list()函数用于扫描活跃LRU链表,包括匿名页面或者文件映射页面,把最近一直没有人访问的页面添加到不活跃LRU链表中。

c 复制代码
// 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);
}

5. shrink_inactive_list()函数

shrink_inactive_list()函数扫描不活跃LRU链表以尝试回收页面,并且返回已经回收的页面的数量。该函数的逻辑过于复杂,因此这里用图来理解,如下图所示(感兴趣的道友可以根据该流程图去阅读该函数的源代码):

6. 页面回收的流程图

相关推荐
律宏阔4 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔4 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴4 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千4 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei374 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei374 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei374 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW4 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW4 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux
yunwei374 小时前
eBPF 开发者教程: 简单的 XDP 负载均衡器
linux·后端·性能优化