为了提高系统性能,Android系统中进程的匿名页、文件页按照一定的策略进行缓存,在内存紧张的时候再进行回收。但内存回收并不总是理想的,在一定条件下,为了保证系统的正常运行,会采用更加激进、直接的方式——杀进程。low memory killer(lmk)。
在android 系统中LMK经历了两次演进。
KLMK在android中存在了很长的时间,其基本原理是基于minfree来控制kill 相关的app的相关测试。例如下一组配置,/sys/module/lowmemorykiller/parameters/minfree :15360,19200,23040,26880,34415,43737;/sys/module/lowmemorykiller/parameters/adj : 0,1,2,4,9,10. 表示当系统可用内存低于26880个page的时候杀oom_score_adj>=4的进程。
Kernel会根据vmpressure的情况产生vmpressure event.KLMK会根据不同的vmpressure事件,选择不同的档位(adj)/minfree 值来挑选APPkill来进行memory的回收。然而根据vmpressure也有不少缺点。如代码维护,由于KLMK 是android 独有的,因此linuxupstream 并不愿意维护这部分代码,因此内核4.12开始,kernel 中KLMK驱动程序。其次vmpressure+minfree+系统空余memory,不能很好的反应系统实际使用memory紧张的情况,比如,如果系统内存碎片太多,而系统freememory 很充足,但是系统使用可能也会很卡顿。如果使用KLMK就不能很好的解决该问题。而PSI(Pressure Stall Information)便能较好的处理这种情况。
在Android10以及以后的版本,android变采用基于PSI 的ULMK
PSI 是Facebook搞的一套东西并在2018 年开源。PSI提供了一种评估系统资源压力的方法。系统有三个基础资源:CPU、Memory 和 IO.PSI 主要也是对这三种资源进行监控。
PSI 把系统缺少资源而stuck的为some 和full.
Some代表至少有一个任务在一段时间内由于缺少某个资源而阻塞。为了便于表示some的严重程度用缺少资源的时间/统计时间。一个%来标识some指标。
Full代表所有的非idle任务同时被阻塞. 为了便于表示full的严重程度用缺少资源的时间/统计时间。一个%来标识full指标。
我们以Memory 的 some 和 full 来举例说明,假设在 60 秒的时间段内,系统有两个task,在 60 秒的周期内的运行情况如下图所示:
红色阴影部分表示任务由于等待Memory 资源而进入阻塞状态。TaskA 和 Task B同时阻塞的部分为full,占比16.66%;至少有一个任务阻塞(仅Task B 阻塞的部分也计算入内)的部分为some,占比 50%。
some 和 full 都是在某一时间段内阻塞时间占比的总和,阻塞时间不一定连续,如下图所示:
PSI 在task_struct 结构中加入了一个成员:PSI_flags,用于标注任务所处状态,状态定义有以下几种:
#define TSK_IOWAIT(1 << NR_IOWAIT) // IO WAIT
#define TSK_MEMSTALL(1 << NR_MEMSTALL) //Memory stall
#define TSK_RUNNING(1 << NR_RUNNING) //tskrunning
状态的标记主要通过函数psi_task_change,这个函数在任务每次进出调度队列时,都会被调用,从而准确标注任务状态。
kernel/sched/psi.c
system/memory/lmkd/lmkd.cpp
首先建立proc/pressure目录,然后 3 个 proc_create 函数创建了io、memory 和 cpu 三个 proc 属性文件:
proc_mkdir("pressure", NULL);
proc_create("pressure/io", 0, NULL,&psi_io_fops);
proc_create("pressure/memory", 0, NULL,&psi_memory_fops);
proc_create("pressure/cpu", 0, NULL,&psi_cpu_fops);
psi_init 函数中初始化统计管理结构和更新任务的周期:
psi_period = jiffies_to_nsecs(PSI_FREQ); //默认2s
group_init(&psi_system);
我们把相关的任务组成一个group,然后针对这个任务组计算其PSI 值。当前系统只有一个PSI group:
static DEFINE_PER_CPU(struct psi_group_cpu,system_group_pcpu);
static struct psi_group psi_system = {
.pcpu = &system_group_pcpu,
};
如果支持cgroup(需要mount cgroup2 文件系统),那么系统中会有多个PSI group,形成层级结构。我们可以在挂载的cgroup 文件系统下面获取per-group 的 PSI 信息。
我们也可以从proc 文件系统下面获取整个系统级别的PSI 信息。Cgroup中各个分组的PSI 信息跟踪是类似的。
struct psi_group 用来定义PSI 统计管理数据,其中包括各cpu 状态、周期性更新函数、更新时间戳、以及各PSI 状态的时间记录。PSI状态一共有六种:
enum psi_states {
PSI_IO_SOME,
PSI_IO_FULL,
PSI_MEM_SOME,
PSI_MEM_FULL,
PSI_CPU_SOME,
/* Only per-CPU, to weigh the CPU in the global average: */
PSI_NONIDLE,
NR_PSI_STATES,
};
当ULMK 后台进程起来的时候,会首先注册some/full PSI 事件。
static struct psi_thresholdpsi_thresholds[VMPRESS_LEVEL_COUNT] = {
{ PSI_SOME, 70}, /* 70ms out of 1sec for partialstall */
{ PSI_SOME, 100}, /* 100ms out of 1sec for partialstall */
{ PSI_FULL, 70 }, /* 70ms out of 1sec for complete stall */
}
static bool init_psi_monitors() {
….
if(!init_mp_psi(VMPRESS_LEVEL_LOW, use_new_strategy)) {
}
if (!init_mp_psi(VMPRESS_LEVEL_MEDIUM,use_new_strategy)) {
}
if (!init_mp_psi(VMPRESS_LEVEL_CRITICAL,use_new_strategy)) {
}
if(!init_mp_psi(VMPRESS_LEVEL_SUPER_CRITICAL, use_new_strategy)) {
} …..
}
PSI 收到ULMK的注册后,会创建psimon kworker。Psimon kworker 会根据ULMK 注册的信息周期性的起来计算当前系统memroy的压力情况,当memory的压力值达到设定的阈值时,psimon 变trigger PSI event。
另一侧ULMK 会监听PSI event。当PSI event 发生时,ULMK 会epoll当前
的PSI event,然后根据PSI event的采用相关的策略。
更新统计数据的函数update_stats,主要有两步:
第一步get_recent_times,对每个 cpu更新各状态的时间并统计各状态系统总时间;
第二步calc_avgs,更新每个状态的10s、60s、300s 三个间隔的时间占比。
计算一个 PSIgroup 的 PS I值的过程示意图如下所示:
哪些情况被认为是Memorystall?