- 四、打开太多文件
- 4.1 错误信息
- 4.2 系统限制
- 4.2 文件描述符优化
- 4.3 文件描述符监控
- 4.4 IO监控
四、打开太多文件
4.1 错误信息
E/art: ashmem_create_region failed for 'indirect ref table': Too many open files Java.lang.OutOfMemoryError: Could not allocate JNI Env
这个问题跟系统、厂商关系比较大
4.2 系统限制
Android是基于Linux内核,/proc/pid/limits
描述着linux系统对每个进程的一些资源限制,
如下图是一台Android 6.0的设备,Max open files的限制是1024
如果没有root权限,可以通过ulimit -n
命令查看Max open files,结果是一样的
ulimit -n
Linux 系统一切皆文件,进程每打开一个文件就会产生一个文件描述符fd(记录在/proc/pid/fd下面)
cd /proc/10654/fd
ls
这些fd文件都是链接文件,通过 ls -l
可以查看其对应的真实文件路径
当fd的数目达到Max open files规定的数目,就会触发Too many open files
的奔溃,这种奔溃在低端机上比较容易复现。
知道了文件描述符这玩意后,看看怎么优化~
4.2 文件描述符优化
对于打开文件数太多的问题,盲目优化其实无从下手,总体的方案是监控为主。
通过如下代码可以查看当前进程的fd信息
private fun dumpFd() { val fdNames = runCatching { File("/proc/self/fd").listFiles() } .getOrElse { return@getOrElse emptyArray() } ?.map { file -> runCatching { Os.readlink(file.path) }.getOrElse { "failed to read link ${file.path}" } } ?: emptyList() Log.d("TAG", "dumpFd: size=${fdNames.size},fdNames=$fdNames") }
4.3 文件描述符监控
监控策略:当fd数大于1000个,或者fd连续递增超过50个,就触发fd收集,将fd对应的文件路径上报到后台。
这里模拟一个bug,打开一个文件多次不关闭,通过dumpFd,可以看到很多重复的文件名,进而大致定位到问题。
当怀疑某个文件有问题之后,我们还需要知道这个文件在哪创建,是谁创建的,这个就涉及到IO监控~
4.4 IO监控
4.4.1 监控内容
监控完整的IO操作,包括open、read、write、close
open :获取文件名、fd、文件大小、堆栈、线程
read/write :获取文件类型、读写次数、总大小,使用buffer大小、读写总耗时
close :打开文件总耗时、最大连续读写时间
4.4.2 Java监控方案:
以Android 6.0 源码为例,FileInputStream
的调用链如下
java : FileInputStream -> IoBridge.open -> Libcore.os.open -> BlockGuardOs.open -> Posix.open
Libcore.java是一个不错的hook点
package libcore.io; public final class Libcore { private Libcore() { } public static Os os = new BlockGuardOs(new Posix()); }
我们可以通过反射获取到这个Os
变量,它是一个接口类型,里面定义了open、read、write、close
方法,具体实现在BlockGuardOs里面。
// 反射获得静态变量 Class<?> clibcore = Class.forName("libcore.io.Libcore"); Field fos = clibcore.getDeclaredField("os");
通过动态代理的方式,在它所有IO方法前后加入插桩代码来统计IO信息
// 动态代理对象 Proxy.newProxyInstance(cPosix.getClassLoader(), getAllInterfaces(cPosix), this); beforeInvoke(method, args, throwable); result = method.invoke(mPosixOs, args); afterInvoke(method, args, result);
此方案缺点如下:
- 性能差,IO调用频繁,使用动态代理和Java的字符串操作,导致性能较差,无法达到线上使用标准
- 无法监控Native代码,这个也是比较重要的
- 兼容性差:需要根据Android 版本做适配,特别是Android P的非公开API限制
4.4.3 Native监控方案
Native Hook方案的核心从 libc.so
中的这几个函数中选定 Hook 的目标函数
int open(const char *pathname, int flags, mode_t mode); ssize_t read(int fd, void *buf, size_t size); ssize_t write(int fd, const void *buf, size_t size); write_cuk int close(int fd);
我们需要选择一些有调用上面几个方法的 library,例如选择libjavacore.so、libopenjdkjvm.so、libopenjdkjvm.so
,可以覆盖到所有的 Java 层的 I/O 调用。
不同版本的 Android 系统实现有所不同,在 Android 7.0 之后,我们还需要替换下面这三个方法。
open64 __read_chk __write_chk
native hook 框架目前使用比较广泛的是爱奇艺的xhook ,以及它的改进版,字节跳动的bhook。
具体的native IO监控代码,可以参考 Matrix-IOCanary,内部使用的是xhook框架。
关于IO涉及到的知识非常多,后面有时间可以单独整理一篇文章。
接下来看看最后一种OOM类型~