
在Kernel hacking中打开CONFIG_DEBUG_KMEMLEAK =y即使能了kmemleak,其实就是开了一个内核线程,该内核线程每10分钟(默认值)扫描内存,并打印发现新的未引用的对象的数量。kmemleak的原理其实就是通过kmalloc、vmalloc、kmem_cache_alloc等内存的分配,跟踪其指针,连同其他的分配大小和堆栈跟踪信息,存储在PRIO搜索树。如果存在相应的释放函数调用跟踪和指针,就会从kmemleak数据结构中移除。下面我们看看具体的用法。
查看内核打印信息详细过程如下:
1、挂载debugfs文件系统
mount -t debugfs nodev /sys/kernel/debug/
2、开启内核自动检测线程
echo scan > /sys/kernel/debug/kmemleak
3、查看打印信息
cat /sys/kernel/debug/kmemleak
4、清除内核检测报告,新的内存泄露报告将重新写入/sys/kernel/debug/kmemleak
echo clear > /sys/kernel/debug/kmemleak
内存扫描参数可以进行修改通过向/sys/kernel/debug/kmemleak 文件写入。 参数使用如下:
off 禁用kmemleak(不可逆)
stack=on 启用任务堆栈扫描(default)
stack=off 禁用任务堆栈扫描
scan=on 启动自动记忆扫描线程(default)
scan=off 停止自动记忆扫描线程
scan=<secs>设置n秒内自动记忆扫描,默认600s
scan 开启内核扫描
clear 清除内存泄露报告
dump=<addr>转存信息对象在<addr>
通过“kmemleak = off”,也可以在启动时禁用Kmemleak在内核命令行。在初始化kmemleak之前,内存的分配或释放这些动作被存储在一个前期日志缓冲区。这个缓冲区的大小通过配CONFIG_DEBUG_KMEMLEAK_EARLY_LOG_SIZE设置。
Linux系统当可用内存较低的时候oom killer机制会根据一定的规则去杀掉一些进程来释放内存,而Android系统的LowMemoryKiller机制就是以此功能为基础做了一些调整。Android系统中的APP在使用完成之后并不会马上被杀掉,而是驻留在内存中,当下一次在此进入此应用的时候可以省去进程创建的过程,加快启动速度。LowMemoryKiller机制会在内存资源紧张的时候,杀掉一些进程来回收内存。
LowMemoryKiller机制分为三个部分
Framework中的ProcessList和Native的lmkd进程通过Socket进行进程间通信,而lmkd和内核中的LowMemoryKiller通过writeFileString向文件节点写内容方法进行通信。
Framework层通过一定的规则调整进程的adj的值和内存空间阀值,然后通过socket发送给lmkd进程,lmkd两种处理方式, 一种将阀值写入文件节点发送给内核的LowMemoryKiller,由内核进行杀进程处理,另一种是lmkd通过cgroup监控内存使用情况,自行计算杀掉进程。
lmkd是一个native进程,由init进程启动,定义在/system/core/lmkd/lmkd.rc中
在lmkd.rc中,启动了lmkd进程,并创建了一个名为lmkd的socket的描述符,用于socket进程间通信。lmkd启动后首先执行main方法。
main方法首先设置了当前进程的调度规则,然后执行了init方法和mainLoop方法。
lmkd的init方法中做的工作
我们先分析内核实现的LowMemoryKiller进程查杀机制, 然后再分析lmkd实现的机制。两者最终的结果都是在内存紧张的时候杀死一些进程来释放内存, 但是实现机制去不太一样。
init执行初始化完成之后, 进入mainloop方法,循环等待epoll事件的上报,init的时候epoll监听的socket连接, 当有socket连接的时候就会调用ctrl_connect_handler方法。
监听到socket连接, 我们知道此时连接lmkd的socket客户端就是framework,当有连接到来的时候accept方法返回连接的socketFD, 然后将连接的socketFD同样加入epoll中, 当socketFD中有可读消息,即framework给lmkd发送消息的时候,epoll唤醒然后会掉ctrl_data_handler方法来处理。
Framework和lmkd进程通过socket来进行进程间通信,在lmkd初始化的时候,通过监听socket描述符lmkd来等待Framework发送的消息。
Framework向lmkd发送命令相关的方法有三个。
上面的三种情况Framework最终是通过socket向lmkd发送了三种消息。
lmkd接收命令处理逻辑
lmkd通过epoll监听socket中是否有数据, 当接受的framework发送的socket命令之后,调用ctrl_cmmand_handler方法处理,显示解析socket中的命令和参数,根据对于的命令来调用不同的方法处理。
对于进程查杀有两种实现方式,一种是内核的LMK,通过shrinker来触发低内存回收, 另一种是lmkd通过cgroup监控内存使用情况,自行计算杀掉进程。两种实现不太一样,需要逐个分析。
设置内存阀值和adj的值就是将从framework收到的数据封装成字符串,通过writefilestring写入到两个文件节点,以供内核LMK使用。
/sys/module/lowmemorykiller/parameters/minfree : 内存级别限额
/sys/module/lowmemorykiller/parameters/adj :内存级别限额对应的要杀掉的进程的adj值.
由于使用内核LMK, 所以调整进程优先级直接将优先级写入对应进程的oom_adj_score文件即可。
移除进程的时候不需要做任何 *** 作
在linux中,有一个名为kswapd的内核线程,当linux回收存放分页的时候,kswapd线程将会遍历一张shrinker链表,并执行回调,或者某个app启动,发现可用内存不足时,则内核会阻塞请求分配内存的进程分配内存的过程,并在该进程中去执行lowmemorykiller来释放内存。虽然之前没有接触过,大体的理解就是向系统注册了这个shrinker回调函数之后,当系统空闲内存页面不足时会调用这个回调函数。 struct shrinker的定义在linux/kernel/include/linux/shrinker.h中:
内核LowMemoryKiller shrinker的注册过程如下:
注册完成之后, 在内存紧张的时候就会回调shrinker, 其中最主要的是lowmem_scan方法。具体实现如下:
内核LMK的原理很简单:首先注册了shrinker,在内存紧张的时候会触发lowmem_scan方法,这个方法要做的就是找打一个进程,然后杀掉他,释放一些内存。
内核LMK的实现逻辑已经分析完了
lmkd实现内存查实的方式是基于 cgroup memory 来实现的。
什么是cgroup memory?
Cgroup的memory子系统,即memory cgroup(本文以下简称memcg),提供了对系统中一组进程的内存行为的管理,从而对整个系统中对内存有不用需求的进程或应用程序区分管理,实现更有效的资源利用和隔离。
cgroup memory相关的文件
简单的了解了下cgroup的原理,再来看lmkd的init方法
先了解下memory pressure_level的用法
init_mp_common方法严格的按照pressure_level的用法,注册了pressure_level的事件回调, pressure_level分为三个等级
当内存达到相应的等级,就会回调mp_event_common方法, 由mp_event_common方法来处理。
lmkd内存查杀原理:
进程查杀的两种实现方式原理类似,都是注册是的回调,当内存紧张的时候根据剩余内存的adj来查杀大于该adj的内存。内核shrinker方式是只有内存紧张的时候才会去释放,而cgroup方式控制更加精细, 根据不同等级来触发内存回收。
问题关键在于理解以下指令:"std repne scasb\n\t"
1、std:方向位DF置位,即DI进行自减 *** 作。
2、repnescasb
这两条组合指令实现循环比较。ecx初值为15*1024,al=0,di初值为&mem_map[15*1024-1],即从数组mem_map的最后一项开始,依次与al(=0)进行比较。假设数组第i项mem_map[i]==0,则结束循环,此时ecx=i, edi=&mem_map[i-1](因为ecx初值为15*1024,di初值为数组最后一项15*1024-1的地址)。找到空闲页面后,将该数组项置1,即*(edi+1)=mem_map[i]=1,即语句“movb $1,1(%%edi)”实现的功能。此时,ecx即为空闲页面索引。
几点说明:
1、rep循环结束条件:
Repeat PrefixTermination Condition 1Termination Condition 2
REP RCX or (E)CX = 0 None
REPE/REPZ RCX or (E)CX = 0 ZF = 0
REPNE/REPNZ RCX or (E)CX = 0 ZF = 1
2、rep循环执行顺序:
WHILE CountReg ≠ 0
DO
Service pending interrupts (if any)
Execute associated string instruction // 1、执行相关指令。例如scansb指令,除了执行al与*di的比较外,di也会被影响,即di自减1(当DF==1时)或自加1(当DF==0时)
CountReg ← (CountReg – 1) // 2、ECX自减
IF CountReg = 0 // 3、判断ECX是否已减到0
THEN exit WHILE loopFI
IF (Repeat prefix is REPZ or REPE) and (ZF = 0) // 4、最后才判断其他相关标志。
or (Repeat prefix is REPNZ or REPNE) and (ZF = 1)
THEN exit WHILE loopFI
OD
3、scasb指令对di的影响:
After the comparison, the (E)DI register is incremented or decremented automatically according to the setting of
the DF flag in the EFLAGS register. If the DF flag is 0, the (E)DI register is incrementedif the DF flag is 1, the (E)DI
register is decremented. The register is incremented or decremented by 1 for byte operations, by 2 for word operations, and by 4 for doubleword operations.
以上指令请参考《Intel 64 and IA-32 Architectures Software Developer's Manual》。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)