Android启动系列之进程杀手--lmkd
本文概要
这是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/lmkdclass coreuser lmkdgroup lmkd system readproccapabilities DAC_OVERRIDE KILL IPC_LOCK SYS_NICE SYS_RESOURCEcriticalsocket lmkd seqpacket+passcred 0660 system systemtask_profiles ServiceCapacityLow省略其他信息......//文件路径:/system/core/rootdir/init.rc,下面的内容是该文件的其中一部分//on init:代表init触发器触发的时候会执行下面的各种命令
on init省略其他命令......# Start lmkd before any other services run so that it can register themwrite /proc/sys/vm/watermark_boost_factor 0chown root system /sys/module/lowmemorykiller/parameters/adjchmod 0664 /sys/module/lowmemorykiller/parameters/adjchown root system /sys/module/lowmemorykiller/parameters/minfreechmod 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等硬件的使用情况,而用户空间的进程是无权直接访问这些硬件资源的。但是内核空间有大佬可以解决这个问题,那就是PSI
、vmpressure
、lowmemorykiller
。
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; //进程idint pidfd; //进程id对应的fduid_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];
用一张图来总结下
谁来"押送"进程
进程监狱
的布局已经设计完成了,那谁来把有可能被杀的进程
"押送"到监狱呢?
进程自己?这肯定是不行的,如若允许这样做了那开发者都希望自己的进程能存活的时间更长,都会在"押送"进程的时候把进程的分数设置成最低,那到最后是谁也杀不掉,因为分数都是最低的。
其实办法也特别简单,可以从用户空间进程
的分类来入手,整个用户空间的进程
主要分为两类:系统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对应的fdctrl_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
和LMK_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也定义了一套依据进程分类和进程状态的打分规则,如下图
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 retryingsKillHandler.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
本文概要 这是Android系统启动的第三篇文章,本文以自述的方式来讲解lmkd进程,通过本文您将了解到lmkd进程在安卓系统中存在的意义,以及它是如何杀进程的。(文中的代码是基于android13) 我是谁 init:“大…...
tex中的边框
文章目录 利用tcolorbox宏包给公式加框 利用tcolorbox宏包 tcolorbox可以创建一个盒子的环境,例如: \documentclass{article} \usepackage{tcolorbox} \begin{document}\begin{tcolorbox}[left1cm, right1cm, top0.5cm, bottom0.5cm,colbackblue!10!wh…...
面试题库之JAVA基础篇(三)
final 被final修饰的类不可以被继承。被final修改的方法不可以被重写。被final修改的方法,jvm会尝试内联,以提高运行效率。被final修改的变量不可变,如果修改的是引用,那么引用不可变,引用指向的对象内容可变。被fin…...
CTF-虚拟机-QEMU-前置知识-操作流程与源码阅读
文章目录 总览内存PCI设备PCI配置空间前64个字节对应源码Memorry空间的BARIO空间的BAR MMIOPMIOIspci访问PCI设备配置空间中的Memory空间和IO空间MMIOPMIO QQM(qemu object model)简洁概要将 TypeInfo 注册 TypeImpl:ObjectClass的初始化&…...
java成神秘籍第一卷
前言 适合还没有入行小白学习,有些朋友会跑来问我这行的一些问题,下面算是详细系统的整理了一下啦。 全当是学习 复盘 整理 记录了 java成神秘籍第一卷 前言一 前提1 要不要考公,考编,考研2 语言选择3 就业岗位4 目标5 考不考虑…...
golang实现文件上传(高并发+分块+断点续传+加密)
运行视频 // todo 根据前端传递文件加密 func (s *FileProcess) FileProcessEncryptionByFront(file multipart.File, h *multipart.FileHeader) interface{} { //根据字节直接处理文件 这个是前端传递的二进制流s.FileProcessInit() //文件初始化 设置原来文件…...
用HeidiSQL在MySQL中新建用户
用HeidiSQL登录到MySQL数据库,注意登录的时候要使用有权限的用户: 选择工具-》用户管理: 点击左上角的“添加”: 输入用户名、密码,并且分配权限: 点击右边的“添加对象”: 可以根据自己…...
【IPv6】IPv6协议
一、IPv6数据报格式 这是与v4报头的对比 1.8bit的版本保留了,v4版本就是4,v6就是6。 2.v6去除了v4的首部长度字段,因为v6的首部长是固定的40字节。 3.服务类型(Type of Service, ToS)和通信类型(Traffi…...
无需服务器,无需魔法,拥有一个微信机器人就是这么简单
前情提要 还没看过的朋友可以看一下上一篇文章《拥有一个微信机器人总共需要几步?》在这篇文章里,我们提到,创建微信机器人需要一个大前提--你得有一台服务器。现在,不再需要了!没错,上一篇提到的Serverles…...
1、命名空间、C++的复合类型、缺省参数
命名空间 1、命名空间的定义 使用namespace定义,使用作用域限定符::访问 #include <iostream> namespace ICBC{int money 0;void save( int m){money m;} } int main( void ){ICBC::save( 100); std::cout << "工行卡余额:"…...
colab notebook导出为PDF
目录 方法一:使用浏览器打印功能 方法二:使用nbconvert转换 方法三:在线转换 方法一:使用浏览器打印功能 一般快捷键是CTRLP 然后改变目标打印机为另存为PDF 这样就可以将notebook保存为PDF了 方法二:使用nbconver…...
【Python动漫系列】名侦探柯南(完整代码)
文章目录 名侦探柯南环境需求完整代码程序分析系列文章名侦探柯南 《名侦探柯南》是由青山刚昌创作的一部侦探漫画,于1994年开始连载,并被改编为动画、电影、游戏等多种形式。故事讲述了高中生侦探工藤新一在破案时被不良组织所毒害,身体缩小成了一个小学生,为了寻找解药并…...
【matlab】QR分解
QR分解 给定一个mn的矩阵A,其中m≥n,即矩阵A是高矩阵或者是方阵,QR分解将矩阵A分解为两个矩阵Q和R的乘积,其中矩阵Q是一个mn的各列正交的矩阵,即QTQI,矩阵R是一个nn的上三角矩阵,其对角线元素为…...
Liunx系统使用超详细(三)
本篇内容开始逐渐描述有关liunx的各种命令的使用方法! 目录 一、目录和文件区别 1.1目录: 1.2文件: 1.3总结: 二、Linux命令的写法 三、linux命令清屏 四、pwd命令 五、ls命令 5.1 ls: 5.2 ls -l:…...
Kubernetes学习笔记-Part.06 Docker安装
目录 Part.01 Kubernets与docker Part.02 Docker版本 Part.03 Kubernetes原理 Part.04 资源规划 Part.05 基础环境准备 Part.06 Docker安装 Part.07 Harbor搭建 Part.08 K8s环境安装 Part.09 K8s集群构建 Part.10 容器回退 第六章 Docker安装 在master、worker、harbor上均需…...
现在的00后,实在是太卷了......
现在的小年轻真的卷得过分了。前段时间我们公司来了个00年的,工作没两年,跳槽到我们公司起薪18K,都快接近我了。后来才知道人家是个卷王,从早干到晚就差搬张床到工位睡觉了。 最近和他聊了一次天,原来这位小老弟家里条…...
Maven项目目录结构
项目结构 目录说明.ideaIDEA工具的配置文件.mvn用于运行Maven项目src源码文件夹target字节码文件夹.gitignore配置git忽略文件HELP.md自述文件mvnw运行Maven命令(Linux)mvnw.cmd运行Maven命令(Windows)pom.xml依赖管理文件 如图…...
感慨!一路从测试小白到现在的高级测试工程师,方向不对真的很艰辛
1 功能测试人员,也就是我们常常俗称的进行点点点测试工程师。前 1-2 年做功能测试,使用其他人开发的工具进行测试。这个阶段的测试工作都比较初级,严重依赖于别人开发的工具和系统。当系统进行升级换代的时候,强哥会明显的感觉到…...
字符串经典基础面试题
关卡名 字符串经典基础面试题 我会了✔️ 内容 1.理解字符串反转的处理方法 ✔️ 2.熟练掌握回文串的判断方法 ✔️ 3.掌握字符串中搜索第一个唯一字符的方法 ✔️ 4.掌握判断是否互为字符串重排的处理技巧 ✔️ 1 反转的问题 我们知道反转是链表的一个重要考点…...
【华为OD题库-062】计算礼品发放的最小分组数目-java
题目 又到了一年的末尾,项目组让小明负责新年晚会的小礼品发放工作。为使得参加晚会的同时所获得的小礼品价值相对平衡,需要把小礼品根据价格进行分组,但每组最多只能包括两件小礼品,并且每个分组的价格总和不能超过一个价格上限。…...
[go 面试] 构建高效微服务通信:选择合适的通信方式
关注公众号【爱发白日梦的后端】分享技术干货、读书笔记、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力! 构建分布式系统或微服务架构时,服务间通信成为至关重要的一环。不同的通信方式各有优劣,因此在选择时需根…...
【华为OD题库-048】拔河比赛-java
题目 公司最近准备进行拔河比赛,需要在全部员工中进行挑选。选拔的规则如下: 1.按照身高优先、体重次优先的方式准备比赛阵容 2.规定参赛的队伍派出10名选手 请实现一个选拔队员的小程序。 输入为一个数组,记录了部门人员的身高、体重信息,如…...
【WebSocket】通信协议基于 node 的简单实践和心跳机制和断线重连的实现
前后端 WebSocket 连接 阮一峰大佬 WebSocket 技术博客 H5 中提供的 WebSocket 协议是基于 TCP 的全双工传输协议。它属于应用层协议,并复用 HTTP 的握手通道。它只需要一次握手就可以创建持久性的连接。 那么什么是全双工呢? 全双工是计算机网络中的…...
【有ISSN、ISBN号!往届均已完成EI检索】第三届电子信息工程、大数据与计算机技术国际学术会议(EIBDCT 2024)
第三届电子信息工程、大数据与计算机技术国际学术会议(EIBDCT 2024) 2024 3rd International Conference on Electronic Information Engineering, Big Data and Computer Technology 第三届电子信息工程、大数据与计算机技术国际学术会议(…...
【Windows】使用SeaFile搭建本地私有云盘并结合内网穿透实现远程访问
1. 前言 现在我们身边的只能设备越来越多,各种智能手机、平板、智能手表和数码相机充斥身边,需要存储的数据也越来越大,一张手机拍摄的照片都可能有十多M,电影和视频更是按G计算。而智能设备的存储空间也用的捉襟见肘。能存储大量…...
Windows本地搭建WebDAV服务并使用内网穿透远程访问【无公网IP】
windows搭建WebDAV服务,并内网穿透公网访问【无公网IP】 文章目录 windows搭建WebDAV服务,并内网穿透公网访问【无公网IP】1. 安装IIS必要WebDav组件2. 客户端测试3. cpolar内网穿透3.1 打开Web-UI管理界面3.2 创建隧道3.3 查看在线隧道列表3.4 浏览器访…...
责任链设计模式
package com.jmj.pattern.responsibility;/*** 请假条类*/ public class LeaveRequest {//姓名private String name;//请假天数private int num;//请假内容private String content;public LeaveRequest(String name, int num, String content) {this.name name;this.num num;…...
12.4 C++ 作业
完成沙发床的多继承 #include <iostream>using namespace std;//封装 沙发 类 class Sofa { private:string *sitting; public://无参构造函数Sofa(){cout << "Sofa::无参构造函数" << endl;}//有参构造函数Sofa(string s):sitting(new string(s)…...
基于ssm品牌会员在线商城源码
基于ssm品牌会员在线商城源码708 idea mysql数据库 navcat 开发技术:后端 ssm 后台管理 vue 用户端 vue.jshtml 演示视频: 基于ssm品牌会员在线商城源码 DROP TABLE IF EXISTS address; /*!40101 SET saved_cs_client character_set_client */; /…...
骨传导耳机音量大了有害吗?骨传导能保护听力吗?
无论是传统耳机还是骨传导耳机,只要使用音量过大,都会对有一定的损伤,然而由于骨传导耳机的传声原理和佩戴方式比较特殊,所以对人体的损伤比较小,想要知道骨传导耳机能否保护听力,就要先了解骨传导耳机的传…...
wordpress 安装 404/关键词歌曲歌词
扫描仪通常被用于计算机外部仪器设备,通过捕获图像并将之转换成计算机可以显示、编辑、存储和输出的数字化输入设备,已经成为了我们办公和生活的必备设备了,但是很多win7系统用户并不知道要怎么连接扫描仪,其实方法很简单哦&#…...
中山地区做网站公司/acca少女网课视频
工作过程中有时候会接收到数据库服务器器load 飙高的报警,比如: load1 15.25 base: 8.52,collect time:2014-08-30 如何处理load 异常飙高的报警呢? 本文尝试从原理,原因,解决方法来阐述这类问题的解决思路。 一 原理分析 CPU作为…...
网站建设受众/广告设计自学教程
2020-06-30日更新 请求方式及url: 请求方式:POST 接口地址:https://tianqiapi.com/api/sing5 请求示例 (注意该接口是POST方式, 请使用Postman测试) https://tianqiapi.com/api/sing5?appid23035354&appsecret8YvlPNrz&urlhttp:/…...
社交网站开发流程/网站开发需要哪些技术
文章目录1.Navicat介绍Navicat for MySQL2.Navicat for MySQL安装1.下载链接2.解压压缩包文件3.通过命令提示符登陆mysql4.复制MySQL数据库文件到Navicat_V11.0.10内的目录下5.配置Navicat for MySQL的环境变量6.启动Navicat for MySQL里的navicat.exe1.Navicat介绍 Navicat是一…...
wordpress广告插件中文/怎么查百度收录
【题目描述】给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 【解题思路1】暴力解法 //1.若给定的节点pNode的右子树不为空,则中序遍历…...
面试简历模板免费/seosem是什么职位
用例设计 目标 1. 掌握如何编写自动化测试用例文档 1. 编写自动化测试用例的原则 1. 自动化测试用例一般只实现核心业务流程或者重复执行率较高的功能。 2. 自动化测试用例的选择一般以“正向”逻辑的验证为主。 3. 不是所有手工用例都可以使用自动化测试来执行。 4. 尽…...