Android帝国之进程杀手–lmkd

移动安全 11个月前 admin
13 0 0

本文概要

这是Android系统启动的第三篇文章,本文以自述的方式来讲解lmkd进程,通过本文您将了解到lmkd进程在安卓系统中存在的意义,以及它是如何杀进程的。(文中的代码是基于android13)

我是谁

init:”大家好,还记得我吗?我是你们的老朋友init进程,直接叫我init吧,今天我把我的第二个孩子lmkd进程介绍给大家认识,我第一个孩子是logd进程,那就让lmkd进程来介绍下自己吧。”

lmkd:”大家好,我是lmkd进程,大家可以叫我lmkd,’lmkd’这几个字母代表啥意思呢?应该大部分人都不清楚,lmkd是’low momery killer demon’的首字母缩写,翻译成中文就是’低内存杀手守护’进程。”

各进程纷纷惊叫到“啊,它是一个杀手,咱们Android大家庭中怎么能存在杀手呢?这不是制造不稳定性因素吗?”

lmkd急忙说到:“大家不要慌,听我解释,我是个杀手不假,可我不是你们想象的那种杀人不眨眼的杀人狂魔,我可是一个合法的、守法的好杀手,并且我是为了大家的利益最大化而存在的。”

“合法、守法,还好杀手,还为我们利益考虑,那我们就听听你是如何狡辩的。”

lmkd整理了下思路不急不慢的说:“我们都生存在Android大家庭里,咱们共用cpu、io、内存等资源,但是这些资源都是有限的,当大家庭里面的进程越来越多的时候,每个进程能分到的资源就越来越少了,那进而整个Android系统会运行越来越慢,整个系统都慢了,那咱们每个进程肯定也就慢了。于是乎针对这种情况,我就出现了,当cpu、io、内存资源出现紧张的时候,我会从所有进程中把一些不重要、不干活但又占据大量内存的用户空间进程杀掉,以释放公共资源供大家使用。”

“这些用户空间进程我杀掉它们不冤吧,它们白白占用大量资源,但却产出很少、甚至没有任何用处。好了我的观点已经表达清楚了,到底给大家带来了利弊,我想大家心理都有数了。”

各进程对lmkd所做的事情都纷纷表达了认可。

好了,我来总结下我自己吧,我的名字是lmkd翻译为中文是低内存杀手守护进程,在Android系统出现系统资源紧张的情况下,就该我就出手了,杀掉那些不重要的进程以释放掉系统资源。既然介绍了我是谁,那该介绍下我的出生了。

我的出生

我出生在一个”单亲家庭”,我只有父亲没有母亲,我的父亲是init进程,我的父亲是一个“不称职的父亲“,为啥这样说呢,它对于我何时创建、创建后叫啥名字等这些信息,它统统不知道,你们说它负责吗,我只需要把这些信息用init脚本语言配置好后交给它即可,剩下的事就全权交给它了,脚本语言配置的信息如下:

//文件路径:/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/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

我的出生是不是非常的简单啊,这其实是脚本语言和init进程的功劳,可以点击 我是init进程 这篇文章来了解init进程的知识、以及如何通过脚本语言来创建子进程

当init把我创建成功后,会执行下面的方法,执行完下面的方法后,我才真正“长大”,才真正有了杀进程的能力。

文件路径: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;
}

杀进程这件事情

我想问大家个问题:“如果让大家来做杀进程这件事情,应该需要考虑哪些关键点呢?“

“没人回答是吧,那我就来说下我的想法。”

杀进程这件事情我觉得需要考虑三个关键点:何时杀意思是什么时候开始杀进程,杀进程是需要一个通知的,不可能无脑的去杀;收集进程把有可能需要杀掉的进程收集起来,这样为杀进程提供目标对象;该杀谁收到杀进程通知后,从收集的进程中选择目标进而杀之。那就从这三个关键点来介绍杀进程这件事情。

何时杀

何时杀进程呢?大家肯定想到的是当内存紧张的时候杀呗,这不用疑惑。确实是这样的,但是对于我lmkd进程来说,我不知道什么时候内存紧张了,我没有这个能力,别以为我不想有这个能力,主要是因为我是一个用户空间的进程,如果想做到就需要检测内存、io、cpu等硬件的使用情况,而用户空间的进程是无权直接访问这些硬件资源的。但是内核空间有大佬可以解决这个问题,那就是PSIvmpressurelowmemorykiller

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

PSI(Pressure Stall Information)是一个可以监控CPU、内存及IO性能异常的内核功能。关于它的介绍可不是三言两语就能讲清楚的(点击PSI进行了解)。知道了它的作用后就结合代码来看下是如何实现监听功能的

文件路径: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;
}

基于以上的代码,当出现资源紧张的时候,mp_event_psi或者mp_event_common方法是会被调用的,被调用的时候也就是开始杀进程的时机了。

收集进程

“lmkd,为啥要收集进程呢?”有个进程疑惑的问道。

lmkd:“这位进程朋友问的好,那我就给大家讲讲”

上面谈到的何时杀只是会通知我应该啥时候开始杀进程,但是具体应该杀哪个进程,它是没有告知我的,也就是说何时杀只告诉我lmkd你应该杀进程了,至于你应该杀哪个进程,PSI是完全不关心的。如果杀的进程还是没有解决内存紧缺的问题,那还会继续通知你lmkd继续杀进程,直到达到内存要求为止。是不是特别类似于一个舍得放权的领导,他会把做的事情通知下属,具体下属应该怎么做他不关心但是一定要做好。

俗话说巧妇难为无米之炊,我这对于系统都起了哪些进程以及哪些进程需要被杀都一无所知,那我还杀进程,杀个鬼吧。因此我lmkd就想到了一个办法就是把有可能被杀的进程收集起来,进而为杀进程提供目标。

我把存放有可能被杀进程的地方叫做进程监狱,这样叫主要是能帮助大家能更好的理解(其实真正代码是procadjslot_list这个数组)。你们人类的监狱的主要作用是为了关闭罪犯,当罪犯犯法严重到需要执行死刑的时候,就需要从监狱里面把对应罪犯找到进而执行死刑。而进程监狱里面存放的是有可能被杀进程,为啥是有可能被杀进程呢而不是一定被杀进程,就像你们人类监狱关的不一定都是要执行死刑的犯罪分子是一回事。在进程监狱存放的进程是有可能被被杀掉的。当需要杀进程的时候会从进程监狱把目标进程找出来进而杀之。

进程监狱

你们人类的监狱是有布局或区域划分的,哪些区域或布局用来关押不同的罪犯。同样进程监狱也是有自己的布局的,这里的布局指用什么数据结构来存储收集到的进程。

首先先来介绍一个概念oom_adj_score,每个被”押送”到进程监狱的进程都有一个对应的属性oom_adj_score,看了这个属性是不是不清楚它的意思,其实只需要关注score(分数)即可,前面的oom_adj都是分数的定语。通俗点讲就是每个被”押送”到进程监狱的进程都有一个对应的分数。

oom_adj_score的作用是啥呢?假设没有它的话,虽然我收集了很多的进程,但是在杀进程的时候我只能随机挑选目标进程去杀了,因为我收集的进程没有一个标志或者属性来区分它们之间的优先级。oom_adj_score的作用就是规定了进程的优先级,这个分数可不像你们人类考试的分数,考试分数是越高越好是吧,而oom_adj_score这个分数是越高就代表优先被杀。oom_adj_score的取值范围是[-1000,1000]只能是整数,-1000的分数代表该进程绝对绝对不会并且不能被杀,而1000则相反代表只要杀进程肯定最先把它杀掉。

既然每个进程在进入监狱的时候都会携带一个对应的分数,那就来想想应该使用什么数据结构来存储收集到的进程?

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

“我真是个天才”lmkd被自己的想法给深深的折服了。

“如果多个进程出现了同样的分数,该怎么解决呢?”突然一个进程说道。

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

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

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

文件路径: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];

用一张图来总结下

Android帝国之进程杀手--lmkd
procc_adj

谁来”押送”进程

进程监狱的布局已经设计完成了,那谁来把有可能被杀的进程“押送”到监狱呢?

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

其实办法也特别简单,可以从用户空间进程的分类来入手,整个用户空间的进程主要分为两类:系统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进程。它们是非常清楚哪些进程可以被杀掉,哪些完全不能杀的,进而给不同的进程打不通的分数,进而”押送”即可。

通信渠道

既然init进程和system_server进程会”押送”进程到“进程监狱”,而它们三个是分别属于不同进程的,那如何解决它们之间通信问题呢或者说它们之间的“通信渠道”如何设计呢?

进程之间的通信方式有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_PROCPRIOLMK_PROCREMOVE是和”押送”进程到进程监狱有关的两个命令,LMK_PROCPRIO是”押送”进程到进程监狱的协议,它的参数分别是pid(进程id)、uid(app安装时候分配的唯一id值)、oom_adj_score(分数);LMK_PROCREMOVE代表从进程监狱把对应进程移除的协议,它的参数就只有一个pid。

打分

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

system_server打分

我是AMS(ActivityManagerService服务的简称为AMS)是常驻于system_server进程的一个服务,主要是负责四大组件的所有相关事情,在后面会有我的专场,就不在赘述了,我是负责给用户空间所有运行java代码的进程打分,那就把打分的过程分享给大家。

作为打分的依据,需要先对运行java代码的进程进行分类,按是否是app进程可以分为非app进程app进程

非app进程它没有包含四大组件,用户完全感觉不到它的存在,但是它确实默默地在后台运行,system_server进程就是其中之一。

app进程指包含了四大组件中任一一个的进程,比如微信、设置等,而app进程还可以分为系统app进程普通app进程,系统app进程如systemui进程等,系统app进程需要在AndroidManifest清单文件中配置android:persistent="true"并且还需要安装在system区域,在系统启动的时候会启动系统app进程,当系统app进程死掉的时候,我AMS会负责重新启动它们;普通app进程app比如:微信、抖音等。

不论是系统app进程还是普通app进程按前后台可以分为前台进程后台失去焦点进程后台进程前台进程指app有一个Activity处于或即将处于resume状态,或者包含一个前台Service、或者正在处理一个广播;后台失去焦点进程是app有一个Activity处于pause状态(即可以看到界面但是不可以与用户交互);后台进程app的所有Activity都处于stop状态(即完全不可见).因此三种状态的进程重要程度是前台进程 > 后台失去焦点进程 > 后台进程,也就是如果打分的话前台进程的分数最低,剩下的分数越来越高。

依据上面进程的分类,我AMS也定义了一套依据进程分类和进程状态的打分规则,如下图Android帝国之进程杀手--lmkd

system_server进程的分数是-900(代表它是不会被杀的),系统app进程的分数是-800(也是不会被杀的),前台app进程的分数是0(基本上不会被杀掉),home进程(桌面进程)退到后台后的分数是600(它的被杀优先级要低于别的退到后台的进程),被缓存的app进程(即后台进程,越是处于缓存队列后面的进程被杀的优先级更高)它的最小分数是900最大分数是999(因此在杀进程时候它们是最先被杀的)

依据上面的这套打分规则,我AMS会把进程对应的分数算出来,如若发现分数有变动,则会通过通信渠道把进程和它的分数“押送”到进程监狱。进程对应的分数(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进程看了AMS的打分后说道:“AMS老兄,你的这个打分过程实在是太复杂了吧,相对你的打分环节我的就简单的不得了啊!”

AMS:“init老兄,有些情况你不了解,首先我管理的进程肯定要比你多的多,并且我面对的进程状态要比你复杂的多,当app进程进入后台后,我会缓存它们,当用户再次进入对应app后就可以立马把它上次的状态展示给用户,给用户带来一个非常好的用户体验,但是缓存也不能缓存过多否则影响系统的性能,并且缓存的进程有很多会在后台继续做一些耗电耗内存的工作,那针对这种不守规矩的进程那我会通过onTrimMemory(int)方法向它们多次发出警告,如若不听则会非常干脆的杀掉它们。”

AMS向init投来羡慕的眼光,继续接着说:“而你init老兄就轻松多了,你管理的都是系统native进程,而这些进程首先都非常重要所以基本不会被杀的,并且也没有那么多状态变化;其次它们都非常的守规矩。而我管理的普通app进程那就不这样了,开发者水平参差不齐,有的app进入后台还想着偷摸的多干点事情甚至在前台的时候对内存管理的不好会导致内存占用过多,像这种情况越来越多的话就导致系统性能不好,你说针对这些情况都是我的责任,我必须处理它们。”

init:“老兄我体会到你的不易了,那我就来介绍下我的打分吧”

打分这个事情对我来说非常简单,我定的规则也是非常简单的,因为我管理的进程都是系统native进程它们都非常重要,所有它们默认的分数就是-1000,-1000代表着它们是非常不会被杀掉的(除非自己死掉)。我把打分的权限放开给每个进程,因为我充分的相信它们,它们可以自己设置分数,而不像AMS是完全不敢把这权限放开给每个进程的,首先它对这些进程不信任,其次如若放开每个进程都设置自己的分数最低以防止被杀,那最后就无进程可杀最终导致系统死翘翘。

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

该杀谁

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,因此也是不会被杀掉的。

总结

以上就是关于我的介绍,我来总结下吧,我是lmkd进程翻译为中文是低内存杀手守护进程,我的主要使命是:在系统资源处于紧张的时候开始杀掉不重要、基本不工作的用户空间进程,以释放系统资源。

那我是如何知道什么时候该杀进程的呢?我是通过使用PSI(Pressure Stall Information)的机制来监听系统资源是否处于紧张,若如出现紧张则mp_event_psi或者mp_event_common方法会收到相应的通知。

因为我既没有init进程那样创建系统native进程的能力,也没有zygote进程那样创建运行java代码进程的能力,因此我是无法知道用户空间都创建了哪些进程,不知道都有哪些进程那如何杀进程呢?因此我想到了收集进程,收集的每个进程都会对应一个分数(oom_adj_score),这分数越高则代表该进程优先被杀,分数的取值范围是[-1000,1000]。收集并不是主动收集,而是由init进程和AMS(ActivityManagerService)把各自管理的进程通过socket“押送”到进程监狱

在收到杀进程的通知后,我会从进程监狱中,从最高分数(1000)开始去查找目标进程,如若找到则杀之;否则继续从999开始查找,不断循环此步骤,直到找到目标进程杀掉它为止。


原文始发于微信公众号(牛晓伟):Android帝国之进程杀手–lmkd

版权声明:admin 发表于 2023年11月27日 上午8:59。
转载请注明:Android帝国之进程杀手–lmkd | CTF导航

相关文章