介绍文件描述符的概念以及工作原理,并通过源码理解 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 创立失败。
五、参考文档
- Linux 源码
- Android 源码
- i-node 介绍
- InputChannel 通信
- Linux 内核文件描述符表的演变