Linux内存回收之drop cache

Linux有自己完备的一套内存回收机制,并不需要人为的干预,但它同时也提供了一种手动释放的手段,可以让我们在调试的时候使用,方法是通过设置"/proc/sys/vm/drop_cache"参数。

这个参数可接收3个数字的输入,分别是1, 2和3,写入1代表只释放page cache的可回收部分,写入2代表只释放slab cache中的可回收部分。可回收的slab cache是指在调用"kmem_cache_create"函数向slab分配器申请内存时,使用了"SLAB_RECLAIM_ACCOUNT"标志位,主要就是dentry cache和inode cache。

写入3代表同时释放page cache和reclaimable slab cache。很好记,1+2=3嘛,事实上,1就是二级制的01,2就是二进制的10,运行drop cache就是靠这两个置1的bit判断的。具体的实现函数是"drop_caches_sysctl_handler"(代码位于/fs/drop_caches.c):

int drop_caches_sysctl_handler(...)
{
    if (sysctl_drop_caches & 1) {
	iterate_supers(drop_pagecache_sb, NULL);
	count_vm_event(DROP_PAGECACHE);
    }
    if (sysctl_drop_caches & 2) {
        drop_slab();
	count_vm_event(DROP_SLAB);
    }
    ...
}

写入后只会执行一次,后续不用再去管它,不需要写入0去清除什么的(其实你写入0也是不会成功的),如果非强迫症犯了想把它清掉,那就写入4好了,4是"100",末尾两位是0就行。

【page cache的释放

在执行"echo 1 > /proc/sys/vm/drop_caches"命令之前和之后,分别查看"meminfo"中page cache的数量:

可见,page cache所占用的内存明显减少,但并没有完全清空。因为这种方式只能清空page cache中"clean"的部分,也就是已经和外部磁盘同步过的部分。

因为"clean"的部分回收起来最简单,既然已经同步过了,直接丢弃即可,下次要用再从磁盘上拷贝回来就可以了,而"dirty"的部分需要先writeback到磁盘,才能释放。所以,"drop cache"准确地应该叫drop clean cache(如果想释放"dirty"的page cache,可以先使用"sync"命令强制同步一下)。

具体实现

以下是向"drop_cache"写入1后的函数调用过程:

与实现自动回收的kswapd线程通过扫描LRU链表不同,手动回收page cache是遍历各个文件系统的各个文件,来寻找可供回收的clean pages。

【slab cache的释放

page cache对应的是文件系统中的文件数据(userdata),而inode cache对应的是文件系统中文件的控制结构(metadata)。对于磁盘文件系统,内存inode存在后备存储,因此同page cache一样,较易在内存中重建,释放的代价较低。dentry虽然在磁盘上没有直接对应的结构,但也可根据文件系统中目录inode的信息进行重建。

还是和之前page cache的实验一样,在执行"echo 2 > /proc/sys/vm/drop_caches"命令之前和之后,分别查看"meminfo"中slab cache的大小。

代表slab cache中可回收部分的"SReclaimable"明显减少,而代表不可回收部分的"SUnreclaim"的值基本没有变化。

再来看下回收前后dentry cache的数量变化,可见,处于"unused"状态的dcache都被回收了,共49721个:

而inode cache的数量,减少了44407个:

一个内存inode被一个或多个dentry指向,如果指向一个inode的dentry都被释放,那么该inode也就没有继续存在于内存的意义,也会被同步释放,且由于这种指向关系,释放的inode object总数应略小于释放的dentry object的总数。

借助crash工具的"struct"命令,可以快速得知一个结构体的大小,以笔者使用的"4.18.0-80.el8.x86_64"内核版本为例,dentry和inode的结构体大小分别是208字节和648字节,计算一下,差不多就等于"SReclaimable"中减少的部分。

具体实现

上述操作对应的函数调用过程大致是这样的:

从dcache和icache的LRU链表获取可回收的数量后,还可以通过"/proc/sys/vm/vfs_cache_pressure"参数来调节slab cache的「回收倾向」。

shrinker -> count_objects (super_cache_count)
total_objects += list_lru_shrink_count(&sb->s_dentry_lru, sc);
total_objects += list_lru_shrink_count(&sb->s_inode_lru, sc);
total_objects = vfs_pressure_ratio(total_objects); // 按百分比调整

该参数的值越小,则回收slab cache的比例也就越低,当值为0时,即使内存资源紧张也不回收slab cache(非常类似于调节anonymous page和page cache回收比例的"swappiness"参数)。笔者的系统采用的是默认值100,所以是全部回收。

如果不是全部回收,那么在确定了回收的slab object总数后,需根据dcache和icache各自所占的比例进行分摊("sc->nr_to_scan"是通过上一个函数返回的"total_objects"经过进一步的调整获得):

shrinker -> scan_objects (super_cache_scan)
dentries = mult_frac(sc->nr_to_scan, dentries, total_objects);
inodes = mult_frac(sc->nr_to_scan, inodes, total_objects);

获得"sc->nr_scanned"的扫描结果之后,就是对dcache和icache实际的清除操作了(由于dentry和inode的指向关系,应先释放dcache),可通过perf工具查看此过程的时间百分比,笔者的一次实验结果如下:

69.43%  prune_dcache_sb
27.41%  prune_icache_sb

【小结

清除page cache和slab cache可以快速释放内存,cache虽然占据了内存空间,但其本来就是为了提高性能而存在的,在手动清除后的一段时间里,系统的整体运行效率将受到影响。如果发现即便负责自动回收的kswapd频繁启动,系统的内存资源依然吃紧,应尝试去寻找部分内存始终不能有效释放的原因,手动清除并不能解决根本问题。

笔者在工作中就曾经遇到过这样的案例:kswapd由于持续运行,导致占据了大量的CPU资源,后来发现root cause是有进程一直再往tmpfs里面打印log,而tmpfs属于shmem,在性质上是anonymous pages,内存回收时应该被swap out到外部的swap space。

但系统没有在外部flash设置swap分区,手动drop cache只能回收page cache和slab cache,对anony page是没有用的。tmpfs里的内容不断增加,导致空闲内存低于low watermark,触发kswapd,但没有swap分区,kswapd怎么使劲也是无能为力的,最终的结果就是内存占用率和CPU占用率都很高,系统可能就挂掉了。

参考内存清理脚本(可根据自己的需求进行调整)

#!/bin/bashecho "开始清除缓存"

sync;sync;sync #写入硬盘,防止数据丢失

sleep 10 #延迟10秒

echo 1 > /proc/sys/vm/drop_caches

echo 2 > /proc/sys/vm/drop_caches

echo 3 > /proc/sys/vm/drop_caches

echo "清理结束"

参考文章:https://zhuanlan.zhihu.com/p/93962657

Comments Closed.