一、背景
对于一些内存比较容易吃紧的系统,除了有合理的内存的规划并持续的优化使用的内存以外,还需要增加一些措施来监控内存的使用,并在系统出现内存压力时及时做出响应。
android的lmkd就是这么一个服务,在这篇博客里,我们介绍lmkd的实现及代码分析。
二、整体介绍
高版本android不再依赖一个专门的定制的ko来辅助做内存紧张时的决定杀哪个进程的决策逻辑,而是完全依赖内核现有的psi的功能来完成内存的压力监测。
lmkd定义了三级内存压力水位,在达到某一级别的内存压力时,找到大于等于这个一别的内存压力水位所对应的oom_adj的所有进程里的"占内存最多的或是oom_adj最高"的任务来杀掉。"占内存最多的或是oom_adj最高"由配置决定。
所以lmkd是一个纯用户态实现,对系统的内核的依赖只在于需要使能CONFIG_PSI,而如果不打开CONFIG_PSI的话,且用的是cgroup v1的话,也可以使用cgroup v1里的vmpressure的相关节点,要注意vmpressure是一个被淘汰的功能,开启cgroup v2的话并不使用它,vmpressure存在大量误报的情况。
在下面一章里,我们分下面两个方面来介绍:
1)lmkd里有哪些配置------这里说的配置不仅包括代码里写死的配置,也包括android项目里的环境变量的配置
2)lmkd的代码主要流程
我们所讲的这份lmkd的源码是链接 android的lmkd的一份源码参考 里的。整理的lmkd的代码流程图见链接 android的lmkd的流程图。另外,lmkd所用到的内核的psi的能力的介绍博客见 内核的PSI的原理及代码分析。
三、细节展开
3.1 lmkd有哪些配置?
下面列举的是比较重要的一些配置值。
3.1.1 代码里定义的三级psi内存水位阈值及对应的oom_adj数值
在源码里的lmkd.c里有定义如下数组:

对应于设置根内存压力节点写入上图里的some/full,对于1秒的周期内,出现70ms/100ms/70ms的部分/部分/全部的因为内存压力而导致的任务停顿。
注意,上图里的这些水位配置是固定死的配置,属于经验数值。
与上面的这三种压力水位对应的有三个oom_adj的数值,代码里是通过获取属性值来拿到:
property_get_int32("ro.lmk.low", OOM_SCORE_ADJ_MAX + 1);
property_get_int32("ro.lmk.medium", 800);
property_get_int32("ro.lmk.critical", 0);
3.1.2 杀掉进程后的"冷静期"时间
刚杀掉一个进程后,有一个冷静期,在这个期间不再杀更多进程:
property_get_int32("ro.lmk.kill_timeout_ms", 0);
3.1.3 压力等级升级和降级相关的配置
这里所谓的升级和降级是指虽然有触碰到上面 3.1.1 里介绍的代码里固定死的内存压力水位,可以通过用户配置,来决定是否升级这个触碰到的某一级别的内存压力。比如,触碰到low的内存压力,根据系统状态看是否升级到medium,又比如,触碰到critical的内存压力,根据系统状态看是否降级到medium。而这里说的所谓"根据系统状态"就是这一节所讲到的配置项。
有关判断是否需要升级的"根据系统状态"的配置项是:
property_get_int32("ro.lmk.upgrade_pressure", 100);
它的运算方式是(usage/usage+swap),即内存总使用量除以(内存总使用量+swap分区使用量),如下图,如果发现当前系统里的(usage/usage+swap)比属性定的阈值upgrade_pressure要小,也就说明swap的使用量比较多,那就需要升级压力等级:

有关判断是否需要降级的"根据系统状态"的配置项有两个:
swap_free_low_percentage =
property_get_int32("ro.lmk.downgrade_pressure", 100);
downgrade_pressure =
(int64_t)property_get_int32("ro.lmk.downgrade_pressure", 100);
能降级的条件是:
free_swap/total_swap大于等于swap_free_low_percentage
且
usage/(usage+swap)的比率大于downgrade_pressure
注意,usage/(usage+swap)的数值,无论是判断升级还是判断降级都是需要用的,用的是同一个数值:


3.1.4 "是否是杀掉内存最多的任务"的配置项
在执行某个压力水位的杀进程的操作时,首先有一个oom_adj的数值,这个数值决定一个oom_adj的下限,注意,进程的oom_score越低,就越不能杀,所以在选要杀的任务时,如下图,判断大于等于oom_adj的数值的这些任务,都可以被选择杀掉的:

逻辑是从max的oom_score不断地减一来遍历,如果oom_score对应的槽位有任务,就返回,用的是ADJTOSLOT宏来通过oom_score找到对应的槽位,但是要根据当前的配置,配置如果是在满足大于等于oom_adj数值的任务里挑选内存使用量最大的任务,则走下图里的这个proc_get_heaviest函数:

反之走下图里的proc_adj_lru:

判断的地方在find_and_kill_process里:
kill_heaviest_task是取自属性值:
kill_heaviest_task =
property_get_bool("ro.lmk.kill_heaviest_task", false);

3.2 lmkd的代码主要流程
lmkd的main函数拆解成下面的三个步骤:
1)获取一系列配置属性值并设置到后面逻辑会用到的全局变量里去
2)epoll的一些初始化逻辑,因为lmkd是一个服务端,得响应客户端发来的请求,虽然目前从代码里看,会给lmkd发请求的任务并不多,虽然不多,但是也得有这个socket交互的逻辑
3)epoll_wait并处理消息

下面我们来分别说一下相关细节:
3.2.1 获取一系列配置属性并设置到全局变量里去
这块逻辑比较简单:

且关键的配置项都已经在上面 3.1 里进行了介绍。
3.2.2 epoll的初始化逻辑,响应客户端发来的请求
main里完成了上面 3.2.1 的配置属性值的获取后,执行init函数:
init函数里创建lmkd的socket,并进行listen:

上图里listen返回的fd,用来作为epoll_ctl的第一个条目:

上图里设置的ctrl_connect_handler也是下面 3.2.3 一节里讲到的mainloop里,epoll响应消息的执行的回调的三个种类之一。
上面添加的是listen的fd的epoll,而listen的fd有事件后,需要执行accept,accept返回的fd才是具体的客户端和lmkd服务端的传输的socket的fd,这块逻辑在下面 3.2.3 里的ctrl_connect_handler里进行介绍。
这里初始化逻辑里,除了listen的fd需要去epoll_ctl以外,还需要设置我们定义的三级psi内存水位,并进行epoll_ctl,这块逻辑在init_psi_monitors里:

init_psi_monitors的实现如下:

依次设置三个水位的epoll_ctl:



3.2.3 epoll_wait并处理消息
main里的这个主循环函数就是mainloop,mainloop里的逻辑比较简单,就是循环进行epoll_wait:

然后再进行回调处理:

跟踪代码后就可以梳理出,这个回调会执行的有下图里的三种:

整理如下:
1)ctrl_connect_handler

2)ctrl_data_handler

3)mp_event_common

依次介绍一下:
1)ctrl_connect_handler的逻辑如下:

accept这个连接请求,拿到fd,把该fd加入到epoll里去(设置处理控制消息的回调函数ctrl_data_handler)
2)ctrl_data_handler也就是具体处理控制消息的回调函数,ctrl_data_handler的逻辑如下:

ctrl_data_handler调用了ctrl_command_handler:

ctrl_command_handler根据传入的记录在epoll_ctl关联的数据内容标识的dsock_idx序号:

通过序号拿到packet这个字符串首地址,ntohl转换,得到cmd,并进行switch case处理:



3)最后,介绍一下mp_event_common里的逻辑,这部分逻辑就是响应达到psi水位事件的逻辑,如下图的逻辑整理:

上面的大部分内容在之前介绍 3.1 配置参数时都已经进行了介绍。