Android-高版本 LMKD 源码解析:基于 PSI 的内存压力监控与查杀全流程

Android 高版本 LMKD 源码解析:基于 PSI 的内存压力监控与查杀全流程

本文基于 system/memory/lmkd/lmkd.cpp(Android S/T 时期版本,约 3800 行),从 init 启动讲到一次完整的查杀流程,最后附一篇 /proc/meminfo 逐行解析(lmkd 决策的第一手输入)。

姊妹篇:《Android PSI 详解:libpsi 源码解析》------讲 lmkd 调用的 init_psi_monitor() 等接口在 libpsi 里如何与内核打交道。

1. 背景:从 in-kernel lmkd 到 PSI

1.1 三代方案

代际 机制 缺陷
1st 内核驱动 lowmemorykiller 基于剩余内存阈值,粗粒度,策略写死在内核
2nd userspace lmkd + memcg vmpressure 基于"事件百分比"触发,无法反映任务实际停顿时长
3rd userspace lmkd + PSI 当前高版本默认方案

PSI(Pressure Stall Information,内核 4.20+)回答的问题是: "过去一段时间内,有多少时间花在了等内存上" ,而不是"还剩多少内存"。它有两个维度:

  • PSI_SOME(partial stall):至少一个任务在等内存回收 → 对应 lmkd 的 medium 级压力;
  • PSI_FULL(complete stall):所有任务都在等 → 对应 critical 级,此时系统接近 ANR。

1.2 代码中的三级压力定义

arduino 复制代码
/* lmkd.cpp:168-180 */
enum vmpressure_level {
    VMPRESS_LEVEL_LOW = 0,
    VMPRESS_LEVEL_MEDIUM,
    VMPRESS_LEVEL_CRITICAL,
    VMPRESS_LEVEL_COUNT
};
​
static const char *level_name[] = {
    "low",
    "medium",
    "critical"
};

PSI 阈值定义(新策略下会被属性覆盖,见第 4 节):

arduino 复制代码
/* lmkd.cpp:187-190, 219-223 */
struct psi_threshold {
    enum psi_stall_type stall_type;
    int threshold_ms;
};
​
static struct psi_threshold psi_thresholds[VMPRESS_LEVEL_COUNT] = {
    { PSI_SOME, 70 },    /* 70ms out of 1sec for partial stall */
    { PSI_SOME, 100 },   /* 100ms out of 1sec for partial stall */
    { PSI_FULL, 70 },    /* 70ms out of 1sec for complete stall */
};

1.3 PSI 的一个关键特性:事件限速

内核 PSI monitor 每个窗口(lmkd 用 1s)最多触发一次事件,这个特性直接决定了 lmkd "事件触发 + 短期轮询" 的混合架构:

arduino 复制代码
/* lmkd.cpp:129-139 */
/*
 * PSI monitor tracking window size.
 * PSI monitor generates events at most once per window,
 * therefore we poll memory state for the duration of
 * PSI_WINDOW_SIZE_MS after the event happens.
 */
#define PSI_WINDOW_SIZE_MS 1000
/* Polling period after PSI signal when pressure is high */
#define PSI_POLL_PERIOD_SHORT_MS 10
/* Polling period after PSI signal when pressure is low */
#define PSI_POLL_PERIOD_LONG_MS 100

2. 启动流程:init → main()

2.1 lmkd.rc 与 socket 创建

init 根据 lmkd.rc 启动 service:

sql 复制代码
service lmkd /system/bin/lmkd
    class core
    user lmkd
    group lmkd system readproc
    socket lmkd seqpacket 0660 system system

init 预创建 lmkd 这个 unix domain socket 并把 fd 传给子进程,lmkd 用 android_get_control_socket("lmkd") 取回它(见 2.4 节)。

2.2 main() 入口

scss 复制代码
/* lmkd.cpp:3789-3847 */
int main(int argc, char **argv) {
    if ((argc > 1) && argv[1] && !strcmp(argv[1], "--reinit")) {
        if (property_set(LMKD_REINIT_PROP, "")) {
            ALOGE("Failed to reset " LMKD_REINIT_PROP " property");
        }
        return issue_reinit();
    }
​
    if (!update_props()) {
        ALOGE("Failed to initialize props, exiting.");
        return -1;
    }
​
    ctx = create_android_logger(KILLINFO_LOG_TAG);
​
    if (!init()) {
        if (!use_inkernel_interface) {
            /*
             * MCL_ONFAULT pins pages as they fault instead of loading
             * everything immediately all at once. ...
             */
            /* CAP_IPC_LOCK required */
            if (mlockall(MCL_CURRENT | MCL_FUTURE | MCL_ONFAULT) && (errno != EINVAL)) {
                ALOGW("mlockall failed %s", strerror(errno));
            }
​
            /* CAP_NICE required */
            struct sched_param param = {
                    .sched_priority = 1,
            };
            if (sched_setscheduler(0, SCHED_FIFO, &param)) {
                ALOGW("set SCHED_FIFO failed %s", strerror(errno));
            }
        }
​
        if (init_reaper()) {
            ALOGI("Process reaper initialized with %d threads in the pool",
                reaper.thread_cnt());
        }
​
        if (!watchdog.init()) {
            ALOGE("Failed to initialize the watchdog");
        }
​
        mainloop();
    }
​
    android_log_destroy(&ctx);
    ALOGI("exiting");
    return 0;
}

几个值得写进博客的细节:

  1. --reinit 分支 :lmkd.rc 里有 on property:lmkd.reinit=1 之类的 trigger,重新执行 /system/bin/lmkd --reinit 时,它作为一个客户端 连接正在运行的 lmkd,发 LMK_UPDATE_PROPS 命令让后者热更新属性(issue_reinit(),lmkd.cpp:3703),自身随即退出。
  2. mlockall:把 lmkd 自己的内存锁住,防止它在执行查杀任务时页被回收/换出。
  3. SCHED_FIFO 实时调度:保证系统内存高压时 lmkd 仍能被调度执行------否则"杀手自己先饿死"。

2.3 update_props():读取全部可调参数

go 复制代码
/* lmkd.cpp:125-127 */
#define GET_LMK_PROPERTY(type, name, def) \
    property_get_##type("persist.device_config.lmkd_native." name, \
        property_get_##type("ro.lmk." name, def))

persist.device_config.lmkd_native.*(实验开关,由 device_config 下发)优先级高于 ro.lmk.*

ini 复制代码
/* lmkd.cpp:3735-3776(节选) */
static bool update_props() {
    /* By default disable low level vmpressure events */
    level_oomadj[VMPRESS_LEVEL_LOW] =
        GET_LMK_PROPERTY(int32, "low", OOM_SCORE_ADJ_MAX + 1);
    level_oomadj[VMPRESS_LEVEL_MEDIUM] =
        GET_LMK_PROPERTY(int32, "medium", 800);
    level_oomadj[VMPRESS_LEVEL_CRITICAL] =
        GET_LMK_PROPERTY(int32, "critical", 0);
    ...
    low_ram_device = property_get_bool("ro.config.low_ram", false);
    ...
    psi_partial_stall_ms = GET_LMK_PROPERTY(int32, "psi_partial_stall_ms",
        low_ram_device ? DEF_PARTIAL_STALL_LOWRAM : DEF_PARTIAL_STALL);
    psi_complete_stall_ms = GET_LMK_PROPERTY(int32, "psi_complete_stall_ms",
        DEF_COMPLETE_STALL);
    thrashing_limit_pct =
            std::max(0, GET_LMK_PROPERTY(int32, "thrashing_limit",
                                         low_ram_device ? DEF_THRASHING_LOWRAM : DEF_THRASHING));
    thrashing_limit_decay_pct = clamp(0, 100, GET_LMK_PROPERTY(int32, "thrashing_limit_decay",
        low_ram_device ? DEF_THRASHING_DECAY_LOWRAM : DEF_THRASHING_DECAY));
    thrashing_critical_pct = std::max(
            0, GET_LMK_PROPERTY(int32, "thrashing_limit_critical", thrashing_limit_pct * 2));
    swap_util_max = clamp(0, 100, GET_LMK_PROPERTY(int32, "swap_util_max", 100));
    filecache_min_kb = GET_LMK_PROPERTY(int64, "filecache_min_kb", 0);
    stall_limit_critical = GET_LMK_PROPERTY(int64, "stall_limit_critical", 100);
    ...
}

关键参数默认值(lmkd.cpp:146-158):

arduino 复制代码
#define DEF_LOW_SWAP 10                        /* swap 剩余 < 10% 视为 swap 低 */
#define DEF_THRASHING_LOWRAM 30
#define DEF_THRASHING 100                      /* 抖动阈值 100% */
#define DEF_THRASHING_DECAY_LOWRAM 50
#define DEF_THRASHING_DECAY 10                 /* 抖动阈值每次衰减 10% */
#define DEF_PARTIAL_STALL_LOWRAM 200
#define DEF_PARTIAL_STALL 70                   /* medium: some 70ms/1s */
#define DEF_COMPLETE_STALL 700                 /* critical: full 700ms/1s */

2.4 init():epoll、socket、探测内核能力

ini 复制代码
/* lmkd.cpp:3443-3548(节选) */
static int init(void) {
    static struct event_handler_info kernel_poll_hinfo = { 0, kernel_event_handler };
    struct reread_data file_data = {
        .filename = ZONEINFO_PATH,     /* "/proc/zoneinfo" */
        .fd = -1,
    };
    ...
    page_k = sysconf(_SC_PAGESIZE);       /* PAGE_SIZE / 1024,换算 KB 用 */
    ...
    epollfd = epoll_create(MAX_EPOLL_EVENTS);
    ...
    ctrl_sock.sock = android_get_control_socket("lmkd");   /* init 传来的 socket */
    ...
    ret = listen(ctrl_sock.sock, MAX_DATA_CONN);
    ...
    epev.events = EPOLLIN;
    ctrl_sock.handler_info.handler = ctrl_connect_handler;
    epev.data.ptr = (void *)&(ctrl_sock.handler_info);
    epoll_ctl(epollfd, EPOLL_CTL_ADD, ctrl_sock.sock, &epev);
​
    /* 探测旧的内核 lmk 驱动是否存在 */
    has_inkernel_module = !access(INKERNEL_MINFREE_PATH, W_OK);
    use_inkernel_interface = has_inkernel_module;
​
    if (use_inkernel_interface) {
        ALOGI("Using in-kernel low memory killer interface");
        ...
    } else {
        if (!init_monitors()) {         /* ★ 现代设备走这里 */
            return -1;
        }
        property_set("sys.lmk.reportkills", "1");
    }
​
    /* 初始化 2002 个 oomadj 桶(双向链表头指向自己) */
    for (i = 0; i <= ADJTOSLOT(OOM_SCORE_ADJ_MAX); i++) {
        procadjslot_list[i].next = &procadjslot_list[i];
        procadjslot_list[i].prev = &procadjslot_list[i];
    }
    ...
    /* 预读 zoneinfo:提前分配读缓冲 */
    if (reread_file(&file_data) == NULL) { ... }
​
    /* 探测 pidfd 支持 */
    pidfd = TEMP_FAILURE_RETRY(pidfd_open(getpid(), 0));
    if (pidfd < 0) {
        pidfd_supported = (errno != ENOSYS);
    } else {
        pidfd_supported = true;
        close(pidfd);
    }
    ALOGI("Process polling is %s", pidfd_supported ? "supported" : "not supported" );
    ...
}

为什么预读 zoneinfo? reread_file() 的注释写得很清楚:

arduino 复制代码
/* lmkd.cpp:616-622 */
/*
 * Read a new or already opened file from the beginning.
 * If the file has not been opened yet data->fd should be set to -1.
 * To be used with files which are read often and possibly during high
 * memory pressure to minimize file opening which by itself requires kernel
 * memory allocation and might result in a stall on memory stressed system.
 */
static char *reread_file(struct reread_data *data) {

在内存高压时 open() 本身可能因内核分配内存而卡顿,所以 lmkd 对 /proc/zoneinfo/proc/meminfo/proc/vmstat/proc/pressure/* 全部采用一次 open、永久复用 fd 和缓冲区 的方式(pread 读到头即可重读)。


3. init_monitors():注册 PSI 监视器

3.1 三级 fallback

scss 复制代码
/* lmkd.cpp:3354-3372 */
static bool init_monitors() {
    /* Try to use psi monitor first if kernel has it */
    use_psi_monitors = GET_LMK_PROPERTY(bool, "use_psi", true) &&
        init_psi_monitors();
    /* Fall back to vmpressure */
    if (!use_psi_monitors &&
        (!init_mp_common(VMPRESS_LEVEL_LOW) ||
        !init_mp_common(VMPRESS_LEVEL_MEDIUM) ||
        !init_mp_common(VMPRESS_LEVEL_CRITICAL))) {
        ALOGE("Kernel does not support memory pressure events or in-kernel low memory killer");
        return false;
    }
    if (use_psi_monitors) {
        ALOGI("Using psi monitors for memory pressure detection");
    } else {
        ALOGI("Using vmpressure for memory pressure detection");
    }
    return true;
}

优先级:PSI → memcg vmpressure → 启动失败ro.lmk.use_psi 默认 true。

3.2 init_psi_monitors():决定用哪套杀进程策略

scss 复制代码
/* lmkd.cpp:3221-3256 */
static bool init_psi_monitors() {
    /*
     * When PSI is used on low-ram devices or on high-end devices without memfree levels
     * use new kill strategy based on zone watermarks, free swap and thrashing stats.
     * Also use the new strategy if memcg has not been mounted in the v1 cgroups hiearchy ...
     */
    bool use_new_strategy =
        GET_LMK_PROPERTY(bool, "use_new_strategy", low_ram_device || !use_minfree_levels);
    if (!use_new_strategy && memcg_version() != MemcgVersion::kV1) {
        ALOGE("Old kill strategy can only be used with v1 cgroup hierarchy");
        return false;
    }
    /* In default PSI mode override stall amounts using system properties */
    if (use_new_strategy) {
        /* Do not use low pressure level */
        psi_thresholds[VMPRESS_LEVEL_LOW].threshold_ms = 0;
        psi_thresholds[VMPRESS_LEVEL_MEDIUM].threshold_ms = psi_partial_stall_ms;
        psi_thresholds[VMPRESS_LEVEL_CRITICAL].threshold_ms = psi_complete_stall_ms;
    }
​
    if (!init_mp_psi(VMPRESS_LEVEL_LOW, use_new_strategy)) {
        return false;
    }
    if (!init_mp_psi(VMPRESS_LEVEL_MEDIUM, use_new_strategy)) {
        destroy_mp_psi(VMPRESS_LEVEL_LOW);
        return false;
    }
    if (!init_mp_psi(VMPRESS_LEVEL_CRITICAL, use_new_strategy)) {
        destroy_mp_psi(VMPRESS_LEVEL_MEDIUM);
        destroy_mp_psi(VMPRESS_LEVEL_LOW);
        return false;
    }
    return true;
}

新策略下:LOW 级被禁用 (threshold_ms=0,init_mp_psi 开头 if (!psi_thresholds[level].threshold_ms) return true; 直接跳过),只注册两级:

  • MEDIUM = psi_partial_stall_ms(默认 70ms)→ PSI_SOME
  • CRITICAL = psi_complete_stall_ms(默认 700ms)→ PSI_FULL

3.3 init_mp_psi():向内核注册 PSI monitor

ini 复制代码
/* lmkd.cpp:3153-3179 */
static bool init_mp_psi(enum vmpressure_level level, bool use_new_strategy) {
    int fd;
​
    /* Do not register a handler if threshold_ms is not set */
    if (!psi_thresholds[level].threshold_ms) {
        return true;
    }
​
    fd = init_psi_monitor(psi_thresholds[level].stall_type,
        psi_thresholds[level].threshold_ms * US_PER_MS,     /* 70ms → 70000us */
        PSI_WINDOW_SIZE_MS * US_PER_MS);                    /* 1000ms → 1000000us */
​
    if (fd < 0) {
        return false;
    }
​
    vmpressure_hinfo[level].handler = use_new_strategy ? mp_event_psi : mp_event_common;
    vmpressure_hinfo[level].data = level;
    if (register_psi_monitor(epollfd, fd, &vmpressure_hinfo[level]) < 0) {
        destroy_psi_monitor(fd);
        return false;
    }
    maxevents++;
    mpevfd[level] = fd;
​
    return true;
}

init_psi_monitor 在 libpsi(system/memory/libpsi/psi.c)中实现,等价于:

lua 复制代码
open("/proc/pressure/memory")
write(fd, "some 70000 1000000")   // 或 "full 700000 1000000"

内核返回一个可 poll 的 fd,当"窗口内停顿时间 ≥ 阈值"时该 fd 可读。之后这个 fd 被挂进 lmkd 的主 epoll,回调函数为 mp_event_psi。(libpsi 内部细节------比如为什么监听 EPOLLPRI 而非 EPOLLIN------见姊妹篇《libpsi 源码解析》。)


4. 控制通路:AMS ↔ lmkd

4.1 连接建立

ini 复制代码
/* lmkd.cpp:1579-1617(节选) */
static void ctrl_connect_handler(int data __unused, uint32_t events __unused,
                                 struct polling_params *poll_params __unused) {
    struct epoll_event epev;
    int free_dscock_idx = get_free_dsock();
​
    if (free_dscock_idx < 0) {
        /* 超过 MAX_DATA_CONN(3) 个连接:全部踢掉重收,防止占坑 */
        for (int i = 0; i < MAX_DATA_CONN; i++) {
            ctrl_data_close(i);
        }
        free_dscock_idx = 0;
    }
​
    data_sock[free_dscock_idx].sock = accept(ctrl_sock.sock, NULL, NULL);
    ...
    data_sock[free_dscock_idx].handler_info.handler = ctrl_data_handler;
    data_sock[free_dscock_idx].async_event_mask = 0;
    epev.events = EPOLLIN;
    epev.data.ptr = (void *)&(data_sock[free_dscock_idx].handler_info);
    epoll_ctl(epollfd, EPOLL_CTL_ADD, data_sock[free_dscock_idx].sock, &epev);
    maxevents++;
}

4.2 命令分发

ini 复制代码
/* lmkd.cpp:1459-1555(节选) */
static void ctrl_command_handler(int dsock_idx) {
    LMKD_CTRL_PACKET packet;
    struct ucred cred;
    ...
    len = ctrl_data_read(dsock_idx, (char *)packet, CTRL_PACKET_MAX_SIZE, &cred);
    ...
    cmd = lmkd_pack_get_cmd(packet);
    nargs = len / sizeof(int) - 1;
    ...
    switch(cmd) {
    case LMK_TARGET:
        targets = nargs / 2;
        ...
        cmd_target(targets, packet);
        break;
    case LMK_PROCPRIO:
        /* process type field is optional for backward compatibility */
        if (nargs < 3 || nargs > 4)
            goto wronglen;
        cmd_procprio(packet, nargs, &cred);
        break;
    case LMK_PROCREMOVE:
        ...
        cmd_procremove(packet, &cred);
        break;
    case LMK_PROCPURGE:
        ...
        cmd_procpurge(&cred);
        break;
    case LMK_GETKILLCNT:
        ...
        break;
    case LMK_SUBSCRIBE:
        ...
        cmd_subscribe(dsock_idx, packet);
        break;
    ...
    case LMK_UPDATE_PROPS:
        ...
        result = -1;
        if (update_props()) {
            if (!use_inkernel_interface) {
                /* Reinitialize monitors to apply new settings */
                destroy_monitors();
                if (init_monitors()) {
                    result = 0;
                }
            } else {
                result = 0;
            }
        }
        ...
        if (!result) {
            ALOGI("Properties reinitilized");
        } else {
            /* New settings can't be supported, crash to be restarted */
            ALOGE("New configuration is not supported. Exiting...");
            exit(1);
        }
        break;
    }
}

注意 ctrl_data_read(lmkd.cpp:714)通过 recvmsgSCM_CREDENTIALS 拿到对端 pid/uid(内核保证不可伪造),后续所有命令都用它做记录归属校验

4.3 核心数据结构:进程表

arduino 复制代码
/* lmkd.cpp:507-534 */
struct proc {
    struct adjslot_list asl;   /* 挂在 oomadj 桶链表上的节点 */
    int pid;
    int pidfd;                 /* 用于等待死亡通知 */
    uid_t uid;
    int oomadj;
    pid_t reg_pid;             /* 注册这条记录的客户端进程 pid */
    bool valid;
    struct proc *pidhash_next; /* pid 哈希桶冲突链 */
};
​
#define PIDHASH_SZ 1024
static struct proc *pidhash[PIDHASH_SZ];
#define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))
​
#define ADJTOSLOT(adj) ((adj) + -OOM_SCORE_ADJ_MIN)
#define ADJTOSLOT_COUNT (ADJTOSLOT(OOM_SCORE_ADJ_MAX) + 1)
​
// protects procadjslot_list from concurrent access
static std::shared_mutex adjslot_list_lock;
static struct adjslot_list procadjslot_list[ADJTOSLOT_COUNT];

两套索引:pid 哈希 (按 pid 快速查找/删除)+ oomadj 桶链表(查杀时按 adj 从 1000 递减扫描,同桶内取尾/取最重)。adj 范围 -1000, 1000 → 共 2002 个桶,所以按桶扫描是 O(1) 定位到桶。

4.4 cmd_procprio():AMS 每次调整 adj 都会调用

ini 复制代码
/* lmkd.cpp:1114-1250(节选) */
static void cmd_procprio(LMKD_CTRL_PACKET packet, int field_count, struct ucred *cred) {
    ...
    lmkd_pack_get_procprio(packet, field_count, &params);
​
    if (params.oomadj < OOM_SCORE_ADJ_MIN ||
        params.oomadj > OOM_SCORE_ADJ_MAX) { ... return; }
​
    /* Check if registered process is a thread group leader */
    if (read_proc_status(params.pid, buf, sizeof(buf))) {
        if (parse_status_tag(buf, PROC_STATUS_TGID_FIELD, &tgid) && tgid != params.pid) {
            ALOGE("Attempt to register a task that is not a thread group leader ...");
            return;
        }
    }
​
    /* 把 oomadj 写进内核,保证内核 OOM killer 与 lmkd 视角一致 */
    snprintf(path, sizeof(path), "/proc/%d/oom_score_adj", params.pid);
    snprintf(val, sizeof(val), "%d", params.oomadj);
    if (!writefilestring(path, val, false)) { ... return; }
​
    if (use_inkernel_interface) { ... return; }
​
    /* per_app_memcg:按 adj 档位写 memcg soft limit */
    if (params.ptype == PROC_TYPE_APP && per_app_memcg) {
        if (params.oomadj >= 600) {
            // Launcher should be perceptible, don't kill it.
            params.oomadj = 200;
            soft_limit_mult = 1;
        } else if (params.oomadj >= 200) {
            soft_limit_mult = 8;
        } else if (params.oomadj >= 100) {
            soft_limit_mult = 10;
        } else if (params.oomadj >=   0) {
            soft_limit_mult = 20;
        } else {
            // Persistent processes will have a large soft limit 512MB.
            soft_limit_mult = 64;
        }
        ...
        snprintf(val, sizeof(val), "%d", soft_limit_mult * EIGHT_MEGA);
        ...
        writefilestring(path.c_str(), val, !is_system_server);
    }
​
    /* 新记录:分配 proc,打开 pidfd,插入两张表 */
    procp = pid_lookup(params.pid);
    if (!procp) {
        int pidfd = -1;
​
        if (pidfd_supported) {
            pidfd = TEMP_FAILURE_RETRY(pidfd_open(params.pid, 0));
            ...
        }
​
        procp = static_cast<struct proc*>(calloc(1, sizeof(struct proc)));
        ...
        procp->pid = params.pid;
        procp->pidfd = pidfd;
        procp->uid = params.uid;
        procp->reg_pid = cred->pid;
        procp->oomadj = params.oomadj;
        procp->valid = true;
        proc_insert(procp);
    } else {
        /* 已有记录:校验归属后换桶 */
        if (!claim_record(procp, cred->pid)) { ... return; }
        proc_unslot(procp);
        procp->oomadj = params.oomadj;
        proc_slot(procp);
    }
}

亮点:

  1. Tgid 校验:拒绝注册非线程组长,防止查杀线程导致整个进程组状态不一致;
  2. pidfd 在注册时就打开:避免将来杀进程时遇到 pid 复用杀错人;
  3. claim_record()(lmkd.cpp:669):只有记录的注册者(或注册者已死)才能修改,防止恶意进程操纵别人的记录。

5. 事件循环:mainloop()

ini 复制代码
/* lmkd.cpp:3601-3700(节选) */
static void mainloop(void) {
    struct event_handler_info* handler_info;
    struct polling_params poll_params;
    ...
    poll_params.poll_handler = NULL;
    poll_params.paused_handler = NULL;
​
    while (1) {
        struct epoll_event events[MAX_EPOLL_EVENTS];
        int nevents;
​
        if (poll_params.poll_handler) {
            /* ★ PSI 事件后处于轮询模式:带超时的 epoll_wait */
            bool poll_now;
​
            clock_gettime(CLOCK_MONOTONIC_COARSE, &curr_tm);
            if (poll_params.update == POLLING_RESUME) {
                /* Just transitioned into POLLING_RESUME, poll immediately. */
                poll_now = true;
                nevents = 0;
            } else {
                /* Calculate next timeout */
                delay = get_time_diff_ms(&poll_params.last_poll_tm, &curr_tm);
                delay = (delay < poll_params.polling_interval_ms) ?
                    poll_params.polling_interval_ms - delay : poll_params.polling_interval_ms;
​
                /* Wait for events until the next polling timeout */
                nevents = epoll_wait(epollfd, events, maxevents, delay);
                ...
                poll_now = (get_time_diff_ms(&poll_params.last_poll_tm, &curr_tm) >=
                    poll_params.polling_interval_ms);
            }
            if (poll_now) {
                call_handler(poll_params.poll_handler, &poll_params, 0);
            }
        } else {
            if (kill_timeout_ms && is_waiting_for_kill()) {
                /* 等待被杀进程死亡:epoll_wait 带 kill 超时 */
                clock_gettime(CLOCK_MONOTONIC_COARSE, &curr_tm);
                delay = kill_timeout_ms - get_time_diff_ms(&last_kill_tm, &curr_tm);
                nevents = (delay > 0) ? epoll_wait(epollfd, events, maxevents, delay) : 0;
                if (nevents == 0) {
                    /* Kill notification timed out */
                    stop_wait_for_proc_kill(false);
                    ...
                }
            } else {
                /* 平时:无限期等待事件,零 CPU 开销 */
                nevents = epoll_wait(epollfd, events, maxevents, -1);
            }
        }
        ...
        /* 第一遍:先处理连接断开(EPOLLHUP) */
        for (i = 0, evt = &events[0]; i < nevents; ++i, evt++) {
            if ((evt->events & EPOLLHUP) && evt->data.ptr) {
                ALOGI("lmkd data connection dropped");
                handler_info = (struct event_handler_info*)evt->data.ptr;
                watchdog.start();
                ctrl_data_close(handler_info->data);
                watchdog.stop();
            }
        }
​
        /* 第二遍:处理其它事件 */
        for (i = 0, evt = &events[0]; i < nevents; ++i, evt++) {
            ...
            if (evt->data.ptr) {
                handler_info = (struct event_handler_info*)evt->data.ptr;
                call_handler(handler_info, &poll_params, evt->events);
            }
        }
    }
}

call_handler 是轮询状态机的驱动器:

rust 复制代码
/* lmkd.cpp:3561-3599(节选) */
static void call_handler(struct event_handler_info* handler_info,
                         struct polling_params *poll_params, uint32_t events) {
    ...
    watchdog.start();                      /* 每次进入回调都喂看门狗 */
    poll_params->update = POLLING_DO_NOT_CHANGE;
    handler_info->handler(handler_info->data, events, poll_params);
    ...
    switch (poll_params->update) {
    case POLLING_START:
        /*
         * Poll for the duration of PSI_WINDOW_SIZE_MS after the
         * initial PSI event because psi events are rate-limited
         * at one per sec.
         */
        poll_params->poll_start_tm = curr_tm;
        poll_params->poll_handler = handler_info;
        break;
    case POLLING_PAUSE:
        poll_params->paused_handler = handler_info;
        poll_params->poll_handler = NULL;
        break;
    case POLLING_RESUME:
        resume_polling(poll_params, curr_tm);
        break;
    case POLLING_DO_NOT_CHANGE:
        if (poll_params->poll_handler &&
            get_time_diff_ms(&poll_params->poll_start_tm, &curr_tm) > PSI_WINDOW_SIZE_MS) {
            /* Polled for the duration of PSI window, time to stop */
            poll_params->poll_handler = NULL;
        }
        break;
    }
    watchdog.stop();
}

也就是说,epoll 上挂着这几类 fd:

fd 回调 作用
lmkd 监听 socket ctrl_connect_handler AMS/init 连入
数据 socket ×3 ctrl_data_handler 命令收发
PSI fd ×2 (medium/critical) mp_event_psi 内存压力事件
被杀进程 pidfd kill_done_handler 死亡确认
reaper pipe kill_fail_handler kill 失败通知

6. 核心:mp_event_psi() 压力事件处理

这是整篇博客的主菜。函数位于 lmkd.cpp:2583,约 320 行。

6.1 节流:杀死没确认前不重复杀

scss 复制代码
/* lmkd.cpp:2631-2642 */
    bool kill_pending = is_kill_pending();
    if (kill_pending && (kill_timeout_ms == 0 ||
        get_time_diff_ms(&last_kill_tm, &curr_tm) < static_cast<long>(kill_timeout_ms))) {
        /* Skip while still killing a process */
        wi.skipped_wakeups++;
        goto no_kill;
    }
    /*
     * Process is dead or kill timeout is over, stop waiting. This has no effect if pidfds are
     * supported and death notification already caused waiting to stop.
     */
    stop_wait_for_proc_kill(!kill_pending);

6.2 采集:vmstat + meminfo

kotlin 复制代码
/* lmkd.cpp:2644-2654 */
    if (vmstat_parse(&vs) < 0) {
        ALOGE("Failed to parse vmstat!");
        return;
    }
    /* Starting 5.9 kernel workingset_refault vmstat field was renamed workingset_refault_file */
    workingset_refault_file = vs.field.workingset_refault ? : vs.field.workingset_refault_file;
​
    if (meminfo_parse(&mi) < 0) {
        ALOGE("Failed to parse meminfo!");
        return;
    }

关注的 vmstat 字段(lmkd.cpp:459-480):workingset_refault(_file)(文件页重失效次数)、pgscan_kswapd(后台回收)、pgscan_direct(直接回收);meminfo 关注 MemFreeSwapFreeSwapTotal 等(详见附录的 meminfo 逐行解析)。

6.3 判定回收状态,没动静就提前退出

ini 复制代码
/* lmkd.cpp:2676-2691 */
    /* Identify reclaim state */
    if (vs.field.pgscan_direct != init_pgscan_direct) {
        init_pgscan_direct = vs.field.pgscan_direct;
        init_pgscan_kswapd = vs.field.pgscan_kswapd;
        reclaim = DIRECT_RECLAIM;
    } else if (vs.field.pgscan_kswapd != init_pgscan_kswapd) {
        init_pgscan_kswapd = vs.field.pgscan_kswapd;
        reclaim = KSWAPD_RECLAIM;
    } else if (workingset_refault_file == prev_workingset_refault) {
        /*
         * Device is not thrashing and not reclaiming, bail out early until we see these stats
         * changing
         */
        goto no_kill;
    }

PSI 说"有压力",但若内核既没在做回收、refault 也没动,说明压力已自行缓解 → 不杀。这是避免过度查杀的第一道闸门。

6.4 计算文件页抖动(thrashing)

ini 复制代码
/* lmkd.cpp:2704-2733 */
    since_thrashing_reset_ms = get_time_diff_ms(&thrashing_reset_tm, &curr_tm);
    if (since_thrashing_reset_ms > THRASHING_RESET_INTERVAL_MS) {   /* 1s */
        long windows_passed;
        /* Calculate prev_thrash_growth if we crossed THRASHING_RESET_INTERVAL_MS */
        prev_thrash_growth = (workingset_refault_file - init_ws_refault) * 100
                            / (base_file_lru + 1);
        windows_passed = (since_thrashing_reset_ms / THRASHING_RESET_INTERVAL_MS);
        /*
         * Decay prev_thrashing unless over-the-limit thrashing was registered in the window we
         * just crossed, which means there were no eligible processes to kill. We preserve the
         * counter in that case to ensure a kill if a new eligible process appears.
         */
        if (windows_passed > 1 || prev_thrash_growth < thrashing_limit) {
            prev_thrash_growth >>= windows_passed;
        }
​
        /* Record file-backed pagecache size when crossing THRASHING_RESET_INTERVAL_MS */
        base_file_lru = vs.field.nr_inactive_file + vs.field.nr_active_file;
        init_ws_refault = workingset_refault_file;
        thrashing_reset_tm = curr_tm;
        thrashing_limit = thrashing_limit_pct;
    } else {
        /* Calculate what % of the file-backed pagecache refaulted so far */
        thrashing = (workingset_refault_file - init_ws_refault) * 100 / (base_file_lru + 1);
    }
    /* Add previous cycle's decayed thrashing amount */
    thrashing += prev_thrash_growth;

thrashing = 文件页缓存中被反复换出又换回的比例>> windows_passed 是按过窗数做指数衰减;但如果上一窗口已超限却"无可杀进程",就保留计数值------等将来出现可杀进程(比如后台又起了个 cached app)时补刀。

6.5 刷新 zone 水位

scss 复制代码
/* lmkd.cpp:2740-2757 */
    /*
     * Refresh watermarks once per min in case user updated one of the margins.
     * TODO: b/140521024 replace this periodic update with an API for AMS to notify LMKD
     * that zone watermarks were changed by the system software.
     */
    if (watermarks.high_wmark == 0 || get_time_diff_ms(&wmark_update_tm, &curr_tm) > 60000) {
        struct zoneinfo zi;
​
        if (zoneinfo_parse(&zi) < 0) { ... return; }
​
        calc_zone_watermarks(&zi, &watermarks);
        wmark_update_tm = curr_tm;
    }
​
    /* Find out which watermark is breached if any */
    wmark = get_lowest_watermark(&mi, &watermarks);
​
    if (!psi_parse_mem(&psi_data)) {
        critical_stall = psi_data.mem_stats[PSI_FULL].avg10 > (float)stall_limit_critical;
    }

解析 /proc/zoneinfo 得到各 zone 的 min/low/high 水位,并判断当前空闲内存跌破到哪一档;同时读一次 /proc/pressure/memoryfull avg10,若超过 stall_limit_critical(默认 100%)则 critical_stall=true------后面允许杀前台。

6.6 查杀决策树(本函数灵魂)

ini 复制代码
/* lmkd.cpp:2762-2845(完整保留判断条件) */
    if (cycle_after_kill && wmark < WMARK_LOW) {
        /*
         * Prevent kills not freeing enough memory which might lead to OOM kill.
         * This might happen when a process is consuming memory faster than reclaim can
         * free even after a kill. Mostly happens when running memory stress tests.
         */
        kill_reason = PRESSURE_AFTER_KILL;
        strncpy(kill_desc, "min watermark is breached even after kill", sizeof(kill_desc));
    } else if (level == VMPRESS_LEVEL_CRITICAL && events != 0) {
        /*
         * Device is too busy reclaiming memory which might lead to ANR.
         * Critical level is triggered when PSI complete stall (all tasks are blocked because
         * of the memory congestion) breaches the configured threshold.
         */
        kill_reason = NOT_RESPONDING;
        strncpy(kill_desc, "device is not responding", sizeof(kill_desc));
    } else if (swap_is_low && thrashing > thrashing_limit_pct) {
        /* Page cache is thrashing while swap is low */
        kill_reason = LOW_SWAP_AND_THRASHING;
        ...
        /* Do not kill perceptible apps unless below min watermark or heavily thrashing */
        if (wmark > WMARK_MIN && thrashing < thrashing_critical_pct) {
            min_score_adj = PERCEPTIBLE_APP_ADJ + 1;
        }
        check_filecache = true;
    } else if (swap_is_low && wmark < WMARK_HIGH) {
        /* Both free memory and swap are low */
        kill_reason = LOW_MEM_AND_SWAP;
        ...
    } else if (wmark < WMARK_HIGH && swap_util_max < 100 &&
               (swap_util = calc_swap_utilization(&mi)) > swap_util_max) {
        /*
         * Too much anon memory is swapped out but swap is not low.
         * Non-swappable allocations created memory pressure.
         */
        kill_reason = LOW_MEM_AND_SWAP_UTIL;
        ...
    } else if (wmark < WMARK_HIGH && thrashing > thrashing_limit) {
        /* Page cache is thrashing while memory is low */
        kill_reason = LOW_MEM_AND_THRASHING;
        ...
        cut_thrashing_limit = true;
        ...
        check_filecache = true;
    } else if (reclaim == DIRECT_RECLAIM && thrashing > thrashing_limit) {
        /* Page cache is thrashing while in direct reclaim (mostly happens on lowram devices) */
        kill_reason = DIRECT_RECL_AND_THRASHING;
        ...
        cut_thrashing_limit = true;
        ...
        check_filecache = true;
    } else if (check_filecache) {
        int64_t file_lru_kb = (vs.field.nr_inactive_file + vs.field.nr_active_file) * page_k;
​
        if (file_lru_kb < filecache_min_kb) {
            /* File cache is too low after thrashing, keep killing background processes */
            kill_reason = LOW_FILECACHE_AFTER_THRASHING;
            ...
            min_score_adj = PERCEPTIBLE_APP_ADJ + 1;
        } else {
            /* File cache is big enough, stop checking */
            check_filecache = false;
        }
    }

整理成表(优先级从上到下,min_score_adj 越低杀得越狠):

# 条件 kill_reason min_score_adj 说明
1 上轮 kill 后仍破 min/low 水位 PRESSURE_AFTER_KILL 0 内存释放赶不上消耗
2 critical PSI 事件(FULL 停顿 700ms/1s) NOT_RESPONDING 0 快 ANR 了,不再保护前台
3 swap 低 + 抖动超限 LOW_SWAP_AND_THRASHING 201 除非破 min 水位或重抖动
4 swap 低 + 破 high 水位 LOW_MEM_AND_SWAP 201
5 破水位 + swap 利用率超上限 LOW_MEM_AND_SWAP_UTIL 0 匿名页换出太多
6 破水位 + 抖动超限 LOW_MEM_AND_THRASHING 201
7 direct reclaim + 抖动超限 DIRECT_RECL_AND_THRASHING 201 典型 lowram 场景
8 抖动过后 file cache 仍太低 LOW_FILECACHE_AFTER_THRASHING 201 持续补杀后台

PERCEPTIBLE_APP_ADJ = 200(lmkd.cpp:96):min_score_adj = 201 意味着不杀用户可感知的应用(前台、可见、播放音乐等 adj ≤ 200 的),只杀 cached/backup 之类。

6.7 执行查杀 + 反馈调节

ini 复制代码
/* lmkd.cpp:2847-2873 */
    /* Kill a process if necessary */
    if (kill_reason != NONE) {
        struct kill_info ki = {
            .kill_reason = kill_reason,
            .kill_desc = kill_desc,
            .thrashing = (int)thrashing,
            .max_thrashing = max_thrashing,
        };
​
        /* Allow killing perceptible apps if the system is stalled */
        if (critical_stall) {
            min_score_adj = 0;
        }
        psi_parse_io(&psi_data);
        psi_parse_cpu(&psi_data);
        int pages_freed = find_and_kill_process(min_score_adj, &ki, &mi, &wi, &curr_tm, &psi_data);
        if (pages_freed > 0) {
            killing = true;
            max_thrashing = 0;
            if (cut_thrashing_limit) {
                /*
                 * Cut thrasing limit by thrashing_limit_decay_pct percentage of the current
                 * thrashing limit until the system stops thrashing.
                 */
                thrashing_limit = (thrashing_limit * (100 - thrashing_limit_decay_pct)) / 100;
            }
        }
    }

阈值自适应 :每次因抖动杀进程后,thrashing_limit 衰减 10%(thrashing_limit_decay_pct),即杀一次不够就继续杀、触发条件越杀越灵敏,直到抖动消失;1s 窗口 reset 时恢复初值。

6.8 轮询控制

rust 复制代码
/* lmkd.cpp:2876-2901 */
no_kill:
    /* Do not poll if kernel supports pidfd waiting */
    if (is_waiting_for_kill()) {
        /* Pause polling if we are waiting for process death notification */
        poll_params->update = POLLING_PAUSE;
        return;
    }
​
    /*
     * Start polling after initial PSI event;
     * extend polling while device is in direct reclaim or process is being killed;
     * do not extend when kswapd reclaims because that might go on for a long time
     * without causing memory pressure
     */
    if (events || killing || reclaim == DIRECT_RECLAIM) {
        poll_params->update = POLLING_START;
    }
​
    /* Decide the polling interval */
    if (swap_is_low || killing) {
        /* Fast polling during and after a kill or when swap is low */
        poll_params->polling_interval_ms = PSI_POLL_PERIOD_SHORT_MS;   /* 10ms */
    } else {
        /* By default use long intervals */
        poll_params->polling_interval_ms = PSI_POLL_PERIOD_LONG_MS;    /* 100ms */
    }

7. 查杀执行:find_and_kill_process() → kill_one_process()

7.1 选受害者

ini 复制代码
/* lmkd.cpp:2421-2466 */
/*
 * Find one process to kill at or above the given oom_score_adj level.
 * Returns size of the killed process.
 */
static int find_and_kill_process(int min_score_adj, struct kill_info *ki, union meminfo *mi,
                                 struct wakeup_info *wi, struct timespec *tm,
                                 struct psi_data *pd) {
    int i;
    int killed_size = 0;
    bool lmk_state_change_start = false;
    bool choose_heaviest_task = kill_heaviest_task;
​
    for (i = OOM_SCORE_ADJ_MAX; i >= min_score_adj; i--) {
        struct proc *procp;
​
        if (!choose_heaviest_task && i <= PERCEPTIBLE_APP_ADJ) {
            /*
             * If we have to choose a perceptible process, choose the heaviest one to
             * hopefully minimize the number of victims.
             */
            choose_heaviest_task = true;
        }
​
        while (true) {
            procp = choose_heaviest_task ?
                proc_get_heaviest(i) : proc_adj_tail(i);
​
            if (!procp)
                break;
​
            killed_size = kill_one_process(procp, min_score_adj, ki, mi, wi, tm, pd);
            if (killed_size >= 0) {
                if (!lmk_state_change_start) {
                    lmk_state_change_start = true;
                    stats_write_lmk_state_changed(STATE_START);
                }
                break;
            }
        }
        if (killed_size) {
            break;
        }
    }
    ...
    return killed_size;
}

策略:adj 从 1000 递减扫桶;adj > 200 的杀"最近注册"的(proc_adj_tail),一旦要杀 adj ≤ 200 的可感知进程,改为杀同桶里 RSS 最大的proc_get_heaviest,lmkd.cpp:2118 遍历链表找最大 VmRSS),期望一次杀够、少杀几个。一次只杀一个,杀完回来重新评估。

7.2 kill_one_process()

csharp 复制代码
/* lmkd.cpp:2304-2415(节选) */
static int kill_one_process(struct proc* procp, int min_oom_score, struct kill_info *ki,
                            union meminfo *mi, struct wakeup_info *wi, struct timespec *tm,
                            struct psi_data *pd) {
    ...
    /* 一连串前置校验 */
    if (!procp->valid || !read_proc_status(pid, buf, sizeof(buf))) {
        goto out;
    }
    if (!parse_status_tag(buf, PROC_STATUS_TGID_FIELD, &tgid)) { ... goto out; }
    if (tgid != pid) {
        ALOGE("Possible pid reuse detected (pid %d, tgid %" PRId64 ")!", pid, tgid);
        goto out;
    }
    // Zombie processes will not have RSS / Swap fields.
    if (!parse_status_tag(buf, PROC_STATUS_RSS_FIELD, &rss_kb)) { goto out; }
    if (!parse_status_tag(buf, PROC_STATUS_SWAP_FIELD, &swap_kb)) { goto out; }
​
    taskname = proc_get_name(pid, buf, sizeof(buf));
    ...
    mem_st = stats_read_memory_stat(per_app_memcg, pid, uid, rss_kb * 1024, swap_kb * 1024);
​
    ...
    trace_kill_start(desc);
​
    /* 把 pidfd(或 pid)挂进 epoll 等死亡通知 */
    start_wait_for_proc_kill(pidfd < 0 ? pid : pidfd);
    /* 交给 reaper 线程池执行 kill(2) */
    kill_result = reaper.kill({ pidfd, pid, uid }, false);
​
    trace_kill_end();
​
    if (kill_result) {
        stop_wait_for_proc_kill(false);
        ALOGE("kill(%d): errno=%d", pid, errno);
        /* Delete process record even when we fail to kill so that we don't get stuck on it */
        goto out;
    }
​
    last_kill_tm = *tm;
    inc_killcnt(procp->oomadj);
​
    if (ki) {
        ...
        ALOGI("Kill '%s' (%d), uid %d, oom_score_adj %d to free %" PRId64 "kB rss, %" PRId64
              "kB swap; reason: %s", taskname, pid, uid, procp->oomadj, rss_kb, swap_kb,
              ki->kill_desc);
    }
    ...
    killinfo_log(procp, min_oom_score, rss_kb, swap_kb, ki, mi, wi, tm, pd);
    ...
    stats_write_lmk_kill_occurred(&kill_st, mem_st);      /* → AMS → statsd */
    ctrl_data_write_lmk_kill_occurred((pid_t)pid, uid);   /* → 订阅者 */
​
    result = rss_kb / page_k;
​
out:
    /*
     * WARNING: After pid_remove() procp is freed and can't be used!
     * Therefore placed at the end of the function.
     */
    pid_remove(pid);
    return result;
}

要点:

  1. 三重防误杀valid 标志、Tgid == pid(防 pid 复用)、RSS/Swap 可解析(排除僵尸);
  2. kill 交给 reaper 线程池 异步执行,主线程立刻返回继续处理事件(reaper 负责发信号 + waitid 收割,避免僵尸进程占 pid);
  3. 无论成败最后都 pid_remove,防卡死在坏记录上。

7.3 死亡确认:pidfd

ini 复制代码
/* lmkd.cpp:2275-2301(节选) */
static void start_wait_for_proc_kill(int pid_or_fd) {
    static struct event_handler_info kill_done_hinfo = { 0, kill_done_handler };
    struct epoll_event epev;
    ...
    last_kill_pid_or_fd = pid_or_fd;
​
    if (!pidfd_supported) {
        /* If pidfd is not supported just store PID and exit */
        return;
    }
​
    epev.events = EPOLLIN;
    epev.data.ptr = (void *)&kill_done_hinfo;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, last_kill_pid_or_fd, &epev) != 0) { ... }
    maxevents++;
}

进程死亡 → pidfd 可读 → kill_done_handler

arduino 复制代码
/* lmkd.cpp:2256-2260 */
static void kill_done_handler(int data __unused, uint32_t events __unused,
                              struct polling_params *poll_params) {
    stop_wait_for_proc_kill(true);
    poll_params->update = POLLING_RESUME;
}

pidfd 不支持时退化为查 /proc/<pid> 是否存在(is_kill_pending,lmkd.cpp:2192)。kill 失败则由 reaper 经 pipe 通知 kill_fail_handler(lmkd.cpp:2262)。


8. 辅助机制

8.1 Watchdog:lmkd 自己卡死了怎么办

主线程每个回调被 watchdog.start()/stop() 包住;若超时 2s(WATCHDOG_TIMEOUT_SEC)没跑完,看门狗线程直接代杀:

ini 复制代码
/* lmkd.cpp:2166-2190(节选) */
static void watchdog_callback() {
    int prev_pid = 0;
​
    ALOGW("lmkd watchdog timed out!");
    for (int oom_score = OOM_SCORE_ADJ_MAX; oom_score >= 0;) {
        struct proc target;
​
        if (!find_victim(oom_score, prev_pid, target)) {
            oom_score--;
            prev_pid = 0;
            continue;
        }
​
        if (target.valid && reaper.kill({ target.pidfd, target.pid, target.uid }, true) == 0) {
            ALOGW("lmkd watchdog killed process %d, oom_score_adj %d", target.pid, oom_score);
            ...
            break;
        }
        prev_pid = target.pid;
    }
}
​
static Watchdog watchdog(WATCHDOG_TIMEOUT_SEC, watchdog_callback);

8.2 属性热更新

LMK_UPDATE_PROPS 命令 → update_props()destroy_monitors() + init_monitors()(lmkd.cpp:1524-1551)。若新配置无法支持(比如要求旧策略但内核是 cgroup v2),直接 exit(1) 让 init 重启自己。

8.3 统计上报

  • inc_killcnt()/get_killcnt()(lmkd.cpp:1322-1368):两级稀疏索引记录各 adj 桶的 kill 计数,供 LMK_GETKILLCNT 查询(AMS 的 ProcessList 用来上报 LowMemoryKiller 统计);
  • killinfo_log()(lmkd.cpp:2035):把 pid/uid/oomadj/rss/全部 meminfo 字段/唤醒信息写进 event log(tag 10195355),排查线上问题必备;
  • stats_write_lmk_kill_occurred():kill 事件经 socket 转发给 AMS,最终进 statsd 的 LMKD_KILL_OCCURRED 埋点。

9. 全流程时序总结

css 复制代码
[启动]
init fork lmkd
  └─ main(): update_props() → init(): epoll + lmkd socket + init_monitors()
       └─ init_psi_monitors(): 写 /proc/pressure/memory
            medium  = "some 70000 1000000"
            critical= "full 700000 1000000"
       └─ mlockall + SCHED_FIFO + init_reaper() + watchdog.init()
       └─ mainloop(): epoll_wait(-1) 静默等待
​
[注册]
AMS 每次调整 oomadj → LMK_PROCPRIO
  └─ cmd_procprio(): 写 /proc/<pid>/oom_score_adj
       → pidfd_open → proc 插入 pidhash + oomadj 桶链表
​
[压力事件]
内核停顿超阈值 → PSI fd 可读 → epoll 返回
  └─ mp_event_psi():
       1. 节流(上次 kill 未确认/未超时 → 跳过)
       2. 读 vmstat/meminfo/zoneinfo/pressure
       3. 判定回收状态,无动静 → return
       4. 算 thrashing(refault/file_lru%,带窗口衰减)
       5. 判水位 breach、swap 低、swap 利用率
       6. 八条决策树 → kill_reason + min_score_adj
       7. find_and_kill_process():
            adj 1000 → min_score_adj 扫桶
            → kill_one_process(): 校验 Tgid → start_wait(pidfd)
              → reaper.kill() 异步 SIGKILL+收割
              → 日志/统计/通知订阅者 → pid_remove()
       8. 反馈:thrashing_limit *= 0.9;轮询 10/100ms 持续 1s
​
[确认]
pidfd 可读 → kill_done_handler() → POLLING_RESUME 继续轮询评估
kill 超时/失败 → stop_wait_for_proc_kill(false) → 继续下一轮

设计精髓:PSI 负责"何时该看一眼"(低开销、真停顿语义),userspace 多因子决策负责"该不该杀、杀到哪一档",oomadj 链表负责"杀谁",pidfd+reaper 负责"杀没杀死",thrashing 反馈回路负责"杀够没有"。


附录:/proc/meminfo 逐行解析(lmkd 决策的第一手输入)

下面是一台真实设备(约 6GiB 可用内存、zram swap、带 CMA 的车机/手机)的 cat /proc/meminfo 输出。这份文件正是 lmkd 决策的第一手输入(meminfo_parse(),lmkd.cpp:1855 逐行解析)。按内核记账的分组逐行解释,最后结合 lmkd 源码算一遍这台设备的真实状态。

makefile 复制代码
MemTotal:        6442208 kB
MemFree:          573412 kB
MemAvailable:    2035888 kB
Buffers:           51680 kB
Cached:          2277792 kB
SwapCached:            0 kB
Active:          4500700 kB
Inactive:         287732 kB
Active(anon):    2403880 kB
Inactive(anon):    88348 kB
Active(file):    2096820 kB
Inactive(file):   199384 kB
Unevictable:       24808 kB
Mlocked:           10408 kB
SwapTotal:       3865320 kB
SwapFree:        3864552 kB
Dirty:                40 kB
Writeback:             0 kB
AnonPages:       2412716 kB
Mapped:           926104 kB
Shmem:             26948 kB
KReclaimable:      84844 kB
Slab:             302268 kB
SReclaimable:      84844 kB
SUnreclaim:       217424 kB
KernelStack:       42008 kB
ShadowCallStack:   10520 kB
PageTables:        98348 kB
SecPageTables:         0 kB
...
AnonHugePages:    790528 kB
...
CmaTotal:         262144 kB
CmaFree:               0 kB
...

A.1 总量与可用性

本机值 含义
MemTotal 6442208 (≈6.14GiB) 内核可管理的物理内存(=物理颗粒 − 固件/pVM/预留段)。整机物理内存大于此值,约 1.9GiB 被扣掉(车机/手机常见)
MemFree 573412 (≈560MB) 完全空闲、可直接分配的页。Linux 会尽量把空闲内存拿去做缓存,所以这个值小不代表紧张
MemAvailable 2035888 (≈1.94GB) 内核估算"不触发 swap、不做痛苦回收就能拿出的内存" ≈ MemFree + 可回收页缓存 − 水位保留。看健康度用它,别用 MemFree

A.2 页缓存与块缓存

本机值 含义
Buffers 51680 块设备元数据缓存(传统 buffer heads),现代设备上很小
Cached 2277792 (≈2.17GB) 文件页缓存(apk/oat/dex/so/媒体文件的内容页),含 tmpfs 部分。内存不够时优先被回收(干净页直接丢,脏页回写后丢)
SwapCached 0 已换出、但内存里还留有一份副本的页(避免重复 swap I/O 的优化)。为 0 与下面 swap 几乎未用一致

A.3 LRU 链表:Active/Inactive 四象限

内核把可回收页分两条 LRU:anon(匿名页,回收需写 swap)file(文件页,回收只需丢弃/回写) ,各自再分 active(热,最近用过)和 inactive(冷,先被回收):

本机值 含义
Active 4500700 活跃页合计 = 下两行 anon + file 之和
Inactive 287732 不活跃页合计
Active(anon) 2403880 热匿名页------正在使用的 Java native 堆、malloc 等。这部分是"真占用",只能靠 swap 或杀进程释放
Inactive(anon) 88348 冷匿名页,swap 的候选
Active(file) 2096820 热文件页(常用代码页、working set)
Inactive(file) 199384 冷文件页,回收的第一梯队
Unevictable 24808 不可回收页:ramfs、被 pin 的 shmem、mlock 的页
Mlocked 10408 mlock() 锁定的页。这里面就有 lmkd 自己 mlockall() 锁住的页(lmkd.cpp:3818),杀手必须保证自己不被换出

本机 file 页 active:inactive ≈ 10:1,说明 2.1GB 页缓存基本是活跃 working set(车机常驻服务 + 常用代码页),真正"随时可丢"的冷缓存只有 195MB。

A.4 Swap(本机是 zram)

本机值 含义
SwapTotal 3865320 (≈3.69GiB) swap 区总大小,Android 上通常是 zram(内存压缩盘) 而非磁盘分区
SwapFree 3864552 剩余 swap。已用仅 768kB(0.02%) ------设备刚开机不久或内存压力很小

A.5 脏页与回写

本机值 含义
Dirty 40 已修改、待写回存储的页
Writeback 0 正在写回的页。两者接近 0 = 无回写积压

A.6 匿名页与映射

本机值 含义
AnonPages 2412716 所有用户进程匿名页总和(与 anon LRU 之和的差 ≈ mlock/不可回收部分)
Mapped 926104 (≈904MB) 映射进页表的文件页:代码段、共享库、dex/oat、字体等
Shmem 26948 tmpfs/ashmem 页(/dev、部分 dmabuf 堆)

A.7 内核自身开销

本机值 含义
KReclaimable 84844 可回收的内核对象(dentry/inode 缓存等),含 SReclaimable
Slab 302268 slab 分配器总量(精确 = 下两项之和)
SReclaimable 84844 slab 中可回收部分(内存紧张时收缩)
SUnreclaim 217424 slab 中不可回收部分(内核关键数据结构)
KernelStack 42008 每线程内核栈(arm64 默认 16KB/线程 → 约 2600 个任务)
ShadowCallStack 10520 Clang 影子调用栈(返回地址存副本,防 ROP 攻击),Android 高版本 arm64 安全特性,随任务数增长
PageTables 98348 各进程页表占的页
SecPageTables 0 pKVM/hypervisor 安全世界页表(未启用)
Percpu 7320 per-CPU 变量区

A.8 虚拟内存与 overcommit

本机值 含义
CommitLimit 7086424 overcommit 记账上限,本机恰等于 RAM×50% + SwapTotal(3221104+3865320)
Committed_AS 100693160 (≈96GB) 已"承诺"的地址空间总和。远超 CommitLimit------因为默认 overcommit 策略是启发式(mode 0),Java/ART 大量预留虚拟地址空间也能分配成功;只有严格模式(mode 2)才会按此限额拒绝
VmallocTotal 133TB vmalloc 虚拟地址区大小(arm64 48 位 VA)
VmallocUsed 104360 已映射的 vmalloc 区
VmallocChunk 0 最大连续空闲块。现代内核不再维护此统计,恒为 0,无参考意义

A.9 大页与 CMA

本机值 含义
AnonHugePages 790528 (≈772MB) THP 透明大页生效中(386 个 2MB 匿名大页),减少 TLB miss
ShmemHugePages / ShmemPmdMapped 0 tmpfs 大页(未用)
FileHugePages / FilePmdMapped 0 文件页大页映射(未用)
CmaTotal 262144 (256MB) CMA 连续内存保留区总量
CmaFree 0 CMA 已全部占满------车机上典型是环视/摄像头 buffer、固件 DMA。这块内存平时普通分配借不到,lmkd 统计时按 0 可用处理
HugePages_* / Hugepagesize / Hugetlb 0 / 2048 / 0 显式 hugetlb 池(预留式大页),Android 不用,用 THP 代替

NFS_Unstable(NFS 服务端未确认写入)、Bounce(bounce buffer 旧设备)、WritebackTmp(FUSE 写回暂存)------现代配置下都是 0。

A.10 这台设备的画像

  • 中高配车机/手机:可用 6.14GiB,zram 3.7GiB,CMA 256MB;
  • 内存状态健康:MemAvailable 1.94GB(30%),swap 仅用 768kB------刚开机或负载很轻;
  • 页缓存偏热:Active(file) 是 Inactive(file) 的 10 倍,真正"唾手可得"的冷页缓存只有 ~195MB,一旦内存增长,回收很快会伤到 working set(→ refault → lmkd 的 thrashing 判定登场);
  • CMA 满载:典型车载摄像头/多媒体 buffer 布局;
  • THP 开启 且大量使用。

A.11 lmkd 是怎么用这份文件的

lmkd 只挑自己要的行(lmkd.cpp:407-427 的 meminfo_field_names):MemFree/Cached/SwapCached/Buffers/Shmem/Unevictable/SwapTotal/SwapFree/Active(anon)/Inactive(anon)/Active(file)/Inactive(file)/SReclaimable/SUnreclaim/KernelStack/PageTables/ION_heap/ION_heap_pool/CmaFree(本机没有 ION_heap 行,按 0 处理;GPU 占用走 BPF map,lmkd.cpp:1840)。注意它不读 MemAvailable------那是给人看的;lmkd 自己用 zone 水位 + PSI + swap + thrashing 组合判断。

三个关键处理:

rust 复制代码
/* lmkd.cpp:1835 ------ 解析时就把 kB 换算成页 */
if (match_res == PARSE_SUCCESS) {
    mi->arr[field_idx] = val / page_k;          /* page_k = 4 (4KB页) */
}
​
/* lmkd.cpp:1877-1880 ------ 两个派生量 */
mi->field.nr_file_pages = mi->field.cached + mi->field.swap_cached + mi->field.buffers;
mi->field.easy_available = mi->field.nr_free_pages + mi->field.inactive_file;
​
/* lmkd.cpp:1889-1891 ------ zram 修正:swap 就占着内存,不能用名义值 */
static inline int64_t get_free_swap(union meminfo *mi) {
    return std::min(mi->field.free_swap, mi->field.easy_available);
}

代入本机数值(换算成 kB 展示):

派生量 计算 结果
nr_file_pages 2277792 + 0 + 51680 ≈2.22GB 文件页
easy_available 573412 + 199384 ≈755MB
get_free_swap min(3864552, 755MB) ≈755MB(zram 修正立竿见影:名义 3.7GB 的空 swap 实际只认 755MB)
swap_is_low 阈值 SwapTotal×10% = 386532kB 755MB > 377MB → 不低
swap 利用率 768/3865320 0.02%,远低于 swap_util_max

老策略 mp_event_common 里的 other_file(可回收文件页)= Cached + Buffers − Shmem − Unevictable − SwapCached ≈ 2.17GB(lmkd.cpp:3013-3018)。

结论:此刻这台设备在 lmkd 眼里处于"绿灯"------水位未破、swap 充裕、无抖动证据,即便 PSI 事件来了,决策树也大概率走到"无 kill_reason"直接返回。

附录 B:/proc/zoneinfo 逐块解析(lmkd 水位判断的数据来源)

/proc/zoneinfo 是内核按 zone 细分的内存账本 + 水位线表 ------正是 lmkd 决策树里 wmark < WMARK_HIGH 那一系列判断的数据来源(zoneinfo_parse(),lmkd.cpp:1742;每 60s 刷新一次,lmkd.cpp:2740)。下面是同一台设备的 cat /proc/zoneinfo 输出(节选),逐块解释。

yaml 复制代码
Node 0, zone    DMA32
  per-node stats
      nr_inactive_anon 430141
      nr_active_anon 196730
      nr_inactive_file 438460
      nr_active_file 107254
      ...
      workingset_refault_anon 170
      workingset_refault_file 112228
      workingset_activate_file 93555
      workingset_restore_file 51352
      ...
  pages free     33683
        boost    0
        min      4198
        low      17660
        high     31122
        spanned  689664
        present  418560
        managed  366830
        cma      65536
        protection: (0, 4858, 4858, 4858)
      nr_free_pages 33683
      ...
      nr_free_cma  0
  pagesets
    cpu: 0
              count: 14739
              high:  2943
              batch: 63
  vm stats threshold: 30
    ...
  start_pfn:           358912
Node 0, zone   Normal
  ...
  pages free     61221
        min      14233
        low      59877
        high     105521
        spanned  1310720
        present  1277952
        managed  1243722
        protection: (0, 0, 0, 0)
      nr_free_pages 61221
      nr_zspages   111
      ...
  start_pfn:           1048576
Node 0, zone   Movable   (空)
Node 0, zone   Device    (空)

B.1 总览:这台设备的物理内存布局

arm64 单 NUMA 节点,四个 zone(后两个为空):

Zone managed(页) 换算 free(页) 角色
DMA32 366830 ≈1.40GB 33683 物理地址 4GB 以下的内存,256MB CMA 嵌在这里
Normal 1243722 ≈4.75GB 61221 主力内存区
Movable 0 --- 0 仅当 CMA 迁移策略启用时才有内容
Device 0 --- 0 设备内存(HMM 等),未用

managed 合计 1610552 页 × 4KB ≈ 6.14GB,正好等于 meminfo 的 MemTotal------meminfo 是"总账",zoneinfo 是"分账"

B.2 per-node stats:节点级统计

bash 复制代码
per-node stats
    nr_inactive_anon 430141     ← 冷匿名页(swap 候选)
    nr_active_anon 196730       ← 热匿名页
    nr_inactive_file 438460     ← 冷文件页(回收第一梯队)★ lmkd 只要这两行
    nr_active_file 107254       ← 热文件页 ★
    nr_unevictable 6202
    nr_slab_reclaimable 20118 / nr_slab_unreclaimable 54178
    nr_isolated_anon/file 0     ← 正在从 LRU 摘下隔离回收的页

workingset 系列(抖动的原始证据,lmkd 从 /proc/vmstat 读的是全系统累计值,这里是同一套统计的节点视图):

字段 含义
workingset_refault_anon 170 匿名页重新故障(换出后又访问)
workingset_refault_file 112228 文件页重新故障------被回收后又被访问,必须从磁盘重读
workingset_activate_file 93555 refault 中被再次激活进 active LRU 的
workingset_restore_file 51352 refault 中"当年在 active 表、被换出后又被访问"而直接恢复进 active 表的页
workingset_nodes 15751 被跟踪的 radix 树节点数
workingset_nodereclaim 0 节点级回收次数

本机 refault 占比:restore/activate 比例高(51352/93555 ≈ 55%),说明这台机器的 working set 大于可用页缓存------典型的"内存略紧但还没到抖动"的形态 ,正是 lmkd thrashing 公式要抓的前兆。

其余:nr_anon_pagesnr_mappednr_file_pagesnr_dirtynr_shmem 等与 meminfo 同名项对应;nr_anon_transparent_hugepages 405(405 个 2MB THP ≈ 790MB,对上 meminfo 的 AnonHugePages);nr_vmscan_write 310(vmscan 路径写入 swap 的页);nr_dirtied/nr_written(脏页产生/回写累计,差值 ≈ 当前脏量);nr_kernel_stacknr_shadow_call_stacknr_page_table_pages 对应 meminfo 同名字段;nr_swapcached 26

B.3 水位线:min / low / high / boost(★ lmkd 的核心输入)

ini 复制代码
pages free     33683      ← 该 zone 当前空闲页
      boost    0          ← watermark_boost:回收后额外抬高的水位(防分配风暴),0=未触发
      min      4198       ← 死亡线:跌破后所有分配都会直接回收(__GFP_HIGH 者除外)
      low      17660      ← kswapd 唤醒线:跌破则后台回收 kswapd 被叫醒
      high     31122      ← kswapd 目标线:回收到此线以上才睡去

三线的协作(内核分配慢路径 __alloc_pages_slowpath):

sql 复制代码
free > high    :太平盛世,kswapd 睡觉
low  < free < high :kswapd 被唤醒,后台异步回收
min  < free < low  :分配者进入 direct reclaim(自己动手回收)→ 产生 PSI some/full 停顿
free < min     :分配阻塞 + OOM killer 候选路径

这正是 PSI 事件与水位判断互为表里的原因 :PSI 报告"停顿了"(果),水位说明"为什么停"(分配已落到慢路径)。lmkd 决策树里 wmark < WMARK_LOW(连后台回收都追不上)比 wmark < WMARK_HIGH 严重,cycle_after_kill && wmark < WMARK_LOW 直接判 PRESSURE_AFTER_KILL。

B.4 spanned / present / managed:三个"大小"

字段 DMA32 含义
spanned 689664 该 zone 地址区间跨度(含空洞)
present 418560 实际有内存的页(1.6GB)
managed 366830 交给伙伴系统管理的页(扣掉内核保留/元数据)------meminfo 只统计 managed

DMA32 spanned 689664 页但 present 只有 418560:中间是固件/预留空洞。

B.5 CMA 相关

ini 复制代码
cma        65536    ← 本 zone 内 CMA 保留区大小(65536×4KB = 256MB,对上 meminfo CmaTotal)
...
nr_free_cma 0       ← CMA 里当前空闲页 = 0

CMA 全满 (与 meminfo CmaFree: 0 互证)。车机上这 256MB 通常被摄像头/环视/固件 DMA 长期占用。后果有两层:① 普通分配在 DMA32 上拿不到 CMA 的页;② lmkd 算水位时把 CmaFree 从空闲里扣掉(get_lowest_watermark,lmkd.cpp:2543)------CMA 满时这一项恰好是 0。

B.6 protection 数组

yaml 复制代码
DMA32:    protection: (0, 4858, 4858, 4858)
Normal:   protection: (0, 0, 0, 0)

低端 zone 的"保护垫":当高端 zone(Normal)水位跌破 min 时,zone_watermark_ok() 会额外要求 DMA32 留出 4858 页不许高端分配挪用。lmkd 解析时取 max_protection(4858)加到每级水位上(lmkd.cpp:2569-2571):

rust 复制代码
/* lmkd.cpp:2557-2573 */
void calc_zone_watermarks(struct zoneinfo *zi, struct zone_watermarks *watermarks) {
    ...
    for (...each zone...) {
        if (!zone->fields.field.present) {
            continue;                       /* Movable/Device 直接跳过 */
        }
        watermarks->high_wmark += zone->max_protection + zone->fields.field.high;
        watermarks->low_wmark  += zone->max_protection + zone->fields.field.low;
        watermarks->min_wmark  += zone->max_protection + zone->fields.field.min;
    }
}

(另一处使用:老策略 mp_event_commontotalreserve_pages = Σ(max_protection + high) 从 MemFree 里扣掉保留量算 other_free,lmkd.cpp:1805、3012。)

B.7 zone 级计数器:与 per-node stats 的勾稽关系

arduino 复制代码
nr_free_pages 33683
nr_zone_inactive_anon 111364 ┐
nr_zone_active_anon   77160  ├ 只统计"驻留在本 zone 物理页上"的页
nr_zone_inactive_file 41507  │
nr_zone_active_file   49145  ┘

校验:nr_zone_inactive_file DMA32 41507 + Normal 396953 = 438460 = per-node nr_inactive_file ✓(anon、active 同样能对上)。页是按"物理上落在哪个 zone"分账的,而 LRU 归属是逻辑概念------所以 zone 表求和等于节点表。

其它:nr_zone_write_pending 161/308 = 本 zone 的 dirty+writeback;nr_mlock 2602(Normal 里被锁的页,含 lmkd 自己 mlockall 的页);nr_zspages 111(仅 Normal 有) ------zram 压缩存储占用的页(每页可装多个压缩页,即 meminfo 里 768kB swap 用量的家底);nr_bounce 0

B.8 pagesets:每 CPU 页缓存(盘点 MemFree 与 zone free 的差额)

yaml 复制代码
pagesets
  cpu: 0
    count: 14739    ← 该 CPU 私有的空闲页数
    high:  2943     ← 超过此值就批量还给 zone 伙伴系统
    batch: 63       ← 每次 pcp 批量加减的页数
  vm stats threshold: 30   ← per-cpu 计数器回刷阈值(统计误差容忍度)

六个 CPU 的 count 加总:DMA32 22617 + Normal 35666 = 58283 页(≈228MB) 。这解释了一个常见困惑:meminfo 的 MemFree 包含 per-CPU 页表(pcp),而 zoneinfo 的 pages free 不包含。本机 zone free 合计 94904 页(≈371MB),加上 pcp 才是完整的"空闲"。

B.9 尾部

ini 复制代码
node_unreclaimable: 0    ← 本节点 slab 全部可回收的标记(kmemcg 语义)
start_pfn: 358912        ← 本 zone 起始物理页帧号(×4KB = 1.37GB 起始物理地址)
                          Normal start_pfn 1048576 = 4GB 物理地址处

start_pfn 印证布局:DMA32 覆盖 1.37GB~4GB,Normal 从 4GB 开始。

B.10 lmkd 视角:代入这台设备算一遍

lmkd 对本文件只提取 6 个 zone 字段 + 2 个 node 字段(lmkd.cpp:294-310、347-355):

lua 复制代码
/* zone 字段:nr_free_pages, min, low, high, present, nr_free_cma
   node 字段:nr_inactive_file, nr_active_file(喂给 thrashing 公式的 base_file_lru)
   另有 protection 行取 max */

Step 1 --- 汇总水位calc_zone_watermarks,Movable/Device 被 present==0 跳过):

水位 计算 页数 ≈MB
min_wmark (4858+4198) + (0+14233) 23289 91
low_wmark (4858+17660) + (0+59877) 82395 322
high_wmark (4858+31122) + (0+105521) 141501 553

Step 2 --- 判定跌破哪级get_lowest_watermark,lmkd.cpp:2540):

ini 复制代码
int64_t nr_free_pages = mi->field.nr_free_pages - mi->field.cma_free;
/* MemFree 573412kB ÷ 4 = 143353 页;CmaFree = 0 → 143353 页 */

143353 vs 三线:> min ✓、> low ✓、 > high 仅高出 1852 页(≈7MB) ------按 meminfo 那一刻的快照,判定为 WMARK_NONE,但这台机器离"跌破 high 水位"只差 7MB:

  • 一旦跌破 highLOW_MEM_AND_THRASHING / LOW_MEM_AND_SWAP 等条件开始具备资格;
  • 跌破 lowcycle_after_kill && wmark < WMARK_LOW 会升级为 PRESSURE_AFTER_KILL(可杀前台);
  • 跌破 min:所有"min_score_adj=201 保护可感知应用"的条件全部解锁。

Step 3 --- thrashing 基线base_file_lru = nr_inactive_file + nr_active_file = 438460 + 107254 = 545714 页 ≈ 2.08GB,同时 workingset_refault_file = 112228 也在同一文件里躺着------lmkd 的 thrashing = Δrefault × 100 / base_file_lru 分子分母都能对上号。

B.11 这台设备的画像(结合附录 A 的 meminfo)

  1. 离 high 水位仅 7MB:看似 MemAvailable 1.9GB 很健康,但在 lmkd 的水位标尺下其实已贴线------MemAvailable 里有大量"要回收才能拿到"的页,而水位只认"马上就能分配的页";
  2. CMA 256MB 全满nr_free_cma 0),全部压在 DMA32;
  3. zram 刚启用nr_zspages 111 ≈ 768kB swap 用量);
  4. working set 偏紧:restore 比例 55%,冷文件页 1.67GB 里已有 11 万次 refault------再涨就是 thrashing 判定的天下;
  5. per-CPU 页表占 228MB:正常体量。
相关推荐
古法安卓1 小时前
Android-PSI 详解:libpsi 源码解析——116 行代码架起 lmkd 与内核之间的桥
android·java·android studio
用户36533043470941 小时前
深入理解 Java Unsafe:从底层魔法到被淘汰的命运
java
宸津-代码粉碎机2 小时前
FastUtil+AI多Agent实战:Java AI项目性能终极加速方案
java·服务器·开发语言·python·安全·php
choumou_M2 小时前
SpringBoot_5:商品秒杀场景的并发实践(初步)
java·spring boot·后端
suaizai_2 小时前
AI Agent如何懂你:四层能力拆解
java·前端·人工智能
javachen__3 小时前
Docker一键清理 + 全局限制,告别日志报满
java·docker·容器
早安试言3 小时前
maven安装
java·maven
攻城有术3 小时前
专项攻克-SpringBoot启动后 Bean的实例数量问题
java·spring boot·后端
步行cgn4 小时前
PageHelper 的用法
java·后端