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