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规则,杀最该杀的进程

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

相关推荐
sun0077005 小时前
android ndk编译valgrind
android
AI视觉网奇6 小时前
android studio 断点无效
android·ide·android studio
jiaxi的天空6 小时前
android studio gradle 访问不了
android·ide·android studio
No Silver Bullet7 小时前
android组包时会把从maven私服获取的包下载到本地吗
android
catchadmin7 小时前
PHP serialize 序列化完全指南
android·开发语言·php
tangweiguo030519878 小时前
Kable使用指南:Android BLE开发的现代化解决方案
android·kotlin
00后程序员张11 小时前
iOS App 混淆与资源保护:iOS配置文件加密、ipa文件安全、代码与多媒体资源防护全流程指南
android·安全·ios·小程序·uni-app·cocoa·iphone
柳岸风12 小时前
Android Studio Meerkat | 2024.3.1 Gradle Tasks不展示
android·ide·android studio
编程乐学12 小时前
安卓原创--基于 Android 开发的菜单管理系统
android
whatever who cares14 小时前
android中ViewModel 和 onSaveInstanceState 的最佳使用方法
android