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

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

相关推荐
aningxiaoxixi6 分钟前
android 之 CALL
android
用户2018792831671 小时前
Android 核心大管家 ActivityManagerService (AMS)
android
春马与夏2 小时前
Android自动化AirScript
android·运维·自动化
键盘歌唱家3 小时前
mysql索引失效
android·数据库·mysql
webbin4 小时前
Compose @Immutable注解
android·android jetpack
无知的前端5 小时前
Flutter开发,GetX框架路由相关详细示例
android·flutter·ios
玲小珑5 小时前
Auto.js 入门指南(十二)网络请求与数据交互
android·前端
webbin5 小时前
Compose 副作用
android·android jetpack
whysqwhw5 小时前
Dokka 插件系统与 Android 文档生成技术全解
android
橙子199110166 小时前
ActionBar 和 Toolbar
android