Android 内存管家 LMK 的一天:从贴标签到赶进程的故事

一、内存危机:房间不够用了!

假设 Android 系统是一家大酒店,每个应用程序都是来入住的客人。酒店设计很贴心:客人 "退房"(退出应用)时不会真正离开房间,而是留在房间里以便随时回来,这样下次入住更快。

但问题来了:随着客人越来越多,酒店房间(内存)越来越紧张。这时候需要一个 "内存管家"LMK(LowMemoryKiller)来管理房间,在内存不足时让不重要的客人离开,这就是 LMK 的核心任务 ------ 在 OOM(Out Of Memory)危机前提前清理低优先级进程。

二、前台管家:给客人贴 "重要程度" 标签

2.1 贴标签的流程:从 AMS 到 ProcessList

酒店前台(AMS,ActivityManagerService)负责给每个客人(进程)评定重要程度,这个重要程度用oom_adj表示,数值越大越不重要(越容易被赶走)。

java

scss 复制代码
// ActivityManagerService.java中给进程贴标签
private boolean applyOomAdjLocked(ProcessRecord app) {
    if (app.curAdj != app.setAdj) {
        // 调用ProcessList的方法,给进程设置adj标签
        ProcessList.setOomAdj(app.pid, app.info.uid, app.curAdj);
        app.setAdj = app.curAdj;
    }
}

2.2 ProcessList:标签快递员

前台管家叫来了快递员(ProcessList),让他把标签送给后台的协调者。快递员用特定格式打包标签:

java

scss 复制代码
// ProcessList.java打包标签并发送
public static void setOomAdj(int pid, int uid, int amt) {
    if (amt == UNKNOWN_ADJ) return;
    // 打包命令:命令类型+pid+uid+adj值,共16字节
    ByteBuffer buf = ByteBuffer.allocate(4 * 4);
    buf.putInt(LMK_PROCPRIO); // 命令类型:设置进程adj
    buf.putInt(pid);          // 进程ID
    buf.putInt(uid);          // 用户ID
    buf.putInt(amt);          // adj值(重要程度)
    writeLmkd(buf);           // 通过socket发送给lmkd
}

2.3 快递方式:Socket 通信

快递员通过酒店内部电话系统(LocalSocket)联系后台协调者 lmkd,这个电话用的是 "顺序包裹" 模式(SOCK_SEQPACKET),保证标签不会送错:

java

csharp 复制代码
private static void writeLmkd(ByteBuffer buf) {
    for (int i = 0; i < 3; i++) {
        if (sLmkdSocket == null) {
            if (!openLmkdSocket()) { // 打开到lmkd的socket连接
                try { Thread.sleep(1000); } catch (...) {}
                continue;
            }
        }
        try {
            sLmkdOutputStream.write(buf.array(), 0, buf.position()); // 发送标签
            return;
        } catch (IOException ex) {
            // 连接失败时重试
            sLmkdSocket.close();
            sLmkdSocket = null;
        }
    }
}

三、后台协调者 lmkd:管理标签和触发清理

3.1 lmkd 的初始化:准备接收标签

lmkd 就像酒店后台的调度员,启动时会创建一个 "收件箱"(socket)来接收前台送来的标签:

c

运行

scss 复制代码
// lmkd.c的main函数
int main() {
    mlockall(MCL_FUTURE); // 锁定内存
    sched_setscheduler(0, SCHED_FIFO, &param); // 设置调度优先级
    if (init()) { // 初始化
        ALOGI("exiting");
        return 0;
    }
    mainloop(); // 进入主循环等待事件
}

static int init() {
    page_k = sysconf(_SC_PAGESIZE) / 1024; // 获取页面大小
    epollfd = epoll_create(MAX_EPOLL_EVENTS); // 创建事件监听
    ctrl_lfd = android_get_control_socket("lmkd"); // 获取lmkd socket
    listen(ctrl_lfd, 1); // 监听连接
    // 注册事件处理函数
    epev.events = EPOLLIN;
    epev.data.ptr = (void *)ctrl_connect_handler;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, ctrl_lfd, &epev);
    return 0;
}

3.2 主循环:时刻监听前台消息

调度员坐在办公桌前(mainloop),时刻监听电话铃声(epoll_wait),一旦收到前台的标签就处理:

c

运行

scss 复制代码
static void mainloop() {
    while (1) {
        int nevents = epoll_wait(epollfd, events, maxevents, -1); // 等待事件
        if (nevents == -1) continue;
        for (int i = 0; i < nevents; ++i) {
            if (events[i].data.ptr)
                (*(void (*)(uint32_t))events[i].data.ptr)(events[i].events);
        }
    }
}

3.3 处理标签:给内核传递命令

当收到前台的标签后,调度员 lmkd 会把标签转交给酒店保安(内核),并告诉保安 "重要程度" 的标准:

c

运行

scss 复制代码
static void ctrl_command_handler() {
    int cmd = ntohl(ibuf[0]);
    switch(cmd) {
    case LMK_PROCPRIO: // 设置进程adj
        if (nargs == 3) {
            int pid = ntohl(ibuf[1]);
            int uid = ntohl(ibuf[2]);
            int oomadj = ntohl(ibuf[3]);
            cmd_procprio(pid, uid, oomadj); // 处理设置adj命令
        }
        break;
    case LMK_TARGET: // 更新内存阈值和对应adj
        cmd_target(targets, &ibuf[1]);
        break;
    }
}

static void cmd_procprio(int pid, int uid, int oomadj) {
    char path[80], val[20];
    // 向/proc/[pid]/oom_score_adj写入adj值,告诉内核这个进程的重要程度
    snprintf(path, sizeof(path), "/proc/%d/oom_score_adj", pid);
    snprintf(val, sizeof(val), "%d", oomadj);
    writefilestring(path, val);
    // 如果使用内核接口,直接返回,由内核处理后续
    if (use_inkernel_interface) return;
    // 否则在用户空间管理进程列表
}

四、酒店保安 kernel:按规则赶人

4.1 保安的巡逻机制:shrinker 注册

保安团队(内核)会定期巡逻(由 kswapd 线程触发),他们通过注册的 "巡逻员"(shrinker)来检查内存情况:

c

运行

csharp 复制代码
// lowmemorykiller.c初始化
static struct shrinker lowmem_shrinker = {
    .scan_objects = lowmem_scan, // 扫描要清理的进程
    .count_objects = lowmem_count, // 计算当前内存使用
    .seeks = DEFAULT_SEEKS * 16
};

static int __init lowmem_init(void) {
    register_shrinker(&lowmem_shrinker); // 注册巡逻员
    return 0;
}

4.2 内存检查:计算当前可用房间

巡逻员先看看酒店还有多少空房间(可用内存):

c

运行

scss 复制代码
static unsigned long lowmem_count(struct shrinker *s, struct shrink_control *sc) {
    // 计算所有活动和非活动的内存页面
    return global_page_state(NR_ACTIVE_ANON) +
           global_page_state(NR_ACTIVE_FILE) +
           global_page_state(NR_INACTIVE_ANON) +
           global_page_state(NR_INACTIVE_FILE);
}

4.3 按规则赶人:选最该离开的客人

当空房间少于预定阈值时,保安开始按规则赶人:优先赶 "重要程度" 低(oom_score_adj 大)且占用房间大(RSS 大)的客人:

c

运行

ini 复制代码
static unsigned long lowmem_scan(struct shrinker *s, struct shrink_control *sc) {
    struct task_struct *selected = NULL;
    int minfree = 0;
    short min_score_adj = OOM_SCORE_ADJ_MAX + 1;
    // 获取当前可用内存
    int other_free = global_page_state(NR_FREE_PAGES) - totalreserve_pages;
    // 遍历阈值配置,找到当前需要清理的adj阈值
    for (int i = 0; i < array_size; i++) {
        if (other_free < lowmem_minfree[i] && other_file < lowmem_minfree[i]) {
            min_score_adj = lowmem_adj[i];
            break;
        }
    }
    // 遍历所有进程,找到符合条件的目标进程
    for_each_process(tsk) {
        if (tsk->flags & PF_KTHREAD) continue; // 不赶工作人员(内核线程)
        struct task_struct *p = find_lock_task_mm(tsk);
        short oom_score_adj = p->signal->oom_score_adj;
        // 只考虑重要程度低于阈值的进程
        if (oom_score_adj < min_score_adj) { task_unlock(p); continue; }
        int tasksize = get_mm_rss(p->mm); // 获取进程占用的内存大小
        // 选择重要程度最低且占用内存最大的进程
        if (selected) {
            if (oom_score_adj < selected_oom_score_adj) continue;
            if (oom_score_adj == selected_oom_score_adj && tasksize <= selected_tasksize) continue;
        }
        selected = p;
        selected_tasksize = tasksize;
    }
    // 找到目标后,发送SIGKILL信号赶人
    if (selected) {
        set_tsk_thread_flag(selected, TIF_MEMDIE);
        send_sig(SIGKILL, selected, 0); // 发送kill信号
        rem += selected_tasksize;
    }
    return rem;
}

五、关键规则:oom_adj 与内存阈值的关系

5.1 客人的 "重要程度" 标签(oom_adj)

  • oom_adj范围:-16 到 15,数值越大越不重要(越容易被赶)
  • 映射到oom_score_adj:比如 oom_adj=15 对应 oom_score_adj=1000,oom_adj=0 对应 0
  • 查看方式:cat /proc/[pid]/oom_adj

5.2 内存警报阈值(minfree)

酒店设置了不同的警报级别(保存在/sys/module/lowmemorykiller/parameters/minfree),比如:

plaintext

arduino 复制代码
// 假设minfree设置为"1024,8192"(单位:page)
// adj设置为"1,6"
当可用内存 < 8192 page时:赶oom_score_adj >=6的进程(不重要的客人)
当可用内存 < 1024 page时:赶oom_score_adj >=1的进程(更紧急,赶更多人)

六、LMK 的整体工作流程总结

  1. 前台贴标签 :AMS 根据进程状态计算oom_adj,通过 ProcessList 发送给 lmkd

  2. 后台传命令 :lmkd 接收命令,更新进程的oom_score_adj并通知内核

  3. 内核赶人 :内核巡逻时发现内存不足,按minfreeadj规则,杀最该杀的进程

整个过程就像酒店管家团队协作:前台评估客人重要性,后台协调传递信息,保安按规则执行清理,确保酒店(系统)不会因为客人太多(内存不足)而崩溃,同时尽量保留重要客人(高优先级进程)。

相关推荐
百锦再1 小时前
第11章 泛型、trait与生命周期
android·网络·人工智能·python·golang·rust·go
会跑的兔子2 小时前
Android 16 Kotlin协程 第二部分
android·windows·kotlin
键来大师2 小时前
Android15 RK3588 修改默认不锁屏不休眠
android·java·framework·rk3588
江上清风山间明月5 小时前
Android 系统超级实用的分析调试命令
android·内存·调试·dumpsys
百锦再5 小时前
第12章 测试编写
android·java·开发语言·python·rust·go·erlang
用户69371750013849 小时前
Kotlin 协程基础入门系列:从概念到实战
android·后端·kotlin
SHEN_ZIYUAN10 小时前
Android 主线程性能优化实战:从 90% 降至 13%
android·cpu优化
曹绍华10 小时前
android 线程loop
android·java·开发语言
雨白10 小时前
Hilt 入门指南:从 DI 原理到核心用法
android·android jetpack
介一安全10 小时前
【Frida Android】实战篇3:基于 OkHttp 库的 Hook 抓包
android·okhttp·网络安全·frida