内核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用它来监听内存状态变化,比如低内存通知。
- adj和minfree:这两个数组是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;
}
你看,就三件事:
- 调用
register_shrinker把LMK的shrinker注册到内核的shrinker链表上。这样当内存紧张时,内核就会调用LMK的lowmem_shrink回调。 - 设置通知链回调函数
lowmem_notifier。 - 注册低内存通知器。
个人经验:我曾经在调试一个内存泄漏问题时,发现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回调给它发消息。收到消息后,它才执行实际的杀进程操作。这样做的好处是:
- 避免了在内存回收路径上直接杀进程导致的死锁
- 可以批量处理杀进程请求,提高效率
- 更容易调试和跟踪
三、总结与避坑指南
好了,LMK驱动的核心数据结构和注册流程就讲完了。我们来回顾一下重点:
- LMK驱动通过shrinker机制注册到内核内存回收框架
- adj和minfree数组是杀进程的判决依据
- 杀进程的逻辑是:找到oom_score_adj大于阈值且内存占用最大的进程
- 新版本使用独立内核线程来执行杀进程操作,避免死锁
避坑指南:我曾经在移植LMK驱动到某个低端平台时,发现杀进程特别慢。后来定位到是内核线程的优先级太低,被其他任务抢占了CPU。解决方案很简单——把内核线程的优先级调高一点。嗯,有时候问题就这么简单,但你不去深挖源码,永远找不到根因。
下一章,我们会深入分析LMK的杀进程策略,看看它是如何选择「幸运观众」的。到时候我会分享一个我在项目中遇到的真实案例——某个应用明明内存占用不大,却总是第一个被杀,最后发现是adj值设置的问题。