内核lmk驱动源码分析:关键数据结构与注册流程

各位同学,今天我们来啃一块硬骨头——内核LMK驱动的源码。说实话,很多做Android开发的兄弟一听到「内核」两个字就头大,觉得那是驱动工程师的事。但我个人觉得,你要真想搞懂低内存杀进程的底层逻辑,这块内容绕不开。

为什么?因为上层AMS的那些杀进程策略,最终都要落到内核的LMK驱动来执行。你想想看,如果连「刽子手」的刀长什么样都不知道,怎么理解它砍人的逻辑?

一、LMK驱动的核心数据结构

我们先从最基础的数据结构说起。内核里一切逻辑都是围绕数据展开的,LMK也不例外。

1.1 lowmemorykiller 结构体

这个结构体是整个LMK驱动的「大脑」。我当年第一次看这个结构体时,觉得它挺简单的,就几个字段。后来踩过坑才发现,越简单的东西越容易出问题。

static struct {
    struct shrinker shrinker;
    struct task_struct *tsk;
    struct notifier_block nb;
    short *adj;
    int *minfree;
    int nr;
} lowmem_shrinker;

来,我们拆开看看:

  • shrinker:这是内核内存回收框架的「钩子」。LMK通过它注册到内核的shrinker链表上,当内存紧张时,内核会回调这个shrinker。
  • tsk:指向一个内核线程。这个线程负责实际执行杀进程操作。嗯,这里要注意,早期的LMK是在shrinker回调里直接杀进程的,后来发现这样容易导致死锁,才改成了异步方式。
  • nb:通知链块。LMK用它来监听内存状态变化,比如低内存通知。
  • adjminfree:这两个数组是LMK的「判决依据」。adj数组存储了不同优先级对应的oom_score_adj阈值,minfree数组存储了对应的内存阈值(以页为单位)。
  • nr:数组长度。

核心要点:adj和minfree是一一对应的。比如adj[0]=0对应minfree[0]=1024页,意思是当可用内存低于1024页时,开始杀oom_score_adj大于等于0的进程。

1.2 oom_score_adj 与进程优先级

这里我要多说一句。很多同学搞不清oom_adj和oom_score_adj的区别。简单说,oom_adj是旧版,范围是-17到15;oom_score_adj是新版,范围是-1000到1000。LMK驱动里用的是oom_score_adj。

我在项目中遇到过一个问题:某个系统服务明明设置了很低的adj值,但还是被杀了。后来查了半天,发现是上层AMS和内核LMK的adj映射表没对齐。说白了,就是鸡同鸭讲。

进程类型 oom_score_adj 说明
系统核心服务 -1000 永远不会被杀
前台进程 0 用户正在交互
可见进程 100 用户可见但不可交互
服务进程 200 后台运行的服务
缓存进程 900 最容易被杀

二、LMK驱动的注册流程

好,数据结构看完了,我们来看看LMK是怎么「上岗」的。注册流程说白了就三步:初始化、注册shrinker、启动内核线程。

2.1 模块初始化

LMK驱动通过module_init宏来注册初始化函数。这个函数叫lowmem_init,代码很简单:

static int __init lowmem_init(void)
{
    register_shrinker(&lowmem_shrinker.shrinker);
    lowmem_shrinker.nb.notifier_call = lowmem_notifier;
    register_lowmemory_notifier(&lowmem_shrinker.nb);
    return 0;
}

你看,就三件事:

  1. 调用register_shrinker把LMK的shrinker注册到内核的shrinker链表上。这样当内存紧张时,内核就会调用LMK的lowmem_shrink回调。
  2. 设置通知链回调函数lowmem_notifier
  3. 注册低内存通知器。

个人经验:我曾经在调试一个内存泄漏问题时,发现LMK的shrinker回调没有被调用。查了半天,原来是register_shrinker返回了错误,但初始化函数没有检查返回值。嗯,从那以后我写代码都会检查每个注册函数的返回值。

2.2 Shrinker回调函数

这是LMK驱动的核心逻辑所在。当内核内存不足时,会调用lowmem_shrink函数。这个函数会遍历所有进程,找到最应该被杀的那个。

static int lowmem_shrink(struct shrinker *s, struct shrink_control *sc)
{
    struct task_struct *p;
    struct task_struct *selected = NULL;
    int rem = 0;
    int tasksize;
    int i;
    short min_score_adj = OOM_SCORE_ADJ_MAX + 1;
    int minfree = 0;
    int selected_tasksize = 0;
    short selected_oom_score_adj;
    int array_size = lowmem_shrinker.nr;
    short *array_adj = lowmem_shrinker.adj;
    int *array_minfree = lowmem_shrinker.minfree;

    // 根据当前可用内存,找到对应的adj阈值
    for (i = 0; i < array_size; i++) {
        minfree = array_minfree[i];
        if (nr_free_pages() < minfree)
            break;
    }
    if (i == 0) {
        // 内存还够,不杀进程
        return 0;
    }
    min_score_adj = array_adj[i];

    // 遍历所有进程,找到oom_score_adj大于等于阈值且内存占用最大的
    rcu_read_lock();
    for_each_process(p) {
        struct signal_struct *sig = p->signal;
        if (p->flags & PF_KTHREAD)
            continue;
        if (p->mm == NULL)
            continue;
        if (sig->oom_score_adj < min_score_adj)
            continue;
        tasksize = get_mm_rss(p->mm);
        if (tasksize <= 0)
            continue;
        if (selected) {
            if (tasksize <= selected_tasksize)
                continue;
        }
        selected = p;
        selected_tasksize = tasksize;
        selected_oom_score_adj = sig->oom_score_adj;
    }
    rcu_read_unlock();

    if (selected) {
        // 发送SIGKILL信号
        send_sig(SIGKILL, selected, 0);
        rem = selected_tasksize;
    }

    return rem;
}

这段代码的逻辑其实很直白:

  • 先根据当前可用内存,找到对应的adj阈值
  • 然后遍历所有用户态进程,找到oom_score_adj大于等于阈值、且内存占用最大的那个
  • 最后发送SIGKILL信号干掉它

注意:这里有个坑。早期的LMK是在shrinker回调里直接发SIGKILL的,但shrinker回调是在内存回收路径上执行的,可能持有各种锁。直接杀进程可能导致死锁。所以后来的内核版本改成了异步方式——把杀进程的任务交给一个独立的内核线程。

2.3 内核线程的启动

我记得在Android 4.4之后,Google改进了LMK的实现,引入了一个内核线程来处理杀进程操作。这个线程在lowmem_init之后启动:

static int lowmem_init(void)
{
    register_shrinker(&lowmem_shrinker.shrinker);
    lowmem_shrinker.tsk = kthread_run(lowmem_scan, NULL, "lowmemorykiller");
    if (IS_ERR(lowmem_shrinker.tsk)) {
        pr_err("Failed to start lowmemorykiller thread\n");
        return PTR_ERR(lowmem_shrinker.tsk);
    }
    return 0;
}

这个内核线程会一直循环,等待shrinker回调给它发消息。收到消息后,它才执行实际的杀进程操作。这样做的好处是:

  1. 避免了在内存回收路径上直接杀进程导致的死锁
  2. 可以批量处理杀进程请求,提高效率
  3. 更容易调试和跟踪

三、总结与避坑指南

好了,LMK驱动的核心数据结构和注册流程就讲完了。我们来回顾一下重点:

  • LMK驱动通过shrinker机制注册到内核内存回收框架
  • adj和minfree数组是杀进程的判决依据
  • 杀进程的逻辑是:找到oom_score_adj大于阈值且内存占用最大的进程
  • 新版本使用独立内核线程来执行杀进程操作,避免死锁

避坑指南:我曾经在移植LMK驱动到某个低端平台时,发现杀进程特别慢。后来定位到是内核线程的优先级太低,被其他任务抢占了CPU。解决方案很简单——把内核线程的优先级调高一点。嗯,有时候问题就这么简单,但你不去深挖源码,永远找不到根因。

下一章,我们会深入分析LMK的杀进程策略,看看它是如何选择「幸运观众」的。到时候我会分享一个我在项目中遇到的真实案例——某个应用明明内存占用不大,却总是第一个被杀,最后发现是adj值设置的问题。