一、内存危机:房间不够用了!
假设 Android 系统是一家大酒店,每个应用程序都是来入住的客人。酒店设计很贴心:客人 "退房"(退出应用)时不会真正离开房间,而是留在房间里以便随时回来,这样下次入住更快。
但问题来了:随着客人越来越多,酒店房间(内存)越来越紧张。这时候需要一个 "内存管家"LMK(LowMemoryKiller)来管理房间,在内存不足时让不重要的客人离开,这就是 LMK 的核心任务 ------ 在 OOM(Out Of Memory)危机前提前清理低优先级进程。
二、前台管家:给客人贴 "重要程度" 标签
2.1 贴标签的流程:从 AMS 到 ProcessList
酒店前台(AMS,ActivityManagerService)负责给每个客人(进程)评定重要程度,这个重要程度用oom_adj
表示,数值越大越不重要(越容易被赶走)。
java
scss
// ActivityManagerService.java中给进程贴标签
private boolean applyOomAdjLocked(ProcessRecord app) {
if (app.curAdj != app.setAdj) {
// 调用ProcessList的方法,给进程设置adj标签
ProcessList.setOomAdj(app.pid, app.info.uid, app.curAdj);
app.setAdj = app.curAdj;
}
}
2.2 ProcessList:标签快递员
前台管家叫来了快递员(ProcessList),让他把标签送给后台的协调者。快递员用特定格式打包标签:
java
scss
// ProcessList.java打包标签并发送
public static void setOomAdj(int pid, int uid, int amt) {
if (amt == UNKNOWN_ADJ) return;
// 打包命令:命令类型+pid+uid+adj值,共16字节
ByteBuffer buf = ByteBuffer.allocate(4 * 4);
buf.putInt(LMK_PROCPRIO); // 命令类型:设置进程adj
buf.putInt(pid); // 进程ID
buf.putInt(uid); // 用户ID
buf.putInt(amt); // adj值(重要程度)
writeLmkd(buf); // 通过socket发送给lmkd
}
2.3 快递方式:Socket 通信
快递员通过酒店内部电话系统(LocalSocket)联系后台协调者 lmkd,这个电话用的是 "顺序包裹" 模式(SOCK_SEQPACKET),保证标签不会送错:
java
csharp
private static void writeLmkd(ByteBuffer buf) {
for (int i = 0; i < 3; i++) {
if (sLmkdSocket == null) {
if (!openLmkdSocket()) { // 打开到lmkd的socket连接
try { Thread.sleep(1000); } catch (...) {}
continue;
}
}
try {
sLmkdOutputStream.write(buf.array(), 0, buf.position()); // 发送标签
return;
} catch (IOException ex) {
// 连接失败时重试
sLmkdSocket.close();
sLmkdSocket = null;
}
}
}
三、后台协调者 lmkd:管理标签和触发清理
3.1 lmkd 的初始化:准备接收标签
lmkd 就像酒店后台的调度员,启动时会创建一个 "收件箱"(socket)来接收前台送来的标签:
c
运行
scss
// lmkd.c的main函数
int main() {
mlockall(MCL_FUTURE); // 锁定内存
sched_setscheduler(0, SCHED_FIFO, ¶m); // 设置调度优先级
if (init()) { // 初始化
ALOGI("exiting");
return 0;
}
mainloop(); // 进入主循环等待事件
}
static int init() {
page_k = sysconf(_SC_PAGESIZE) / 1024; // 获取页面大小
epollfd = epoll_create(MAX_EPOLL_EVENTS); // 创建事件监听
ctrl_lfd = android_get_control_socket("lmkd"); // 获取lmkd socket
listen(ctrl_lfd, 1); // 监听连接
// 注册事件处理函数
epev.events = EPOLLIN;
epev.data.ptr = (void *)ctrl_connect_handler;
epoll_ctl(epollfd, EPOLL_CTL_ADD, ctrl_lfd, &epev);
return 0;
}
3.2 主循环:时刻监听前台消息
调度员坐在办公桌前(mainloop),时刻监听电话铃声(epoll_wait),一旦收到前台的标签就处理:
c
运行
scss
static void mainloop() {
while (1) {
int nevents = epoll_wait(epollfd, events, maxevents, -1); // 等待事件
if (nevents == -1) continue;
for (int i = 0; i < nevents; ++i) {
if (events[i].data.ptr)
(*(void (*)(uint32_t))events[i].data.ptr)(events[i].events);
}
}
}
3.3 处理标签:给内核传递命令
当收到前台的标签后,调度员 lmkd 会把标签转交给酒店保安(内核),并告诉保安 "重要程度" 的标准:
c
运行
scss
static void ctrl_command_handler() {
int cmd = ntohl(ibuf[0]);
switch(cmd) {
case LMK_PROCPRIO: // 设置进程adj
if (nargs == 3) {
int pid = ntohl(ibuf[1]);
int uid = ntohl(ibuf[2]);
int oomadj = ntohl(ibuf[3]);
cmd_procprio(pid, uid, oomadj); // 处理设置adj命令
}
break;
case LMK_TARGET: // 更新内存阈值和对应adj
cmd_target(targets, &ibuf[1]);
break;
}
}
static void cmd_procprio(int pid, int uid, int oomadj) {
char path[80], val[20];
// 向/proc/[pid]/oom_score_adj写入adj值,告诉内核这个进程的重要程度
snprintf(path, sizeof(path), "/proc/%d/oom_score_adj", pid);
snprintf(val, sizeof(val), "%d", oomadj);
writefilestring(path, val);
// 如果使用内核接口,直接返回,由内核处理后续
if (use_inkernel_interface) return;
// 否则在用户空间管理进程列表
}
四、酒店保安 kernel:按规则赶人
4.1 保安的巡逻机制:shrinker 注册
保安团队(内核)会定期巡逻(由 kswapd 线程触发),他们通过注册的 "巡逻员"(shrinker)来检查内存情况:
c
运行
csharp
// lowmemorykiller.c初始化
static struct shrinker lowmem_shrinker = {
.scan_objects = lowmem_scan, // 扫描要清理的进程
.count_objects = lowmem_count, // 计算当前内存使用
.seeks = DEFAULT_SEEKS * 16
};
static int __init lowmem_init(void) {
register_shrinker(&lowmem_shrinker); // 注册巡逻员
return 0;
}
4.2 内存检查:计算当前可用房间
巡逻员先看看酒店还有多少空房间(可用内存):
c
运行
scss
static unsigned long lowmem_count(struct shrinker *s, struct shrink_control *sc) {
// 计算所有活动和非活动的内存页面
return global_page_state(NR_ACTIVE_ANON) +
global_page_state(NR_ACTIVE_FILE) +
global_page_state(NR_INACTIVE_ANON) +
global_page_state(NR_INACTIVE_FILE);
}
4.3 按规则赶人:选最该离开的客人
当空房间少于预定阈值时,保安开始按规则赶人:优先赶 "重要程度" 低(oom_score_adj 大)且占用房间大(RSS 大)的客人:
c
运行
ini
static unsigned long lowmem_scan(struct shrinker *s, struct shrink_control *sc) {
struct task_struct *selected = NULL;
int minfree = 0;
short min_score_adj = OOM_SCORE_ADJ_MAX + 1;
// 获取当前可用内存
int other_free = global_page_state(NR_FREE_PAGES) - totalreserve_pages;
// 遍历阈值配置,找到当前需要清理的adj阈值
for (int i = 0; i < array_size; i++) {
if (other_free < lowmem_minfree[i] && other_file < lowmem_minfree[i]) {
min_score_adj = lowmem_adj[i];
break;
}
}
// 遍历所有进程,找到符合条件的目标进程
for_each_process(tsk) {
if (tsk->flags & PF_KTHREAD) continue; // 不赶工作人员(内核线程)
struct task_struct *p = find_lock_task_mm(tsk);
short oom_score_adj = p->signal->oom_score_adj;
// 只考虑重要程度低于阈值的进程
if (oom_score_adj < min_score_adj) { task_unlock(p); continue; }
int tasksize = get_mm_rss(p->mm); // 获取进程占用的内存大小
// 选择重要程度最低且占用内存最大的进程
if (selected) {
if (oom_score_adj < selected_oom_score_adj) continue;
if (oom_score_adj == selected_oom_score_adj && tasksize <= selected_tasksize) continue;
}
selected = p;
selected_tasksize = tasksize;
}
// 找到目标后,发送SIGKILL信号赶人
if (selected) {
set_tsk_thread_flag(selected, TIF_MEMDIE);
send_sig(SIGKILL, selected, 0); // 发送kill信号
rem += selected_tasksize;
}
return rem;
}
五、关键规则:oom_adj 与内存阈值的关系
5.1 客人的 "重要程度" 标签(oom_adj)
oom_adj
范围:-16 到 15,数值越大越不重要(越容易被赶)- 映射到
oom_score_adj
:比如 oom_adj=15 对应 oom_score_adj=1000,oom_adj=0 对应 0 - 查看方式:
cat /proc/[pid]/oom_adj
5.2 内存警报阈值(minfree)
酒店设置了不同的警报级别(保存在/sys/module/lowmemorykiller/parameters/minfree
),比如:
plaintext
arduino
// 假设minfree设置为"1024,8192"(单位:page)
// adj设置为"1,6"
当可用内存 < 8192 page时:赶oom_score_adj >=6的进程(不重要的客人)
当可用内存 < 1024 page时:赶oom_score_adj >=1的进程(更紧急,赶更多人)
六、LMK 的整体工作流程总结
-
前台贴标签 :AMS 根据进程状态计算
oom_adj
,通过 ProcessList 发送给 lmkd -
后台传命令 :lmkd 接收命令,更新进程的
oom_score_adj
并通知内核 -
内核赶人 :内核巡逻时发现内存不足,按
minfree
和adj
规则,杀最该杀的进程
整个过程就像酒店管家团队协作:前台评估客人重要性,后台协调传递信息,保安按规则执行清理,确保酒店(系统)不会因为客人太多(内存不足)而崩溃,同时尽量保留重要客人(高优先级进程)。