现象分析
现场排查
起因keeptesting休眠唤醒出现vmap allocation申请内存失败,log打印如下
1 |
|
通过查看显示相关的进程1
2
3
4
5
6
7
8
9
10
11
12
13
14t7-p3:/ # procrank
PID Vss Rss Pss Uss Swap PSwap USwap ZSwap cmdline
2387 1050156K 141496K 43392K 32760K 0K 0K 0K 0K com.android.systemui
------ ------ ------ ------ ------ ------ ------
479314K 354572K 708K 708K 708K 534K TOTAL
ZRAM: 676K physical used for 896K in swap (754616K total swap)
RAM: 1006160K total, 50708K free, 2984K buffers, 513540K cached, 1016K shmem, 46584K slab
查看对应的dma_buf分配情况,发现个数不断的增大
procmem -p 2387 | grep dmabuf | grep 1948 | wc -l
通过在ion_map的时候将进程信息添加到对应的ion debug节点中,unmap再删除,看到异常的dmabuf对应的进程;对应的是2880 systemui和launcher
1 | t7-p3:/ # cat /sys/kernel/debug/ion/heaps/cma | grep dmabuf | grep 1994 |
跟踪logcat中gralloc alloc的时候的打印,发现异常截图size与截图的大小一致;
1 | 03-26 19:34:17.096 1980 2160 D SurfaceFlinger: captureLayers here sourcecrop = 0 0 - 1920 720 |
内存回收
首先怀疑是否为安卓内存没有回收导致
强制触发内存回收
方式一:kill -10 pid
方式二:am dumpheap com.test.test /sdcard/test.hprof
方式三:在进程onReceive中添加System.gc() ;然后在adb shell中输入 am broadcast -a INTENT_ACTION_NAME_HERE
安卓系统内存回收可以分为三种情况:
第一,用户程序调用StartAcitity(),使当前的活动的Activity被覆盖;
第二,用户按BACK键,退出当前应用程序;
第三:启动一个新的应用程序。这些能够触发内存回收的时间最终调用的函数接口就是activityIdleInternal
通过kill -10 pid 的方式,触发安卓内存进行回收,发现内存没有释放;
从内核debug节点也看到此问题是上层systemui和launcher映射的fd没有释放导致;
休眠截屏流程
1 | services/core/java/com/android/server/wm/TaskSnapshotController.java |
在休眠老化过程中,将shouldDisableSnapshots默认配置为true,没有出现内存泄漏,更加确定与截屏流程有关;
进一步确认:
配合dumpsys命令,传入虚拟内存地址和长度,最终调用如下代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14static void native_dumpAddr(JNIEnv* /* env */, jobject /* clazz */, jint addr, jint len) {
std::stringstream ss;
ss << "/storage/emulated/0/" << std::hex << addr;
const char * path = ss.str().c_str();
int fd = open(path, O_CREAT | O_WRONLY | O_NOFOLLOW | O_CLOEXEC | O_APPEND, 0666);
if (fd < 0) {
ALOGI("error opening: %s: %s", path, strerror(errno));
return;
}
write(fd, (void *)addr, len);
close(fd);
}
直接在泄漏进程中直接dump虚拟内存内容,转换成图片格式后,确认为截图;
测试场景脚本化
在出现异常的现场时,如果能简化到某些行为,可以使debug的范围很大程度的缩小;在验证问题是否解决的时,也更容易判断
1 | while true; |
怀疑方向
从泄漏问题来看,有两点有疑问,一: 主界面为什么不泄露,设置界面泄漏?二:为什么与截屏流程有关,且影响到systemUI和launcher?
主界面没有泄漏原因
从log上看主界面之所以没有泄漏,是因为主界面不走截屏的流程;那么下一步的动作就是看为什么主界面没有截屏
1 | /** Activity type is currently not defined. */ |
结合打印和代码可以发现,由于主界面activity属性为ACTIVITY_TYPE_HOME,故不截屏;而设置界面中,activity属性为ACTIVITY_TYPE_STANDARD;故进行截屏;
dumpsys window
并无出现截图数量递增的情况;
安卓P上默认无论是否为低内存设备不再提供禁止截屏的接口,把截屏的第一帧动画当做第一帧进行显示;低内存只能更改第一帧的采样率
services/core/java/com/android/server/wm/TaskSnapshotController.java
final float scaleFraction = isLowRamDevice ? 1f : 1f;
截屏作用
1 | ./services/core/java/com/android/server/am/ActivityRecord.java showStartingWindow |
即截图的图片在启动动画的时候会用来做第一帧动画;如果有keyguard的时候,唤醒启动的时候则会去那休眠前截好的图作为第一帧动画显示;没有则不用走到这里来;T7没有keyguard,故休眠唤醒的流程也不会经过这里;
SystemUi截屏相关流程
1 | 启动流程: |
在休眠唤醒的流程里面,添加打印,并未发现有systemui进入到getSnapshot的相关操作。至此在应用排查阶段一直没有进展;另外烧录gsi同样能发现内存泄漏的问题;怀疑还是自己平台修改导致的可能性比较大;故在目光转移至gralloc分配流程;
进程的地址空间
进程的地址空间(address space)由允许进程使用的全部线性地址(memery region,其含义通常所指的虚拟内存的一个区间,可以称为虚存区 VMA Virtual Memory Area )组成。每个进程所看到的线性地址集成是不同的,一个进程所使用的地址和另外一个进程所使用的地址之间没有什么关系。后面我们还会看到,内核可以通过增加或删除某些线性地址区间来动态地修改进程的地址空间;
内存描述符:与进程地址空间有关的全部信息都包含在一个叫做内存描述符(memory descriptor)的数据结构中,这个类型为mm_struct,进程描述符的mm字段就指向这个内存结构;
ION基本概念
ION作用:用于用户空间的进程之间或者内核空间的模块之间进行内存共享;而且这种共享是0拷贝的;
对于ION的基本概念可以看下篇文章;
free时机
一般我们认为当ref_count = 0的时候就会去release这块内存
1 | drivers/staging/android/ion/ion.c |
可以看到ion buffer的free动作是在在引用计数为0的时候去清理的;实际此时当不同进程都map到同一个buffer的时候,这时候还有一个引用计数会决定是否真正的close;
通过ioct free的时候
Object | Operations | Ion buffer ref count | Ion buf status |
---|---|---|---|
allocator@2.0-s(alloc_device_alloc) | 1.ion_alloc | 1 | allocated |
2.ion share:dmabuf fd1 | 2 | — | |
surfaceflinger | gralloc_register_buffer (map) |
2 | |
surfaceflinger | gralloc_lock | 2 | |
surfaceflinger | gralloc_unlock | 2 | |
surfaceflinger | gralloc_unregister_buffer (unmap) |
2 | |
allocator@2.0-s(alloc_device_free) | 3.close(ion buffer个数不减少) | 2 | — |
4.ion_free | 1(2 -> 1) | using | |
systemUI | gralloc_register_buffer (map) |
1 | using |
systemUI | FinalizerDaemon gralloc_unregister_buffer (unmap) |
0(1 -> 0) | freed |
内存映射
mmap是一种内存映射文件的方法,即将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和虚拟内存
mmap三个阶段:
一:进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;
二:调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
三:进程发起对这片映射空间的访问,引发缺页异常;实现文件内容到物理内存(主存)的拷贝
内存映射类型:
私有内存映射:多个进程会创建一个新的映射,各个进程不共享,也不会反应到物理文件中,比如linux.so动态库就是采用这种形式映射到各个进程虚拟地址空间中;
私有匿名映射:mmap会创建一个新的映射,各个进程不共享,这种使用主要用于分配内存
共享文件映射:多个进程通过虚拟内存技术共享同样的物理内存空间,对内存文件会反映到实际物理文件中,他也是进程通信的机制
共享匿名映射:这种机制在fork的时候不会采用写时复制,父子进程完全共享同样的物理内存,这也就实现了父子间通信(IPC)
匿名的意思是需不需要一个fd,正常来讲,当需要进行内存映射的时候,我们都需要依赖一个文件才能实现;通常需要open一个temp文件,穿件后unlink,close掉,比较麻烦;可以直接使用匿名映射来代替,linux也提供了这么一套机制给我们;无需依赖文件即可创建。:
如:int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);映射流程:
1 | DPATH="/sys/kernel/debug/tracing" |
1 | mappedAddress = (unsigned char *)mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, hnd->share_fd, 0); |
do_map
1 | do_mmap |
unmap流程
1 | do_munmap |
释放时的log
1 | [ 1689.870445] ion_buffer_put count:0 phys_adr 72100000 |
只有当映射同一块进程都解除映射后,file->f_count,为0;才会调到ion_buffer_put,此时ion_buffer计数为0,释放该buffer
userspace 使用
mali-utgard/gralloc/src/gralloc_module.cpp
进程创建buffer的流程1
2
3
4
5
6
7
8
9
10
11
12
13static struct hw_module_methods_t gralloc_module_methods =
{
.open = gralloc_device_open
};
gralloc_device_open
alloc_device_open
m->ion_client = ion_open();
alloc_device_alloc
gralloc_alloc_buffer
ret = aw_ion_alloc(&(m->ion_client), size, 0, heap_mask, flags, &(ion_hnd), usage);
ret = ion_share(m->ion_client, ion_hnd, &shared_fd);
cpu_ptr = mmap(NULL, size, map_mask, MAP_SHARED, shared_fd, 0);
结构体gralloc_module_t定义在文件hardware/libhardware/include/hardware/gralloc.h中,它主要是定义了四个用来操作图形缓冲区的成员函数,如下所示:1
2
3
4
5
6
7
8
9private_module_t::private_module_t()
{
```
base.registerBuffer = gralloc_register_buffer;
base.unregisterBuffer = gralloc_unregister_buffer;
base.lock = gralloc_lock;
base.unlock = gralloc_unlock;
```
}
registerBuffer和unregisterBuffer分别用来注册和注销一个指定的图形缓冲区,这个指定的图形缓冲区使用一个buffer_handle_t句柄来描述。所谓注册图形缓冲区,实际上就是将一块图形缓冲区映射到一个进程的地址空间,注销图形缓冲区则是相反的过程;成员函数lock和unlock分别用来锁定和解锁一个缓冲区,例如向一块图形缓冲写入内容的时候,需要将图形缓冲区锁定。用来避免访问冲突;
gralloc 流程debug
对于systemui来讲,在此过程并不会去申请buffer,而是通过内存映射,所以我们主要关注register和unregiser的流程
添加引用计数:
gralloc_register_buffer
if (size == 1990720) {
getNameByPid(hnd->pid,task_name);
count = count + 1;
AERR(" 0x%p process %d task_name:%s size:%d share_fd:0x%x count:%d ", hnd, hnd->pid, task_name, size, hnd->share_fd, count);
}
gralloc_unregister_buffer
if (size == 1990720) {
getNameByPid(hnd->pid,task_name);
count= count -1;
AERR(" 0x%p process %d task_name:%s size:%d share_fd:0x%x count:%d ", hnd, hnd->pid, task_name, size, hnd->share_fd, count);
}
应用计数log:1
2
3gralloc_unregister_buffer:345 0x0xa96c52c0 process 2327 task_name:ndroid.systemui size:1990720 share_fd:0x5b count:2
gralloc_unregister_buffer:345 0x0xaeca7400 process 2327 task_name:ndroid.systemui size:1990720 share_fd:0x59 count:1
gralloc_unregister_buffer:345 0x0xaeccd7f0 process 2327 task_name:ndroid.systemui size:1990720 share_fd:0x4c count:0
通过打印发现,systemUI的register动作和unregister的动作是匹配的,count也能清零;但依然进程中的dmabuf异常存在泄漏
对应的dmabuf如下:1
2t7-p3:/ # procmem -p 2327 | grep dmabuf
1948K 0K 0K 0K 0K 0K 0K 0K anon_inode:dmabuf-c3
且kill -10 后系统也没有完成回收,说明此时已经存在内存泄漏
查看unregister相关代码发现
1 | gralloc_unregister_buffer |
当存在LOCK_WRITE标识的时候,此时不会被unmap,那么什么时候这个标志是什么时候被赋值的呢?surfaceFlinger在创建这块内存时,对于surfaceFlinger而言;由于截图的动作比较频繁,会调用lock为buffer添加锁保护,当写完之后unlock;这时候systemUI恰好映射到这块内存恰好会将其标志拷贝;对于systemUI而言,本身不会有写的操作,故更不会有unlock的动作;所以就发生了泄漏;解决这个问题的方法。就是在进程映射内存的时候;将这个标志清除掉;
1 | gralloc_register_buffer |
安卓O同样存在同样的问题,但由于项目一开始为了减少设备的内存使用,所以禁止了相关截图的功能,在androidP上,谷歌没有提供相关的全局禁止功能,故问题就暴露出来;
补丁如下:
1 | 在方案下面的:venus_a1.mk |