在像FreeBSD这样的类似Unix的现代系统上,“交换”是指将内存内容分页到磁盘,然后按需分页的活动。页出活动是由于系统中缺少可用内存而发生的:内核试图识别可能在不久的将来无法访问的内存页面,并将其内容复制到磁盘中以进行安全保存,直到需要它们为止再次。当应用程序尝试访问已换出的内存时,它会在内核从交换磁盘获取已保存的内存的同时阻塞,然后恢复执行,就好像什么都没发生一样。
以上所有听起来听起来很明智。毕竟,磁盘通常比RAM大得多,那么为什么不使用它们来缓存不经常访问的内存页面呢?但是,许多经验丰富的系统管理员将交换视为异常活动,这表明存在某些问题。这是合理的:直到最近,通常用于交换的磁盘的访问等待时间比RAM高出数百万倍。
也就是说,等待从交换中调回内存的应用程序必须等待数十毫秒,而常规RAM访问则需要数十纳秒。因此,大量使用换出的内存会破坏系统的性能,因此交换活动通常被视为系统需要更多RAM或需要调整内存使用量的信号。常见的“解决方案”是完全禁用交换,迫使操作系统在必要时诉诸其他手段来释放内存。
在2021年,廉价的SSD变得越来越普遍,并且具有更适合于交换的性能特征,因此似乎有必要重新审视FreeBSD中的交换工作原理,并尝试提供一些常见问题的见解。
计算机系统具有固定数量的RAM。由操作系统来优化其用法。理想情况下,操作系统将能够展望未来,以查看将要访问哪些数据。利用这些信息,他们可以确保在访问数据之前在RAM中可用这些数据。但是,由于受限于现实世界,它们使用一组试探法来尝试预测将来的内存访问。一种有效且常用的启发式方法是将最近访问的数据缓存在内存中,因为有可能在不久的将来再次访问它们。当没有可用内存可用并且应用程序访问未缓存的数据时,FreeBSD会确定最近最少访问的内存并逐出其内容以为新数据腾出空间。该算法称为最近最少使用(LRU)。
注意,内核不能简单地丢弃数据[1]。如果磁盘上没有副本,则必须先将数据调出页面,然后再释放后备内存。因此,交换活动与LRU紧密相关。
精确地实现LRU会带来很多不必要的开销,因此FreeBSD实现了一种近似的LRU –它试图查找长时间未访问的内存,并将其逐出。作为此实现的一部分,FreeBSD将系统的内存划分为一组队列:活动队列,非活动队列和洗衣队列[2]。这些队列的大小由top(1)表示:
内存:活跃2591M,无效6576M,洗衣1389M,有线4155M,buf 1543M,1130M免费交换:总计8192M,已使用1623M,免费6569M,已使用19%
“有线”页面不适合被调出,因此不参与LRU。活动页面经常被引用;通常,它们被映射到一个或多个进程的地址空间。例如,由malloc(3)返回的内存最初将驻留在活动队列中。为了确定不再引用哪些活动页面,称为“页面守护程序”的内核进程会定期检查每个页面的最近访问历史。未引用的页面将从活动队列中老化,并进入非活动队列。
非活动队列包含最近未访问过的页面。如果内核需要处理可用内存不足的情况,则此类页面是重用的不错选择。队列通过最近访问来帮助对页面进行排序:新的非活动页面被插入到队列的尾部,页面从队列的开头被回收。
前面我们提到,行为良好的操作系统在处理可用内存不足时一定不能丢弃数据。如果某些数据在内存页面中,并且稳定存储中不存在该数据的副本,则该页面被称为“脏”,并且在可以重复使用之前,必须将其内容分页到存储中。否则,页面是“干净的”。例如,如果一个人使用grep(1)搜索文件,则该文件的数据必须加载到内存中,但是由于grep(1)仅读取该数据,因此该内存将是干净的,并且可以随时重用。
活动和非活动队列将包含干净页和脏页。当回收内存以减轻短缺时,页面守护程序将从空闲队列的头部释放干净页面。必须首先通过调出脏页来清理脏页以交换或文件系统。这是一项繁重的工作,因此页面守护程序会将它们移至洗衣队列以进行延迟处理。洗衣队列由专用线程(洗衣线程)管理,该线程负责确定何时以及要退出多少页面。这些队列之间的关系如下所示:
–页面守护程序将未引用的页面从活动队列迁移到非活动队列(1)。
–要释放内存,页面守护程序将扫描非活动队列开头的页面(2),释放干净页面(6),然后将脏页面移动到洗衣队列的尾部(3)。
–当洗衣线程决定清洁某些脏页(4)时,它将它们交给分页器,该分页器将其内容写入稳定的存储器中,并将已清洁的页放入非活动队列(6)。
–如果在将页面放入非活动队列或洗衣队列后对其进行引用,则该页面将被懒惰地移回活动队列。
洗衣线程的一种可能策略是不执行任何操作,依靠回收干净页面来满足对空闲内存的需求。确实,这是完全禁用交换时发生的情况。但是,根据定义,洗衣队列中的页面是不活动的,并且未使用的内存是浪费的内存。另一种可能的策略是在页面进入队列后立即对其进行清洗,但这可能导致不必要的I / O。
洗衣线程使用前两个信号来控制“后台”洗涤,而第三个信号用于驱动“不足”洗涤。
后台清洗背后的想法是,在出现干净的不活动页面不足之前,尝试确保将一些脏页面调出。当系统同时没有可用页面和干净的非活动页面时,需要可用内存的应用程序实际上会卡住,等待某些页面输出交换完成。因此,洗衣线程试图确保洗衣队列不会变得太大:队列大小的比率(1)越大,洗衣线程执行脏内存页面输出的频率就越高。由于在不存在可用内存不足的情况下,调出脏内存浪费了I / O带宽,因此洗衣线程会监视页面守护程序的活动,以确定其应多久执行一次页面输出。
将脏页的内容换出页面进行交换后,该页将被标记为干净并可以回收。此时,页面的内容与保存到交换设备的副本完全匹配。 (该页面可能再次被弄脏,在这种情况下,该页面最近已被访问并属于活动队列。)假设该页面已释放,然后应用程序尝试读取数据。将分配一个新页面,并从交换页面调回数据,这时应用程序可以再次运行并使用该数据。此时,页面仍是干净的-仅在写入数据后页面才会被标记为脏-因此交换设备中的副本仍然有效,没有理由丢弃它。
更一般地,一次写入多次读取访问模式对于某些类型的数据可能是常见的。寿命长的进程可能会在启动期间分配并写入内存区域,然后例如仅从该内存中读取。如果该内存被调出,FreeBSD将保留该副本处于交换状态,只要它仍然有效。否则,为了回收该内存,它将必须执行另一次昂贵的页面输出操作。
因此,即使有足够的可用内存,也经常会看到交换空间使用量适中[3]:在过去的某个时候,对可用内存的需求触发了换页,换出的数据仍然有效。
在某些情况下,不足量清洗可能不足以减轻可用内存的不足。一个进程可能有失控的内存泄漏,或者系统可能被过度订阅,以至于它变得完全无响应。洗衣线程可能正在尽快调出内存,但不能满足需求,或者交换设备可能已满。在这一点上,内核别无选择,只能尝试杀死进程以回收内存并恢复系统的稳定性-可怕的OOM(内存不足)杀死。
FreeBSD将在两种情况下触发OOM杀死。首先,如果页面守护程序反复无法从非活动队列中回收_any_页面,它将最终触发OOM终止。如果交换设备已满,则洗衣线程将无法将页面从洗衣队列移动到非活动队列,因此这种情况可能潜在地触发OOM终止,并因此释放一些交换空间。
如果FreeBSD检测到线程被卡在页面错误处理程序中,它也会触发OOM终止。处理硬页错误需要分配一些内存,如果应用程序无法在此基本操作中取得进展,则内核将开始终止进程。这有助于捕获缓慢的可回收页面trick流阻止首次试探法启动但系统仍然没有响应的情况。
为了找到要杀死的进程,内核会估计每个可运行进程的内存使用情况,并选择使用率最高的进程。当用户空间内存泄漏触发OOM终止时,这种启发式方法效果很好,因为它将识别出内存泄漏的进程。但是,目标进程可能被阻止,因此杀死它不会立即释放任何内存。在这种情况下,内核可能执行多次背对背的OOM终止,并最终诉诸于终止重要的系统进程。
该代码可以使用madvise(MADV_PROTECT)阻止OOM杀手选择调用进程。子进程不会继承此保护。 FreeBSD基本系统中的许多基本守护程序(例如syslogd和sshd)都使用此方法。
protect(1)程序可用于启动启用了OOM保护的进程。对于使用rc启动的服务,可以将$ {name} _oomprotect rc.conf变量设置为“ YES”以在启用OOM保护的情况下运行该过程。
很难提供一般建议。在基于FreeBSD的设备(例如嵌入式路由器或NAS设备)上,禁用交换功能是很常见的。这样的系统可能没有任何适合交换的磁盘[4],或者它们的设计人员可能不愿意接受因分页内存故障导致的潜在应用程序延迟。在FreeBSD 11.0之前的版本中,页面守护程序既负责释放干净的非活动页面,又用于清洗脏的非活动页面,因此大量的页面活动可能会延迟回收内存并触发冻结。
许多系统设计人员反对启用交换,因为过度交换会导致无限延迟。这种厌恶情绪很大程度上与使用慢速硬盘作为交换设备的经验有关,尤其是与繁忙的文件系统共享时。我们认为,志趣相投的FreeBSD用户值得探索再次启用交换功能。 NVMe驱动器很常见,访问延迟只有几十微秒,比十年前的标准小几个数量级。 FreeBSD在内存压力下的性能不断提高,并且不断对内核进行严格的压力测试。可以使用mlock(2)连接对内存访问延迟特别敏感的应用程序,而整个系统可以受益于交换可以提供的更高的内存效率。
[2]在NUMA系统上,每个内存域有一组队列。每个域的队列由单独的页面守护程序和洗衣线程管理。
[4] SD卡不能用作交换设备。它们的耐用性较低,并且I / O延迟可能很高。
在Klara,我们有一个完整的团队致力于为您提供FreeBSD项目。无论您是在计划FreeBSD项目,还是正在参与某个项目,并且需要一些额外的见识,我们都将为您提供帮助!