第1章:lmkd守护进程启动流程:init.rc配置与main函数解析

大家好,我是你们的Android内核讲师。今天我们来聊聊lmkd——这个在Android系统中默默守护内存的「隐形卫士」。说实话,我刚开始接触lmkd时也觉得它挺神秘的,直到有一次在项目中排查系统卡顿问题,才真正领教了它的重要性。

1.1 lmkd是什么?为什么需要它?

lmkd,全称Low Memory Killer Daemon,是Android系统专门用来处理低内存场景的守护进程。你想想看,手机内存就那么大,App们又都贪心,总想多占点资源。当内存快被吃光时,系统就会卡顿、应用闪退,甚至直接死机。

lmkd就是干这个的——它监控系统内存水位,当内存紧张时,按照优先级「请走」一些不重要的进程。说白了,就是系统最后的「清道夫」。

核心要点:lmkd不是简单的杀进程工具,它有一套完整的优先级策略,确保最不重要的进程先被清理。

1.2 init.rc中的lmkd配置

lmkd是在系统启动早期由init进程拉起来的。它的启动配置写在init.rc文件中。我习惯先看这个文件,因为从这里能看出lmkd的启动参数和依赖关系。

在AOSP源码中,你可以在system/core/lmkd/目录下找到相关文件。init.rc中关于lmkd的配置大致如下:

service lmkd /system/bin/lmkd
    class core
    user root
    group root
    critical
    onrestart restart zygote
    socket lmkd seqpacket 0660 root system
    writepid /dev/cpuset/system-background/tasks

我来逐行解释一下这些配置的含义:

配置项 含义 我的理解
service lmkd 定义名为lmkd的服务 这是init进程管理的服务名
/system/bin/lmkd 可执行文件路径 编译后生成的可执行文件
class core 属于core类服务 core类服务在系统启动早期就会启动
user root 以root用户运行 需要root权限才能杀其他进程
critical 标记为关键服务 如果挂了,系统会重启
onrestart restart zygote 重启时同时重启zygote lmkd挂了会影响zygote,所以一起重启
socket lmkd 创建名为lmkd的socket 用于与上层framework通信
writepid 将pid写入cgroup 控制进程的CPU亲和性

个人经验:我曾经在项目中遇到过一个问题——lmkd启动后socket创建失败,导致上层无法发送内存压力通知。排查了半天,最后发现是SELinux策略没放行。所以大家如果遇到lmkd相关的问题,别忘了检查SELinux。

1.3 main函数入口解析

好,配置看完了,我们来看看lmkd的main函数。源码在system/core/lmkd/lmkd.c中。main函数是整个守护进程的入口,它负责初始化各种资源,然后进入事件循环。

main函数的简化流程如下:

int main(int argc, char **argv) {
    // 1. 解析命令行参数
    // 2. 初始化日志系统
    // 3. 初始化epoll事件循环
    // 4. 创建并绑定socket
    // 5. 初始化内存压力监控
    // 6. 进入主循环
    for (;;) {
        // 等待事件发生
        // 处理socket请求
        // 检查内存状态
        // 执行杀进程操作
    }
}

嗯,这里要注意,lmkd的主循环是一个无限循环,它通过epoll来监听多个事件源。我刚开始看这段代码时,觉得它和普通的网络服务程序很像,但仔细看就会发现,它的事件处理逻辑非常特殊。

1.4 关键初始化步骤详解

我们挑几个关键的初始化步骤来深入看看:

1.4.1 创建socket

lmkd会创建一个socket,用于接收来自framework层的命令。这个socket的类型是SEQPACKET,它保证了消息的边界和顺序。

// 创建socket
ctrl_sock = android_get_control_socket("lmkd");
if (ctrl_sock < 0) {
    // 创建失败,直接退出
    return -1;
}
// 监听socket
if (listen(ctrl_sock, 1) < 0) {
    return -1;
}

避坑指南:我曾经遇到过一个问题——lmkd启动后socket一直处于CLOSE_WAIT状态。后来发现是因为framework层没有正确关闭socket连接。所以大家在调试lmkd时,可以用netstat -anp | grep lmkd来查看socket状态。

1.4.2 初始化内存压力监控

lmkd通过读取/proc/pressure/memory文件来获取内存压力信息。这个文件是Linux内核提供的PSI(Pressure Stall Information)接口。

// 打开PSI文件
psi_mem_fd = open("/proc/pressure/memory", O_RDONLY | O_CLOEXEC);
if (psi_mem_fd < 0) {
    // 如果PSI不可用,回退到旧方案
    use_psi = false;
}

你想想看,如果没有PSI接口,lmkd就只能通过轮询/proc/meminfo来估算内存压力,效率低不说,还不准确。PSI接口的出现,让lmkd能够实时感知内存压力变化。

1.4.3 注册epoll事件

lmkd使用epoll来管理所有的事件源。它会将socket和PSI文件描述符都注册到epoll中:

// 创建epoll实例
epoll_fd = epoll_create1(EPOLL_CLOEXEC);

// 注册socket事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = ctrl_sock;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, ctrl_sock, &ev);

// 注册PSI事件
if (use_psi) {
    ev.events = EPOLLPRI;
    ev.data.fd = psi_mem_fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, psi_mem_fd, &ev);
}

关键点:PSI事件使用的是EPOLLPRI标志,而不是普通的EPOLLIN。这是因为PSI文件通过SIGPRIO信号来通知事件,所以需要用EPOLLPRI来捕获。

1.5 主循环的工作机制

初始化完成后,lmkd就进入了主循环。这个循环的逻辑其实不复杂:

  1. 调用epoll_wait等待事件
  2. 如果是socket事件,处理来自framework的命令
  3. 如果是PSI事件,检查内存压力等级
  4. 如果内存压力超过阈值,执行杀进程操作

我记得有一次在调试时,发现lmkd的epoll_wait超时时间设置得很短,只有100毫秒。这意味着即使没有事件发生,lmkd也会每100毫秒醒来一次,检查内存状态。这种设计是为了保证lmkd能够及时响应内存变化。

1.6 本章小结

好了,我们来总结一下lmkd的启动流程:

  • init进程根据init.rc配置启动lmkd
  • lmkd的main函数完成socket、PSI、epoll等初始化
  • 进入主循环,等待并处理事件
  • 当内存压力超过阈值时,执行杀进程操作

下一章,我们会深入分析lmkd是如何判断内存压力的,以及它如何选择要杀掉的进程。说实话,这部分才是lmkd的核心算法,也是最有意思的地方。

学习建议:我建议大家在学习lmkd源码时,先不要急着看杀进程的逻辑,而是先把启动流程和事件处理框架搞清楚。就像盖房子一样,地基打好了,上层建筑才能稳固。

公众号:蓝海资料掘金营,微信deep3321