概述
lowmemorykiller的作用就是当内存比较紧张的时候去及时杀掉一些对用户来说不那么重要的进程,回收内存,保证手机的正常运行。
安卓平台lowmemorykiller机制演进可以描述为:从早期的Kernel space Lowmemorykiller 到 UserSpace Lowmemorykiller (监听vmpressure),再到UserSpace Lowmemorykiller (监听PSI)。
内核空间LMK
Kernel LMK相关概念
- /sys/module/lowmemorykiller/parameters/minfree:
18432,23040,27648,32256,55296,80640
数字代表一个内存级别
当手机内存低于80640时,就去杀掉优先级906以及以上级别的进程,当内存低于55296时,就去杀掉优先级900以及以上的进程.
- proc/pid/oom_adj: 代表当前进程的优先级,这个优先级是kernel中的进程优先级,只读文件。
- /proc/pid/oom_score_adj: 上层优先级,跟ProcessList中的优先级对应,就是让用户空间调节oom_score的接口。(就是上面的odj值)
AMS: 安卓系统框架层的ActivityManagerService
Lmkd: 安卓用户态的userspace lowmemory killer daemon
Lowmemorykiller 驱动程序允许用户空间指定一组内存阈值,当内存使用达到这些阈值时,具有不同 oom_score_adj 值的进程将被终止。
实践应用举例:
有时在解Android系统连续启动app性能问题时,就要调整上面的lmk水线,即minfree和adj值。因为Android系统现在随着多媒体这些耗内存资源的功能日益强大,
所以为了在有限的内存世界里面,满足app启动性能,就不得不这样调整lmk水线,来灵活干掉后台无挂紧要进程,来释放内存。
AMS与lmkd通过socket通信
主要分为三种command,每种command代表一种数据控制方式:
- LMK_TARGET:更新/sys/module/lowmemorykiller/parameters/中的minfree以及adj
- LMK_PROCPRIO:更新指定进程的优先级,也就是oom_score_adj
- LMK_PROCREMOVE:移除进程。
Lowmemorykiller init
初始化lowmemorykiller,注册shrinker,当空闲内存页面不足时, 内核进程kswpd会调用注册的shrink回调函数来回收页面。
回收内存流程时会被调用,lowmem_scan挂到了register_shrinker里,shrink_slab_node里会scan_objects
调用栈:
• [<c082a824>] (lowmem_scan) from [<c01e0ba4>]
(shrink_slab_node+0x204/0x3d0)
• [<c01e0ba4>] (shrink_slab_node) from [<c01e12b8>]
(shrink_slab+0x70/0xe4)
• [<c01e12b8>] (shrink_slab) from [<c01e3ba8>]
(try_to_free_pages+0x3c0/0x74c)
• [<c01e3ba8>] (try_to_free_pages) from [<c01d9188>]
(__alloc_pages_nodemask+0x578/0x92c)
lowmem_scan首先看看能不能找到oom_score_adj,如果找不到就认为内存充足不杀进程
- 判断内存是否充足的条件就是other_free和other_file两个都必须同时小于lowmem_minfree中的用户设定值,other_free基本上是free pages,other_file基本上是file pages,两者可以分别看成MemFree和Cached大小
- 遍历所有的进程for_each_process,查找 oom_score_adj要比min_score_adj大且rss最大的进程,通过send_sig(SIGKILL, selected, 0)杀掉。
Issues with lowmemorykiller kernel driver
1:Hooks into slab shrinker API that was not designed for this purpose. Shrinkers are supposed to quickly drop unused caches and exit in order to avoid slowing down the vmscan process.
Workload that lowmemorykiller performs includes searching for heavy processes and killing them, which are not quick operations。
2:还有个重要的缺陷: 内核是做机制工作的,再实现一些策略工作会比较臃肿。所以尽量剥离一些本来属于策略上的东西(比如上面的依据进程rss大小来选择杀哪些进程),分给Android 上层代码去做。
因为上层代码离业务层很近,更能根据业务需求,来做适合Android场景的low memory killer.
ULMK ‐VMPRESSURE
这个lowmemorykiller实现架构解决了上面lowmemorykiller kernel driver实现的不足。
基本工作思路:去掉了以前内核态杀进程的策略部分实现,只监听kernel的vmpressure events, 然后由lmkd根据进程adj以及内存level来决定杀哪些进程。
和以前的lmkd行为相比有个变化:
- lmk仅根据oom_score_adj值的大小选择杀进程,而不会考虑进程本身占用内存的大小
- native进程的oom_score_adj的值由rc文件设置或者继承自父进程,一般都是静态的,不会变化. native进程的oom_score_adj都小于0,其中很多重要的系统支撑进程的oom_score_adj值为 -1000,oom killer都杀不了
lmk默认只管理oom_score_adj大于等于0的进程,所以只能杀死apk进程。
由这个 变化可以看出:lmkd杀进程的选择策略已经跟内核慢慢脱离关系了,完全是用户态决定了。
具体:
-
AMS 与 Lmkd 通过soket通信。
-
Lmkd 与 kernel的Memcg通信
a. kernel 会向 lmkd 报告memory pressure event
b. Lmkd 会根据kernel报告的critical memory pressure 或medium memorypressure 杀app
c. 根据进程的adj以及minfree来选择app杀掉
Lmkd 监听vmpressure
vmpressure本身就定义了low, medium, critical三类内存压力状态,
vmpressure本身就定义了low, medium, critical三类内存压力状态,
LMKD 的初始化流程:
‐> init_mp_common(low,medium,critical)
open(MEMCG_SYSFS_PATH "memory.pressure_level", O_RDONLY | O_CLOEXEC);
evctlfd = open(MEMCG_SYSFS_PATH "cgroup.event_control", O_WRONLY | O_CLOEXEC);
write(evctlfd, buf, strlen(buf) + 1)
‐> memcg_write_event_control
event‐>register_event = vmpressure_register_event;
event‐>register_event(memcg, event‐>eventfd, buf);
Lmkd 如何处理vmpressure
对于 memory pressure 事件,处理函数是 mp_event_common,传递给他的 data 是memory
pressure level
Lmkd::init
‐>init_mp_common
vmpressure_hinfo[level_idx].data = level_idx;
vmpressure_hinfo[level_idx].handler = mp_event_common;
epev.data.ptr = (void *)&vmpressure_hinfo[level_idx];
ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, evfd, &epev);
在未开启use_minfree_levels的情况下,需要结合mp level以及swap 分区空余大小来决定是否kill app,
kill app,
‐>!use_minfree_levels
if (mi.field.nr_free_pages < low_pressure_mem.max_nr_free_pages) {
pages_to_free = low_pressure_mem.max_nr_free_pages ‐
mi.field.nr_free_pages; }
if (level < VMPRESS_LEVEL_CRITICAL &&
mi.field.free_swap > low_pressure_mem.max_nr_free_pages)
return;
min_score_adj = level_oomadj[level];
find_and_kill_processes(min_score_adj, 0);
Issues with vmpressure for memory pressure detection
1:Reflects current reclaim efficiency rather than memory pressure level
从小米的psi工作文档里面的Documentation/cgroup-v1/memory.txt里面的vmpressure描述,能够看出来这个确实是反映的是current reclaim efficiency。
2:Difficult to tune because of no direct link between reclaim efficiency and its effects on user experience。
这一点很重要,Android上的lowmemory killer的运用,主要是解决用户操作手机卡顿,改善用户体验的。但是这个卡顿和内存回收效率并无直接关系,需要呼唤psi了。
3:Tightly coupled with vmscan implementation, changes in vmscan
mechanisms may result in behavior change
4:In testing, highly depends on the system memory size and particular
workload.
ULMK‐‐PSI
PSI improvements
- More accurate pressure detection compared to vmpressure (2‐10x fewer false positives)
google在Android Q上已经基于PSI改进了lmkd,可以更加及时地检测到内存过载和进行回收.根据google的内存压力测试,新机制的检测准确率超过上面的vmpressure的十倍。
- Thresholds are configurable making tuning possible
- PSI signals are rate‐limited and userspace can decide how often to poll after the first signal
- Supports unlimited number of triggers
PSI解释
PSI将各个任务延迟汇总为资源压力指标,这些指标反映工作负载运行状况和资源利用率方面的问题。
基准productivity:可以在CPU上执行任务的时间。
压力:表示由于资源争用而无法执行任务的时间量。
productivity的概念包括两个部分:workload和CPU。 为了衡量压力对两者的影响,我们定义了两个
资源的争用状态:SOME和FULL。
- SOME: Time percentage due to the stalling of a few tasks caused by lack of a specific kind of resource.
- FULL: Time percentage due to the stalling of all tasks caused by lack of a specific kind of resource.
Psi旨在提供可供用户配置的低延迟的短期压力监测机制。 在用户定义的时间窗口内度量延迟高于用户定义的阈值时通知用户。
时间窗口和阈值都以usecs表示,可以同时监视具有不同阈值和窗大小的多个psi资源,PSI监测的资源包括:memory,IO以及cpu
PSI如何监控资源压力
用户需要注册trigger,当资源压力超过门限值时通过poll()通知用户。
- Trigger描述的是特定时间窗口内的最大累计停顿时间,例如 任何500ms窗口内100ms的总停顿时间生成唤醒事件
- 要注册trigger,用户必须在proc / pressure /下打开psi接口文件,表示要监视的资源,并写入所需的阈值和时间窗口。例如,将“some 150000 1000000”写入/ proc / pressure / memory,将为在1秒时间窗口内测量的部分内存停顿150ms阈值。
- PSI监视器仅在系统进入受监测的psi度量标准的卡顿状态时激活, 并在退出卡顿状态时停用。
系统记录memory stall状态
系统通过psi_memstall_enter以及psi_memstall_leave记录memory stall 状态,
在下面几个内存相关的文件将对memstall 状态进行记录
mm/compaction.c
mm/filemap.c, mm/page_alloc.c mm/vmscan.c
Psi 更新trigger
‐>psi_memstall_enter/psi_memstall_leave
‐>psi_task_change
‐>psi_schedule_poll_work
‐>psi_poll_work
‐>collect_percpu_times
‐>update_triggers
growth = window_update(&t‐>win, now, total[t‐>state]);
if (growth < t‐>threshold) //比较窗口时间内的stall时间是否> threshold
continue;
if (cmpxchg(&t‐>event, 0, 1) == 0)
wake_up_interruptible(&t‐>event_wait); //产生event,唤醒等待队列进程
通知事件
psi_fop_poll此函数是用户态发起poll()系统调用后,会有psi此函数对接,检查在此poll之前的时间内是否有事件发生,如果有则设置相应事件signal,如果无则通过poll_wait()让其等待。所以用户每次监听io/mem/cpu的任一文件,都会引发此对接函数的调用,根据已有的trigger判断事件监听情况。
Lmkd 处理PSI通知
处理函数依然是mp_event_common
mp_event_common
‐>zone_watermarks_ok
‐>file_cache_to_adj
‐>find_and_kill_process
不再使用use_minfree_levels, 通过zone 水线判断是否需要kill process, 若需要kill则通过file_cache_to_adj来获取adj.
Android现有ulmk+psi的不足
有个问题,psi的初衷跟我的单线程工作性能解析初衷是一样的,想衡量单线程在cpu,mem和io这三大块资源的等待延迟时间。
所以很好,以后我的单线程工作性能解析,进一步地发展就是跟psi靠拢吧。
但是psi上传的事件信息,很难看出来是哪个task延迟的,这样不好吧,那还不如做成mem cgroup分组后,这样最起码知道不同的mem cgroup的psi值状况。
例如知道前台group里面有psi内存压力导致该group内的task卡顿时,就去有选择地杀掉后台group里面的进程。
psi上传信息为什么不带上task的pid一点分析:
这是个需要调查的问题点,如果真带上的话,哇,那就完美了,我的单线程工作性能解析以后就有救了,可以利用psi信息,就知道解压缩线程的工作性能状况了。
不过要想做到这个,还得加上mem psi的具体卡在什么地方的信息(mem reclaim, mem thrashing, mem compact or others)。
这样还不如用那个kernel里面tools目录下面的get_task_delay工具了。
所以最后总结:psi这样设计上报用户态接口信息,是有依据的,只是上报下统计数据信息,不想上报内存压力各阶段耗时,因为上报信息多了,就会对内核性能产生干扰了
标签:psi,内存,memory,进程,Android,adj,event From: https://www.cnblogs.com/linhaostudy/p/18577320