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, ¶m)) {
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;
}
几个值得写进博客的细节:
--reinit分支 :lmkd.rc 里有on property:lmkd.reinit=1之类的 trigger,重新执行/system/bin/lmkd --reinit时,它作为一个客户端 连接正在运行的 lmkd,发LMK_UPDATE_PROPS命令让后者热更新属性(issue_reinit(),lmkd.cpp:3703),自身随即退出。mlockall:把 lmkd 自己的内存锁住,防止它在执行查杀任务时页被回收/换出。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)通过 recvmsg 的 SCM_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, ¶ms);
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);
}
}
亮点:
- Tgid 校验:拒绝注册非线程组长,防止查杀线程导致整个进程组状态不一致;
- pidfd 在注册时就打开:避免将来杀进程时遇到 pid 复用杀错人;
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 关注 MemFree、SwapFree、SwapTotal 等(详见附录的 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/memory 的 full 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;
}
要点:
- 三重防误杀 :
valid标志、Tgid == pid(防 pid 复用)、RSS/Swap 可解析(排除僵尸); - kill 交给 reaper 线程池 异步执行,主线程立刻返回继续处理事件(reaper 负责发信号 +
waitid收割,避免僵尸进程占 pid); - 无论成败最后都
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_pages、nr_mapped、nr_file_pages、nr_dirty、nr_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_stack、nr_shadow_call_stack、nr_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_common 用 totalreserve_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:
- 一旦跌破
high:LOW_MEM_AND_THRASHING/LOW_MEM_AND_SWAP等条件开始具备资格; - 跌破
low:cycle_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)
- 离 high 水位仅 7MB:看似 MemAvailable 1.9GB 很健康,但在 lmkd 的水位标尺下其实已贴线------MemAvailable 里有大量"要回收才能拿到"的页,而水位只认"马上就能分配的页";
- CMA 256MB 全满 (
nr_free_cma 0),全部压在 DMA32; - zram 刚启用 (
nr_zspages 111≈ 768kB swap 用量); - working set 偏紧:restore 比例 55%,冷文件页 1.67GB 里已有 11 万次 refault------再涨就是 thrashing 判定的天下;
- per-CPU 页表占 228MB:正常体量。