LMKD(Low Memory Killer Daemon)原理初识
一、背景
早期 Android 基于 Linux 内核的 lowmemorykiller 驱动,通过静态阈值触发杀进程。但是该机制如下缺陷:
响应滞后:依赖固定内存阈值(如 6,8,16,32,64,128MB),无法实时感知内存压力
策略僵化:仅按进程优先级(oom_adj)和内存占用排序,缺乏灵活性
为优化这一问题,Android 5.0(Lollipop) 引入了 Low Memory Killer Daemon (LMKD),将内存回收逻辑从内核迁移至用户空间,实现动态策略,结合 PSI(Pressure Stall Information) 监控内存压力,更精准触发回收。
二、原理
lmkd触发的时候是通过psi的压力值触发,当内存水位不足时,根据oomAdjScore选择性杀进程,并将内存紧张事件通知存活的App,保证系统的稳定运行。
2.1 何时杀
因为lmkd是一个用户空间的进程,如果想做到就需要检测内存、io、cpu等硬件的使用情况,而用户空间的进程是无权直接访问这些硬件资源的。但是内核空间有大佬可以解决这个问题,那就是PSI、vmpressure、lowmemorykiller。
2.1.1 PSI(Pressure Stall Information)
PSI在Android高版本上基本都采用了这种监听方式,而vmpressure在Android高版本上已经不在使用,并且lowmemorykiller内核杀进程的方式也已经不再采用了,因此来介绍下PSI。
PSI(Pressure Stall Information)是一个可以监控CPU、内存及IO性能异常的内核功能。
文件路径:system/memory/lmkd/lmkd.cpp
文件路径:system/memory/lmkd/lmkd.cpp
//init方法在上面的main方法中会被调用
static int init(void) {
省略代码......
//在高版本use_inkernel_interface这值为false,也就是不会使用lowmemorykiller这个功能
if (use_inkernel_interface) {
省略代码......
} else {
//初始化监听器,查看 [1.2]
if (!init_monitors()) {
return -1;
}
/* let the others know it does support reporting kills */
property_set("sys.lmk.reportkills", "1");
}
省略代码......
return 0;
}
//初始化监听器
[1.2]
static bool init_monitors() {
/* Try to use psi monitor first if kernel has it */
//use_psi_monitors为true代表使用PSI来实现资源紧张监控,在高版本为true, 如果 use_psi为true,代表使用PSI则使用init_psi_monitors()来初始化PSI 查看[1.3]
use_psi_monitors = GET_LMK_PROPERTY(bool, "use_psi", true) &&
init_psi_monitors();
//use_psi_monitors不可用的时候,使用vmpressure实现监控
/* Fall back to vmpressure */
if (!use_psi_monitors &&
(!init_mp_common(VMPRESS_LEVEL_LOW) ||
!init_mp_common(VMPRESS_LEVEL_MEDIUM) ||
!init_mp_common(VMPRESS_LEVEL_CRITICAL))) {
ALOGE("Kernel does not support memory pressure events or in-kernel low memory killer");
returnfalse;
}
if (use_psi_monitors) {
ALOGI("Using psi monitors for memory pressure detection");
} else {
ALOGI("Using vmpressure for memory pressure detection");
}
returntrue;
}
初始化PSI监听器
[1.3]
static bool init_psi_monitors() {
/*
* When PSI is used on low-ram devices or on high-end devices without memfree levels
* use new kill strategy based on zone watermarks, free swap and thrashing stats.
* Also use the new strategy if memcg has not been mounted in the v1 cgroups hiearchy since
* the old strategy relies on memcg attributes that are available only in the v1 cgroups
* hiearchy.
*/
bool use_new_strategy =
GET_LMK_PROPERTY(bool, "use_new_strategy", low_ram_device || !use_minfree_levels);
if (!use_new_strategy && memcg_version() != MemcgVersion::kV1) {
ALOGE("Old kill strategy can only be used with v1 cgroup hierarchy");
returnfalse;
}
/* In default PSI mode override stall amounts using system properties */
if (use_new_strategy) {
/* Do not use low pressure level */
psi_thresholds[VMPRESS_LEVEL_LOW].threshold_ms = 0;
psi_thresholds[VMPRESS_LEVEL_MEDIUM].threshold_ms = psi_partial_stall_ms;
psi_thresholds[VMPRESS_LEVEL_CRITICAL].threshold_ms = psi_complete_stall_ms;
}
//初始化低级别,查看 [1.4]
if (!init_mp_psi(VMPRESS_LEVEL_LOW, use_new_strategy)) {
returnfalse;
}
//初始化中级别,查看 [1.4]
if (!init_mp_psi(VMPRESS_LEVEL_MEDIUM, use_new_strategy)) {
destroy_mp_psi(VMPRESS_LEVEL_LOW);
returnfalse;
}
//初始化高级别,查看 [1.4]
if (!init_mp_psi(VMPRESS_LEVEL_CRITICAL, use_new_strategy)) {
destroy_mp_psi(VMPRESS_LEVEL_MEDIUM);
destroy_mp_psi(VMPRESS_LEVEL_LOW);
returnfalse;
}
returntrue;
}
[1.4]
static bool init_mp_psi(enum vmpressure_level level, bool use_new_strategy) {
int fd;
/* Do not register a handler if threshold_ms is not set */
if (!psi_thresholds[level].threshold_ms) {
returntrue;
}
fd = init_psi_monitor(psi_thresholds[level].stall_type,
psi_thresholds[level].threshold_ms * US_PER_MS,
PSI_WINDOW_SIZE_MS * US_PER_MS);
if (fd < 0) {
returnfalse;
}
//当有资源紧张的通知时,会调用mp_event_psi或者mp_event_common方法
vmpressure_hinfo[level].handler = use_new_strategy ? mp_event_psi : mp_event_common;
vmpressure_hinfo[level].data = level;
//使用epoll机制来监听fd上的通知,有资源紧张的通知会在fd上有数据写入
if (register_psi_monitor(epollfd, fd, &vmpressure_hinfo[level]) < 0) {
destroy_psi_monitor(fd);
returnfalse;
}
maxevents++;
mpevfd[level] = fd;
returntrue;
}
基于以上的代码,当出现资源紧张的时候,mp_event_psi或者mp_event_common方法是会被调用的,被调用的时候也就是开始杀进程的时机了。
2.2 根据oom_adj_score决定杀哪些进程
oom_adj_score的作用就是规定了进程的优先级,oom_adj_score的取值范围是[-1000,1000]只能是整数,-1000的分数代表该进程绝对绝对不会并且不能被杀,而1000则相反代表只要杀进程肯定最先把它杀掉。
ADJ 分数 | 英文标识 | 中文描述 | 典型进程类型 |
-1000 | SYSTEM | 系统核心进程(如 system_server ) | Android 系统服务 |
-900 | PERSISTENT_SYSTEM | 系统常驻进程 | 关键守护进程(如 surfaceflinger ) |
-800 | PERSISTENT_PROC | 常驻应用进程 | 厂商预装不可卸载应用 |
0 | FOREGROUND_APP | 前台交互应用(Activity 可见) | 用户正在操作的 App |
100 | VISIBLE_APP | 可见但不可交互(如悬浮窗) | 半透明 Activity 或画中画 |
200 | PERCEPTIBLE_APP | 可感知进程(如后台音乐播放) | 播放音乐/导航的后台应用 |
300 | BACKUP_APP | 备份进程 | 正在执行备份操作的 App |
400 | HEAVY_WEIGHT_APP | 重量级进程 | 占用大量资源的后台应用 |
500 | SERVICE_APP | 服务进程 | 通过 startService() 运行的进程 |
600 | HOME_APP | 桌面进程 | Launcher(如 Pixel Launcher) |
700 | PREVIOUS_APP | 上一个应用 | 用户最近使用的 App(非当前前台) |
900 | CACHED_APP_MIN | 缓存进程(最低优先级) | 完全后台无 Activity 的进程 |
999 | CACHED_APP_MAX | 缓存进程(最高优先级) | 即将被系统回收的进程 |
该杀哪个进程的核心逻辑是非常简单的,首先从最大分数1000开始从数组的的最末尾位置开始找进程,如果找到了杀掉这个进程;否则从分数999开始从数组的次末尾位置找进程,找到则杀掉进程;否则继续重复上面的逻辑直到找到了要杀的进程为止。整个循环肯定不能一直循环下去(因为一些关键进程是不能杀的比如system_server进程),分数直到min_score_adj后就结束。
文件路径:system/memory/lmkd/lmkd.cpp
文件路径:system/memory/lmkd/lmkd.cpp
//查找需要杀掉的进程, min_score_adj代表查找到最小分数截止
static int find_and_kill_process(int min_score_adj, struct kill_info *ki, union meminfo *mi,
struct wakeup_info *wi, struct timespec *tm,
struct psi_data *pd) {
int i;
int killed_size = 0;
bool choose_heaviest_task = kill_heaviest_task;
//OOM_SCORE_ADJ_MAX:代表最大分数它的值是1000,从最大分数开始查找
for (i = OOM_SCORE_ADJ_MAX; i >= min_score_adj; i--) {
struct proc *procp;
//choose_heaviest_task代表是否杀掉任务繁重的进程,PERCEPTIBLE_APP_ADJ的值是200
//下面逻辑代表:choose_heaviest_task不为true并且分数小于200的时候,需要杀任务繁重的进程了
if (!choose_heaviest_task && i <= PERCEPTIBLE_APP_ADJ) {
/*
* If we have to choose a perceptible process, choose the heaviest one to
* hopefully minimize the number of victims.
*/
choose_heaviest_task = true;
}
//循环去找进程
while (true) {
//如果是杀繁重任务的进程,则调用proc_get_heaviest(i)方法找到繁重任务进程;否则调用proc_adj_tail(i)去查找
//分数i对应的索引处的循环双向链表的尾节点的进程
procp = choose_heaviest_task ?
proc_get_heaviest(i) : proc_adj_tail(i);
if (!procp)
break;
//调用kill_one_process方法开始杀进程,killed_size代表释放的空间
killed_size = kill_one_process(procp, min_score_adj, ki, mi, wi, tm, pd);
//若释放的空间大于0,则跳出循环
if (killed_size >= 0) {
break;
}
}
//若释放空间大于0,则跳出查杀进程循环逻辑
if (killed_size) {
break;
}
}
//返回释放空间大小
return killed_size;
}
那min_score_adj的具体值是多少呢,依据PSI(PSI上面提到,会在需要杀进程的时候发出通知给lmkd)和watchdog(watchdog是会监听lmkd的主线程是否出现耗时,耗时的话watchdog就会去杀进程)会分别定义不同的值。非常明确的一点是min_score是有最小值的,最小值是0,也就是所有分数为负值的进程肯定是不会被lmkd杀掉的,在系统资源极度极度紧张的情况下,分数大于等于0的进程都会被杀掉(前台进程、后台无焦点进程、后台进程)
2.2.1 PSI取值逻辑
min_score_adj在PSI条件下,一般情况下它的值是201(PREVIOUS_APP_ADJ + 1),内存极度极度紧张的情况下它的值为0,具体代码位于
system/memory/lmkd/lmkd.cpp的mp_event_psi方法
2.2.2 watchdog取值逻辑
min_score_adj在watchdog条件下,它的值为0,具体代码位于system/memory/lmkd/lmkd.cpp的watchdog_callback方法
以上就是该杀谁的内容,其实杀的进程都是app进程,后台app进程是先被杀掉的,像system_server进程、系统persistent进程它们都是负值是不会被杀掉的,甚至系统native进程它们的分数基本都是-1000,因此也是不会被杀掉的。
三、查看手机的LMKD配置信息
命令:
adb shell getprop | grep sys.lmk.minfree_levels [sys.lmk.minfree_levels]: [18432:0,23040:100,27648:200,32256:250,55296:900,80640:950]
内存阈值 (KB) | 内存阈值 (MB) | ADJ 分数 | 目标进程类型 | 进程示例 |
18432 | 18 | 0 | 前台应用(FOREGROUND_APP) | 用户正在操作的 App |
23040 | 22.5 | 100 | 可见但无交互(VISIBLE_APP) | 画中画、悬浮窗 |
27648 | 27 | 200 | 可感知进程(PERCEPTIBLE_APP) | 后台音乐播放、导航 |
32256 | 31.5 | 250 | 备份进程(BACKUP_APP) | 正在执行备份的 App |
55296 | 54 | 900 | 缓存进程(CACHED_APP_MIN) | 完全后台无 Activity 的进程 |
80640 | 78.75 | 950 | 缓存进程(CACHED_APP_MAX) | 即将被回收的进程 |
即内存水位与进程终止规则对照表
进程优先级 (ADJ分数) | 内存水位 (页) | 内存水位(MB) | 终止规则 |
0 | 18,432 | 72 MB | 当可用内存 ≤ 72MB,杀死 ADJ > 0的进程(包括前台应用外的所有进程)。 |
100 | 23,040 | 90 MB | 当可用内存 ≤ 90MB,杀死 ADJ > 100的进程(如后台服务、非可见应用)。 |
200 | 27,648 | 108 MB | 当可用内存 ≤ 108MB,杀死 ADJ > 200的进程(如后台音乐播放等)。 |
250 | 32,256 | 126 MB | 当可用内存 ≤ 126MB,杀死 ADJ > 250的进程(如备份操作中的进程)。 |
900 | 36,864 | 144 MB | 当可用内存 ≤ 144MB,杀死 ADJ > 900的进程(最低优先级的缓存进程)。 |
950 | 46,080 | 180 MB | 当可用内存 ≤ 180MB,杀死 ADJ > 950的进程(极端情况下的强制回收)。 |
内存水位以 页(Page) 为单位(通常 1页 = 4KB),转换为 MB 的公式:MB=1024页数×4。,例如:18,432×4/1024=72MB
四、缺陷
在mtk上lmdk,swappiness可能会用满,totalused也可能会很多(UsedRAM很大)。lmkd里面策略相对比较缓和,故需增加swap&total用量的判断
在高通项目上,各类情况都出现。
1、psi不正确,没有多少内存使用的时候出现高内存压力
2、cached kernel+ free也出现很大,但是cached很少
3、swap用满了
4、totalused很多,swap没怎么用
ps:
需在lmkd增加swap、cached kernel+ free、total used、cached app nums的判断
进程回收存在以下问题:
1、进程回收功能比较耗时,且没有即时退出功能。
2、进程回收发生在adj改变的时刻,没有考虑计算场景,可能带来性能影响,引起流畅性问题。
3、没有针对系统做内存水线调节机制。只有lmkd在内存极端紧张的时候,做强制性进程剔除,此时内存压力较大,可能出现流畅性问题。同时由于lmkd只做基于oomadj的强制性内存剔除,不做到更弹性的策略性查杀,不利于系统做策略性优化。
另外,基于进程维度的内存回收(压缩)方式,效率极低。由于需要遍历vma,且有大量的pte为空,进行一次进程内存回收,需要耗费很多空循环。频繁的进行进程维度的内存回收,会有性能衰退的风险。
scaned:285005, empty: 226640, unnormal: 468, unevict: 0, tried: 57897,reclaimed: 57894
scaned:196916, empty: 157842, unnormal: 476, unevict: 0, tried: 38598,reclaimed: 38595
scaned:257753, empty: 200805, unnormal: 483, unevict: 0, tried: 56465,reclaimed: 56461
scaned:214477, empty: 138932, unnormal: 547, unevict: 0, tried: 74998,reclaimed: 74995
文章参考:
最新Android 内存管理之LMKD
Android帝国之进程杀手--lmkd