Android 系统启动流程 之 lmkd 进程

文中代码基于android13

【what】 lmkd进程创建

当cpu、io、内存资源出现紧张的时候,需要lmkd从所有进程中把一些不重要、不干活但又占据大量内存的 用户空间进程杀掉,以释放公共资源供大家使用。

/system/core/rootdir/init.rc 》》启动 start lmkd 》》执行 lmkd.rc 》》执行 lmkd可执行程序》调用lmkd.cpp#main()方法

//文件路径:/system/core/rootdir/init.rc,下面的内容是该文件的其中一部分

//on init:代表init触发器触发的时候会执行下面的各种命令
on init

    省略其他命令......

    # Start lmkd before any other services run so that it can register them
    write /proc/sys/vm/watermark_boost_factor 0
    chown root system /sys/module/lowmemorykiller/parameters/adj
    chmod 0664 /sys/module/lowmemorykiller/parameters/adj
    chown root system /sys/module/lowmemorykiller/parameters/minfree
    chmod 0664 /sys/module/lowmemorykiller/parameters/minfree
  
    //start lmkd:start命令会创建进程,lmkd与下面lmkd.rc中service后面的lmkd一致
    start lmkd
-----------------------------------------------------------------------------------
//上面lmkd库在 /system/memory/lmkd/Android.bp 引用lmkd.rc
cc_binary {
    name: "lmkd",
    ....
    init_rc: ["lmkd.rc"],
}
-----------------------------------------------------------------------------------
//lmkd.rc文件路径:/system/memory/lmkd/lmkd.rc

//lmkd是进程的名字,/system/bin/lmkd 代表当init进程fork lmkd成功后,需要执行的可执行文件
service lmkd /system/bin/lmkd
    class core
    user lmkd
    group lmkd system readproc
    capabilities DAC_OVERRIDE KILL IPC_LOCK SYS_NICE SYS_RESOURCE
    critical
    socket lmkd seqpacket+passcred 0660 system system
    task_profiles ServiceCapacityLow
  
    省略其他信息......

-----------------------------------------------------------------------------------
//文件路径:system/memory/lmkd/lmkd.cpp
int main(int argc, char **argv) {
  
    省略代码......
  
    //init方法成功返回0,从而进入下面的逻辑
    if (!init()) {
  
        省略代码......

        //初始化reaper主要用来杀进程
        if (init_reaper()) {
            ALOGI("Process reaper initialized with %d threads in the pool",
                reaper.thread_cnt());
        }

        //watchdog用来监听lmkd的主线程是否有耗时,有耗时的话,watchdog会来尝试杀进程
        if (!watchdog.init()) {
            ALOGE("Failed to initialize the watchdog");
        }

        mainloop();
    }

    android_log_destroy(&ctx);

    ALOGI("exiting");
    return 0;
}

【when】何时去杀进程–收到通知就去杀

why:lmkd无权直接知道何时杀进程

用户空间的进程是无权直接访问这些硬件资源(检测内存、io、cpu等硬件的使用情况)。

lmkd进程:一个用户空间的进程。

因此,lmkd无权直接知道何时杀进程。

how:使用其他可监听内核空间的工具,把需要杀进程时机的消息传递给lmkd。lmkd收到通知就去杀进程

PSIvmpressurelowmemorykiller可访问内核空间。

PSI在Android高版本上基本都采用了这种监听方式,而 vmpressure在Android高版本上已经不在使用,并且 lowmemorykiller的方式也已经不再采用了,因此来介绍下PSI。

PSI(Pressure Stall Information)是一个可以监控CPU、内存及IO性能异常的内核功能

文件路径:system/memory/lmkd/lmkd.cpp

//init方法在上面的main方法中会被调用
static int init(void) {
  
    省略代码......

    //在高版本use_inkernel_interface这值为false,也就是不会使用lowmemorykiller这个功能
    if (use_inkernel_interface) {
        省略代码......
    } else {
        //初始化监听器,查看 [1.2]
        if (!init_monitors()) {
            return -1;
        }
        /* let the others know it does support reporting kills */
        property_set("sys.lmk.reportkills", "1");
    }

    省略代码......

    return 0;
}

//初始化监听器
[1.2]
static bool init_monitors() {
    /* Try to use psi monitor first if kernel has it */
    //use_psi_monitors为true代表使用PSI来实现资源紧张监控,在高版本为true, 如果 use_psi为true,代表使用PSI则使用init_psi_monitors()来初始化PSI  查看[1.3]
    use_psi_monitors = GET_LMK_PROPERTY(bool, "use_psi", true) &&
        init_psi_monitors();

    //use_psi_monitors不可用的时候,使用vmpressure实现监控
    /* 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监听器
[1.3]
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 since
     * the old strategy relies on memcg attributes that are available only 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;
    }

    //初始化低级别,查看 [1.4]
    if (!init_mp_psi(VMPRESS_LEVEL_LOW, use_new_strategy)) {
        return false;
    }

    //初始化中级别,查看 [1.4]
    if (!init_mp_psi(VMPRESS_LEVEL_MEDIUM, use_new_strategy)) {
        destroy_mp_psi(VMPRESS_LEVEL_LOW);
        return false;
    }

    //初始化高级别,查看 [1.4]
    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;
}

[1.4]
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,
        PSI_WINDOW_SIZE_MS * US_PER_MS);

    if (fd < 0) {
        return false;
    }

    //当有资源紧张的通知时,会调用mp_event_psi或者mp_event_common方法
    vmpressure_hinfo[level].handler = use_new_strategy ? mp_event_psi : mp_event_common;
    vmpressure_hinfo[level].data = level;

    //使用epoll机制来监听fd上的通知,有资源紧张的通知会在fd上有数据写入
    if (register_psi_monitor(epollfd, fd, &vmpressure_hinfo[level]) < 0) {
        destroy_psi_monitor(fd);
        return false;
    }
    maxevents++;
    mpevfd[level] = fd;

    return true;
}

lmkd使用epoll机制来监听fd上的通知,有资源紧张的通知会在fd上有数据写入,

mp_event_psi或者 mp_event_common方法就会被调用,开始杀进程

// /system/memory/lmkd/lmkd.cpp
mp_event_psi->find_and_kill_process->kill_one_process //具体杀进程的方法

当用户空间的进程使用 epoll_ctl 函数注册 proc/pressure/memory 文件描述符时,会指定需要监听的事件类型,包括 EPOLLIN 和EPOLLPR1。当内核检测到 proc/pressure/memory 文件描述符上发生了 EPOLLPRI 事件时,会通过 epoll_wait 函数将该事件通知给用户空间的进程。
在处理 EPOLLPRI 事件时,用户空间的进程会读取 proc/pressure/memory 文件的内容并进行相应的处理,如判断内存压力等级是否超过一定值,进行相应的内存管理操作等。对于 proc/pressure/memory 文件的内容的解析,则是通过除法来实现的。具体来说,内核会计算当前内存使用情况和最大限制之间的比率,然后将其乘以 1000 后向下取整,得到一个整数,即为内存压力指数。

【who】 哪些进程需要杀–收集进程

收集进程

有可能被杀的进程收集起来(其实真正代码是 procadjslot_list这个数组),进而为杀进程提供目标。

Q1:为什么 有可能被杀进程而不是一定被杀进程呢?

就像人类监狱关的不一定都是要执行死刑的犯罪分子是一回事。

Q2:用什么数据结构来存储收集到的进程?

背景:每一个进程都被设置了一个对应的oom_adj_score,用来区分进程被杀的优先级。oom_adj_score的取值范围是[-1000,1000]只能是整数,-1000的分数代表该进程绝对绝对不会并且不能被杀,而1000则相反代表只要杀进程肯定最先把它杀掉。

要求:查找效率最高:数组,数组的每个元素存储的是进程。但是怎样来确定进程应该存储在哪个位置呢?这个容易,每个进程都对应一个分数,分数刚好都是整数类型,并且存在负数,那就通过算法把分数转换为数组索引,进而把进程存储到数组的对应索引处即可,转换算法为oom_adj_score + 1000,比如最小分数-1000经过转换后是不是就对应到了数组的0索引,分数0是不是就对应数组的1000索引。

Q3:如果多个进程出现了同样的分数,数组该怎么解决呢?

可以参照HashMap的做法,存储进程的数组每一个元素就不只存一个进程了,而是一个进程链表,为了在进程链表上更快、更方便的去查找进程,可以使用双向循环链表。

总结:存储进程的数据结构就是一个数组,数组的索引是从[0,2000],数组的每个元素是一个双向循环链表,链表的每一个节点指向一个进程

具体的数据结构如下,有兴趣可以看下:

文件路径:system/memory/lmkd/lmkd.cpp

//ADJTOSLOT(adj)定义了 分数转换为索引的算法
#define ADJTOSLOT(adj) ((adj) + -OOM_SCORE_ADJ_MIN)
//数组长度
#define ADJTOSLOT_COUNT (ADJTOSLOT(OOM_SCORE_ADJ_MAX) + 1)

//链表节点,定义了next和prev
struct adjslot_list {
    struct adjslot_list *next;
    struct adjslot_list *prev;
};


//进程信息,
struct proc {
    struct adjslot_list asl; //指向上面的数据结构
    int pid; //进程id
    int pidfd; //进程id对应的fd
    uid_t uid; //uid,在app安装后就会分配一个唯一的id值,与身份证类似
    int oomadj; //分数
    pid_t reg_pid; /* PID of the process that registered this record */
    bool valid;
    struct proc *pidhash_next;  //指向的下个进程
};


//adjslot_list数组,索引从0到2000,进程的分数会通过 ADJTOSLOT(adj) 算法转换为数组的索引
static struct adjslot_list procadjslot_list[ADJTOSLOT_COUNT];

在这里插入图片描述

谁来押送进程信息到lmkd

Q4:存储进程的数据结构设计好了,谁来把进程放入这个数据结构呢?

进程自己?这肯定是不行的,如若允许这样做了那开发者都希望自己的进程能存活的时间更长,把进程的分数设置成最低,那到最后是谁也杀不掉。

因此从用户空间进程的分类来入手。整个用户空间的进程主要分为两类:系统native进程和运行java代码的进程。

系统native进程主要是由init进程创建的,比如logd、lmkd进程,这些进程的特性是系统级别的、万一有一个死掉系统就出现问题,并且它们是不能运行java代码的,因此这些进程基本上是不会被杀掉的。

运行java代码的进程它们是由zygote进程创建的,比如system_server、launcher进程,一个app进程一般就是一个应用(system_server进程除外),这些进程的重要程度是有所区别的,比如zygote、system_server进程是最重要的也是最不能被杀掉的,像一些退到后台的进程就变的没那么重要了,那它们是可以被杀掉的。

在这里插入图片描述

还存在一类进程:由运行java代码的进程创建的native进程(通过fork/clone创建的进程,在android低版本的时候用来做保活),这类进程可以不关心,因为在android高版本的时候杀掉它们的父进程的时候也会把它们杀掉。

因此依据上面进程的分类,"押送"进程的任务完全可以交给它们的各自的大管家:init进程和system_server进程(主要有运行在其中的AMS服务来处理)。它们是非常清楚哪些进程可以被杀掉,哪些完全不能杀的,进而给不同的进程打不通的分数,进而"押送"到上面设计好的数据结构中即可。

押送过程中的进程通信渠道

Q5:既然init进程和system_server进程会"押送"进程信息传递给lmkd进程,那如何解决它们之间通信问题呢或者说它们之间的“通信渠道”如何设计呢?

进程之间的通信方式有socket、binder、signal、共享内存等。该选用哪种通信方式呢?结合场景选择通信方式。

lmkd进程既可以与init进程通信也可以与system_server进程通信,这完全就是c/s模式,lmkd是server端,init进程和system_server进程是client端;其次在通信渠道上传输的数据都是非常简单的(主要有进程id、uid等);再其次传输的数据频率并不是很频繁,对于传输速度也没有非常高的要求;最后对于传输的数据是排队式的传输的,传递完一个再去传递下一个。

因此基于上面所描述的使用场景,socket通信方式是非常适合的,因为传输的数据简单虽然在传输过程中有两次复制,但数据简单都可以忽略不计,其次socket就是一种c/s的模式,传递的数据时候也是排队式的。有兴趣的同学可以看下对应的代码,代码如下:

文件路径:system/memory/lmkd/lmkd.cpp
//在main方法会调用该方法
static int init(void) {
    省略代码......

    //通过lmkd获得socket对应的fd
    ctrl_sock.sock = android_get_control_socket("lmkd");
    if (ctrl_sock.sock < 0) {
        ALOGE("get lmkd control socket failed");
        return -1;
    }

    省略代码......

    return 0;
}

押送的数据协议

通信渠道搭建好了,那来确定下渠道上传递的数据的协议吧,数据协议是非常的简单的,格式如下:

//cmd是对应命令值,后面的xxx是该命令对应的参数,每种命令的参数是不一样的
cmd xxx xxx xxx

//下面是一些例子
//LMK_PROCPRIO代表注册一个进程,并设置它的oom_adj_score
LMK_PROCPRIO pid uid oom_adj_score
//刚好与上面相反,注销一个进程,参数pid
LMK_PROCREMOVE pid

//代表client订阅一些事件,evt_type是事件类型
LMK_SUBSCRIBE evt_type

LMK_PROCPRIO是"押送"进程到lmkd进程的协议,它的参数分别是pid(进程id)、uid(app安装时候分配的唯一id值)、oom_adj_score(分数);

LMK_PROCREMOVE代表从lmkd进程中把对应进程信息移除的协议,它的参数就只有一个pid。

进程打分的设定规则

cat /proc/[pid]/oom_score_adj

lmkd规定了"押送"到进程监狱的进程需要携带一个分数,这分数用来确定进程被杀的优先级,同时也规定了分数的一个取值范围,但是对于进程应该打多少分这个事情我是完全不关心的,也就没有这个能力。我把打分的这个权利全权放开给了init和system_server,因此让它俩来介绍下打分这个重要的环节吧。

system_server打分

AMS(ActivityManagerService服务的简称为AMS)是常驻于system_server进程的一个服务,主要是负责四大组件的所有相关事情,负责给用户空间所有运行java代码的进程打分

在这里插入图片描述

不论是系统app进程还是普通app进程按前后台可以分为前台进程、后台失去焦点进程、后台进程。

依据上面进程的分类,AMS也定义了一套依据进程分类和进程状态的打分规则,如下图

在这里插入图片描述

system_server进程的分数是-900(代表它是不会被杀的),

系统app进程的分数是-800(也是不会被杀的),

前台app进程的分数是0(基本上不会被杀掉),

home进程(桌面进程)退到后台后的分数是600(它的被杀优先级要低于别的退到后台的进程),

被缓存的app进程(即后台进程,越是处于缓存队列后面的进程被杀的优先级更高)它的最小分数是900最大分数是999(因此在杀进程时候它们是最先被杀的)

若发现分数有变动,则会通过通信渠道把进程和它的分数“押送”到进程监狱。进程对应的分数(oom_adj_score)并不是一成不变的,会随着进程的状态发生变化而变化,比如前台进程的app,用户按了home键回到桌面,则这个进程会变为后台进程它的分数会变为PERCEPTIBLE_APP_ADJ(值为700),而桌面进程的分数变为FOREGROUND_APP_ADJ(值为0)

通信渠道和传输的代码如下,有兴趣可以看下

//与lmkd建立socket链接代码
//文件路径:frameworks/base/services/core/java/com/android/server/am/LmkdConnection.java
private LocalSocket openSocket() {
    final LocalSocket socket;
    try {
        socket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
        //"lmkd"代表server端
        socket.connect(
            new LocalSocketAddress("lmkd",
                    LocalSocketAddress.Namespace.RESERVED));
    } catch (IOException ex) {
        Slog.e(TAG, "Connection failed: " + ex.toString());
        return null;
    }
    return socket;
}

//传输数据到lmkd
//文件路径:frameworks/base/services/core/java/com/android/server/am/ProcessList.java
private static boolean writeLmkd(ByteBuffer buf, ByteBuffer repl) {
    //如若链接没有创建,则创建
    if (!sLmkdConnection.isConnected()) {
        // try to connect immediately and then keep retrying
        sKillHandler.sendMessage(
                sKillHandler.obtainMessage(KillHandler.LMKD_RECONNECT_MSG));
        // wait for connection retrying 3 times (up to 3 seconds)
        if (!sLmkdConnection.waitForConnection(3 * LMKD_RECONNECT_DELAY_MS)) {
            return false;
        }
    }
    //把二进制数据发送给lmkd server端
    return sLmkdConnection.exchange(buf, repl);
}

init打分

init管理的进程都是系统native进程它们都非常重要,所有它们默认的分数就是-1000,-1000代表着它们是非常不会被杀掉的(除非自己死掉)。我把打分的权限放开给每个进程,因为我充分的相信它们,它们可以自己设置分数,而不像AMS是完全不敢把这权限放开给每个进程的。

在我的子进程创建的时候,如若发现它的分数(oom_adj_score)不为-1000的时候,我才会把这个进程和它的分数通过socket通信“押送”到lmkd进程

【how】 怎么去杀进程,杀哪个进程

何时杀 步骤中PSI只通知lmkd应该杀进程了,至于应该杀哪个进程,PSI是完全不关心的。如果杀的进程还是没有解决内存紧缺的问题,PSI还会继续通知lmkd继续杀进程,直到达到内存要求为止。

该杀哪个进程的核心逻辑是非常简单的,因为进程监狱中使用数组来存放收集到的进程,它的索引值越大代表进程分数越高,比如索引2000处对应进程的分数是1000、索引0处对应进程的分数是-1000。因此杀谁的逻辑就非常清晰了,首先从最大分数1000开始从数组的的最末尾位置开始找进程,如果找到了杀掉这个进程;否则从分数999开始从数组的次末尾位置找进程,找到则杀掉进程;否则继续重复上面的逻辑直到找到了要杀的进程为止。整个循环肯定不能一直循环下去(因为一些关键进程是不能杀的比如system_server进程),分数直到min_score_adj后就结束。对应代码如下,有兴趣可以看下:

文件路径:system/memory/lmkd/lmkd.cpp

//查找需要杀掉的进程, min_score_adj代表查找到最小分数截止
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 choose_heaviest_task = kill_heaviest_task;

    //OOM_SCORE_ADJ_MAX:代表最大分数它的值是1000,从最大分数开始查找
    for (i = OOM_SCORE_ADJ_MAX; i >= min_score_adj; i--) {
        struct proc *procp;

        //choose_heaviest_task代表是否杀掉任务繁重的进程,PERCEPTIBLE_APP_ADJ的值是200
        //下面逻辑代表:choose_heaviest_task不为true并且分数小于200的时候,需要杀任务繁重的进程了
        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) {
            //如果是杀繁重任务的进程,则调用proc_get_heaviest(i)方法找到繁重任务进程;否则调用proc_adj_tail(i)去查找
            //分数i对应的索引处的循环双向链表的尾节点的进程
            procp = choose_heaviest_task ?
                proc_get_heaviest(i) : proc_adj_tail(i);

            if (!procp)
                break;

            //调用kill_one_process方法开始杀进程,killed_size代表释放的空间
            killed_size = kill_one_process(procp, min_score_adj, ki, mi, wi, tm, pd);

            //若释放的空间大于0,则跳出循环
            if (killed_size >= 0) {
                break;
            }
        }

        //若释放空间大于0,则跳出查杀进程循环逻辑
        if (killed_size) {
            break;
        }
    }

    //返回释放空间大小
    return killed_size;
}

那min_score_adj的具体值是多少呢,依据PSI(PSI上面提到,会在需要杀进程的时候发出通知给lmkd)和watchdog(watchdog是会监听lmkd的主线程是否出现耗时,耗时的话watchdog就会去杀进程)会分别定义不同的值。非常明确的一点是min_score是有最小值的,最小值是0,也就是所有分数为负值的进程肯定是不会被lmkd杀掉的,在系统资源极度极度紧张的情况下,分数大于等于0的进程都会被杀掉(前台进程、后台无焦点进程、后台进程)

PSI取值逻辑

min_score_adj在PSI条件下,一般情况下它的值是201(PREVIOUS_APP_ADJ + 1),内存极度极度紧张的情况下它的值为0,具体代码位于system/memory/lmkd/lmkd.cpp的mp_event_psi方法

watchdog取值逻辑

min_score_adj在watchdog条件下,它的值为0,具体代码位于system/memory/lmkd/lmkd.cpp的watchdog_callback方法。

好了,以上就是该杀谁的内容,其实杀的进程都是app进程,后台app进程是先被杀掉的,像system_server进程、系统persistent进程它们都是负值是不会被杀掉的,甚至系统native进程它们的分数基本都是-1000,因此也是不会被杀掉的。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/niurenwo/article/details/134701621

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值