Android系统及应用问题定位方法

1. 分析系统问题的思路

  • 从以下几方面入手进行分析:
    • 内存
      • 虚拟内存不足
      • 物理内存不足
    • 线程
    • 句柄

1.1 老化测试需关注的数据

# 说明:以下命令均需在root用户下执行,xx:为关注的进程名,pid: 为关注进程的ID
# 物理内存是否泄漏 
procrank | grep xx  # 重点关注USS<独占物理内存>大小
cat /proc/meminfo | grep MemAvailable

# 线程数量是否在不停增加
ps -t pid | wc -l
# 句柄数量是否在不停增加
ls -al /proc/pid/fd | wc -l
# 虚拟内存是否泄漏
cat /proc/pid/smaps | grep "anon:thread stack" | wc -l
cat /proc/pid/smaps | grep "anon:thread signal stack" | wc -l
cat /proc/pid/status | grep VmPeak  # 关注VmPeak是否不断增长

2. 分析内存

  • 目标:分析内存泄漏问题

2.1 进程内存使用总量

$ procrank | grep proc_name
  • procrank命令输出说明
    • VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存
    • RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)
    • PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
    • USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
  • 定位内存泄漏的方法
    • USS 的大小代表了只属于本进程正在使用的内存大小,这些内存在此进程被杀掉之后,会被完整的回收掉。
    • USS 是针对某个进程开始有可疑内存泄露的情况,进行检测的最佳数字。

2.2 进程内存使用细节

$ dumpsys meminfo  #查看所有的
$ dumpsys meminfo mediaserver #查看指定进程的

2.3 进程内存分布细节

$ cat /proc/pid/maps
$ cat /proc/pid/smaps | grep "anon:thread signal stack" | wc -l    #  java thread保护page数量
$ cat /proc/pid/smaps | grep "anon:thread stack" | wc -l     # native thread保护page数量

3. 分析线程

3.1 查看进程所有线程(含name)

$ ps -t pid | wc -l
  • 为方便分析问题,所有线程必须有Name
  • 分析线程泄漏时,除了关注个数变化之外,还必须关注线程TID的变化

4. 分析文件句柄

  • 目标:分析文件句柄泄漏问题

4.1 查看系统中所有文件句柄数:

$ lsof | busybox wc -l
#或
$ ls -al /proc/pid/fd | wc -l

4.2 查看指定进程的文件句柄数

$ cd /proc/pid/fd
$ ls | busybox wc -l 
# 或
$ lsof -p $pid | busybox wc -l 
# 查看进程句柄详细信息
ls -al  /proc/<pid>/fd | wc -l
# 按时间排序
ls -la /proc/6146/fd | sort -t " " -k 6    # 顺序
ls -la /proc/6146/fd | sort -t " " -k 6 -r  # 逆序
# -t 指定文本分隔符
# -k 指定排序列
# -n 按数字进行排序
# -r 翻转排序结果

4.3 查看系统最大文件限制

$ cat /proc/sys/fs/file-max 

4.4 OOM (Out Of Memory)基础知识

  • 虚拟内存管理

    • 虚拟空间的管理是以进程为基础的,每个进程都有各自的虚拟空间

    • 虚拟内存中虚拟地址/逻辑地址是连续的,便于灵活分配;虚拟内存可以是计算机呈现出比实际内存大的多的内存

    • 每个进程的“内核空间”是为所有的进程所共享的

    • 32位进程地址空间(0~4GB),Linux进程地址空间(虚拟内存)的组成:
      在这里插入图片描述

    • 一个进程的虚拟地址空间主要由两个数据结构来描述: mm_struct 和vm_area_struct
      在这里插入图片描述

  • Linux 在分配内存时, 为了节省内存, 按需分配, 使用了延时分配以及Copy-On-Write 的策略.

    • 延时分配即针对user space 申请memory 时, 先只是明面上的分配虚拟空间, 等到真正操作memory 时, 才真正分配具体的物理内存, 这个需要借助MMU 的data abort 转换成page fault 来达成. 这样就可以极大的避免因user space 过度申请memory, 或者错误申请memory 造成的memory 浪费.
    • 而Copy-On-Write 即是在进程fork 时, 子进程和父进程使用同一份memory, 只有当某块memory 被更新时, 才重新copy 出新的一份. 这个在android 上表现也非常显著, 上层app 包括system server 都由zygote fork 出来, 并且没重新exec 新的bin, ART VM/Lib 的memory 都是共享的, 可以极大的节省Memory 的使用.
  • 内存的整体使用情况

    • 要分析memory leaks, 你需要知道总体的内存使用情况和划分. 以判断内存泄露是发生在user space, kernel space, mulit-media 等使用的memory, 从而进一步去判断具体的memory leaks.
    • user space 使用的memory 即通常包括从进程直接申请的memory, 比如 malloc: 先mmap/sbrk 整体申请大块Memory 后再malloc 细分使用, 比如stack memory, 直接通过mmap 从系统申请; 以及因user space 进程打开文件所使用的page cache, 以及使用ZRAM 压缩 user space memory 存储所占用的memory
    • kernel space 使用的memory 通常包括 kernel stack, slub, page table, vmalloc, shmem 等.
    • mulit-media 使用的memory 通常使用的方式包括 ion, gpu 等.
    • 其他方式的memory 使用, 此类一般直接从buddy system 中申请出以page 为单位的memory, android 中比较常见如ashmem.
    • 而从进程的角度来讲, 通常情况下进程所使用的memory, 都会通过mmap 映射到进程空间后访问使用(注: 也会一些非常特别异常的流程, 没有mmap 到进程空间), 所以进程的memory maps 资讯是至关重要的.
  • Android中导致OOM的主要原因:
    在这里插入图片描述

4.4.1 堆内存分配失败

// 系统源码文件:/art/runtime/gc/heap.cc
void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type)
//抛出时的错误信息:
    oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free  << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM";
// 调用/art/runtime/thread.cc中如下函数抛出异常
void Thread::ThrowOutOfMemoryError(const char* msg) // 参数msg中有OOM时的错误信息    
  • 这是在进行堆内存分配时抛出的OOM错误,这里也可以细分成两种不同的类型:
    • 为对象分配内存时达到进程的内存上限。由Runtime.getRuntime.MaxMemory()可以得到Android中每个进程被系统分配的内存上限,当进程占用内存达到这个上限时就会发生OOM,这也是Android中最常见的OOM类型。
    • 没有足够大小的连续地址空间。这种情况一般是进程中存在大量的内存碎片导致的,其堆栈信息会比第一种OOM堆栈多出一段信息:failed due to fragmentation (required continguous free “<< required_bytes << “ bytes for a new buffer where largest contiguous free ” << largest_continuous_free_pages << “ bytes)”; 其详细代码在art/runtime/gc/allocator/rosalloc.cc中

4.4.2 创建线程失败

//系统源码文件:/art/runtime/thread.cc
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon)
//抛出时的错误信息:
//    "Could not allocate JNI Env"
//  或者
//    StringPrintf("pthread_create (%s stack) failed: %s", PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
  • 这是创建线程时抛出的OOM错误,且有多种错误信息。下面是根据源码整理的Android中创建线程的步骤,其中两个关键节点是创建JNIEnv结构体和创建线程,而这两步均有可能抛出OOM
    在这里插入图片描述
  • 创建线程也可以归纳为两个步骤:
    • 调用mmap分配栈内存。这里mmap flag中指定了MAP_ANONYMOUS,即匿名内存映射。这是在Linux中分配大块内存的常用方式。其分配的是虚拟内存,对应页的物理内存并不会立即分配,而是在用到的时候触发内核的缺页中断,然后中断处理函数再分配物理内存
    • 调用clone方法进行线程创建
  • 分配栈内存失败是由于进程的虚拟内存不足,抛出错误信息如下:
W/libc: pthread_create failed: couldn't allocate 1073152-bytes mapped space: Out of memory
W/tch.crowdsourc: Throwing OutOfMemoryError with VmSize  4191668 kB "pthread_create (1040KB stack) failed: Try again"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
        at java.lang.Thread.nativeCreate(Native Method)
        at java.lang.Thread.start(Thread.java:753)
  • clone方法失败是因为线程数超出了限制,抛出错误信息如下:
W/libc: pthread_create failed: clone failed: Out of memory
W/art: Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
  at java.lang.Thread.nativeCreate(Native Method)
  at java.lang.Thread.start(Thread.java:1078)

4.4.3 创建JNIEnv失败

  • JNI创建步骤:
    • 通过Andorid的匿名共享内存(Anonymous Shared Memory)分配 4KB(一个page)内核态内存。
    • 再通过Linux的mmap调用映射到用户态虚拟内存地址空间
  • 创建匿名共享内存时,需要打开/dev/ashmem文件,所以需要一个FD(文件描述符)。此时,如果创建的FD数已经达到上限,则会导致创建JNIEnv失败,抛出错误信息如下:
E/art: ashmem_create_region failed for 'indirect ref table': Too many open files
 java.lang.OutOfMemoryError: Could not allocate JNI Env
   at java.lang.Thread.nativeCreate(Native Method)
   at java.lang.Thread.start(Thread.java:730)
  • 调用mmap时,如果进程虚拟内存地址空间耗尽,也会导致创建JNIEnv失败,抛出错误信息如下:
E/art: Failed anonymous mmap(0x0, 8192, 0x3, 0x2, 116, 0): Operation not permitted. See process maps in the log.
java.lang.OutOfMemoryError: Could not allocate JNI Env
  at java.lang.Thread.nativeCreate(Native Method)
  at java.lang.Thread.start(Thread.java:1063)

4.5 OOM (Out Of Memory)实例分析

  • mmap failed: Out of Memory
    • mmap只负责分配虚拟内存,出现OOM的原因是虚拟空间用完,即虚拟地址泄漏
  • 线程已退出,但其在smaps中的保护page没有释放,即
$ ps -t pid | wc -l  # 数量不变
$ cat /proc/pid/smaps | grep "anon:thread stack" | wc -l  # 数量不断增加
  • 本质原因
    • pthread_create()创建的线程默认是joinable的,joinable线程没有被其他线程joined就会造成内存泄漏
    • 对于joinable线程,系统会分配私有内存存储线程结束状态、线程栈、线程ID等等资源。这些资源会一直存在,直到线程结束并且线程被其他线程joined
    • 确保joinable线程资源得到释放的两个条件是:线程退出、被其他线程joined
    • 对于detached线程,如果其退出,那么系统会自动回收其占用的资源
  • joinable线程没有被其他线程joined造成内存泄漏的验证
    • 有虚拟地址空间泄漏
#include<stdio.h>
#include<pthread.h>

void run() {
   pthread_exit(0);
}

int main () {
    pthread_t thread;
    int rc;
    long count = 0;
    while(1) {
        if(rc = pthread_create(&thread, 0, run, 0) ) {
            printf("ERROR, rc is %d, so far %ld threads created\n", rc, count);
            perror("Fail:");
            return -1;
        }
        usleep(10);
        count++;
    }
    return 0;
}
  • continue
    • 无虚拟地址空间泄漏
#include<stdio.h>
#include<pthread.h>

void run() {
   pthread_exit(0);
}

int main () {
    pthread_t thread;
    int rc;
    long count = 0;
    while(1) {
        if(rc = pthread_create(&thread, 0, run, 0) ) {
            printf("ERROR, rc is %d, so far %ld threads created\n", rc, count);
            perror("Fail:");
            return -1;
        }
        pthread_join(thread, NULL);  // 将释放stack和anon:thread stack guard page
        usleep(10);
        count++;
    }
    return 0;
}

5. JNI 参考表溢出

5.1 全局参考表溢出

  • 相关错误信息
JNI ERROR (app bug): global reference table overflow (max=512000)
  • 它是一个进程的JNI参考表
  • 从以下两方便定位此问题:
    • 在JNI中引用之后,没有正常释放(此类问题一看代码便知)
    • 由线程泄漏引起,此类问题会非常快速地引起溢出

5.2 局部参考表溢出

  • 相关错误信息
JNI ERROR (app bug): local reference table overflow (max=512)
  • 它是一个线程的JNI参考表
  • 解决方法:此类问题是由于没有正常释放引用导致的

6. 待机问题

6.1 应用程序持有wakelock

$ dumpsys power
$ dumpsys power | busybox grep "Wake Locks"   # 查看多少个应用持有WakeLock
  • 查看Wake Locks: size的值, 若其值不为0,则不能进入深度待机,然后分析哪些应用持有wake_lock

6.2 设备驱动持有wakelock

  • 查看内核持有的wake_lock状态
$ cat /sys/power/wake_lock
  • 在待机过程中被wakeup_sources唤醒
$ cat /sys/kernel/debug/wakeup_sources 
  • 在linux-3.10上,wake_lock的本质就是wakeup_sources
  • 查看active_since的值是否为非0,若为非0,表明此wakeup_source已经阻止系统进入休眠多少ms

6.3 待机唤醒Kernel入口 state_store

  • 文件名:linux-3.10\kernel\power\main.c
static ssize_t state_store(struct kobject *kobj, struct kobj_attribute *attr,
			   const char *buf, size_t n)
{
	suspend_state_t state;
	int error;

    // added by luohj for debug on 2019.8.17
    printk(KERN_ERR "***%s:%d, buf=%s\n", __FUNCTION__, __LINE__, buf);

	error = pm_autosleep_lock();
	if (error)
		return error;

	if (pm_autosleep_state() > PM_SUSPEND_ON) {
		error = -EBUSY;
		goto out;
	}

	state = decode_state(buf, n);
	if (state < PM_SUSPEND_MAX)
		error = pm_suspend(state);
	else if (state == PM_SUSPEND_MAX)
		error = hibernate();
	else
		error = -EINVAL;

 out:
	pm_autosleep_unlock();
	return error ? error : n;
}

7. 查看所有USB设备

$ cat /sys/kernel/debug/usb/devices   // 可看到其驱动名

8. 禁止鼠标唤醒

  • 文件名: frameworks\base\policy\src\com\android\internal\policy\impl\PhoneWindowManager.java
    public int interceptMotionBeforeQueueingWhenScreenOff(int policyFlags) {
        int result = 0;
        final boolean isWakeMotion = (policyFlags
                & (WindowManagerPolicy.FLAG_WAKE | WindowManagerPolicy.FLAG_WAKE_DROPPED)) != 0;
        if (isWakeMotion) {
            result |= ACTION_WAKE_UP;
        }
        //return result;
		return 0; // modified by luohj to prohibit mouse wakeup the system 2019.8.17
    }
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值