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在内存还剩80MB时就杀了前台进程。原因是系统把前台进程的oom_score_adj设得太高。后来我们不得不手动调整阈值,但这只是治标不治本。

二、旧版LMK的三大痛点

旧版LMK的问题,我总结为三点:

  1. 静态阈值,不够灵活——不同设备的内存大小、使用场景都不一样,但旧版LMK的阈值是写死的。你想想看,2GB内存的手机和8GB内存的手机,用同一套阈值,这合理吗?
  2. 只看内存,不看行为——它只关心进程占了多少内存,不关心这个进程是不是用户正在使用的。这就导致「杀错人」的情况频繁发生。
  3. 内核态决策,信息不足——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:当前哪些进程可以杀?哪些不能动?最后才执行杀动作。

核心变化:旧版LMK是「内核主动杀」,新版LMKD是「内核报告、用户空间决策」。这个转变,让内存管理从「机械式」变成了「智能化」。

四、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:先查一下,微信是用户最近使用的,淘宝是半小时前打开的。那就杀淘宝,保留微信。因为用户大概率会马上切回微信。

你看,这就是「智能」和「机械」的区别。

个人经验:我在调优LMKD时,最常用的手段是调整ro.lmk.*系列属性。比如ro.lmk.swap_free_low_percentage可以控制swap分区的使用率阈值。这些参数在旧版LMK里根本不存在。

六、总结:演进背后的思考

从旧版LMK到LMKD,表面上看是代码的重写,实际上是设计哲学的转变。我经常跟团队说:一个好的内存管理机制,不是「杀得越多越好」,而是「杀得越准越好」。

旧版LMK的问题在于它「太简单」了,简单到不考虑用户体验。而LMKD通过引入用户空间的决策能力,让杀进程这件事变得「有温度」了。

当然,LMKD也不是完美的。比如它依赖vmpressure事件的准确性,如果内核报告的压力不准确,LMKD也会做出错误决策。但相比旧版LMK,它已经前进了一大步。

下一章,我会深入讲解LMKD的源码实现,包括它如何监听vmpressure、如何计算进程优先级、以及如何执行杀进程。到时候我会带大家一行一行地看代码,保证让你彻底搞懂。

好,今天就到这里。有什么问题,欢迎在评论区留言。我是你们的Android内核讲师,我们下章见。