一文帮你搞懂 Android 文件描述符

avatar
vivo互联网技术 @vivo互联网

介绍文件描述符的概念以及工作原理,并通过源码了解 Android 中常见的 FD 泄漏。

一、什么是文件描述符?

文件描述符是在 Linux 文件系统的被使用,由于Android基 于Linux 系统,所以Android也继承了文件描述符系统。我们都知道,在 Linux 中一切皆文件,所以系统在运行时有大量的文件操作,内核为了高效管理已被打开的文件会创建索引,用来指向被打开的文件,这个索引即是文件描述符,其表现形式为一个非负整数。

可以通过命令  ls -la /proc/$pid/fd 查看当前进程文件描述符使用信息。

上图中 箭头前的数组部分是文件描述符,箭头指向的部分是对应的文件信息。

Android系统中可以打开的文件描述符是有上限的,所以分到每一个进程可打开的文件描述符也是有限的。可以通过命令 cat /proc/sys/fs/file-max 查看所有进程允许打开的最大文件描述符数量。

当然也可以查看进程的允许打开的最大文件描述符数量。Linux默认进程最大文件描述符数量是1024,但是较新款的Android设置这个值被改为32768。

可以通过命令 ulimit -n 查看,Linux 默认是1024,比较新款的Android设备大部分已经是大于1024的,例如我用的测试机是:32768。

通过概念性的描述,我们知道系统在打开文件的时候会创建文件操作符,后续就通过文件操作符来操作文件。那么,文件描述符在代码上是怎么实现的呢,让我们来看一下Linux中用来描述进程信息的 task_struct 源码。

struct task_struct
{
// 进程状态
long               state;
// 虚拟内存结构体
struct mm_struct *mm;
// 进程号
pid_t              pid;
// 指向父进程的指针
struct task_struct*parent;
// 子进程列表
struct list_head children;
// 存放文件系统信息的指针
struct fs_struct* fs;
// 存放该进程打开的文件指针数组
struct files_struct *files;
};

task_struct 是 Linux  内核中描述进程信息的对象,其中files指向一个文件指针数组 ,这个数组中保存了这个进程打开的所有文件指针。 每一个进程会用 files_struct 结构体来记录文件描述符的使用情况,这个 files_struct 结构体为用户打开表,它是进程的私有数据,其定义如下:

/*
 * Open file table structure
 */
struct files_struct {
  /*
   * read mostly part
   */
    atomic_t count;//自动增量
    bool resize_in_progress;
    wait_queue_head_t resize_wait;
 
    struct fdtable __rcu *fdt; //fdtable类型指针
    struct fdtable fdtab;  //fdtable变量实例
  /*
   * written part on a separate cache line in SMP
   */
    spinlock_t file_lock ____cacheline_aligned_in_smp;
    unsigned int next_fd;
    unsigned long close_on_exec_init[1];//执行exec时需要关闭的文件描述符初值结合(从主进程中fork出子进程)
    unsigned long open_fds_init[1];//todo 含义补充
    unsigned long full_fds_bits_init[1];//todo 含义补充
    struct file __rcu * fd_array[NR_OPEN_DEFAULT];//默认的文件描述符长度
};

一般情况,“文件描述符”指的就是文件指针数组 files 的索引。

Linux  在2.6.14版本开始通过引入struct fdtable作为file_struct的间接成员,file_struct中会包含一个struct fdtable的变量实例和一个struct fdtable的类型指针。

struct fdtable {
    unsigned int max_fds;
    struct file __rcu **fd;      //指向文件对象指针数组的指针
    unsigned long *close_on_exec;
    unsigned long *open_fds;     //指向打开文件描述符的指针
    unsigned long *full_fds_bits;
    struct rcu_head rcu;
};

在file_struct初始化创建时,fdt指针指向的其实就是当前的的变量fdtab。当打开文件数超过初始设置的大小时,file_struct发生扩容,扩容后fdt指针会指向新分配的fdtable变量。

struct files_struct init_files = {
    .count      = ATOMIC_INIT(1),
    .fdt        = &init_files.fdtab,//指向当前fdtable
    .fdtab      = {
        .max_fds    = NR_OPEN_DEFAULT,
        .fd     = &init_files.fd_array[0],//指向files_struct中的fd_array
        .close_on_exec  = init_files.close_on_exec_init,//指向files_struct中的close_on_exec_init
        .open_fds   = init_files.open_fds_init,//指向files_struct中的open_fds_init
        .full_fds_bits  = init_files.full_fds_bits_init,//指向files_struct中的full_fds_bits_init
    },
    .file_lock  = __SPIN_LOCK_UNLOCKED(init_files.file_lock),
    .resize_wait    = __WAIT_QUEUE_HEAD_INITIALIZER(init_files.resize_wait),
};

RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用。

RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数据的时候不对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改(修改的时候,需要加锁)。

RCU适用于需要频繁的读取数据,而相应修改数据并不多的情景,例如在文件系统中,经常需要查找定位目录,而对目录的修改相对来说并不多,这就是RCU发挥作用的最佳场景。

struct file 处于内核空间,是内核在打开文件时创建,其中保存了文件偏移量,文件的inode等与文件相关的信息,在 Linux  内核中,file结构表示打开的文件描述符,而inode结构表示具体的文件。在文件的所有实例都关闭后,内核释放这个数据结构。

struct file {
    union {
        struct llist_node   fu_llist; //用于通用文件对象链表的指针
        struct rcu_head     fu_rcuhead;//RCU(Read-Copy Update)是Linux 2.6内核中新的锁机制
    } f_u;
    struct path     f_path;//path结构体,包含vfsmount:指出该文件的已安装的文件系统,dentry:与文件相关的目录项对象
    struct inode        *f_inode;   /* cached value */
    const struct file_operations    *f_op;//文件操作,当进程打开文件的时候,这个文件的关联inode中的i_fop文件操作会初始化这个f_op字段
 
    /*
     * Protects f_ep_links, f_flags.
     * Must not be taken from IRQ context.
     */
    spinlock_t      f_lock;
    enum rw_hint        f_write_hint;
    atomic_long_t       f_count; //引用计数
    unsigned int        f_flags; //打开文件时候指定的标识,对应系统调用open的int flags参数。驱动程序为了支持非阻塞型操作需要检查这个标志
    fmode_t         f_mode;//对文件的读写模式,对应系统调用open的mod_t mode参数。如果驱动程序需要这个值,可以直接读取这个字段
    struct mutex        f_pos_lock;
    loff_t          f_pos; //目前文件的相对开头的偏移
    struct fown_struct  f_owner;
    const struct cred   *f_cred;
    struct file_ra_state    f_ra;
 
    u64         f_version;
#ifdef CONFIG_SECURITY
    void            *f_security;
#endif
    /* needed for tty driver, and maybe others */
    void            *private_data;
 
#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_head    f_ep_links;
    struct list_head    f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space    *f_mapping;
    errseq_t        f_wb_err;
    errseq_t        f_sb_err; /* for syncfs */
}

整体的数据结构示意图如下:

到这里,文件描述符的基本概念已介绍完毕。

二、文件描述符的工作原理

上文介绍了文件描述符的概念和部分源码,如果要进一步理解文件描述符的工作原理,需要查看由内核维护的三个数据结构。

i-node是 Linux  文件系统中重要的概念,系统通过i-node节点读取磁盘数据。表面上,用户通过文件名打开文件。实际上,系统内部先通过文件名找到对应的inode号码,其次通过inode号码获取inode信息,最后根据inode信息,找到文件数据所在的block,读出数据。

三个表的关系如下:

进程的文件描述符表为进程私有,该表的值是从0开始,在进程创建时会把前三位填入默认值,分别指向 标准输入流,标准输出流,标准错误流,系统总是使用最小的可用值。

正常情况一个进程会从fd[0]读取数据,将输出写入fd[1],将错误写入fd[2]

每一个文件描述符都会对应一个打开文件,同时不同的文件描述符也可以对应同一个打开文件。这里的不同文件描述符既可以是同一个进程下,也可以是不同进程。

每一个打开文件也会对应一个i-node条目,同时不同的文件也可以对应同一个i-node条目。

光看对应关系的结论有点乱,需要梳理每种对应关系的场景,帮助我们加深理解。

**问题:**如果有两个不同的文件描述符且最终对应一个i-node,这种情况下对应一个打开文件和对应多个打开文件有什么区别呢?

**答:**如果对一个打开文件,则会共享同一个文件偏移量。

举个例子:

fd1和fd2对应同一个打开文件句柄,fd3指向另外一个文件句柄,他们最终都指向一个i-node。

如果fd1先写入“hello”,fd2再写入“world”,那么文件写入为“helloworld”。

fd2会在fd1偏移之后添加写,fd3对应的偏移量为0,所以直接从开始覆盖写。

三、Android中FD泄漏场景

上文介绍了 Linux 系统中文件描述符的含义以及工作原理,下面我们介绍在Android系统中常见的文件描述符泄漏类型。

3.1 HandlerThread泄漏

HandlerThread是Android提供的带消息队列的异步任务处理类,他实际是一个带有Looper的Thread。正常的使用方法如下:

//初始化
private void init(){
   //init
  if(null != mHandlerThread){
     mHandlerThread = new HandlerThread("fd-test");
     mHandlerThread.start();
     mHandler = new Handler(mHandlerThread.getLooper());
  }
}
 
//释放handlerThread
private void release(){
   if(null != mHandler){
      mHandler.removeCallbacksAndMessages(null);
      mHandler = null;
   }
   if(null != mHandlerThread){
      mHandlerThread.quitSafely();
      mHandlerThread = null;
   }
}

HandlerThread在不需要使用的时候,需要调用上述代码中的release方法来释放资源,比如在Activity退出时。另外全局的HandlerThread可能存在被多次赋值的情况,需要做空判断或者先释放再赋值,也需要重点关注。

HandlerThread会泄漏文件描述符的原因是使用了Looper,所以如果普通Thread中使用了Looper,也会有这个问题。下面让我们来分析一下Looper的代码,查看到底是在哪里调用的文件操作。

HandlerThread在run方法中调用Looper.prepare();

public void run() {
    mTid = Process.myTid();
    Looper.prepare();
    synchronized (this) {
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();
    mTid = -1;
}

Looper在构造方法中创建MessageQueue对象。

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

MessageQueue,也就是我们在Handler学习中经常提到的消息队列,在构造方法中调用了native层的初始化方法。

MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();//native层代码
}

MessageQueue对应native代码,这段代码主要是初始化了一个NativeMessageQueue,然后返回一个long型到Java层。

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    if (!nativeMessageQueue) {
        jniThrowRuntimeException(env, "Unable to allocate native queue");
        return 0;
    }
    nativeMessageQueue->incStrong(env);
    return reinterpret_cast<jlong>(nativeMessageQueue);
}

NativeMessageQueue初始化方法中会先判断是否存在当前线程的Native层的Looper,如果没有的就创建一个新的Looper并保存。

NativeMessageQueue::NativeMessageQueue() :mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
    mLooper = Looper::getForThread();
    if (mLooper == NULL) {
        mLooper = new Looper(false);
        Looper::setForThread(mLooper);
    }
}

在Looper的构造函数中,我们发现“eventfd”,这个很有文件描述符特征的方法。

Looper::Looper(bool allowNonCallbacks): mAllowNonCallbacks(allowNonCallbacks),
      mSendingMessage(false),
      mPolling(false),
      mEpollRebuildRequired(false),
      mNextRequestSeq(0),
      mResponseIndex(0),
      mNextMessageUptime(LLONG_MAX) {
    mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));//eventfd
    LOG_ALWAYS_FATAL_IF(mWakeEventFd.get() < 0, "Could not make wake event fd: %s", strerror(errno));
    AutoMutex _l(mLock);
    rebuildEpollLocked();
}

从C++代码注释中可以知道eventfd函数会返回一个新的文件描述符。

/**
 * [eventfd(2)](http://man7.org/linux/man-pages/man2/eventfd.2.html) creates a file descriptor
 * for event notification.
 *
 * Returns a new file descriptor on success, and returns -1 and sets `errno` on failure.
 */
int eventfd(unsigned int __initial_value, int __flags);

3.2 IO泄漏

IO操作是Android开发过程中常用的操作,如果没有正确关闭流操作,除了可能会导致内存泄漏,也会导致FD的泄漏。常见的问题代码如下:

private void ioTest(){
    try {
        File file = new File(getCacheDir(), "testFdFile");
        file.createNewFile();
        FileOutputStream out = new FileOutputStream(file);
        //do something
        out.close();
    }catch (Exception e){
        e.printStackTrace();
    }
}

如果在流操作过程中发生异常,就有可能导致泄漏。正确的写法应该是在final块中关闭流。

private void ioTest() {
    FileOutputStream out = null;
    try {
        File file = new File(getCacheDir(), "testFdFile");
        file.createNewFile();
        out = new FileOutputStream(file);
        //do something
        out.close();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (null != out) {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

同样,我们在从源码中寻找流操作是如何创建文件描述符的。首先,查看 FileOutputStream 的构造方法 ,可以发现会初始化一个名为fd的 FileDescriptor 变量,这个 FileDescriptor 对象是Java层对native文件描述符的封装,其中只包含一个int类型的成员变量,这个变量的值就是native层创建的文件描述符的值。

public FileOutputStream(File file, boolean append) throws FileNotFoundException
{
   //......
  this.fd = new FileDescriptor();
   //......
  open(name, append);
   //......
}

open方法会直接调用jni方法open0.

/**
 * Opens a file, with the specified name, for overwriting or appending.
 * @param name name of file to be opened
 * @param append whether the file is to be opened in append mode
 */
private native void open0(String name, boolean append)
    throws FileNotFoundException;
 
private void open(String name, boolean append)
    throws FileNotFoundException {
    open0(name, append);
}

Tips:  我们在看android源码时常常遇到native方法,通过Android Studio无法跳转查看,可以在 androidxref 网站,通过“Java类名_native方法名”的方法进行搜索。例如,这可以搜索 FileOutputStream_open0 。

接下来,让我们进入native方法查看对应实现。

JNIEXPORT void JNICALL
FileOutputStream_open0(JNIEnv *env, jobject this, jstring path, jboolean append) {
    fileOpen(env, this, path, fos_fd,
             O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC));
}

在fileOpen方法中,通过handleOpen生成native层的文件描述符(fd),这个fd就是这个所谓对面的文件描述符。

void fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
    WITH_PLATFORM_STRING(env, path, ps) {
        FD fd;
        //......
        fd = handleOpen(ps, flags, 0666);
        if (fd != -1) {
            SET_FD(this, fd, fid);
        } else {
            throwFileNotFoundException(env, path);
        }
    } END_PLATFORM_STRING(env, ps);
}
 
 
FD handleOpen(const char *path, int oflag, int mode) {
    FD fd;
    RESTARTABLE(open64(path, oflag, mode), fd);//调用open,获取fd
    if (fd != -1) {
        //......
        if (result != -1) {
            //......
        } else {
            close(fd);
            fd = -1;
        }
    }
    return fd;
}

到这里就结束了吗?

回到开始,FileOutputStream构造方法中初始化了Java层的文件描述符类 FileDescriptor,目前这个对象中的文件描述符的值还是初始的-1,所以目前它还是一个无效的文件描述符,native层完成fd创建后,还需要把fd的值传到 Java层。

我们再来看SET_FD这个宏的定义,在这个宏定义中,通过反射的方式给Java层对象的成员变量赋值。由于上文内容可知,open0是对象的jni方法,所以宏中的this,就是初始创建的FileOutputStream在Java层的对象实例。

#define SET_FD(this, fd, fid) \
    if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \
        (*env)->SetIntField(env, (*env)->GetObjectField(env, (this), (fid)),IO_fd_fdID, (fd))

而fid则会在native代码中提前初始化好。

static void FileOutputStream_initIDs(JNIEnv *env) {
    jclass clazz = (*env)->FindClass(env, "java/io/FileOutputStream");
    fos_fd = (*env)->GetFieldID(env, clazz, "fd", "Ljava/io/FileDescriptor;");
}

收,到这里FileOutputStream的初始化跟进就完成了,我们已经找到了底层fd初始化的路径。Android的IO操作还有其他的流操作类,大致流程基本类似,这里不再细述。

并不是不关闭就一定会导致文件描述符泄漏,在流对象的析构方法中会调用close方法,所以这个对象被回收时,理论上也是会释放文件描述符。但是最好还是通过代码控制释放逻辑。

3.3 SQLite泄漏

在日常开发中如果使用数据库SQLite管理本地数据,在数据库查询的cursor使用完成后,亦需要调用close方法释放资源,否则也有可能导致内存和文件描述符的泄漏。

public void get() {
    db = ordersDBHelper.getReadableDatabase();
    Cursor cursor = db.query(...);
    while (cursor.moveToNext()) {
      //......
    }
    if(flag){
       //某种原因导致retrn
       return;
    }
    //不调用close,fd就会泄漏
    cursor.close();
}

按照理解query操作应该会导致文件描述符泄漏,那我们就从query方法的实现开始分析。

然而,在query方法中并没有发现文件描述符相关的代码。

经过测试发现,moveToNext 调用后才会导致文件描述符增长。通过query方法可以获取cursor的实现类SQLiteCursor。

public Cursor query(CursorFactory factory, String[] selectionArgs) {
    final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal);
    final Cursor cursor;
      //......
      if (factory == null) {
          cursor = new SQLiteCursor(this, mEditTable, query);
      } else {
          cursor = factory.newCursor(mDatabase, this, mEditTable, query);
      }
      //......
}

在SQLiteCursor的父类找到moveToNext的实现。getCount 是抽象方法,在子类SQLiteCursor实现。

@Override
public final boolean moveToNext() {
    return moveToPosition(mPos + 1);
}
public final boolean moveToPosition(int position) {
    // Make sure position isn't past the end of the cursor
    final int count = getCount();
    if (position >= count) {
        mPos = count;
        return false;
    }
    //......
}

getCount 方法中对成员变量mCount做判断,如果还是初始值,则会调用fillWindow方法。

@Override
public int getCount() {
    if (mCount == NO_COUNT) {
        fillWindow(0);
    }
    return mCount;
}
private void fillWindow(int requiredPos) {
    clearOrCreateWindow(getDatabase().getPath());
    //......
}

clearOrCreateWindow 实现又回到父类 AbstractWindowedCursor 中。

protected void clearOrCreateWindow(String name) {
    if (mWindow == null) {
        mWindow = new CursorWindow(name);
    } else {
        mWindow.clear();
    }
}

在CursorWindow的构造方法中,通过nativeCreate方法调用到native层的初始化。

public CursorWindow(String name, @BytesLong long windowSizeBytes) {
    //......
    mWindowPtr = nativeCreate(mName, (int) windowSizeBytes);
    //......
}

在C++代码中会继续调用一个native层CursorWindow的create方法。

static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring nameObj, jint cursorWindowSize) {
    //......
    CursorWindow* window;
    status_t status = CursorWindow::create(name, cursorWindowSize, &window);
    //......
    return reinterpret_cast<jlong>(window);
}

在CursorWindow的create方法中,我们可以发现fd创建相关的代码。

status_t CursorWindow::create(const String8& name, size_t size, CursorWindow** outCursorWindow) {
    String8 ashmemName("CursorWindow: ");
    ashmemName.append(name);
    status_t result;
    int ashmemFd = ashmem_create_region(ashmemName.string(), size);
    //......
}

ashmem_create_region 方法最终会调用到open函数打开文件并返回系统创建的文件描述符。这部分代码不在赘述,有兴趣的可以自行查看 。

native完成初始化会把fd信息保存在CursorWindow中并会返回一个指针地址到Java层,Java层可以通过这个指针操作c++层对象从而也能获取对应的文件描述符。

3.4 InputChannel 导致的泄漏

WindowManager.addView  

通过WindowManager反复添加view也会导致文件描述符增长,可以通过调用removeView释放之前创建的FD。

private void addView() {
    View windowView = LayoutInflater.from(getApplication()).inflate(R.layout.layout_window, null);
    //重复调用
    mWindowManager.addView(windowView, wmParams);
}

WindowManagerImpl中的addView最终会走到ViewRootImpl的setView。

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    //......
    root = new ViewRootImpl(view.getContext(), display);
    //......
    root.setView(view, wparams, panelParentView);
}

setView中会创建InputChannel,并通过Binder机制传到服务端。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    //......
    //创建inputchannel
    if ((mWindowAttributes.inputFeatures
        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
        mInputChannel = new InputChannel();
    }
    //远程服务接口
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
        getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
        mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);//mInputChannel 作为参数传过去
    //......
    if (mInputChannel != null) {
        if (mInputQueueCallback != null) {
            mInputQueue = new InputQueue();
            mInputQueueCallback.onInputQueueCreated(mInputQueue);
        }
        //创建 WindowInputEventReceiver 对象
        mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
            Looper.myLooper());
    }
}

addToDisplay是一个AIDL方法,它的实现类是源码中的Session。最终调用的是 WindowManagerService 的 addWIndow 方法。

public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
        int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
        Rect outStableInsets,
        DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
        InsetsState outInsetsState, InsetsSourceControl[] outActiveControls) {
    return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
            outContentInsets, outStableInsets, outDisplayCutout, outInputChannel,
            outInsetsState, outActiveControls, UserHandle.getUserId(mUid));
}

WMS在 addWindow 方法中创建 InputChannel 用于通讯。

public int addWindow(Session session, IWindow client, int seq,
        LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
        Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
        DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel) {
        //......
        final boolean openInputChannels = (outInputChannel != null
        && (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
        if  (openInputChannels) {
            win.openInputChannel(outInputChannel);
        }
        //......
}

在 openInputChannel 中创建 InputChannel ,并把客户端的传回去。

void openInputChannel(InputChannel outInputChannel) {
    //......
    InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
    mInputChannel = inputChannels[0];
    mClientChannel = inputChannels[1];
    //......
}

InputChannel 的 openInputChannelPair 会调用native的 nativeOpenInputChannelPair ,在native中创建两个带有文件描述符的 socket 。

int socketpair(int domain, int type, int protocol, int sv[2]) {
    //创建一对匿名的已经连接的套接字
    int rc = __socketpair(domain, type, protocol, sv);
    if (rc == 0) {
        //跟踪文件描述符
        FDTRACK_CREATE(sv[0]);
        FDTRACK_CREATE(sv[1]);
    }
    return rc;
}

WindowManager 的分析涉及WMS,WMS内容比较多,本文重点关注文件描述符相关的内容。简单的理解,就是进程间通讯会创建socket,所以也会创建文件描述符,而且会在服务端进程和客户端进程各创建一个。另外,如果系统进程文件描述符过多,理论上会造成系统崩溃。

四、如何排查

如果你的应用收到如下这些崩溃堆栈,恭喜你,你的应用存在文件描述符泄漏。

  • abort message 'could not create instance too many files'
  • could not read input file descriptors from parcel
  • socket failed:EMFILE (Too many open files)
  • ...

文件描述符导致的崩溃往往无法通过堆栈直接分析。道理很简单: 出问题的代码在消耗文件描述符同时,正常的代码逻辑可能也同样在创建文件描述符,所以崩溃可能是被正常代码触发了。

4.1 打印当前FD信息

遇到这类问题可以先尝试本体复现,通过命令 ‘ls -la /proc/$pid/fd’ 查看当前进程文件描述符的消耗情况。一般android应用的文件描述符可以分为几类,通过对比哪一类文件描述符数量过高,来缩小问题范围。

4.2 dump系统信息

通过dumpsys window ,查看是否有异常window。用于解决 InputChannel 相关的泄漏问题。

4.3 线上监控

如果是本地无法复现问题,可以尝试添加线上监控代码,定时轮询当前进程使用的FD数量,在达到阈值时,读取当前FD的信息,并传到后台分析,获取FD对应文件信息的代码如下。

if (Build.VERSION.SDK_INT >= VersionCodes.L) {
    linkTarget = Os.readlink(file.getAbsolutePath());
} else {
    //通过 readlink 读取文件描述符信息
}

4.4 排查循环打印的日志

除了直接对 FD相关的信息进行分析,还需要关注logcat中是否有频繁打印的信息,例如:socket创建失败。

五、参考文档

  1. Linux 源码
  2. Android源码
  3. i-node介绍
  4. InputChannel通信
  5. Linux 内核文件描述符表的演变