Introduce the concept of file descriptors and how they work, and learn about FD leaks in Android through source code.

What is a file descriptor?

File descriptors are used on Linux file systems, and since Android is based on Linux, Android also inherits the file descriptor system. As we all know, in Linux everything is a file, so the system runs a lot of file operations. To efficiently manage opened files, the kernel creates an index to point to the opened file. This index is the file descriptor, which is a non-negative integer.

You can run the ls -la /proc/$pid/fd command to view the file descriptor usage of the current process.

The array before the arrow in the figure above is the file descriptor, and the arrow points to the corresponding file information.

There is a limit to the number of file descriptors that can be opened in The Android system, so the number of file descriptors that can be opened by each process is limited. You can run the cat /proc/sys/fs/file-max command to view the maximum number of file descriptors that can be opened for all processes.

You can also check the maximum number of file descriptors that a process can open. The Linux default maximum number of process file descriptors is 1024, but newer Android sets this value to 32768.

You can run the ulimit -n command to check that the default value of Linux is 1024. Most of the newer Android devices are already higher than 1024. For example, the test machine I used is 32768.

From a conceptual description, we know that the system creates file operators when it opens a file, and then operates on the file using the file operators. Task_struct is used to describe process information in Linux.

struct task_struct
{
// Process status
long               state;
// Virtual memory structure
struct mm_struct *mm;
/ / process
pid_t              pid;
// A pointer to the parent process
struct task_struct*parent;
// List of child processes
struct list_head children;
// A pointer to file system information
struct fs_struct* fs;
// Hold an array of Pointers to open files for the process
struct files_struct *files;
};
Copy the code

Task_struct is an object in the Linux kernel that describes process information, where files point to an array of file Pointers that hold all open file Pointers for the process. Each process uses the files_struct structure to record the use of file descriptors. The files_struct structure opens the table for the user. It is the private data of the process and is defined as follows:

/* * Open file table structure */
struct files_struct {
  /* * read mostly part */
    atomic_t count;// Automatic increment
    bool resize_in_progress;
    wait_queue_head_t resize_wait;
 
    struct fdtable __rcu *fdt; // FdTable type pointer
    struct fdtable fdtab;  //fdtable variable instance
  /* * 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];// Initial value combination of file descriptors to be closed when executing exec (fork child process from main process)
    unsigned long open_fds_init[1];// Todo adds meaning
    unsigned long full_fds_bits_init[1];// Todo adds meaning
    struct file __rcu * fd_array[NR_OPEN_DEFAULT];// The default file descriptor length
};
Copy the code

In general, “file descriptor” refers to the index of the file pointer array files.

Linux began with 2.6.14 by introducing struct fdtable as an indirect member of file_struct. File_struct contains a variable instance of the struct FDtable and a pointer to the type of the struct FDtable.

struct fdtable {
    unsigned int max_fds;
    struct file __rcu **fd;      // A pointer to an array of file object Pointers
    unsigned long *close_on_exec;
    unsigned long *open_fds;     // Pointer to the open file descriptor
    unsigned long *full_fds_bits;
    struct rcu_head rcu;
};
Copy the code

When file_struct is initialized, the FDT pointer points to the current variable fdTAB. When the number of open files exceeds the initial size, file_struct expands and the FDT pointer points to the newly allocated FDtable variable.

struct files_struct init_files = {
    .count      = ATOMIC_INIT(1),
    .fdt        = &init_files.fdtab,// points to the current FDtable
    .fdtab      = {
        .max_fds    = NR_OPEN_DEFAULT,
        .fd     = &init_files.fd_array[0].// points to fd_array in files_struct
        .close_on_exec  = init_files.close_on_exec_init,// point to close_on_exec_init in files_struct
        .open_fds   = init_files.open_fds_init,// point to open_fds_init in files_struct
        .full_fds_bits  = init_files.full_fds_bits_init,// point to full_fds_bits_init in files_struct
    },
    .file_lock  = __SPIN_LOCK_UNLOCKED(init_files.file_lock),
    .resize_wait    = __WAIT_QUEUE_HEAD_INITIALIZER(init_files.resize_wait),
};
Copy the code

RCU (Read-copy Update) is a method of data synchronization, which plays an important role in the current Linux kernel.

RCU is mainly aimed at linked lists. The purpose of RCU is to improve the efficiency of traversing and reading data. In order to achieve this goal, RCU mechanism is used to read data without time-consuming locking operations on linked lists. This allows multiple threads to read the list at the same time, and allows one thread to modify the list (with a lock).

RCU works best when you need to read data frequently and modify data infrequently. For example, in a file system, you need to locate directories frequently and modify directories relatively infrequently.

Struct file is created in kernel space when the kernel opens the file, which stores the file offset, file inode and other file-related information. In Linux kernel, file structure represents the open file descriptor, while inode structure represents the specific file. After all instances of the file are closed, the kernel frees the data structure.

struct file {
    union {
        struct llist_node   fu_llist; // A pointer to a generic list of file objects
        struct rcu_head     fu_rcuhead;//RCU(read-copy Update) is a new locking mechanism in the Linux 2.6 kernel
    } f_u;
    struct path     f_path;// Path structure, including vfsmount: indicates the installed file system for the file, and dentry: the directory entry object associated with the file
    struct inode        *f_inode;   /* cached value */
    const struct file_operations    *f_op;When the process opens the file, the i_fop file operation in the file's associated inode initializes the f_op field
 
    /* * 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; // Reference count
    unsigned int        f_flags; // The flag specified when the file is opened, corresponding to the int flags argument of the system call open. The driver needs to check this flag in order to support non-blocking operations
    fmode_t         f_mode;// The mode of reading and writing files corresponds to the mod_t mode parameter of the system call open. If the driver needs this value, it can read the field directly
    struct mutex        f_pos_lock;
    loff_t          f_pos; // Offset from the beginning of the current file
    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 */
}
Copy the code

The overall data structure is shown as follows:

At this point, the basic concepts of file descriptors are covered.

How file descriptors work

The concept of file descriptors and some of the source code were introduced above, but to understand how file descriptors work, you need to look at three data structures maintained by the kernel.

I-node is an important concept in Linux file system. The system uses i-Node to read disk data. Ostensibly, the user opens the file by filename. In fact, the system first finds the corresponding inode number based on the file name, then obtains the inode information based on the inode number, and finally finds the block where the file data resides based on the inode information and reads the data.

The relationship between the three tables is as follows:

The file descriptor table for a process is private to the process. The value of this table starts from 0. When a process is created, the first three digits are filled with default values to point to the standard input stream, standard output stream, and standard error stream respectively.

Normally a process will read data from fd[0], write output to fd[1], and write errors to fd[2].

Each file descriptor corresponds to an open file, and different file descriptors can also correspond to the same open file. The different file descriptors here can be in the same process or different processes.

Each open file corresponds to an I-Node entry, and different files can correspond to the same I-Node entry.

Just looking at the conclusion of the corresponding relationship is a bit confusing. We need to sort out the scenes of each corresponding relationship to help us deepen our understanding.

** Question: ** If there are two different file descriptors that ultimately correspond to an I-Node, what is the difference between one open file and multiple open files in this case?

** Answer: ** If you open a file, the same file offset will be shared.

Here’s an example:

Fd1 and fd2 correspond to the same open file handle, fd3 points to another file handle, and they both end up pointing to an I-node.

If fd1 writes “hello” first and fd2 writes “world”, the file is written to “helloWorld”.

Fd2 adds writes after the fd1 offset. Fd3 corresponds to an offset of 0, so writes are overwritten directly from the start.

3. FD leakage scenario in Android

The definition and working principle of file descriptors in Linux have been introduced. Now we introduce the common leak types of file descriptors in Android.

3.1 HandlerThread leak

HandlerThread is an Android asynchronous task processing class with a message queue, which is actually a Thread with a Looper. The normal usage is as follows:

/ / initialization
private void init(a){
   //init
  if(null! = mHandlerThread){ mHandlerThread =new HandlerThread("fd-test");
     mHandlerThread.start();
     mHandler = newHandler(mHandlerThread.getLooper()); }}/ / release handlerThread
private void release(a){
   if(null! = mHandler){ mHandler.removeCallbacksAndMessages(null);
      mHandler = null;
   }
   if(null! = mHandlerThread){ mHandlerThread.quitSafely(); mHandlerThread =null; }}Copy the code

The HandlerThread needs to call the release method in the code above to release resources when it is not needed, such as when the Activity exits. In addition, the global HandlerThread may be assigned multiple times, so it needs to be shorted or released first and then assigned.

Handlerthreads leak file descriptors because they use Looper, so if Looper is used in regular threads, the same problem can occur. Let’s take a look at Looper’s code to see exactly where the file operation is invoked.

HandlerThread calls looper.prepare () in the run method;

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

Looper creates the MessageQueue object in the constructor.

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

MessageQueue, the MessageQueue we often refer to in Handler learning, calls the initialization method of the native layer in the constructor.

MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();/ / native code
}
Copy the code

MessageQueue corresponds to native code, which basically initializes a NativeMessageQueue and then returns a long to the Java layer.

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);
}
Copy the code

NativeMessageQueue initialization method will first determine whether there is a Native layer Looper of the current thread, if not, create a new Looper and save.

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

In Looper’s constructor, we find “eventfd”, a method that has the characteristics of a file descriptor.

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

You can see from the C++ code comments that the eventfd function returns a new file descriptor.

/** * [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);
Copy the code

3.2 the IO leak

IO operation is a common operation in Android development. If the stream operation is not properly turned off, it may leak memory as well as FDS. Common problem codes are as follows:

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

If an exception occurs during a flow operation, it can result in a leak. The correct way to write this is to close the stream ina final block.

private void ioTest(a) {
    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(); }}}}Copy the code

Again, we looked at the source code for how stream operations create file descriptors. First of all, if you look at the constructor of FileOutputStream, you can see that it initializes a FileDescriptor variable named fd. This FileDescriptor object is the Java layer’s encapsulation of the native FileDescriptor. It contains only one member variable of type int, whose value is the file descriptor created by native layer.

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

The open method calls the JNI method open0 directly.

/**
 * 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);
}
Copy the code

Tips: When looking at Android source code, we often encounter native methods, which can not be jumped to view through Android Studio. You can search through the “Java class name _native method name” method on androidXref website. For example, this can search FileOutputStream_open0.

Next, let’s go into the native method and see the corresponding implementation.

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));
}
Copy the code

In the fileOpen method, the native layer file descriptor (FD) is generated through handleOpen. This FD is the so-called opposite file descriptor.

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);// Call open to get fd
    if(fd ! = -1) {
        / /...
        if(result ! = -1) {
            / /...
        } else {
            close(fd);
            fd = -1; }}return fd;
}
Copy the code

Is this the end of it?

Back to the beginning, the FileOutputStream constructor initializes the FileDescriptor class of the Java layer FileDescriptor. Currently, the FileDescriptor in this object is still the original value -1, so it is still an invalid FileDescriptor. After the fd is created in the native layer, You also need to pass the fd value to the Java layer.

Let’s look at the definition of the SET_FD macro, which assigns values to member variables of Java layer objects by reflection. Open0 is the jNI method of the object, so this in the macro is the object instance of the originally created FileOutputStream at the Java layer.

#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))

Copy the code

Fid is pre-initialized in native code.

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

Now that FileOutputStream initialization is complete, we have found the path to the underlying FD initialization. Android IO operations and other flow operations, the general process is basically similar, not detailed here.

The close method is called in the destructor method of the stream object, so when the object is recycled, the file descriptor is theoretically released. But it is best to control the release logic through code.

3.3 the SQLite leak

In daily development, if the database SQLite is used to manage local data, after the database query cursor is used, it also needs to call the close method to release resources, otherwise it may lead to memory and file descriptor leakage.

public void get(a) { db = ordersDBHelper.getReadableDatabase(); Cursor cursor = db.query(...) ;while (cursor.moveToNext()) {
      / /...
    }
    if(flag){
       // For some reason retrn
       return;
    }
    // If close is not called, the fd will leak
    cursor.close();
}
Copy the code

Since we understand that the Query operation should cause file descriptor leakage, we will start with the implementation of the Query method.

However, no file descriptor-related code is found in the Query method.

When tested, the file descriptor grows only after a moveToNext call. The implementation class SQLiteCursor of cursor can be obtained using the Query method.

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);
      }
      / /...
}
Copy the code

Find the implementation of moveToNext in the parent class of SQLiteCursor. GetCount is an abstract method implemented in subclass SQLiteCursor.

@Override
public final boolean moveToNext(a) {
    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;
    }
    / /...
}
Copy the code

The getCount method evaluates the member variable mCount and calls fillWindow if it is still the initial value.

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

The clearOrCreateWindow implementation goes back to the Parent Class AbstractWindowedCursor.

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

In the constructor of CursorWindow, initialization of the native layer is called through the nativeCreate method.

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

In C++ code, the create method of a native layer CursorWindow is continued.

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);
}
Copy the code

In the Create method of CursorWindow, we can find the code related to fd creation.

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);
    / /...
}
Copy the code

The ashmem_create_region method eventually calls the open function to open the file and return the file descriptor created by the system. This part of the code is not described, interested can view.

Native saves the fd information in the CursorWindow and returns a pointer address to the Java layer. The Java layer can use this pointer to manipulate c++ layer objects and get the corresponding file descriptor.

3.4 Leakage caused by InputChannel

WindowManager.addView  

Repeatedly adding views through WindowManager can also cause file descriptors to grow, and you can release previously created FDS by calling removeView.

private void addView(a) {
    View windowView = LayoutInflater.from(getApplication()).inflate(R.layout.layout_window, null);
    // repeat the call
    mWindowManager.addView(windowView, wmParams);
}
Copy the code

AddView in Windows ManagerImpl will eventually go to setView in View OtimPL.

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

Inputchannels are created in setView and passed to the server through Binder mechanisms.

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    / /...
    / / create inputchannel
    if ((mWindowAttributes.inputFeatures
        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
        mInputChannel = new InputChannel();
    }
    // Remote service interface
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
        getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
        mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);//mInputChannel is passed as a parameter
    / /...
    if(mInputChannel ! =null) {
        if(mInputQueueCallback ! =null) {
            mInputQueue = new InputQueue();
            mInputQueueCallback.onInputQueueCreated(mInputQueue);
        }
        // Create a WindowInputEventReceiver object
        mInputEventReceiver = newWindowInputEventReceiver(mInputChannel, Looper.myLooper()); }}Copy the code

AddToDisplay is an AIDL method whose implementation class is Session in the source code. The addWIndow method of WindowManagerService is finally called.

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));
}
Copy the code

WMS creates InputChannel in the addWindow method for communication.

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 booleanopenInputChannels = (outInputChannel ! =null
        && (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
        if  (openInputChannels) {
            win.openInputChannel(outInputChannel);
        }
        / /...
}
Copy the code

Create an InputChannel in openInputChannel and pass the client back.

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

InputChannel openInputChannelPair will call native nativeOpenInputChannelPair, create two in native socket with the file descriptor.

int socketpair(int domain, int type, int protocol, int sv[2]) {
    // Create a pair of anonymous connected sockets
    int rc = __socketpair(domain, type, protocol, sv);
    if (rc == 0) {
        // Trace the file descriptor
        FDTRACK_CREATE(sv[0]);
        FDTRACK_CREATE(sv[1]);
    }
    return rc;
}
Copy the code

The analysis of WindowManager involves WMS, which has a lot of content. This article focuses on the content related to file descriptors. A file descriptor is created for the server process and for the client process, because the socket is created for interprocess communication. In addition, too many system process file descriptors can theoretically cause a system crash.

Four, how to check

If your application receives one of these crash stacks, congratulations, your application has a file descriptor leak.

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

Crashes caused by file descriptors often cannot be analyzed directly by the stack. The reason is simple: the code in question is consuming file descriptors at the same time that normal code logic is creating file descriptors, so the crash could be triggered by normal code.

4.1 Printing the current FD information

Run the ls -la /proc/$pid-fd command to check the file descriptor consumption of the current process. Common Android application file descriptors can be divided into several categories, by comparing which type of file descriptors are too high, to narrow the scope of the problem.

4.2 Dump system information

Run dumpsys Window to check whether an exception window exists. Used to resolve inputChannel-related leakage problems.

4.3 Online Monitoring

If the problem is that the file cannot be reproduced locally, you can add an online monitoring code to poll the number of FDS used by the current process periodically. When the number reaches the threshold, the information of the current FD is read and sent to the background for analysis. The code for obtaining the file information of the FD is as follows.

if (Build.VERSION.SDK_INT >= VersionCodes.L) {
    linkTarget = Os.readlink(file.getAbsolutePath());
} else {
    // Read file descriptor information from readlink
}
Copy the code

4.4 Checking The Logs Periodically Printed

In addition to analyzing fD-related information, pay attention to whether logcat contains frequently printed information, for example, failed to create a socket.

5. Reference documents

  1. The Linux source
  2. The Android source code
  3. I – node is introduced
  4. InputChannel communication
  5. Evolution of the Linux kernel file descriptor table