Linux中的内存回收[二]

发布时间 2023-08-29 15:06:44作者: tomato-haha

https://zhuanlan.zhihu.com/p/72998605

Linux中的内存回收[一]

在NUMA系统中,如果使用页面cache所带来的的收益超过数据存储在不同zone/node的损失,那么可以选择在当前zone内存不足时不进行回收(以保留页面cache),而是使用其他zone/node的空闲内存。

反之,如果数据的locality更加重要,则应该选择在当前zone进行本地回收。可通过"/proc/sys/vm/zone_reclaim_mode"设置是否优先使用本地回收,该参数的默认值是0,表示不开启本地回收。

不过,对于大多数的UMA系统来说,本地回收则是更普遍的现象。回收的触发大致分为两种。一种是在内存分配时发现空闲内存严重不足,直接启动内存回收操作,这种内存回收被称为"direct reclaim"。在direct reclaim模式下,分配和回收是同步的关系,内存分配的过程会被内存回收的操作所阻塞,增加等待的时间。

对于anonymous page的回收,swap out必然会引起相对慢速的I/O操作。对于page cache的回收,如果是"dirty"的,也需要write back,也会引起I/O操作。Direct reclaim造成内存分配的等待已经让它很不好意思了,为了避免I/O操作加剧等待时间,它通常会选择"clean"的page cache来作为回收的对象。

一个更理想的情况是,我们能在内存压力不那么大的时候,就提前启动内存回收。而且,在某些场景下(比如在interrupt context或持有spinlock时),内存分配根本就是不能等待的。因此,Linux中另一种更为常见的内存回收机制是使用kswapd

每个node对应一个内核线程kswapd,kswapd在被唤醒后将扫描各个zone的内存使用状态,并据此进行必要的内存回收操作,因此kswapd的回收方式又被称为"background reclaim"。至于什么情况下触发direct reclaim,什么情况下又会触发background reclaim,是由内存的"watermark"决定的(关于这一块的详细介绍,请参考这篇文章)。

Kswapd虽然名字中含有"swap",但它不光处理anonymous page的swap out回收,同样处理page cache的回收,而且它还肩负着平衡active list和inactive list的重任,所以被它调用的函数叫做balance_pgdat()。

不管是direct reclaim,还是kswapd,最终都是调用shrink_zone() --> shrink_page_list() 进行回收操作。

static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan,
				 struct lruvec *lruvec, struct scan_control *sc)
{
	if (is_active_lru(lru)) {
		if (inactive_list_is_low(lruvec, is_file_lru(lru), sc, true))
			shrink_active_list(nr_to_scan, lruvec, sc, lru);
		return 0;
	}

	return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);
}

当inactive list中的页面比较少时,shrink_active_list()会从active list尾端转移一部分页面到inactive list中。这里的“少”是相对的,通常物理内存越大,inactive list的页面占比可以越小(页面总数还是增加的),因为inactive list的长度主要是用来减少短时间内refault的(参考上篇文章)。

shrink_inactive_list --> shrink_page_list 将从inactive list尾端移除选定数目的页面,进行释放。

回收一个页面之前,如果该页面当前是被映射的(根据struct page的_mapcount域判断),需要调用try_to_unmap(),通过reserve mapping更改所有指向这个页面的PTEs。对于anonymous page,还需要在swap space中分配slot,并且将这个page标记为dirty的。anonymous page是没有backing store的,从dirty的角度,它可以算是一直dirty的。

前面提到过,不是所有的页面都可以被回收的。如果检测到页面的flag是PG_locked或者是PG_reserved的,则只能跳过。对于正在回写的(flag是PG_writeback的),通常也是放弃回收,有这功夫去等待回写完成,还不如去找链表上其他的clean page。

之后,对于flag是PG_dirty的页面,启动pageout()将这些页面备份或者同步到外部磁盘,这里“备份”针对的是anonymous page,“同步”针对的是page cache。

免死金牌

内存回收算法是根据过去的情况预测未来,而且作为工程应用,融入了很多经验的元素,有可能存在一个进程的页面不断被回收,而这个进程因为需要继续运行又不断申请内存页面的情况,导致大部分的时间耗费在磁盘访问上,而进程无法实质性地运行下去。这样的情况虽然少见,但还是有一定发生的几率。为了保证执行完成,进程可以用申请swap token,拥有了swap token,就可以被内存回收算法暂时豁免(相当于免死金牌),除非,内存实在已经紧张的不行了。

Swap token机制是从linux内核2.6.9版本引入的,整个系统只有一个,用swap_token_mm表示,swap_token_mm指向当前持有swap token的进程的mm_struct。为了保证公平性,每个进程持有swap token的时间是有限的,持有时间由一个定时器控制,定时器timeout后,swap token就会转移给下一个进程。

之后的2.6.20版本引入了更合理的swap token抢占机制:如果一个进程在一段时间内换入的页面较多,说明它可能受到页面换出的影响较大,则这样的进程可以获得在swap token抢占中更高的优先级。随着linux虚拟内存模型的不断演进,swap token机制并不能很好的和cgoups以及NUMA placement融合,因而在3.4版本中被移除了,详情请参考这个patch

壮士断腕

尽管kswapd是很努力的,但它毕竟是周期性执行的,难免出现某个时刻系统中剩余的内存极少,少到可能连回收内存操作本身需要的内存都不够了。这时候只能使出终极武器了,那就是OOM killer,做法是选择一个进程(out_of_memory --> select_bad_process),然后kill掉(oom_kill_process中发送SIGKILL信号),把它占用的内存释放出来,牺牲了这个进程,保全了整个系统。虽然OOM killer有时候可能导致严重的损失,但总比系统完全崩溃要好。这个无辜的bad process(其实应该是victim啦)可不是随便挑的,应该优先选择这些:

  • 占有page frames比较多的,占有的多释放的才多,kill掉才有意义。
  • 静态优先级比较低的。

而不能选择这些:

  • 内核线程,因为内核线程往往执行的都是比较关键的任务。
  • 进程号为1的init进程。如果父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将被init进程托管,如果init进程都挂了,那就……所以啊,kill掉init进程是内核不允许的行为。
  • 直接访问硬件设备的进程,如果强行终结这样的进程,可能将硬件置于一个不确定的状态。

 

参考:

为什么手工drop_caches之后cache值并未减少

 

原创文章,转载请注明出处。