关于android:一文帮你搞懂-Android-文件描述符

38次阅读

共计 16262 个字符,预计需要花费 41 分钟才能阅读完成。

介绍文件描述符的概念以及工作原理,并通过源码理解 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 内核文件描述符表的演变

正文完
 0