前言

在操作体系中,任何资源的运用都是受约束的,诸如进程数量、fd最大数量、信号缓存数量都是受到约束的。关于FD而言,FD不足或许引发OOM,其他情况下,还会形成Socket网络连接翻开失败、进程无法创立成功以及I/O无法正常进行等问题,对FD的监控和走漏检测明显也是必要的。

咱们先来读下 com.android.server.Watchdog.OpenFdMonitor源码中这样一段注释

英文:
Number of FDs below the soft limit that we trigger a runtime restart at. This was
chosen arbitrarily, but will need to be at least 6 in order to have a sufficient 
number of FDs in reserve to complete a dump.
中文:
当翻开的文件描述符(File Descriptors, FDs)数量低于某个软约束阈值时,咱们触发运行时重启的策略。
这个阈值是恣意设定的,但为了保证有满足的文件描述符来完结一个备份或数据dump操作,它至少需求设置为6

其实,过低的话就会重启system_server ,假如连system_server 都要重启,那么,一般app该怎么处理呢?

在体系中,咱们能够读取到一些相关资源的约束,比方open files (FD)、lock files (文件锁)等,咱们需求尽或许防止触最大数量。

root@vbox86p:/proc/2351 # cat limits
        Limit                     Soft Limit           Hard Limit           Units
        Max cpu time              unlimited            unlimited            seconds
        Max file size             unlimited            unlimited            bytes
        Max data size             unlimited            unlimited            bytes
        Max stack size            8388608              unlimited            bytes
        Max core file size        0                    unlimited            bytes
        Max resident set          unlimited            unlimited            bytes
        Max processes             7997                 7997                 processes
        Max open files            1024                 4096                 files
        Max locked memory         65536                65536                bytes
        Max address space         unlimited            unlimited            bytes
        Max file locks            unlimited            unlimited            locks
        Max pending signals       7997                 7997                 signals
        Max msgqueue size         819200               819200               bytes
        Max nice priority         40                   40
        Max realtime priority     0                    0
        Max realtime timeout      unlimited            unlimited            us  

走漏检测和水位监控

一般情况下,监控FD有两种办法,也用于两种场景

  • 第一种场景对比任务从开端到完毕期间的FD差异数据,找出走漏的FD和新增的FD,经过这样的办法来检查FD是不是存在走漏。
  • 第二种场景便是水位预警,水位预警是设置一个阈值,然后在某一时刻对proc/{pid}/fd目录中的文件数量进行检查,超越阈值之后就会触发预警。

目前类似KOOM的完成便是第二种完成的,实际上在Android 9版别开端,在System_server进程也完成了FD的水位预警,可是这个预警办法只支撑 Android 9版别,此外只适用于DEBUG模式。

本篇,咱们会参阅Android 体系的中的水位预警,完成一个能够简略运用的水位预警办法,尽或许兼容一切版别,同时,咱们会运用一种更加友好的办法对FD进行监控。

以上是检测逻辑,那么,水位监控怎么完成呢?

监控现有完成

在Android SystemServer中,官方的com.android.server.Watchdog中供给了OpenFDMonitor,然后守时检测,这点其实和KOOM类似,不同的当地是,体系版别获取到了最大的数量约束,这个明显要比KOOM的配置阈值完成要精确一些。

关键逻辑

在OpenFDMonitor 中,最中心的逻辑是对文件FD进行检测,假设咱们1024为最大数量,那么抵达900或许就需求进行预警,而Android体系利用fd的一些特性完成检测,详细是什么特性,咱们持续往下看

// fdMaxLimit - 最大约束数量
// FD_HIGH_WATER_MARK 冗余数量
final File fdThreshold = new File("/proc/self/fd/" + (fdMaxLimit - FD_HIGH_WATER_MARK));

假如到达水位,那么意味着文件现已生成了。

isReachedWaterMark = fdThreadshold.exists();

体系特性

到这儿或许有这样一个疑问,fd生成之后,FD文件的序号是递加的么?

假如递加,那么/proc/{pid}/fd/1 到/proc/{pid}/fd/100之间,假如有空地怎么办?

实际上,这个忧虑是没有必要的,官方完成必定考虑到这种问题了,不过,咱们用一个简略的实验来证明这个忧虑是不必要的。

下面,咱们写1002个文件

try {
    File file = new File("/proc/self/fd");
    File[] listFiles = file.listFiles();
    File startMaxFdFile = listFiles[listFiles.length -1];
    FileOutputStream first = new FileOutputStream(getCacheDir()+"/0.text");
    first.write(100);
    first.close();
    for (int i = 1; i < 1000; i++) {
        FileOutputStream fis = new FileOutputStream(getCacheDir()+"/" + i + ".text");
        fis.write('A');
        fis.close();
    }
    FileOutputStream end = new FileOutputStream(getCacheDir()+"/end.text");
    end.write('A');
    end.close();
  listFiles = file.listFiles();
  File endMaxFdFile = listFiles[listFiles.length -1];
  Log.d(TAG,"end maxFdFile ="+ endMaxFdFile +", start maxFdFile ="+startMaxFdFile);
} catch (IOException e) {
    e.printStackTrace();
}

最终,日志输出如下,明显,在合理close之后,这个fd数量并没有明显变大。

end maxFdFile =/proc/self/fd/104, start maxFdFile =/proc/self/fd/84

那是不是存在空地呢?咱们只需求看看listFiles最终一次的结果就知道了

Android FD水位监控完成原理

从图上就能看出来,这些fd序号是坚持最小准则的,也是坚持接连的。

因而,咱们定论如下

FD 序号在坚持最小准则和坚持接连的情况下是递加的,也便是说,优先最小准则、其次是次序、最终是递加。

fd maxLimit获取

这个过程咱们需求拿到fd的最大数量

在Android 9中,获取办法简略的多,只不过这些类是@hide,需求借助freeflection这样的工具去完成反射。

final StructRlimit rlimit;
try {
    rlimit = android.system.Os.getrlimit(OsConstants.RLIMIT_NOFILE);
} catch (ErrnoException errno) {
    Slog.w(TAG, "Error thrown from getrlimit(RLIMIT_NOFILE)", errno);
    return null;
}

StructRlimit类的主要字段如下

public final class StructRlimit {
    public final long rlim_cur;  //soft limit - 软约束 ,fd最大的数量
    public final long rlim_max;  //hard limit - 硬约束,也是fd的数量,可是这个值暂时无法解释
    public StructRlimit(long rlim_cur, long rlim_max) {
        this.rlim_cur = rlim_cur;
        this.rlim_max = rlim_max;
    }
    @Override 
    public String toString() {
        return Objects.toString(this);
    }
}

可是,这个类并不能直接拜访,假如要运用本篇的功用,请在下面目录将StructRlimit代码放进去。

src/main/java/android/system

当然,为了低版别不依赖进去,建议专门建一个android moudle,然后compileOnly此模块,假如要反躲藏的类更多,compileOnly的优势就会很明显,究竟这种其实更加便利的削减反射和类冲突。

即便是这样,Android 9.0之前的版别依然无法运用。

Android 9.0之前的版别怎么处理呢?

实际上,咱们运用 “adb shell ulimit -n “是最简略的,可是在应用中不让调用,可是,在本篇最初命令中,咱们是能够读取到limits的。咱们能够运用i/O的办法,读取下面的文件,进行解析即可

/proc/{pid}/limits

下面是Android 9.0之前的版别兼容逻辑,咱们以本篇最初的内容,解分出soft limits便是fd的最大数量了。

private static long getFdMaxLimit() {
    long[] limit = new long[2];
    try {
        String FD_LINE = "Max open files";
        File file = new File("/proc/" + android.os.Process.myPid() + "/limits");
        InputStream inputStream = new FileInputStream(file);
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String line;
        while ((line = br.readLine()) != null) {
            String trimLine = line.trim();
            if (!trimLine.startsWith(FD_LINE)) {
                continue;
            }
            String data = trimLine.substring(FD_LINE.length());
            String[] strData = readStringValue(data);
            if (strData == null || strData.length < 3) {
                throw new ParcelFormatException("Parse data error : " + data);
            }
            Log.d(TAG, "Soft Limit =" + strData[0] + ",Hard Limit=" + strData[1] + ",Units=" + strData[2]);
            limit[0] = strToLongNumber(strData[0]);
            limit[1] = strToLongNumber(strData[1]);
        }
        br.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return limit[0];
}

这样,咱们就能够拿到FD的约束数量,也便是说,咱们拿到了最高水位,咱们只需求运用最高水位减去一个定值,就能计算出预警文件序号。

这儿要说的一个问题是,Android中水位过低问题十分严峻,就有或许杀死其他进程,鉴于OpenFDMonitor源码中这个阈值是12,那作为一般app,这个值咱们能够更大一些。

为什么这样说呢,究竟WatchDog是运行在体系进程的,它的存活能力是强于一般app的,因而,一般进程理论上死的更早。

private static final int FD_HIGH_WATER_MARK = 128;
//假如最大值减去这个序号生成的fd存在,那么就意味着fd或许耗尽了

检测办法完成

上面咱们说过,中心原理便是监控文件是不是存在,详细完成代码如下。

public boolean monitor() {
    if (mFdHighWaterMark.exists()) {
        dumpOpenDescriptors();
        return true;
    }
    return false;
}

在代码中咱们dump一下到底有多少个FD走漏。

dump 逻辑

区别于运用Os.readLink的办法,此办法不兼容Android 6.0之前的体系,此外此办法容易呈现读取异常,由于自身很多FD并不是文件,或许是Messenger的中的epoll,也或许是socket,体系中的OpenFdMonitor运用了lsof去检测,不仅仅兼容性强(Os.readLink不支撑低版别),不过受限于Permission机制,只能知道详细的FD节点link,但详细指向哪个socket、文件,详细是什么类型就无法知道了

lsof 命令是linux中运行时定位资源进程的工具,有爱好的话能够看看linux相关常识。

下面的逻辑我做了调整

  • 写入首行,由于包括标题信息
  • FD挑选,是不是要包括体系FD
  • 兼容低版别,体系的监控运用了高版别api,因而咱们需求兼容一下

下面是dump的中心完成

public void dumpOpenDescriptors() {
    try {
        File dumpFile = new File(mDumpDir, "anr_fd_" + SystemClock.elapsedRealtime());
        int pid = Process.myPid();
        java.lang.Process proc = new ProcessBuilder()
                .command("/system/bin/lsof", "-p", String.valueOf(pid))
                .redirectErrorStream(true)
                .start();
        int returnCode = proc.waitFor();
        if (returnCode != 0) {
            Log.w(TAG, "Unable to dump open descriptors, lsof return code: "
                    + returnCode);
            dumpFile.delete();
        } else {
            FileOutputStream fis = new FileOutputStream(dumpFile);
            InputStream inputStream = proc.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            boolean isFirstLine = true;
            while ((line = br.readLine()) != null) {
                if (isFirstLine || containSystemFd || line.contains(" " + pid + " ")) {
                    isFirstLine = false;
                    fis.write(line.getBytes());
                    fis.write("n".getBytes());
                }
            }
            fis.close();
            br.close();
        }
    } catch (IOException | InterruptedException ex) {
        Log.w(TAG, "Unable to dump open descriptors: " + ex);
    }
}

检测 & 运用

一般情况下,在适宜的方位,咱们调用下面的办法即可。

fdMonitor.monitor()

假如到达阈值,就会触发dump 逻辑,直接将文件写入本地文件

COMMAND     PID       USER   FD      TYPE             DEVICE  SIZE/OFF       NODE NAME
com.yuyan 17841     u0_a80  exe       ???                ???       ???        ??? /system/bin/app_process32
com.yuyan 17841     u0_a80    0       ???                ???       ???        ??? /dev/null

检测频率

常见的开源结构是守时检测的,当然,这种基于功能好一些的设备,关于功能差一些的设备,要尽或许削减频次。

总归,关于一般app,守时检测是适宜的,可是关于低配设备,应该削减检测频率。

举个比如:假如是视频播放应用,理论上在关闭播放页时检测在某个RetainFragment的finalize办法中检测就差不多了。

别的,还有一种较好的办法,咱们能够利用Lifecycle的机制,监控Fragment和Activity生命周期,在onDestroyed时分检测会比较适宜。

OpenFdMonitor openFdMonitor = OpenFdMonitor.create(getApplicationContext());
mFragmentManager.registerFragmentLifecycleCallbacks(new FragmentManager.FragmentLifecycleCallbacks() {
    @Override
    public void onFragmentDestroyed(FragmentManager fm, @NonNull Fragment f) {
        super.onFragmentDestroyed(fm, f);
        boolean isReached = openFdMonitor.monitor();
        if(isReached){
            Event.Happen(EVENT_FD_LIMIT_WARNING);
            Log.d(TAG,"FD 触发预警");
            return;
        }
    }
});

问题排除和修正

咱们收到预警之后,必定要去关闭一些FD,防止引发OOM。同时,也要分分出走漏问题。这方面其实很简略,这儿不在赘述了。

总结

本篇就到这儿,咱们本篇主要是FD ,还有一点要补充的是,这个FD的约束并不是每个体系固定的,别的,FD的数量也是体系中的数量,而不单单是单个app中的数量,因而,运用的时分必定要注意这个问题。

别的,在本篇之前,咱们还写过《Android HandlerThread FD 优化》一文,其间利用共享Looper机制,完成MessageQueue削减创立,进一步削减fd的数量,由于MessageQueue创立之后会创立至少2个FD,因而,经过这种手段能够有效下降FD走漏问题,目前,该计划也在推进中。

源码

public final class OpenFdMonitor {
    /**
     * Number of FDs below the soft limit that we trigger a runtime restart at. This was
     * chosen arbitrarily, but will need to be at least 6 in order to have a sufficient number
     * of FDs in reserve to complete a dump.
     */
    private static final int FD_HIGH_WATER_MARK = 128;
    //假如最大值减去这个序号生成的fd存在,那么就意味着fd或许耗尽了
    private static final String TAG = "OpenFdMonitor";
    private static final int RLIMIT_NOFILE = getRLimitNoFile();
    private final File mDumpDir;
    private final File mFdHighWaterMark;
    private boolean containSystemFd = false;
    private boolean isReached = false;
    private static int getRLimitNoFile() {
        if (Build.VERSION.SDK_INT <= 28) {
            return -1;
        }
        try {
            Field rlimitNofile = OsConstants.class.getDeclaredField("RLIMIT_NOFILE");
            rlimitNofile.setAccessible(true);
            return rlimitNofile.getInt(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return -1;
    }
    public static OpenFdMonitor create(Context context) {
        final String dumpDirStr = SystemProperties.get("dalvik.vm.stack-trace-dir", context.getCacheDir().getAbsolutePath());
        if (dumpDirStr.isEmpty()) {
            return null;
        }
        long fdMaxLimit = 0;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            final StructRlimit rlimit;
            try {
                rlimit = android_systemOs_getrlimit(RLIMIT_NOFILE);
                fdMaxLimit = rlimit.rlim_cur;
            } catch (Exception errno) {
                Log.w(TAG, "Error thrown from getrlimit(RLIMIT_NOFILE)", errno);
                return null;
            }
        } else {
            fdMaxLimit = getFdMaxLimit();
        }
        if (fdMaxLimit <= 0) {
            Log.w(TAG, "fdMaxLimit is invalid");
            return null;
        }
        final File fdThreshold = new File("/proc/self/fd/" + (fdMaxLimit - FD_HIGH_WATER_MARK));
        return new OpenFdMonitor(new File(dumpDirStr), fdThreshold);
    }
    private static long getFdMaxLimit() {
        long[] limit = new long[2];
        try {
            String FD_LINE = "Max open files";
            File file = new File("/proc/" + android.os.Process.myPid() + "/limits");
            InputStream inputStream = new FileInputStream(file);
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            while ((line = br.readLine()) != null) {
                String trimLine = line.trim();
                if (!trimLine.startsWith(FD_LINE)) {
                    continue;
                }
                String data = trimLine.substring(FD_LINE.length());
                String[] strData = readStringValue(data);
                if (strData == null || strData.length < 3) {
                    throw new ParcelFormatException("Parse data error : " + data);
                }
                Log.d(TAG, "Soft Limit =" + strData[0] + ",Hard Limit=" + strData[1] + ",Units=" + strData[2]);
                limit[0] = strToLongNumber(strData[0]);
                limit[1] = strToLongNumber(strData[1]);
            }
            br.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return limit[0];
    }
    private static long strToLongNumber(String strDatum) {
        try {
            return Long.parseLong(strDatum);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return 0;
    }
    private static String[] readStringValue(String data) {
        List<String> list = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < data.length(); i++) {
            char c = data.charAt(i);
            if (c >= 'A' && c <= 'z' || c >= '0' && c <= '9') {
                sb.append(c);
                continue;
            }
            int length = sb.length();
            if (length > 0) {
                list.add(sb.toString());
                sb.setLength(0);
            }
        }
        int length = sb.length();
        if (length > 0) {
            list.add(sb.toString());
            sb.setLength(0);
        }
        return list.toArray(new String[list.size()]);
    }
    @SuppressLint("SoonBlockedPrivateApi")
    private static StructRlimit android_systemOs_getrlimit(int rlimitNofile) {
        try {
            Method getrlimit = Os.class.getDeclaredMethod("getrlimit", int.class);
            getrlimit.setAccessible(true);
            return (StructRlimit) getrlimit.invoke(null, rlimitNofile);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    OpenFdMonitor(File dumpDir, File fdThreshold) {
        mDumpDir = dumpDir;
        mFdHighWaterMark = fdThreshold;
    }
    public void dumpOpenDescriptors() {
        try {
            File dumpFile = new File(mDumpDir, "anr_fd_" + SystemClock.elapsedRealtime());
            int pid = Process.myPid();
            java.lang.Process proc = new ProcessBuilder()
                    .command("/system/bin/lsof", "-p", String.valueOf(pid))
                    .redirectErrorStream(true)
                    .start();
            int returnCode = proc.waitFor();
            if (returnCode != 0) {
                Log.w(TAG, "Unable to dump open descriptors, lsof return code: "
                        + returnCode);
                dumpFile.delete();
            } else {
                FileOutputStream fis = new FileOutputStream(dumpFile);
                InputStream inputStream = proc.getInputStream();
                BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
                String line;
                boolean isFirstLine = true;
                while ((line = br.readLine()) != null) {
                    if (isFirstLine || containSystemFd || line.contains(" " + pid + " ")) {
                        isFirstLine = false;
                        fis.write(line.getBytes());
                        fis.write("n".getBytes());
                    }
                }
                fis.close();
                br.close();
            }
        } catch (IOException | InterruptedException ex) {
            Log.w(TAG, "Unable to dump open descriptors: " + ex);
        }
    }
    /**
     * @return {@code true} if the high water mark was breached and a dump was written,
     * {@code false} otherwise.
     */
    public boolean monitor() {
        if (mFdHighWaterMark.exists()) {
            isReached = true;
            dumpOpenDescriptors();
            return true;
        }
        isReached = false;
        return false;
    }
    public void setContainSystemFd(boolean containSystemFd) {
        this.containSystemFd = containSystemFd;
    }
    public boolean isReached() {
        return isReached;
    }
}

别的,咱们要反躲藏的类,务必放到 android.system 包名下。

package android.system;
import java.util.Objects;
public final class StructRlimit {
    public final long rlim_cur;
    public final long rlim_max;
    public StructRlimit(long rlim_cur, long rlim_max) {
        this.rlim_cur = rlim_cur;
        this.rlim_max = rlim_max;
    }
    @Override public String toString() {
        return Objects.toString(this);
    }
}

引申考虑

FD数量这是一种体系保护机制,假如咱们的FD不受约束的无限增加,那么理论上就会导致system_server重启,FD让System_server重启和发生OOM哪个先发生呢?这点显而易见必定是OOM,可是,咱们这儿接下来需求考虑别的两个问题。

  • FD 抵达必定的阈值之后,会不会触发体系杀死后台或者第三方体系进程?
  • FD 数量监控是监控整个体系中的FD数量,那么假如第三方app在后台申请了很多fd,你的app在前台频频触发oom该怎么办?

这个问题先留在这儿,今后研讨,当然有清楚的能够在谈论中康复。