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

介紹文件描述符的概念以及工作原理,并通過(guò)源碼了解 Android 中常見(jiàn)的 FD 泄漏。

一、什么是文件描述符?

文件描述符是在 Linux 文件系統(tǒng)的被使用,由于Android基 于Linux 系統(tǒng),所以Android也繼承了文件描述符系統(tǒng)。我們都知道,在 Linux 中一切皆文件,所以系統(tǒng)在運(yùn)行時(shí)有大量的文件操作,內(nèi)核為了高效管理已被打開(kāi)的文件會(huì)創(chuàng)建索引,用來(lái)指向被打開(kāi)的文件,這個(gè)索引即是文件描述符,其表現(xiàn)形式為一個(gè)非負(fù)整數(shù)。

可以通過(guò)命令 ls -la /proc/$pid/fd 查看當(dāng)前進(jìn)程文件描述符使用信息。

01.png

上圖中 箭頭前的數(shù)組部分是文件描述符,箭頭指向的部分是對(duì)應(yīng)的文件信息。

02.png

Android系統(tǒng)中可以打開(kāi)的文件描述符是有上限的,所以分到每一個(gè)進(jìn)程可打開(kāi)的文件描述符也是有限的。可以通過(guò)命令 cat /proc/sys/fs/file-max 查看所有進(jìn)程允許打開(kāi)的最大文件描述符數(shù)量。

03.png

當(dāng)然也可以查看進(jìn)程的允許打開(kāi)的最大文件描述符數(shù)量。Linux默認(rèn)進(jìn)程最大文件描述符數(shù)量是1024,但是較新款的Android設(shè)置這個(gè)值被改為32768。

04.png

可以通過(guò)命令 ulimit -n 查看,Linux 默認(rèn)是1024,比較新款的Android設(shè)備大部分已經(jīng)是大于1024的,例如我用的測(cè)試機(jī)是:32768。

通過(guò)概念性的描述,我們知道系統(tǒng)在打開(kāi)文件的時(shí)候會(huì)創(chuàng)建文件操作符,后續(xù)就通過(guò)文件操作符來(lái)操作文件。那么,文件描述符在代碼上是怎么實(shí)現(xiàn)的呢,讓我們來(lái)看一下Linux中用來(lái)描述進(jìn)程信息的 task_struct 源碼。

struct task_struct
{
// 進(jìn)程狀態(tài)
long               state;
// 虛擬內(nèi)存結(jié)構(gòu)體
struct mm_struct *mm;
// 進(jìn)程號(hào)
pid_t              pid;
// 指向父進(jìn)程的指針
struct task_struct*parent;
// 子進(jìn)程列表
struct list_head children;
// 存放文件系統(tǒng)信息的指針
struct fs_struct* fs;
// 存放該進(jìn)程打開(kāi)的文件指針數(shù)組
struct files_struct *files;
};

task_struct 是 Linux 內(nèi)核中描述進(jìn)程信息的對(duì)象,其中files指向一個(gè)文件指針數(shù)組 ,這個(gè)數(shù)組中保存了這個(gè)進(jìn)程打開(kāi)的所有文件指針。 每一個(gè)進(jìn)程會(huì)用 files_struct 結(jié)構(gòu)體來(lái)記錄文件描述符的使用情況,這個(gè) files_struct 結(jié)構(gòu)體為用戶(hù)打開(kāi)表,它是進(jìn)程的私有數(shù)據(jù),其定義如下:

/*
 * Open file table structure
 */
struct files_struct {
  /*
   * read mostly part
   */
    atomic_t count;//自動(dòng)增量
    bool resize_in_progress;
    wait_queue_head_t resize_wait;
 
    struct fdtable __rcu *fdt; //fdtable類(lèi)型指針
    struct fdtable fdtab;  //fdtable變量實(shí)例
  /*
   * 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];//執(zhí)行exec時(shí)需要關(guān)閉的文件描述符初值結(jié)合(從主進(jìn)程中fork出子進(jìn)程)
    unsigned long open_fds_init[1];//todo 含義補(bǔ)充
    unsigned long full_fds_bits_init[1];//todo 含義補(bǔ)充
    struct file __rcu * fd_array[NR_OPEN_DEFAULT];//默認(rèn)的文件描述符長(zhǎng)度
};

一般情況,“文件描述符”指的就是文件指針數(shù)組 files 的索引。

Linux 在2.6.14版本開(kāi)始通過(guò)引入struct fdtable作為file_struct的間接成員,file_struct中會(huì)包含一個(gè)struct fdtable的變量實(shí)例和一個(gè)struct fdtable的類(lèi)型指針。

struct fdtable {
    unsigned int max_fds;
    struct file __rcu **fd;      //指向文件對(duì)象指針數(shù)組的指針
    unsigned long *close_on_exec;
    unsigned long *open_fds;     //指向打開(kāi)文件描述符的指針
    unsigned long *full_fds_bits;
    struct rcu_head rcu;
};

在file_struct初始化創(chuàng)建時(shí),fdt指針指向的其實(shí)就是當(dāng)前的的變量fdtab。當(dāng)打開(kāi)文件數(shù)超過(guò)初始設(shè)置的大小時(shí),file_struct發(fā)生擴(kuò)容,擴(kuò)容后fdt指針會(huì)指向新分配的fdtable變量。

struct files_struct init_files = {
    .count      = ATOMIC_INIT(1),
    .fdt        = &init_files.fdtab,//指向當(dāng)前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)是數(shù)據(jù)同步的一種方式,在當(dāng)前的Linux內(nèi)核中發(fā)揮著重要的作用。

RCU主要針對(duì)的數(shù)據(jù)對(duì)象是鏈表,目的是提高遍歷讀取數(shù)據(jù)的效率,為了達(dá)到目的使用RCU機(jī)制讀取數(shù)據(jù)的時(shí)候不對(duì)鏈表進(jìn)行耗時(shí)的加鎖操作。這樣在同一時(shí)間可以有多個(gè)線(xiàn)程同時(shí)讀取該鏈表,并且允許一個(gè)線(xiàn)程對(duì)鏈表進(jìn)行修改(修改的時(shí)候,需要加鎖)。

RCU適用于需要頻繁的讀取數(shù)據(jù),而相應(yīng)修改數(shù)據(jù)并不多的情景,例如在文件系統(tǒng)中,經(jīng)常需要查找定位目錄,而對(duì)目錄的修改相對(duì)來(lái)說(shuō)并不多,這就是RCU發(fā)揮作用的最佳場(chǎng)景。

struct file 處于內(nèi)核空間,是內(nèi)核在打開(kāi)文件時(shí)創(chuàng)建,其中保存了文件偏移量,文件的inode等與文件相關(guān)的信息,在 Linux 內(nèi)核中,file結(jié)構(gòu)表示打開(kāi)的文件描述符,而inode結(jié)構(gòu)表示具體的文件。在文件的所有實(shí)例都關(guān)閉后,內(nèi)核釋放這個(gè)數(shù)據(jù)結(jié)構(gòu)。

struct file {
    union {
        struct llist_node   fu_llist; //用于通用文件對(duì)象鏈表的指針
        struct rcu_head     fu_rcuhead;//RCU(Read-Copy Update)是Linux 2.6內(nèi)核中新的鎖機(jī)制
    } f_u;
    struct path     f_path;//path結(jié)構(gòu)體,包含vfsmount:指出該文件的已安裝的文件系統(tǒng),dentry:與文件相關(guān)的目錄項(xiàng)對(duì)象
    struct inode        *f_inode;   /* cached value */
    const struct file_operations    *f_op;//文件操作,當(dāng)進(jìn)程打開(kāi)文件的時(shí)候,這個(gè)文件的關(guān)聯(lián)inode中的i_fop文件操作會(huì)初始化這個(gè)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; //引用計(jì)數(shù)
    unsigned int        f_flags; //打開(kāi)文件時(shí)候指定的標(biāo)識(shí),對(duì)應(yīng)系統(tǒng)調(diào)用open的int flags參數(shù)。驅(qū)動(dòng)程序?yàn)榱酥С址亲枞筒僮餍枰獧z查這個(gè)標(biāo)志
    fmode_t         f_mode;//對(duì)文件的讀寫(xiě)模式,對(duì)應(yīng)系統(tǒng)調(diào)用open的mod_t mode參數(shù)。如果驅(qū)動(dòng)程序需要這個(gè)值,可以直接讀取這個(gè)字段
    struct mutex        f_pos_lock;
    loff_t          f_pos; //目前文件的相對(duì)開(kāi)頭的偏移
    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 */
}

整體的數(shù)據(jù)結(jié)構(gòu)示意圖如下:

image

到這里,文件描述符的基本概念已介紹完畢。

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

上文介紹了文件描述符的概念和部分源碼,如果要進(jìn)一步理解文件描述符的工作原理,需要查看由內(nèi)核維護(hù)的三個(gè)數(shù)據(jù)結(jié)構(gòu)。

image

i-node是 Linux 文件系統(tǒng)中重要的概念,系統(tǒng)通過(guò)i-node節(jié)點(diǎn)讀取磁盤(pán)數(shù)據(jù)。表面上,用戶(hù)通過(guò)文件名打開(kāi)文件。實(shí)際上,系統(tǒng)內(nèi)部先通過(guò)文件名找到對(duì)應(yīng)的inode號(hào)碼,其次通過(guò)inode號(hào)碼獲取inode信息,最后根據(jù)inode信息,找到文件數(shù)據(jù)所在的block,讀出數(shù)據(jù)。

三個(gè)表的關(guān)系如下:

06.jpg

進(jìn)程的文件描述符表為進(jìn)程私有,該表的值是從0開(kāi)始,在進(jìn)程創(chuàng)建時(shí)會(huì)把前三位填入默認(rèn)值,分別指向 標(biāo)準(zhǔn)輸入流,標(biāo)準(zhǔn)輸出流,標(biāo)準(zhǔn)錯(cuò)誤流,系統(tǒng)總是使用最小的可用值。

正常情況一個(gè)進(jìn)程會(huì)從fd[0]讀取數(shù)據(jù),將輸出寫(xiě)入fd[1],將錯(cuò)誤寫(xiě)入fd[2]

每一個(gè)文件描述符都會(huì)對(duì)應(yīng)一個(gè)打開(kāi)文件,同時(shí)不同的文件描述符也可以對(duì)應(yīng)同一個(gè)打開(kāi)文件。這里的不同文件描述符既可以是同一個(gè)進(jìn)程下,也可以是不同進(jìn)程。

每一個(gè)打開(kāi)文件也會(huì)對(duì)應(yīng)一個(gè)i-node條目,同時(shí)不同的文件也可以對(duì)應(yīng)同一個(gè)i-node條目。

光看對(duì)應(yīng)關(guān)系的結(jié)論有點(diǎn)亂,需要梳理每種對(duì)應(yīng)關(guān)系的場(chǎng)景,幫助我們加深理解。

image

問(wèn)題:如果有兩個(gè)不同的文件描述符且最終對(duì)應(yīng)一個(gè)i-node,這種情況下對(duì)應(yīng)一個(gè)打開(kāi)文件和對(duì)應(yīng)多個(gè)打開(kāi)文件有什么區(qū)別呢?

答:如果對(duì)一個(gè)打開(kāi)文件,則會(huì)共享同一個(gè)文件偏移量。

舉個(gè)例子:

fd1和fd2對(duì)應(yīng)同一個(gè)打開(kāi)文件句柄,fd3指向另外一個(gè)文件句柄,他們最終都指向一個(gè)i-node。

如果fd1先寫(xiě)入“hello”,fd2再寫(xiě)入“world”,那么文件寫(xiě)入為“helloworld”。

fd2會(huì)在fd1偏移之后添加寫(xiě),fd3對(duì)應(yīng)的偏移量為0,所以直接從開(kāi)始覆蓋寫(xiě)。

三、Android中FD泄漏場(chǎng)景

上文介紹了 Linux 系統(tǒng)中文件描述符的含義以及工作原理,下面我們介紹在Android系統(tǒng)中常見(jiàn)的文件描述符泄漏類(lèi)型。

3.1 HandlerThread泄漏

HandlerThread是Android提供的帶消息隊(duì)列的異步任務(wù)處理類(lèi),他實(shí)際是一個(gè)帶有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在不需要使用的時(shí)候,需要調(diào)用上述代碼中的release方法來(lái)釋放資源,比如在Activity退出時(shí)。另外全局的HandlerThread可能存在被多次賦值的情況,需要做空判斷或者先釋放再賦值,也需要重點(diǎn)關(guān)注。

HandlerThread會(huì)泄漏文件描述符的原因是使用了Looper,所以如果普通Thread中使用了Looper,也會(huì)有這個(gè)問(wèn)題。下面讓我們來(lái)分析一下Looper的代碼,查看到底是在哪里調(diào)用的文件操作。

HandlerThread在run方法中調(diào)用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在構(gòu)造方法中創(chuàng)建MessageQueue對(duì)象。

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

MessageQueue,也就是我們?cè)贖andler學(xué)習(xí)中經(jīng)常提到的消息隊(duì)列,在構(gòu)造方法中調(diào)用了native層的初始化方法。

MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();//native層代碼
}

MessageQueue對(duì)應(yīng)native代碼,這段代碼主要是初始化了一個(gè)NativeMessageQueue,然后返回一個(gè)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初始化方法中會(huì)先判斷是否存在當(dāng)前線(xiàn)程的Native層的Looper,如果沒(méi)有的就創(chuàng)建一個(gè)新的Looper并保存。

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

在Looper的構(gòu)造函數(shù)中,我們發(fā)現(xiàn)“eventfd”,這個(gè)很有文件描述符特征的方法。

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函數(shù)會(huì)返回一個(gè)新的文件描述符。

/**
 * [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開(kāi)發(fā)過(guò)程中常用的操作,如果沒(méi)有正確關(guān)閉流操作,除了可能會(huì)導(dǎo)致內(nèi)存泄漏,也會(huì)導(dǎo)致FD的泄漏。常見(jiàn)的問(wèn)題代碼如下:

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();
    }
}

如果在流操作過(guò)程中發(fā)生異常,就有可能導(dǎo)致泄漏。正確的寫(xiě)法應(yīng)該是在final塊中關(guān)閉流。

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();
            }
        }
    }
}

同樣,我們?cè)趶脑创a中尋找流操作是如何創(chuàng)建文件描述符的。首先,查看 FileOutputStream 的構(gòu)造方法 ,可以發(fā)現(xiàn)會(huì)初始化一個(gè)名為fd的 FileDescriptor 變量,這個(gè) FileDescriptor 對(duì)象是Java層對(duì)native文件描述符的封裝,其中只包含一個(gè)int類(lèi)型的成員變量,這個(gè)變量的值就是native層創(chuàng)建的文件描述符的值。

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

open方法會(huì)直接調(diào)用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: 我們?cè)诳碼ndroid源碼時(shí)常常遇到native方法,通過(guò)Android Studio無(wú)法跳轉(zhuǎn)查看,可以在 androidxref 網(wǎng)站,通過(guò)“Java類(lèi)名_native方法名”的方法進(jìn)行搜索。例如,這可以搜索 FileOutputStream_open0 。

接下來(lái),讓我們進(jìn)入native方法查看對(duì)應(yīng)實(shí)現(xiàn)。

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方法中,通過(guò)handleOpen生成native層的文件描述符(fd),這個(gè)fd就是這個(gè)所謂對(duì)面的文件描述符。

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);//調(diào)用open,獲取fd
    if (fd != -1) {
        //......
        if (result != -1) {
            //......
        } else {
            close(fd);
            fd = -1;
        }
    }
    return fd;
}

到這里就結(jié)束了嗎?

回到開(kāi)始,F(xiàn)ileOutputStream構(gòu)造方法中初始化了Java層的文件描述符類(lèi) FileDescriptor,目前這個(gè)對(duì)象中的文件描述符的值還是初始的-1,所以目前它還是一個(gè)無(wú)效的文件描述符,native層完成fd創(chuàng)建后,還需要把fd的值傳到 Java層。

我們?cè)賮?lái)看SET_FD這個(gè)宏的定義,在這個(gè)宏定義中,通過(guò)反射的方式給Java層對(duì)象的成員變量賦值。由于上文內(nèi)容可知,open0是對(duì)象的jni方法,所以宏中的this,就是初始創(chuàng)建的FileOutputStream在Java層的對(duì)象實(shí)例。

#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則會(huì)在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的初始化跟進(jìn)就完成了,我們已經(jīng)找到了底層fd初始化的路徑。Android的IO操作還有其他的流操作類(lèi),大致流程基本類(lèi)似,這里不再細(xì)述。

并不是不關(guān)閉就一定會(huì)導(dǎo)致文件描述符泄漏,在流對(duì)象的析構(gòu)方法中會(huì)調(diào)用close方法,所以這個(gè)對(duì)象被回收時(shí),理論上也是會(huì)釋放文件描述符。但是最好還是通過(guò)代碼控制釋放邏輯。

3.3 SQLite泄漏

在日常開(kāi)發(fā)中如果使用數(shù)據(jù)庫(kù)SQLite管理本地?cái)?shù)據(jù),在數(shù)據(jù)庫(kù)查詢(xún)的cursor使用完成后,亦需要調(diào)用close方法釋放資源,否則也有可能導(dǎo)致內(nèi)存和文件描述符的泄漏。

public void get() {
    db = ordersDBHelper.getReadableDatabase();
    Cursor cursor = db.query(...);
    while (cursor.moveToNext()) {
      //......
    }
    if(flag){
       //某種原因?qū)е聄etrn
       return;
    }
    //不調(diào)用close,fd就會(huì)泄漏
    cursor.close();
}

按照理解query操作應(yīng)該會(huì)導(dǎo)致文件描述符泄漏,那我們就從query方法的實(shí)現(xiàn)開(kāi)始分析。

然而,在query方法中并沒(méi)有發(fā)現(xiàn)文件描述符相關(guān)的代碼。

經(jīng)過(guò)測(cè)試發(fā)現(xiàn),moveToNext 調(diào)用后才會(huì)導(dǎo)致文件描述符增長(zhǎng)。通過(guò)query方法可以獲取cursor的實(shí)現(xiàn)類(lèi)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的父類(lèi)找到moveToNext的實(shí)現(xiàn)。getCount 是抽象方法,在子類(lèi)SQLiteCursor實(shí)現(xiàn)。

@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 方法中對(duì)成員變量mCount做判斷,如果還是初始值,則會(huì)調(diào)用fillWindow方法。

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

clearOrCreateWindow 實(shí)現(xiàn)又回到父類(lèi) AbstractWindowedCursor 中。

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

在CursorWindow的構(gòu)造方法中,通過(guò)nativeCreate方法調(diào)用到native層的初始化。

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

在C++代碼中會(huì)繼續(xù)調(diào)用一個(gè)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方法中,我們可以發(fā)現(xiàn)fd創(chuàng)建相關(guān)的代碼。

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 方法最終會(huì)調(diào)用到open函數(shù)打開(kāi)文件并返回系統(tǒng)創(chuàng)建的文件描述符。這部分代碼不在贅述,有興趣的可以自行查看 。

native完成初始化會(huì)把fd信息保存在CursorWindow中并會(huì)返回一個(gè)指針地址到Java層,Java層可以通過(guò)這個(gè)指針操作c++層對(duì)象從而也能獲取對(duì)應(yīng)的文件描述符。

3.4 InputChannel 導(dǎo)致的泄漏

WindowManager.addView

通過(guò)WindowManager反復(fù)添加view也會(huì)導(dǎo)致文件描述符增長(zhǎng),可以通過(guò)調(diào)用removeView釋放之前創(chuàng)建的FD。

private void addView() {
    View windowView = LayoutInflater.from(getApplication()).inflate(R.layout.layout_window, null);
    //重復(fù)調(diào)用
    mWindowManager.addView(windowView, wmParams);
}

WindowManagerImpl中的addView最終會(huì)走到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中會(huì)創(chuàng)建InputChannel,并通過(guò)Binder機(jī)制傳到服務(wù)端。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    //......
    //創(chuàng)建inputchannel
    if ((mWindowAttributes.inputFeatures
        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
        mInputChannel = new InputChannel();
    }
    //遠(yuǎn)程服務(wù)接口
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
        getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
        mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);//mInputChannel 作為參數(shù)傳過(guò)去
    //......
    if (mInputChannel != null) {
        if (mInputQueueCallback != null) {
            mInputQueue = new InputQueue();
            mInputQueueCallback.onInputQueueCreated(mInputQueue);
        }
        //創(chuàng)建 WindowInputEventReceiver 對(duì)象
        mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
            Looper.myLooper());
    }
}

addToDisplay是一個(gè)AIDL方法,它的實(shí)現(xiàn)類(lèi)是源碼中的Session。最終調(diào)用的是 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 方法中創(chuàng)建 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 中創(chuàng)建 InputChannel ,并把客戶(hù)端的傳回去。

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

InputChannel 的 openInputChannelPair 會(huì)調(diào)用native的 nativeOpenInputChannelPair ,在native中創(chuàng)建兩個(gè)帶有文件描述符的 socket 。

int socketpair(int domain, int type, int protocol, int sv[2]) {
    //創(chuàng)建一對(duì)匿名的已經(jīng)連接的套接字
    int rc = __socketpair(domain, type, protocol, sv);
    if (rc == 0) {
        //跟蹤文件描述符
        FDTRACK_CREATE(sv[0]);
        FDTRACK_CREATE(sv[1]);
    }
    return rc;
}

WindowManager 的分析涉及WMS,WMS內(nèi)容比較多,本文重點(diǎn)關(guān)注文件描述符相關(guān)的內(nèi)容。簡(jiǎn)單的理解,就是進(jìn)程間通訊會(huì)創(chuàng)建socket,所以也會(huì)創(chuàng)建文件描述符,而且會(huì)在服務(wù)端進(jìn)程和客戶(hù)端進(jìn)程各創(chuàng)建一個(gè)。另外,如果系統(tǒng)進(jìn)程文件描述符過(guò)多,理論上會(huì)造成系統(tǒng)崩潰。

四、如何排查

如果你的應(yīng)用收到如下這些崩潰堆棧,恭喜你,你的應(yīng)用存在文件描述符泄漏。

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

文件描述符導(dǎo)致的崩潰往往無(wú)法通過(guò)堆棧直接分析。道理很簡(jiǎn)單: 出問(wèn)題的代碼在消耗文件描述符同時(shí),正常的代碼邏輯可能也同樣在創(chuàng)建文件描述符,所以崩潰可能是被正常代碼觸發(fā)了。

4.1 打印當(dāng)前FD信息

遇到這類(lèi)問(wèn)題可以先嘗試本體復(fù)現(xiàn),通過(guò)命令 ‘ls -la /proc/$pid/fd’ 查看當(dāng)前進(jìn)程文件描述符的消耗情況。一般android應(yīng)用的文件描述符可以分為幾類(lèi),通過(guò)對(duì)比哪一類(lèi)文件描述符數(shù)量過(guò)高,來(lái)縮小問(wèn)題范圍。

image

4.2 dump系統(tǒng)信息

通過(guò)dumpsys window ,查看是否有異常window。用于解決 InputChannel 相關(guān)的泄漏問(wèn)題。

4.3 線(xiàn)上監(jiān)控

如果是本地?zé)o法復(fù)現(xiàn)問(wèn)題,可以嘗試添加線(xiàn)上監(jiān)控代碼,定時(shí)輪詢(xún)當(dāng)前進(jìn)程使用的FD數(shù)量,在達(dá)到閾值時(shí),讀取當(dāng)前FD的信息,并傳到后臺(tái)分析,獲取FD對(duì)應(yīng)文件信息的代碼如下。

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

4.4 排查循環(huán)打印的日志

除了直接對(duì) FD相關(guān)的信息進(jìn)行分析,還需要關(guān)注logcat中是否有頻繁打印的信息,例如:socket創(chuàng)建失敗。

五、參考文檔

  1. Linux 源碼
  2. Android源碼
  3. i-node介紹
  4. InputChannel通信
  5. Linux 內(nèi)核文件描述符表的演變
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容