LMK机制演进:从旧版LMK到新版LMKD,设计思路的变迁
大家好,我是你们的Android内核讲师。今天我们来聊聊LMK的演进史。说实话,这个主题我特别喜欢,因为它完整地展现了Android系统在内存管理上的一次「自我革命」。
你想想看,一个手机用着用着突然卡死,或者App被莫名其妙地杀掉——这种体验,是不是很熟悉?嗯,这背后就是LMK在起作用。但旧版LMK和新版LMKD,它们的处理方式完全不同。我当年在调优某款旗舰机时,就深刻体会到了这种差异。
一、旧版LMK:简单粗暴的「一刀切」
旧版LMK,全称Low Memory Killer,是Android早期引入的机制。它的设计思路其实很直白:当系统内存低于某个阈值时,直接杀掉一些进程来释放内存。
说白了,它就是个「看门狗」。系统给它设定几个水位线,比如:
- 内存剩余< 100MB:杀掉空进程
- 内存剩余< 60MB:杀掉后台服务
- 内存剩余< 20MB:杀掉可见进程
代码实现上,它是在内核里注册了一个shrinker回调。每当内存紧张时,内核会遍历所有进程,根据进程的oom_score_adj值来决定杀谁。
// 旧版LMK的核心逻辑(简化版)
static int lowmem_shrink(struct shrinker *s, struct shrink_control *sc)
{
struct task_struct *p;
int selected_oom_score_adj = OOM_SCORE_ADJ_MAX;
int selected_tasks = 0;
// 遍历所有进程
for_each_process(p) {
int oom_score_adj = p->signal->oom_score_adj;
// 找到oom_score_adj最大的进程
if (oom_score_adj > selected_oom_score_adj) {
selected_oom_score_adj = oom_score_adj;
selected_tasks = 1;
}
}
// 发送SIGKILL信号
if (selected_tasks)
send_sig(SIGKILL, p, 0);
return 0;
}
这段代码看着简单吧?但问题恰恰出在「简单」上。我在项目中遇到过,旧版LMK经常误杀前台应用。比如用户正在看视频,突然切到微信回个消息,回来发现视频App已经被杀了。为什么?因为旧版LMK只看内存水位,不看进程的实际使用状态。
二、旧版LMK的三大痛点
旧版LMK的问题,我总结为三点:
- 静态阈值,不够灵活——不同设备的内存大小、使用场景都不一样,但旧版LMK的阈值是写死的。你想想看,2GB内存的手机和8GB内存的手机,用同一套阈值,这合理吗?
- 只看内存,不看行为——它只关心进程占了多少内存,不关心这个进程是不是用户正在使用的。这就导致「杀错人」的情况频繁发生。
- 内核态决策,信息不足——LMK运行在内核态,它看不到上层App的优先级、用户的使用习惯。说白了,它就是个「盲人摸象」。
我记得有一次,用户反馈手机总是在玩游戏时卡顿。我们抓了日志一看,发现是LMK把游戏进程杀了,但游戏明明在前台运行。为什么?因为游戏占用了大量内存,触发了LMK的阈值。但LMK不知道这个进程是用户正在玩的——它只知道「内存不够了,杀一个大的」。
三、新版LMKD:从「一刀切」到「精准打击」
到了Android 8.0,Google终于意识到旧版LMK的问题,推出了全新的LMKD。这个「D」代表Daemon,意味着它从内核移到了用户空间。
为什么要这么做?我个人习惯从两个角度理解:
- 信息更丰富——用户空间的守护进程可以获取到App的优先级、使用频率、前台/后台状态等。这些信息内核拿不到。
- 决策更灵活——LMKD可以根据当前的内存压力等级,动态调整杀进程的策略。不再是简单的「到了阈值就杀」。
LMKD的核心设计思路,我画了个简图:
+----------------+ +----------------+ +----------------+
| 内核内存通知 | --> | LMKD守护进程 | --> | ActivityManager |
| (vmpressure) | | (用户空间) | | (进程优先级) |
+----------------+ +----------------+ +----------------+
| | |
| 报告内存压力 | 查询进程优先级 | 返回oom_score_adj
v v v
[内存不足] [决策: 杀谁?] [执行: 发送信号]
你看,LMKD不再直接在内核里杀进程,而是通过监听内核的vmpressure事件来感知内存压力。然后它去问ActivityManager:当前哪些进程可以杀?哪些不能动?最后才执行杀动作。
四、LMKD的三大改进
LMKD相比旧版LMK,具体改进了哪些地方?我总结了三方面:
| 对比维度 | 旧版LMK | 新版LMKD |
|---|---|---|
| 运行位置 | 内核空间 | 用户空间 |
| 触发方式 | 静态内存阈值 | 动态vmpressure事件 |
| 决策依据 | 仅oom_score_adj | oom_score_adj + 进程状态 + 使用频率 |
| 杀进程策略 | 一刀切,杀最大 | 分级杀,优先杀不重要的 |
| 可配置性 | 需修改内核参数 | 通过属性文件动态调整 |
嗯,这里要注意一点:LMKD并不是完全替代了内核里的LMK。实际上,内核里仍然保留了一个简化版的LMK作为「最后一道防线」。如果LMKD来不及响应,内核会自己动手。这叫「双保险」。
五、设计思路的变迁:从「被动响应」到「主动预防」
说到底,旧版LMK和LMKD的本质区别,是设计思路的不同。
旧版LMK是「被动响应」——内存不够了?好,我杀一个进程。它不考虑后果,也不考虑用户体验。就像一个消防员,只会在火灾发生后去灭火,但不会预防火灾。
而LMKD是「主动预防」——它通过vmpressure事件提前感知到内存压力,然后根据进程的优先级和使用情况,选择最合适的「牺牲品」。它甚至会考虑:这个进程被杀后,用户会不会马上重新打开它?如果会,那就不杀,换一个。
我举个例子。假设你手机里同时运行着微信和淘宝:
- 旧版LMK:谁占内存大杀谁。微信占300MB,淘宝占200MB,那就杀微信。结果你切回微信时发现它被杀了,还得重新加载。
- 新版LMKD:先查一下,微信是用户最近使用的,淘宝是半小时前打开的。那就杀淘宝,保留微信。因为用户大概率会马上切回微信。
你看,这就是「智能」和「机械」的区别。
ro.lmk.*系列属性。比如ro.lmk.swap_free_low_percentage可以控制swap分区的使用率阈值。这些参数在旧版LMK里根本不存在。
六、总结:演进背后的思考
从旧版LMK到LMKD,表面上看是代码的重写,实际上是设计哲学的转变。我经常跟团队说:一个好的内存管理机制,不是「杀得越多越好」,而是「杀得越准越好」。
旧版LMK的问题在于它「太简单」了,简单到不考虑用户体验。而LMKD通过引入用户空间的决策能力,让杀进程这件事变得「有温度」了。
当然,LMKD也不是完美的。比如它依赖vmpressure事件的准确性,如果内核报告的压力不准确,LMKD也会做出错误决策。但相比旧版LMK,它已经前进了一大步。
下一章,我会深入讲解LMKD的源码实现,包括它如何监听vmpressure、如何计算进程优先级、以及如何执行杀进程。到时候我会带大家一行一行地看代码,保证让你彻底搞懂。
好,今天就到这里。有什么问题,欢迎在评论区留言。我是你们的Android内核讲师,我们下章见。